The last ticks in x-axis and y-axis don't have values drawn next to them. I cannot figure out why these values are missing.
This is the JS code :
var xAxisScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.x; })])
.range([padding, containerWidth - padding * 2]);
var xAxis = d3.svg.axis().scale(xAxisScale).orient("bottom");
var xGuide = chartContainer.append('g')
.attr("class", "xAxis")
.call(xAxis)
.attr('transform', 'translate(0 ,' + (containerHeight -padding) + ')');
/* Code for adding y axis in chart
*
* */
var yAxisScale = d3.scale.linear()
.domain([0, d3.max(data, function(d){return d.y})])
.range([containerHeight-padding, padding ]);
var yAxis = d3.svg.axis().scale(yAxisScale).orient("left");
var yGuide = chartContainer.append('g')
.attr("class", "yAxis")
.call(yAxis)
.attr('transform', 'translate('+padding + ',0)');
This is my live demo.
This can be done by overriding the default behavior for determining tick values using axis.tickValues():
var yAxis = d3.svg.axis().scale(yAxisScale).orient("left")
.tickValues(yAxisScale.ticks().concat(yAxisScale.domain()));
This still resorts to the automatically generated tick values by calling yAxisScale.ticks() which is the default behavior for D3's axes. These values are then supplemented with the outer bounds of your data values, i.e. the array returned by yAxisScale.ticks(). To set just the upper bound, if would be sufficient to specify yAxisScale.domain()[1], although it won't hurt having duplicate values in the array provided to .tickValues().
Doing it this way frees you from any hardcoding of tick values.
Have a look at this working example:
var padding = 70;
//Width and height
var containerWidth = 1000;
var containerHeight = 500;
var data = [{
"x": 82,
"y": 1730
}, {
"x": 533,
"y": 16385
}, {
"x": 41,
"y": 783
}, {
"x": 20.5,
"y": 5873
}, {
"x": 553.5,
"y": 25200
}, {
"x": 61.5,
"y": 30000
}, {
"x": 184.5,
"y": 2853
}, {
"x": 1476,
"y": 83775
}];
//Create scale functions
var xScale = d3.scale.linear()
var chartContainer = d3.select("body")
.append("svg")
.attr("class", "chartContainer")
.attr("width", containerWidth)
.attr("height", containerHeight);
$(".chartContainer").css({
"background-color": "",
"position": "absolute",
"top": 50,
"left": 15
});
var xAxisScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) { return d.x; })])
.range([padding, containerWidth - padding * 2]);
var xAxis = d3.svg.axis().scale(xAxisScale).orient("bottom")
.tickValues(xAxisScale.ticks().concat(xAxisScale.domain()));
var xGuide = chartContainer.append('g')
.attr("class", "xAxis")
.call(xAxis)
.attr('transform', 'translate(0 ,' + (containerHeight -padding) + ')');
xGuide.selectAll('path')
.style({ fill: 'none', stroke: "#000"});
xGuide.selectAll('line')
.style({ stroke: "#000"});
/* Code for adding y axis in chart
*
* */
var yAxisScale = d3.scale.linear()
.domain([0, d3.max(data, function(d){return d.y})])
.range([containerHeight-padding, padding ]);
var yAxis = d3.svg.axis().scale(yAxisScale).orient("left")
.tickValues(yAxisScale.ticks().concat(yAxisScale.domain()[1]));
var yGuide = chartContainer.append('g')
.attr("class", "yAxis")
.call(yAxis)
.attr('transform', 'translate('+padding + ',0)');
chartContainer.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) {
return xAxisScale(d.x);
})
.attr("cy", function(d) {
return yAxisScale(d.y);
})
.attr("r", 4)
.attr("fill", "red")
.attr({
"z-index": "9999"
});
yGuide.selectAll('path')
.style({ fill: 'none', stroke: "#000"});
yGuide.selectAll('line')
.style({ stroke: "#000"});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Add .nice() to line 69 of your live demo. will solve the issue. See graph below for the end result.
For more details and explanation, please refer to this closed issue on the d3-scale library. mbostock commented on it on Dec 6, 2016 and provided the solution.
You are using d3's automatic tick generator which makes a best guess on how best to draw your axes given the data in the chart.
You have two options for overriding this:
Setting a number of tick values to show. For example:
var xAxis = d3.svg.axis()
.scale(xAxisScale)
.orient("bottom")
.ticks(10);
Passing a tick values array to explicitly tell d3 what values you want on your x or y axis; like this:
var xAxis = d3.svg.axis()
.scale(xAxisScale)
.orient("bottom")
.tickValues([0, 100, 200, 300, 400]);
If you want to get rid of the ticks that are missing values (in your case, the outer ticks) just set .outerTickSize(0) on the x or y-axis.
You should also probably read up on the d3's axes API here.
Related
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>
I am fighting to make this bar chart automatic. It should display properly after I pass in 6, 8 or 15 elements in the array. It works properly for 16 elements + 1. The first is an empty element ( which is a bit annoying as well).
This is the barchart: http://bl.ocks.org/kiranml1/6872226
I dont really know what I should change to make it work. Spent few good hours already..(d3 newbie)
I cut down some of the stuff, like grid and x- axis ( I dont need it ).
Here is my code (same as example on website, except some functionality)
var categories = ['', 'Accessories', 'Audiophile', 'Camera & Photo', 'Cell Phones', 'Computers', 'eBook Readers', 'Gadgets', 'GPS & Navigation'];
var dollars = [213, 209, 190, 179, 156, 209, 190, 179];
var colors = ['#0000b4', '#0082ca', '#0094ff', '#0d4bcf', '#0066AE', '#074285', '#00187B', '#285964', '#405F83', '#416545', '#4D7069', '#6E9985', '#7EBC89', '#0283AF', '#79BCBF', '#99C19E'];
var xscale = d3.scale.linear()
.domain([10, 260])
.range([0, 622]);
var yscale = d3.scale.linear()
.domain([0, categories.length])
.range([0, 480]);
var colorScale = d3.scale.quantize()
.domain([0, categories.length])
.range(colors);
var canvas = d3.select('#twoDbInfo')
.append('svg')
.attr({
'width': 900,
'height': 550
});
var yAxis = d3.svg.axis();
yAxis
.orient('left')
.scale(yscale)
.tickSize(2)
.tickFormat(function (d, i) {
return categories[i];
})
.tickValues(d3.range(17));
var y_xis = canvas.append('g')
.attr("transform", "translate(150,0)")
.attr('id', 'yaxis')
.call(yAxis)
.style("fill", "White");
var chart = canvas.append('g')
.attr("transform", "translate(150,0)")
.attr('id', 'bars')
.selectAll('rect')
.data(dollars)
.enter()
.append('rect')
.attr('height', 19)
.attr({
'x': 0,
'y': function (d, i) {
return yscale(i) + 19;
}
})
.style('fill', function (d, i) {
return colorScale(i);
})
.attr('width', function (d) {
return 0;
});
var transit = d3.select("svg").selectAll("rect")
.data(dollars)
.transition()
.duration(1000)
.attr("width", function (d) {
return xscale(d);
});
var transitext = d3.select('#bars')
.selectAll('text')
.data(dollars)
.enter()
.append('text')
.attr({
'x': function (d) {
return xscale(d) - 200;
},
'y': function (d, i) {
return yscale(i) + 35;
}
})
.text(function (d) {
return d + "$";
}).style({
'fill': '#fff',
'font-size': '14px'
});
I update a little bit on the plnkr, add margin and padding, comment part of code unrelated to the question(grid and bars transition, you can add it back easily):
plnkr
The most important thing to you question, I think you should use ordinal scale for yAxis instead of linear scale:
var yscale = d3.scale.ordinal()
.domain(categories)
.rangeBands([0, height], .1);
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.
For some reason my bars (rects) are drawing really wide- I think its because the dates are not parsing correctly. here is the code
var fakeData= [
{"date":2013-10,"shortFig":10},
{"date":2013-11,"shortFig":-15},
{"date":2013-12,"shortFig":15},
{"date":2014-01,"shortFig":39},
{"date":2014-02,"shortFig":-38},
{"date":2014-03,"shortFig":33},
{"date":2014-04,"shortFig":-35},
{"date":2014-05,"shortFig":-2},
{"date":2014-06,"shortFig":-39},
{"date":2014-07,"shortFig":-46},
{"date":2014-08,"shortFig":23},
{"date":2014-09,"shortFig":45}
]
..this data becomes "thedata" in my chart building function where I try to parse the data and build the x scales and x axis:
// parse dates
var parseDate = d3.time.format("%Y–%m").parse;
thedata.forEach(function(d) {
var date = d.date.toString();
d.date = parseDate(date);
});
//The X scale
var xScale=d3.scale.ordinal()
.rangeRoundBands([0, width], .1)
.domain(thedata.map(function(d) { return d.date; }));
//With the X scale, set up the X axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.tickValues([thedata[0].date]);
//Call the X axis
baseGroup.append("g")
.attr("class", "xaxis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
baseGroup.selectAll("rect")
.data(thedata)
.enter()
.append("rect")
.attr("class", function(d){ if(d.shortFig >= 0){return "green3"}else{return "red3"} })
.attr({
"x": function(d) {return xScale(d.date);},
"y": function(d) {return yScale(Math.max(0, d.shortFig));}, //Return the number with the highest value
"height": function(d) {return Math.abs(yScale(d.shortFig) - yScale(0));}, //Return the absolute value of a number, so negative numbers will be positive
"width": xScale.rangeBand()
});
Just a typo, the date parameters should be strings. Tested this, it works, just replace the fakeData array and you should be set.
var fakeData= [
{"date":"2013-10","shortFig":10},
{"date":"2013-11","shortFig":-15},
{"date":"2013-12","shortFig":15},
{"date":"2014-01","shortFig":39},
{"date":"2014-02","shortFig":-38},
{"date":"2014-03","shortFig":33},
{"date":"2014-04","shortFig":-35},
{"date":"2014-05","shortFig":-2},
{"date":"2014-06","shortFig":-39},
{"date":"2014-07","shortFig":-46},
{"date":"2014-08","shortFig":23},
{"date":"2014-09","shortFig":45}
];
var parseDate = d3.time.format("%Y-%m").parse;
fakeData.forEach(function(d){
console.log(parseDate(d.date));
});
Pre-emptive apologies for any novice mistakes made in the underlying code. I am attempting to generate a simple line chart with d3.js, with dates on the x-axis and integer values on the y-axis. The data is supplied by two arrays: xDataGet and yData from The issue I'm having is, from what I can gather, that the strings in xDataGet are not being properly converted to Dates. In tinkering with the code, I can manage to have the Y-Axis appear, but have had no such luck with the X-Axis. Could someone indicate where I'm going wrong?
var margin = [80, 80, 80, 80]; // margins
var width = 1000 - margin[1] - margin[3]; // width
var height = 400 - margin[0] - margin[2]; // height
var xDataGet = [ '03/18/2014', '03/01/2014', '02/15/2014', '02/01/2014', '01/18/2014', '12/07/2013', '11/16/2013' ]
function getDate(d) {
formatted = d3.time.format("%m-%d-%Y")
return formatted(new Date(d));
}
var xData = getDate(xDataGet)
var yData = [ 33, 36, 42, 27, 36, 21, 18 ]
var x = d3.time.scale()
.range([0, width])
.domain([d3.min(xData), d3.max(xData)])
var y = d3.scale.linear()
.range([height, 0])
.domain([0, d3.max(yData)]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
var line = d3.svg.line()
.x(function(d) { return x(xData); })
.y(function(d) { return y(yData); })
.interpolate("linear");
var svg = d3.select("body").append("svg")
.attr("width", width + 80 + 80)
.attr("height", height + 80 + 80)
.append("g")
.attr("transform", "translate(" + 80 + "," + 80 + ")");
svg.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(-25,0)")
.call(yAxis);
svg.append("svg:path").attr("d", line);