Anshad Vattapoyil
Anshad Vattapoyil

Reputation: 23463

Limit number of Y axis ticks by keeping a top tick above the bar in d3

In my d3 bar chart, I should have a top Y-axis tick (with horizontal grid line) above the tallest bar if it goes above the last tick. This is achieved by calculating the last tick, then applied using tickValues().

Also there should be maximum 5 ticks and grid lines including the x-axis domain (0). I have tried this using ticks() but it is not working with tickValues(). Any solution for this?

// container size
var margin = {top: 10, right: 10, bottom: 30, left: 30},
width = 400,
height = 300;

var data = [
{"month":"DEC","setup":{"count":26,"id":1,"label":"Set Up","year":"2016","graphType":"setup"}},
{"month":"JAN","setup":{"count":30,"id":1,"label":"Set Up","year":"2017","graphType":"setup"}},
{"month":"FEB","setup":{"count":30,"id":1,"label":"Set Up","year":"2017","graphType":"setup"}}];

var name = 'dashboard';

// x scale
var xScale = d3.scale.ordinal()
.rangeRoundBands([0, width], 0.2);

// set x and y scales
xScale.domain(data.map(function(d) { return d.month; }));

// x axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient('bottom')
.outerTickSize(0);

var yScale = d3.scale.linear()
.domain([0, d3.max(data, function(d) {  
    return d.setup.count;
})])
.range([height, 0]);

var ticks = yScale.ticks(),
lastTick = ticks[ticks.length-1];    
var newLastTick = lastTick + (ticks[1] - ticks[0]);  
if (lastTick < yScale.domain()[1]){
    ticks.push(lastTick + (ticks[1] - ticks[0]));
}

// adjust domain for further value
yScale.domain([yScale.domain()[0], newLastTick]);

// y axis
var yAxis = d3.svg.axis()
.scale(yScale)
.orient('left')
.tickSize(-width, 0, 0) 
.tickFormat(d3.format('d'))
.tickValues(ticks);


// create svg container
var svg = d3.select('#chart')
.append('svg')
.attr('class','d3-setup-barchart')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
//.on('mouseout', tip.hide);        

// apply tooltip
//svg.call(tip);

// Horizontal grid (y axis gridline)
svg.append('g')         
.attr('class', 'grid horizontal')
.call(d3.svg.axis()
      .scale(yScale)
      .orient('left')
      .tickSize(-width, 0, 0) 
      .tickFormat('')
      .tickValues(ticks)
      );

// create bars
var bars = svg.selectAll('.bar')
.data(data)
.enter()
.append('g');

bars.append('rect')
.attr('class', function(d,i) {
    return 'bar';
})
.attr('id', function(d, i) {
    return name+'-bar-'+i;
})
.attr('x', function(d) { return xScale(d.month); })
.attr('width', xScale.rangeBand())
.attr('y', function(d) { return yScale(d.setup.count); })
.attr('height', function(d) { return height - yScale(d.setup.count); })
.on('click', function(d, i) {
    d3.select(this.nextSibling)
    .classed('label-text selected', true);
    d3.select(this)
    .classed('bar selected', true);  
    d3.select('#'+name+'-axis-text-'+i)
    .classed('axis-text selected', true);
});
//.on('mouseover', tip.show)
//.on('mouseout', tip.hide);

// apply text at the top
bars.append('text')
.attr('class',function(d,i) {
    return 'label-text';
})
.attr('x', function(d) { return xScale(d.month) + (xScale.rangeBand()/2) - 10; })
.attr('y', function(d) { return yScale(d.setup.count) + 2 ; })
.attr('transform', function() { return 'translate(10, -10)'; })
.text(function(d) { return d.setup.count; });

// draw x axis
svg.append('g')
.attr('id', name+'-x-axis')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis);

// apply class & id to x-axis texts
d3.select('#'+name+'-x-axis')
.selectAll('text')
.attr('class', function(d,i) {
    return 'axis-text';
})
.attr('id', function(d,i) { return name+'-axis-text-' + i; });

// draw y axis
svg.append('g')
.attr('class', 'y axis')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('y', 6)
.attr('dy', '.71em')
.style('text-anchor', 'end');

// remove 0 in y axis
svg.select('.y')
.selectAll('.tick')
.filter(function (d) { 
    return d === 0 || d % 1 !== 0;     
}).remove();

svg
.select('.horizontal')
.selectAll('.tick')
.filter(function (d) { 
    return d === 0 || d % 1 !== 0;     
}).remove();

JSFiddle

Upvotes: 3

Views: 2026

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

As I told you in my comment, this would be very easy if you were using D3 v4.x: you could simply set the tickValues using d3.ticks or d3.range.

But there is a solution if you want to stick with D3 v3.

The default approach in your case would be setting the number of ticks using scale.ticks. However, as the API says,

If count is a number, then approximately count ticks will be returned. If count is not specified, it defaults to 10. The specified count is only a hint; the scale may return more or fewer values depending on the input domain. (emphasis mine)

So, you can't use scale.ticks here to set a fixed number of 5 ticks.

My solution, therefore, involves creating your own function to calculate the ticks. It's not complicated at all. This is it:

function createTicks(start, stop, count) {
    var difference = stop - start;
    var steps = difference / (count - 1);
    var arr = [start];
    for (var i = 1; i < count; i++) {
        arr.push(~~(start + steps * i))
    }
    return arr;
}

This function takes three arguments: the first value (start), the last value (stop) and the number of ticks (count). I'm using the double NOT because, for whatever reason, you are filtering out non-integer values.

So, we just need to set the maximum tick in the yScale domain itself. For instance, making the maximum tick 10% bigger than the maximum value:

var yScale = d3.scale.linear()
    .domain([0, d3.max(data, function(d) {
        return d.setup.count;
    }) * 1.1])
    //    ^----- 10% increase
    .range([height, 0]);

(if you want, you can keep your math to get the new last tick, I'm just showing a different way to set a maximum value for the domain which is different from the maximum value in the data)

Then, we define the ticks for the y axis:

var axisTicks = createTicks(yScale.domain()[0], yScale.domain()[1], 5);

Using our customised function with your domain, it returns this array:

[0, 8, 16, 24, 33]

Then, it's just a matter of using that array in axis.tickValues.

Here is your updated fiddle: https://jsfiddle.net/7ktzpnno/

And here the same code in the Stack snippet:

// container size
var margin = {
    top: 10,
    right: 10,
    bottom: 30,
    left: 30
  },
  width = 400,
  height = 300;

var data = [{
  "month": "DEC",
  "setup": {
    "count": 26,
    "id": 1,
    "label": "Set Up",
    "year": "2016",
    "graphType": "setup"
  }
}, {
  "month": "JAN",
  "setup": {
    "count": 30,
    "id": 1,
    "label": "Set Up",
    "year": "2017",
    "graphType": "setup"
  }
}, {
  "month": "FEB",
  "setup": {
    "count": 30,
    "id": 1,
    "label": "Set Up",
    "year": "2017",
    "graphType": "setup"
  }
}];

var name = 'dashboard';

// x scale
var xScale = d3.scale.ordinal()
  .rangeRoundBands([0, width], 0.2);

// set x and y scales
xScale.domain(data.map(function(d) {
  return d.month;
}));

// x axis
var xAxis = d3.svg.axis()
  .scale(xScale)
  .orient('bottom')
  .outerTickSize(0);

var yScale = d3.scale.linear()
  .domain([0, d3.max(data, function(d) {
    return d.setup.count;
  }) * 1.1])
  .range([height, 0]);

var axisTicks = createTicks(yScale.domain()[0], yScale.domain()[1], 5);

function createTicks(start, stop, count) {
  var difference = stop - start;
  var steps = difference / (count - 1);
  var arr = [start];
  for (var i = 1; i < count; i++) {
    arr.push(~~(start + steps * i))
  }
  return arr;
}

// y axis
var yAxis = d3.svg.axis()
  .scale(yScale)
  .orient('left')
  .tickSize(-width, 0, 0)
  .tickValues(axisTicks);

// tooltip
// var tip = d3.tip()
// .attr('class', 'd3-tip')
// .offset([-10, 0])
// .html(function(d) {
// 	return '<span class="tooltip-line">'+d.patientSetup.label+': '+
// 	d.patientSetup.count + '</span><span>'+d.patientNotSetup.label+': '+
// 	d.patientNotSetup.count + '</span>';
// });

// create svg container
var svg = d3.select('#chart')
  .append('svg')
  .attr('class', 'd3-setup-barchart')
  .attr('width', width + margin.left + margin.right)
  .attr('height', height + margin.top + margin.bottom)
  .append('g')
  .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
//.on('mouseout', tip.hide);        

// apply tooltip
//svg.call(tip);

// Horizontal grid (y axis gridline)
svg.append('g')
  .attr('class', 'grid horizontal')
  .call(d3.svg.axis()
    .scale(yScale)
    .orient('left')
    .tickSize(-width, 0, 0)
    .tickValues(axisTicks)
  );

// create bars
var bars = svg.selectAll('.bar')
  .data(data)
  .enter()
  .append('g');

bars.append('rect')
  .attr('class', function(d, i) {
    return 'bar';
  })
  .attr('id', function(d, i) {
    return name + '-bar-' + i;
  })
  .attr('x', function(d) {
    return xScale(d.month);
  })
  .attr('width', xScale.rangeBand())
  .attr('y', function(d) {
    return yScale(d.setup.count);
  })
  .attr('height', function(d) {
    return height - yScale(d.setup.count);
  })
  .on('click', function(d, i) {
    d3.select(this.nextSibling)
      .classed('label-text selected', true);
    d3.select(this)
      .classed('bar selected', true);
    d3.select('#' + name + '-axis-text-' + i)
      .classed('axis-text selected', true);
  });
//.on('mouseover', tip.show)
//.on('mouseout', tip.hide);

// apply text at the top
bars.append('text')
  .attr('class', function(d, i) {
    return 'label-text';
  })
  .attr('x', function(d) {
    return xScale(d.month) + (xScale.rangeBand() / 2) - 10;
  })
  .attr('y', function(d) {
    return yScale(d.setup.count) + 2;
  })
  .attr('transform', function() {
    return 'translate(10, -10)';
  })
  .text(function(d) {
    return d.setup.count;
  });

// draw x axis
svg.append('g')
  .attr('id', name + '-x-axis')
  .attr('class', 'x axis')
  .attr('transform', 'translate(0,' + height + ')')
  .call(xAxis);

// apply class & id to x-axis texts
d3.select('#' + name + '-x-axis')
  .selectAll('text')
  .attr('class', function(d, i) {
    return 'axis-text';
  })
  .attr('id', function(d, i) {
    return name + '-axis-text-' + i;
  });

// draw y axis
svg.append('g')
  .attr('class', 'y axis')
  .call(yAxis)
  .append('text')
  .attr('transform', 'rotate(-90)')
  .attr('y', 6)
  .attr('dy', '.71em')
  .style('text-anchor', 'end');

// remove 0 in y axis
svg.select('.y')
  .selectAll('.tick')
  .filter(function(d) {
    return d === 0 || d % 1 !== 0;
  }).remove();

svg
  .select('.horizontal')
  .selectAll('.tick')
  .filter(function(d) {
    return d === 0 || d % 1 !== 0;
  }).remove();
.d3-setup-barchart {
	background-color: #666666;
}

.d3-setup-barchart .axis path {
	fill: none;
	stroke: #000;
}

.d3-setup-barchart .bar {
	fill: #ccc;
}

.d3-setup-barchart .bar:hover {
	fill: orange;
	cursor: pointer;
}

.d3-setup-barchart .bar.selected {
	fill: orange;
	stroke: #fff;
	stroke-width: 2;
}

.d3-setup-barchart .label-text {
	text-anchor: middle;
	font-size: 12px;
	font-weight: bold;
	fill: orange;
	opacity: 0;
}

.d3-setup-barchart .label-text.selected {
	opacity: 1;
}

.d3-setup-barchart .axis text {
	fill: rgba(255, 255, 255, 0.6);
	font-size: 9px;
}

.d3-setup-barchart .axis-text.selected {
	fill: orange;
}

.d3-setup-barchart .y.axis path {
	display: none;
}

.d3-setup-barchart .y.axis text {
	font-size: 6px;
}

.d3-setup-barchart .x.axis path {
	fill: none;
	stroke: #353C41;
}

.d3-setup-barchart .grid .tick {
	stroke: #fff;
	opacity: .18 !important;
	stroke-width: 0;
}

.d3-setup-barchart .grid .tick line {
	stroke-width: .5 !important;
}

.d3-setup-barchart .grid path {
	stroke-width: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<div id="chart"></div>

PS: In your question, you said "there should be maximum 5 ticks and grid lines including the x-axis domain (0).". However, in your code, you are deliberately removing the 0 tick. If you want to see the 0 tick in the y axis, remove that block: https://jsfiddle.net/jz0q547u/

Upvotes: 2

Related Questions