how to automatically resize d3.js graph to include axis - d3.js

I have a bit of a problem building a bar chart. I'm learning d3.js for the first time and being someone who always worked with PHP/MySQL, I haven't had to learn javascript. As a result, I'm struggling a bit.
My question is more conceptual in nature. If, let's say in a bar chart, the Y axis is contained in a g element and the bars are contained in another one, how can I ensure that my axis takes a dyanmic width based on the data presented?
I managed to generate a bar chart and it works great, but the padding is a fixed number (let's say 50px). it works great now, because my numbers go from 0 to 50, so everything fits. What happens if I get trillions instead? The width of the axis will change, yet my padding remains 50px, which means it will clip my content.
What is the "convention" when it comes to this? Any tricks?
Thanks

One trick you might use here is what I like to call the "double-render". You essentially draw the axis first (before the rest of the plot) and get the width of the greatest tick label. The, You can draw the plot conventionally with that value as the margin. This trick is especially useful for string "category" labels, but will work for numbers as well.
Here's a commented example. Run it multiple times to see how it refits the axis:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.bar {
fill: steelblue;
}
.bar:hover {
fill: brown;
}
.axis--x path {
display: none;
}
</style>
<svg width="300" height="300"></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="//chancejs.com/chance.min.js"></script>
<script>
// set up some random data
// pick a random max value to render on the yaxis
var maxVal = chance.integer({
min: 1,
max: chance.pickone([1e1, 1e5, 1e10])
}),
// generate some fake data
data = [{
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}, {
x: chance.word(),
y: chance.floating({
min: 0,
max: maxVal
})
}];
// create svg and set up a y scale, the height value doesn't matter
var svg = d3.select("svg"),
y = d3.scaleLinear().rangeRound([100, 0]);
// set domain
y.domain([0, d3.max(data, function(d) {
return d.y;
})]);
// draw fake axis
var yAxis = svg.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(y));
// determine max width of text label
var mW = 0;
yAxis.selectAll(".tick>text").each(function(d) {
var w = this.getBBox().width;
if (w > mW) mW = w;
});
// remove fake yaxis
yAxis.remove();
// draw plot normally
var margin = {
top: 20,
right: 20,
bottom: 30,
left: mW + 10 // max with + padding fudge
},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom;
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// reset to actual height
y.range([height, 0]);
var x = d3.scaleBand().rangeRound([0, width]).padding(0.1);
x.domain(data.map(function(d) {
return d.x;
}));
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));
g.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x(d.x);
})
.attr("y", function(d) {
return y(d.y);
})
.attr("width", x.bandwidth())
.attr("height", function(d) {
return height - y(d.y);
});
</script>

Related

d3js v7, the circle moves in the opposite direction in y coordinate when I drag the circle by the cursor

I am trying to move a circle by dragging it in d3js, but I can't get it to move in the y-axis direction.
I am using v7 of d3js and from several attempts I have found that the event.x and event.y in the drag function are the coordinates on the graph of the cursor being dragged. Therefore, I tried to transform these two coordinates with the scale transformation function to obtain the coordinates of the display on the svg.
There are two problems here.
The first is that the y-coordinates obtained by event.y are the opposite of the actual vertical movement. For example, if I actually run the code and drag the circle to the upper right, the circle will move to the lower right. This is a line symmetry-like movement in the y-coordinate of the circle's initial position; the positive and negative values of the y-coordinate movement are reversed. This is also true for event.dy. The x-coordinates in event.x and event.dx are working fine. Also, d3.pointer(event) does not work as intended.
The second problem is that the circle jumps at the beginning of the drag. I found the cause of this by looking within the function that is executed during the drag. The initial values of event.x and event.y are the initial values of the circle's coordinates, even after multiple drags, which causes the circle to always start moving from the initial value of the circle when it is dragged.
I have a program that reproduces the problem.
How can these problems be resolved?
Thanks.
Here is the code.
var dataset = [
{x: 5, y: 20, name: "one"},
{x: 100, y: 33, name: "two"},
{x: 250, y: 50, name: "three"},
{x: 480, y: 90, name: "four"},
];
const width = 400;
const height = 300;
const margin = { "top": 30, "bottom": 60, "right": 30, "left": 60 };
const svgWidth = width + margin.left + margin.right;
const svgHeight = height + margin.top + margin.bottom;
var svg = d3.select("#graph-area").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border-radius", "20px 20px 20px 20px")
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
var xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.x; }) + 10])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.y; }) + 10])
.range([height, 0]);
var axisx = d3.axisBottom(xScale).ticks(5);
var axisy = d3.axisLeft(yScale).ticks(5);
var x = svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(axisx)
.attr("class", "x_axis")
x.append("text")
.attr("x", (width - margin.left - margin.right) / 2 + margin.left)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("font-size", "10pt")
.attr("font-weight", "bold")
.text("X Label")
.attr("class", "x_axis")
var y = svg.append("g")
.call(axisy)
.attr("class", "y_axis")
y.append("text")
.attr("x", -(height - margin.top - margin.bottom) / 2 - margin.top)
.attr("y", -35)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "10pt")
.text("Y Label")
.attr("class", "y_axis")
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
var circle = svg.append("g")
.attr("id", "scatterplot")
.attr("clip-path", "url(#clip)");
circle.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(d.x); })
.attr("cy", function(d) { return yScale(d.y); })
.attr("fill", "steelblue")
.attr("r", 20)
.on('mouseover',function(elem, data) {
//console.log("over data name: " + data.name);
})
.on('mouseout',function (elem, data) {
//console.log("out data name: " + data.name);
})
.on("mousedown", function(elem, data){
console.log("down data name: " + data.name);
})
.on("mouseup", function(elem, data){
console.log("up data name: " + data.name);
})
.call(d3.drag().on("drag", circleDragged).on("start", circleStartDrag))
function circleStartDrag(event){
console.log("start drag");
}
function circleDragged(event){
d3.select(this).attr('cx', xScale(event.x)).attr('cy', yScale(event.y));
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 Scatter Plot</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<div id="graph-area" stype="position: relative;"></div
</body>
</html>
Edit (Perception of coordinate calculation in the drag function)
The drag function refers to the values set in the data (cx and cy) and indicates the new coordinates of the cursor reflecting dy and dx from those values. So in my case I have the unscaled data values set as event.subject, so when I drag directly above, it will be calculated as (cx=250, cy=-40) based on (cx=250, cy=-50) and dy=-1. This is then reflected in event.y. This is the reason why the y-coordinates of the circle I was talking about during the drag were upside down.
The correct value is set as (cx=204, cy=150) as a scaled value of the data at the time of enter() execution. When the circle is dragged directly up, event.y is set to cy=140 based on dy=-1. In pixel coordinates in svg, a decrease in the y coordinate value means an upward movement, so the circle moves up. At this point I need to update d.x=event.y to take into account the reference to the value of event.subject in the next drag.
This is the expected result given your code.
You initially position the data points with scale(d.y) which converts your data vales to pixel values. When you drag you take the pixel value (event.y) and apply the scale again - scaling pixel values as though they were data.
The data value of d.y = 0 is mapped to height by the scale. If you drag a circle that is at the top of the SVG (y=0) then the scale will return height which is at the bottom of the SVG - hence your inversion.
The solution is to just use the event.y / event.x values as these values represent pixel values already - there is no need to scale them. If you want to convert the pixel values to data values, then you can use scale.invert() - but this is only useful in updating the data, not the pixel coordinates of each circle.
Removing the scaling functions from the drag reveals one last issue - while the drag moves along with the mouse, you still have a jump on drag start: the drag subject (reference coordinates of the thing being dragged) is by default the x,y properties of your datum. As this represents the data values in your datum, rather than scaled pixel values, you get a jump. The quickest solution would be to redefine the x,y properties of the datum as the scaled pixel values and update these on drag.
var dataset = [
{x: 5, y: 20, name: "one"},
{x: 100, y: 33, name: "two"},
{x: 250, y: 50, name: "three"},
{x: 480, y: 90, name: "four"},
];
const width = 400;
const height = 300;
const margin = { "top": 30, "bottom": 60, "right": 30, "left": 60 };
const svgWidth = width + margin.left + margin.right;
const svgHeight = height + margin.top + margin.bottom;
var svg = d3.select("#graph-area").append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border-radius", "20px 20px 20px 20px")
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
var xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.x; }) + 10])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d.y; }) + 10])
.range([height, 0]);
var axisx = d3.axisBottom(xScale).ticks(5);
var axisy = d3.axisLeft(yScale).ticks(5);
var x = svg.append("g")
.attr("transform", "translate(" + 0 + "," + height + ")")
.call(axisx)
.attr("class", "x_axis")
x.append("text")
.attr("x", (width - margin.left - margin.right) / 2 + margin.left)
.attr("y", 35)
.attr("text-anchor", "middle")
.attr("font-size", "10pt")
.attr("font-weight", "bold")
.text("X Label")
.attr("class", "x_axis")
var y = svg.append("g")
.call(axisy)
.attr("class", "y_axis")
y.append("text")
.attr("x", -(height - margin.top - margin.bottom) / 2 - margin.top)
.attr("y", -35)
.attr("transform", "rotate(-90)")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("font-size", "10pt")
.text("Y Label")
.attr("class", "y_axis")
var clip = svg.append("defs").append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", width )
.attr("height", height )
.attr("x", 0)
.attr("y", 0);
var circle = svg.append("g")
.attr("id", "scatterplot")
.attr("clip-path", "url(#clip)");
circle.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return d.x = xScale(d.x); })
.attr("cy", function(d) { return d.y = yScale(d.y); })
.attr("fill", "steelblue")
.attr("r", 20)
.on('mouseover',function(elem, data) {
//console.log("over data name: " + data.name);
})
.on('mouseout',function (elem, data) {
//console.log("out data name: " + data.name);
})
.on("mousedown", function(elem, data){
console.log("down data name: " + data.name);
})
.on("mouseup", function(elem, data){
console.log("up data name: " + data.name);
})
.call(d3.drag().on("drag", circleDragged).on("start", circleStartDrag))
function circleStartDrag(event){
console.log("start drag");
}
function circleDragged(event){
d3.select(this).attr('cx', d=>d.x=event.x).attr('cy', d=>d.y=event.y);
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>D3 Scatter Plot</title>
<script src="https://code.jquery.com/jquery-3.6.0.slim.min.js" integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI=" crossorigin="anonymous"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>
<div id="graph-area" stype="position: relative;"></div
</body>
</html>

d3 multi line alignment with y axis data

I have created the d3 Multi-line chart. I managed to generate both x and y axis along with the Lines. But unfortunately i got stuck at this point where I am not able to align the Path-lines with the Y axis i.e. ScaleBand(). I have no idea how do to that and guide line for that would be great. I have attached my code with sample data in snippet. Thanks in advance.
var dataDomains = ['automated', 'manual']
var data = [
{ automated: 1000 , manual: 3000 },
{ automated: 5000 , manual: 6000 },
{ automated: 10000, manual: 9000 },
{ automated: 50000, manual: 12000 },
{ automated: 100000, manual: 15000 },
]
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 960 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleBand().range([height, 0]);
// define the 1st line
var valueline = d3.line()
.x(function (d) { return x(d.automated); })
.y(function (d) { return y('automated'); });
// define the 2nd line
var valueline2 = d3.line()
.x(function (d) { return x(d.manual); })
.y(function (d) { return y('manual'); });
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("#cl-AVM").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 + ")");
// Scale the range of the data
x.domain([0, d3.max(data, function (d) { return Math.max(d.automated, d.manual) })])
y.domain(dataDomains);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the valueline2 path.
svg.append("path")
.data([data])
.attr("class", "line")
.style("stroke", "red")
.attr("d", valueline2);
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="cl-AVM"></div>
You could use a scalePoint instead of a scaleBand as your line can be considered as just a point on the y axis.
Then you can add some padding so that the values for the two points are not 0 and 100 (I picked 0.6 but you can play around with it and see what best suits you).
You can see usage of scalePoint here in more details: https://observablehq.com/#d3/d3-scalepoint
Hope that helps ! :)
var dataDomains = ['automated', 'manual']
var data = [
{ automated: 1000 , manual: 3000 },
{ automated: 5000 , manual: 6000 },
{ automated: 10000, manual: 9000 },
{ automated: 50000, manual: 12000 },
{ automated: 100000, manual: 15000 },
]
// set the dimensions and margins of the graph
var margin = { top: 20, right: 20, bottom: 30, left: 50 },
width = 960 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleLinear().range([0, width]);
var y = d3.scalePoint().range([height, 0]).padding(0.6); // modified here
// define the 1st line
var valueline = d3.line()
.x(function (d) { return x(d.automated); })
.y(function (d) { return y('automated'); });
// define the 2nd line
var valueline2 = d3.line()
.x(function (d) { return x(d.manual); })
.y(function (d) { return y('manual'); });
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("#cl-AVM").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 + ")");
// Scale the range of the data
x.domain([0, d3.max(data, function (d) { return Math.max(d.automated, d.manual) })])
y.domain(dataDomains);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the valueline2 path.
svg.append("path")
.data([data])
.attr("class", "line")
.style("stroke", "red")
.attr("d", valueline2);
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y));
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div id="cl-AVM"></div>

y axis ticks disappear in responsive chart in d3.js v4

I had perfectly adequate ticks in my earlier statically sized plot using d3.js v4; once I made it resizable, the ticks and values disappeared from the y axis.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Test Plot Viewer</title>
<script src="js/lib/d3.v4.min.js"></script>
<script src="js/lib/jquery.min.js"></script>
<style>
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
#chart {
position: fixed;
left: 55px;
right: 15px;
top: 10px;
bottom: 55px;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
var chartDiv = document.getElementById("chart");
var svg = d3.select(chartDiv).append("svg");
// parse the date time
var parseTime = d3.timeParse("%m/%d %H:%M");
function render() {
$("svg").empty();
// Extract the width and height that was computed by CSS.
var width = chartDiv.clientWidth;
var height = chartDiv.clientHeight;
// Use the extracted size to set the size of an SVG element.
svg
.attr("width", width)
.attr("height", height);
var margin = {top: 10, right: 15, bottom: 55, left: 55};
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var line = d3.line()
.x(function(d) { return x(d.time); })
.y(function(d) { return y(d.solar); });
// Get the data
d3.csv("data_fred.csv", function(error, data) {
if (error) throw error;
// format the data
data.forEach(function(d) {
d.time = parseTime(d.time);
d.solar = +d.solar;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.time; }));
y.domain([0, d3.max(data, function(d) { return d.solar; })]);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", line);
// Add the X Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.tickFormat(d3.timeFormat("%m/%d %H:%M ")));
// Add the Y Axis
svg.append("g")
.call(d3.axisLeft(y))
.ticks(10);
});
}
render();
// Redraw based on the new size whenever the browser window is resized
window.addEventListener("resize", render);
</script>
</body>
</html>
The submitter function wants more details, but I have none...
blah
blah
blah
blah
characters added to pad non-code content.
The ticks are now gone on the y axis. I've added the .tick attribute to the y axis, but no joy.
How do I get my y axis ticks back on this responsive version of the chart? TIA
Posted later: Anyone? My non-responsive version of the code is drawing correctly; "responsifying" it makes the y-axis ticks and units disappear. I've tried almost every permutation of command ordering and placement, but no luck.
Whats happening here is your Y axis ticks are getting hidden because they're not in the viewport. What you need to do is put all the elements in your svg in a <g> wrapper and translate it by left and top margins.
Here's a fiddle
var chartDiv = document.getElementById("chart");
var svg = d3.select(chartDiv).append("svg");
var g = svg.append('g');
function render() {
$('svg').empty();
// Extract the width and height that was computed by CSS.
var width = $('#chart').width();
var height = $('#chart').height();
// Use the extracted size to set the size of an SVG element.
svg
.attr("width", width)
.attr("height", height);
// set the dimensions and margins of the graph
var margin = {
top: 20,
right: 20,
bottom: 50,
left: 40
};
width = width - margin.left - margin.right,
height = height - margin.top - margin.bottom;
// parse the date time
var parseTime = d3.timeParse("%m/%d %H:%M");
// set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
// define the line
var valueline = d3.line()
.x(function(d) {
return x(d.time);
})
.y(function(d) {
return y(d.solar);
});
// Get the data
var data = [{
'time': '11/30 04:55',
'solar': -1.1
}, {
'time': '11/30 05:00',
'solar': -1.1
}, {
'time': '11/30 05:05',
'solar': -1.5
}, {
'time': '11/30 05:10',
'solar': -2
}, {
'time': '11/30 05:15',
'solar': 1
}]
// format the data
data.forEach(function(d) {
d.time = parseTime(d.time);
d.solar = +d.solar;
});
console.log(data)
// Scale the range of the data
x.domain(d3.extent(data, function(d) {
return d.time;
}));
var yExtent = d3.extent(data, function(d) {
return d.solar;
})
y.domain(yExtent);
g.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
// Add the valueline path.
g.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x)
.tickFormat(d3.timeFormat("%m/%d %H:%M ")))
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em")
.attr("transform", "rotate(-45)");
// Add the Y Axis
g.append("g")
.call(d3.axisLeft(y));
}
// d3.select("svg").remove();
// svg.remove();
// d3.selectAll("g > *").remove()
// d3.selectAll("chartDiv.path.line").remove();
// d3.select("path.line").remove();
render();
// Redraw based on the new size whenever the browser window is resized.
window.addEventListener("resize", render);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="chart"></div>
Happy coding :)
Got it - the axis ticks were disappearing off the left edge of the window - fixed that with a transform/translate:
// Add the Y Axis
svg.append("g")
.attr("transform", "translate(40 ,10)")
.call(d3.axisLeft(y));
...with a similar translation of the x axis and path to match.
Also, the axis scale now appeared with an extent of 0 to 1.0, as it wasn't being passed out of the file read loop since it was an asynchronous operation. Bringing the svg.append's into the data read loop restored my "normal" units to the axis.

Dynamically set the axisBottom number of ticks to the highest integer in an array in D3v4

I want to show the axisBottom value as an integer, with nothing in between the whole numbers. In the below image I use tickFormat which switched the values to integers, but now I need to only have the values of '1, 2, 3..etc" rather than the duplicate integer values. I need the number of ticks to be dynamically generated, meaning I can't statically say there are 3 ticks. The data I pass may have a max value of 3 or any other number, but they will all be whole numbers.
Data (JSON)
[ { yAxis: '15.1.1', xAxis: 2 },
{ yAxis: '15.1.2', xAxis: 2 },
{ yAxis: '15.1.3', xAxis: 1 },
{ yAxis: '15.1.4', xAxis: 3 },
{ yAxis: '15.1.5', xAxis: 0 },
{ yAxis: '15.1.6', xAxis: 1 },
{ yAxis: '15.1.7', xAxis: 0 },
{ yAxis: '15.1.8', xAxis: 0 } ]
Images and code below.
var data = !{dataObj}; //using Jade as template engine
// set the dimensions and margins of the graph
var margin = {top: 20, right: 20, bottom: 30, left: 80},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// set the ranges
var y = d3.scaleBand()
.range([height, 0])
.padding(0.4);
var x = d3.scaleLinear()
.range([0, width]);
var svg = d3.select(".barChartContainer").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 + ")");
// format the data
data.forEach(function(d) {
d.xAxis = +d.xAxis;
});
// Scale the range of the data in the domains
x.domain([0, d3.max(data, function(d){ return d.xAxis; })])
y.domain(data.map(function(d) { return d.yAxis; }));
//y.domain([0, d3.max(data, function(d) { return d.prereqs; })]);
// append the rectangles for the bar chart
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
//.attr("x", function(d) { return x(d.prereqs); })
.attr("width", function(d) {return x(d.xAxis); } )
.attr("y", function(d) { return y(d.yAxis); })
.attr("height", y.bandwidth());
// add the x Axis
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x));
// add the y Axis
svg.append("g")
.call(d3.axisLeft(y));
You have two options here.
The first one is showing only the integers, but keeping the ticks. This can be done testing if the number is an integer inside tickFormat:
.tickFormat(function(d) {
return d % 1 ? null : d;
});
Here is a demo:
var svg = d3.select("svg");
var scale = d3.scaleLinear()
.range([20, 480])
.domain([0, 3]);
var axis = d3.axisBottom(scale)
.tickFormat(function(d) {
return d % 1 ? null : d;
});
var gX = svg.append("g")
.attr("transform", "translate(0, 50)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="100"></svg>
However, if you want to show only the ticks for the integers, the solution is using tickValues:
.tickValues(d3.range(scale.domain()[0], scale.domain()[1] + 1, 1))
Here is a demo:
var svg = d3.select("svg");
var scale = d3.scaleLinear()
.range([20, 480])
.domain([0, 3]);
var axis = d3.axisBottom(scale)
.tickValues(d3.range(scale.domain()[0], scale.domain()[1] + 1, 1))
.tickFormat(function(d) {
return ~~d;
});
var gX = svg.append("g")
.attr("transform", "translate(0, 50)")
.call(axis);
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="100"></svg>

d3 bar chart using rangeRoundBands - why is there outer padding?

I'm creating a bar chart using an ordinal scale for the x axis, and using rangeRoundBands. I'm following this example: https://bl.ocks.org/mbostock/3885304
However, my chart has outer padding -- big spaces at the beginning and end of the axis, where I'd like the bars to fully extend. The screen shot below shows the spaces I'm referring to circled in red.
How can I remove these spaces? I need my margins and the svg width and height to remain the same.
Here is a Plunker with the chart as it is now:
https://plnkr.co/edit/gMw7jvieKSlFbHLTNY9o?p=preview
Code is also below:
<!doctype html>
<html>
<head>
<style>
.axis path{
fill: none;
stroke: #cccccc;
stroke-width: 2px;
}
.x.axis text{
display:none;
}
.bar{
fill: blue;
}
body{
font-family:Helvetica, Arial, sans-serif;
}
</style>
</head>
<body>
<div id="barchart"></div>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
var margin = {top: 20, right: 0, bottom: 50, left: 300},
width = 800 - margin.left - margin.right;
height = 465 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .5);
var y = d3.scale.linear().range([height, 0]);
var yAxis = d3.svg.axis().scale(y)
.orient("left").ticks(5);
var xAxis = d3.svg.axis().scale(x)
.orient("bottom");
var barsvg = d3.select("#barchart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("class", "barchartbox")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Get the data
d3.json("population.json", function(error, data1) {
x.domain(data1.map(function(d) { return d.country; }));
y.domain([0, d3.max(data1, function(d) { return d.value; })]);
barsvg.selectAll(".axis").remove();
// Add the Y Axis
barsvg.append("g")
.attr("class", "y axis")
.call(yAxis);
// Add the X Axis
barsvg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
var bars = barsvg.selectAll(".bar")
.data(data1);
bars.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return x(d.country); })
.attr("width", x.rangeBand())
.attr("y", function(d) {return y(d.value); })
.attr("height", function(d) { return height - y(d.value); });
bars.exit().remove();
});
</script>
</body>
</html>
You doing.
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .5);
Instead use rangeBands.
var x = d3.scale.ordinal()
.rangeBands([0, width], .5);
working code here
It is specified in the d3 document that while using rangeRoundBands, rounding 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.
Reference: rangeRoundBands
So the solution would be to use following lines after setting the x axis domain:
var mult = Math.max (1, Math.floor (width / x.domain().length));
x.rangeRoundBands ([0, (x.domain().length * mult)], 0.1, 0);

Resources