3

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

Anshad Vattapoyil
  • 23,145
  • 18
  • 84
  • 132
  • Just like [your other question](http://stackoverflow.com/q/43174907/5768908), this would be so easy in D3 v4... You could just use `d3.ticks` to set the array with only 5 values, including the modified last tick, and pass that array to `tickValues`. I know it's a lot of work, but move your codes to v4, you're gonna see that things will be easier! – Gerardo Furtado Apr 17 '17 at 12:29
  • Thanks @GerardoFurtado I will move all my charts to d3 v4. – Anshad Vattapoyil Apr 17 '17 at 16:21
  • @GerardoFurtado For quick fix, is there any possibility to fix this by changing `lastTick` calculation steps? – Anshad Vattapoyil Apr 18 '17 at 17:47

1 Answers1

2

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/

Community
  • 1
  • 1
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Thanks, I have started migrating all my charts to v4. But need to fix this right away that's why requested v3 solution. Here in the fiddle I can see the tick numbers reflecting twice (you can see repeated tick value with little bigger font size). Is it something because of horizontal grid line approach? – Anshad Vattapoyil Apr 23 '17 at 13:39
  • Yes, I notice that... I thought that was on purpose. So, it was not? – Gerardo Furtado Apr 23 '17 at 13:40
  • No, I will hide it by applying CSS selector. Let me know if you have any other better solution. – Anshad Vattapoyil Apr 23 '17 at 13:41
  • Here is a quick fix: https://jsfiddle.net/m08wk26u/. But I advise you create your horizontal grid using another approach. For instance, have a look at my answer here: http://stackoverflow.com/a/41153514/5768908. In this answer I don't call the axis generator twice (which would create two sets of ticks, as in your code), but I simply get the ticks' positions and append horizontal lines using those positions. – Gerardo Furtado Apr 23 '17 at 13:42
  • I have a doubt here,Can't we get the tick numbers in equal distance? Another point can't we pass the tallest bar count instead of passing domain as end value? `0,33 - 8, 16, 24, 33` here it gives equal sum but not in this `0,66 - 16, 33, 49, 66` – Anshad Vattapoyil Apr 24 '17 at 07:12
  • I'm not understanding, the distances **are** equal. Besides that, you can pass anything you want. – Gerardo Furtado Apr 24 '17 at 07:17
  • I am little week in maths so just want to clear. The second set starts with 16 so the next number should be 32 right? Here between 16 and 33 the difference is 17, but again with 33 and 49 it comes 16. – Anshad Vattapoyil Apr 24 '17 at 07:21
  • The function I wrote in the answer is rounding. To get the exact values, remove the double NOT (the two ~). – Gerardo Furtado Apr 24 '17 at 07:31
  • Thanks for the detailed clarification :) – Anshad Vattapoyil Apr 24 '17 at 07:48