how to set the domain and scale on an axis on a nvd3.js multiBarChart
Asked Answered
T

3

13

In d3.js you can set an x axis to use d3.time.scale() then set x.domain([start_date, end_date]) and it will 'fill in' any missing dates that aren't in the data with 0 values. I want to do the same with a nvd3.js mulitBarChart.

This code (can be pasted directly into http://nvd3.org/livecode/#codemirrorNav) shows a bar chart of totals by year, there are missing values for 2002 & 2003. I want to set the scale to be d3.time.scale() and then the domain to the first and last years of the dataset so the missing years are automatically added with 0 values. How do I do that?

nv.addGraph(function() {
    var chart = nv.models.multiBarChart();

    chart.xAxis
        .tickFormat(function(d){ return d3.time.format('%y')(new Date(d)); });

    chart.yAxis
        .tickFormat(d3.format(',f'));

    chart.reduceXTicks(false);
    chart.showControls(false);

    var data = [{
      'key': 'GB by year',
      'values': [
        {x: new Date().setFullYear('2001'), y: 0.12},
        {x: new Date().setFullYear('2004'), y: 0.03},
        {x: new Date().setFullYear('2005'), y: 0.53},
        {x: new Date().setFullYear('2006'), y: 0.43},
        {x: new Date().setFullYear('2007'), y: 5.5},
        {x: new Date().setFullYear('2008'), y: 9.9},
        {x: new Date().setFullYear('2009'), y: 26.85},
        {x: new Date().setFullYear('2010'), y: 0.03},
        {x: new Date().setFullYear('2011'), y: 0.12}
      ]
    }];        

    d3.select('#chart svg')
        .datum(data)
      .transition().duration(500).call(chart);

    nv.utils.windowResize(chart.update);

    return chart;
});
Tallent answered 22/1, 2013 at 14:41 Comment(1)
Since multiBarChart doesnt support the forceX function, you need to zero-fill your interpolated data.Simoniac
T
5

There is really no need to interpolate your values. You actually can modify the scale of most nvd3 charts, including multiBarCharts, although there is some extra work that needs to be done to make it work.

The basic thing you need to do is this:

var xScale = d3.time.scale();
chart.multibar.xScale(xScale);

Then that should just work! Except it doesn't, because the multiBarChart assumes that the xScale is d3.scale.ordinal(). So you will need to fake being that type by setting xScale.rangeBands and xScale.rangeBand:

xScale.rangeBands = xScale.range;
xScale.rangeBand = function() { return (1 - chart.groupSpacing()) * SOME_VALUE };

The problem now is getting SOME_VALUE. This needs to equal the width of an individual bar, which depends on two things: the width of the whole chart and the number of ticks there would be, including the zero values that are missing in the data.

Here's how nvd3 gets the available width internally:

var container = d3.select('#chart svg'),
    availableWidth = (chart.width() || parseInt(container.style('width')) || 960) - chart.margin().left - chart.margin().right;

However, if the window resizes, you will need to refresh this value:

nv.utils.windowResize(function() {
    availableWidth = (chart.width() || parseInt(container.style('width')) || 960) - chart.margin().left - chart.margin().right;
});

As for getting the number of ticks, this depends solely on your data. In your case, there will be 11 ticks: every year between 2001 and 2011. So we'll go with that. Therefore, the entire scale definition looks like this:

var container = d3.select('#chart svg'),
    availableWidth,
    numTicks = 11,
    xScale = d3.time.scale();

function updateAvailableWidth() {
    availableWidth = (chart.width() || parseInt(container.style('width')) || 960) - chart.margin().left - chart.margin().right;
}
updateAvailableWidth();
nv.utils.windowResize(updateAvailableWidth);

xScale.rangeBands = xScale.range;
xScale.rangeBand = function() { return (1 - chart.groupSpacing()) * availableWidth / numTicks; };

chart.multibar.xScale(xScale);

Finally, you need to set your xDomain manually. If you did this with the ordinal scale it had before, it would fail, but with a linear time scale it will work excellently:

chart.xDomain([new Date().setFullYear('2001'), new Date().setFullYear('2011')]);

Putting it all together, here is your example code (pasteable into http://nvd3.org/livecode/#codemirrorNav):

nv.addGraph(function() {
    var chart = nv.models.multiBarChart(),
        container = d3.select('#chart svg'),
        availableWidth,
        numTicks = 11,
        xScale = d3.time.scale();

    function updateAvailableWidth() {
        availableWidth = (chart.width() || parseInt(container.style('width')) || 960) - chart.margin().left - chart.margin().right;
    }
    updateAvailableWidth();
    nv.utils.windowResize(updateAvailableWidth);

    xScale.rangeBands = xScale.range;
    xScale.rangeBand = function() { return (1 - chart.groupSpacing()) * availableWidth / numTicks; };

    chart.multibar.xScale(xScale);
    chart.xDomain([new Date().setFullYear('2001'), new Date().setFullYear('2011')]);

    chart.xAxis
        .tickFormat(function(d){ return d3.time.format('%y')(new Date(d)); });

    chart.yAxis
        .tickFormat(d3.format(',f'));

    chart.reduceXTicks(false);
    chart.showControls(false);

    var data = [{
      'key': 'GB by year',
      'values': [
        {x: new Date().setFullYear('2001'), y: 0.12},
        {x: new Date().setFullYear('2004'), y: 0.03},
        {x: new Date().setFullYear('2005'), y: 0.53},
        {x: new Date().setFullYear('2006'), y: 0.43},
        {x: new Date().setFullYear('2007'), y: 5.5},
        {x: new Date().setFullYear('2008'), y: 9.9},
        {x: new Date().setFullYear('2009'), y: 26.85},
        {x: new Date().setFullYear('2010'), y: 0.03},
        {x: new Date().setFullYear('2011'), y: 0.12}
      ]
    }];        

    container.datum(data).transition().duration(500).call(chart);

    nv.utils.windowResize(chart.update);

    return chart;
});
Thorley answered 7/12, 2015 at 20:17 Comment(4)
I haven't used nvd3 for some time and haven't tested it, but this sounds like it would work so I'm marking it as the correct answer.Tallent
I was having a similar problem and finally found the solution a while after I first found this question so I thought I'd share ;) Although now I'm banging my head into the wall because I just figured out that the engine that gives me my data (elasticsearch) has options to interpolate zero values. All this trouble for nothing...Thorley
@Thorley would you check why it's not working for my use case please? #53576221Exigent
Hi @meustrus, can you check if there are similar solution for this other problem here?Radarman
S
12

Based on the above answer, you can do this with numeric x values (not Date objects) as well as a forced X range and specified tickValues.... for certain types of charts.

Bar charts do not seem to have the capability, however nvd3.lineCharts do what you'd like. The multiBarChart model does not allow the use of the forceX function to be applied (right now, ever?).

A solution to your problem would be to fill in the 0's or to use a sequential chart type (e.g. lineChart)

nv.addGraph(function() {
  var chart = nv.models.lineChart()
      .forceX(2001,2011);

  var tickMarks = [2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2011]

  chart.xAxis
      .tickValues(tickMarks)
      .tickFormat(function(d){ return d });

  chart.yAxis
      .tickFormat(d3.format(',f'));

  var data = [{
    'key': 'GB by year',
    'values': [
      {x: 2001, y: 0.12},
      {x: 2004, y: 0.03},
      {x: 2005, y: 0.53},
      {x: 2006, y: 0.43},
      {x: 2007, y: 5.5},
      {x: 2008, y: 9.9},
      {x: 2009, y: 26.85},
      {x: 2010, y: 0.03},
      {x: 2011, y: 0.12}
    ]
  }];        

  d3.select('#chart svg')
    .datum(data)
    .transition().duration(500).call(chart);

  nv.utils.windowResize(chart1.update);

  return chart;
});   
Simoniac answered 2/4, 2013 at 22:7 Comment(1)
Hi @Thomas or tmarthal, can you check if there are similar solution for this other problem here?Radarman
F
5

You can do this in 2 ways:

A) You either rewrite the axis component of nvd3 to use d3.time.scale() / make another axis component for this use case...

Or the easiest way:

B) You use the custom values for the axis. First of all you use the + operator ( +(date) ) to have the values in ms. There is a tickValues function in d3 that allows you to pass custom values for the ticks.. To force the X scale you have the forceX() method from the scatter (I assume you already know about this) and you write a simple function that takes custom values for ticks.... So if you force your scale to have values between Jan 1 2002 and Dec 31 2012 and then decide to have 4 ticks you can use either ticks directly or tickValues...

So it goes like this (add something similar to the multiBarChart.js file):

  lines.forceX(minValue, maxValue) //where minValue and maxValue are the values
  //converted to ms already after you did +(date)

  //then you just rewrite the ticks - if you want a custom number of ticks you can do it like this

  //numberOfTicks is a method I added to the axis component (axis.js) to give the number of ticks the user would like to have

  //x.domain() now contains the forced values instead of the values you initially used..
  var maxTicks = xAxis.numberOfTicks()-1, xMin = x.domain()[0], xMax = x.domain()[1], 
      xDiff = (xMax - xMin)/maxTicks, tickInterval = [];

  tickInterval[0] = xMin;

  for(i=1; i<maxTicks; i++){
    var current = xMin + i*xDiff;
    tickInterval[i] = current;
  }

  tickInterval[maxTicks] = xMax;

  //tickInterval already contains the values you want to pass to the tickValues function
  xAxis.tickValues(tickInterval);

Hope this helps... I know it's hack but it worked in my case :) And of course if you already formatted the date to be displayed as year you will get the values for the years when displaying the ticks :)

This is how I did it for lines. For multiBarChart you will need to add an extra step: you need to deal with the reduceTicks functionality (set it to false, delete that part of the code, do whatever you like with it...)

Frere answered 24/1, 2013 at 11:54 Comment(1)
Thanks for your answer but this isn't quite what I'm looking for. There's an issue on github about it github.com/novus/nvd3/issues/29Tallent
T
5

There is really no need to interpolate your values. You actually can modify the scale of most nvd3 charts, including multiBarCharts, although there is some extra work that needs to be done to make it work.

The basic thing you need to do is this:

var xScale = d3.time.scale();
chart.multibar.xScale(xScale);

Then that should just work! Except it doesn't, because the multiBarChart assumes that the xScale is d3.scale.ordinal(). So you will need to fake being that type by setting xScale.rangeBands and xScale.rangeBand:

xScale.rangeBands = xScale.range;
xScale.rangeBand = function() { return (1 - chart.groupSpacing()) * SOME_VALUE };

The problem now is getting SOME_VALUE. This needs to equal the width of an individual bar, which depends on two things: the width of the whole chart and the number of ticks there would be, including the zero values that are missing in the data.

Here's how nvd3 gets the available width internally:

var container = d3.select('#chart svg'),
    availableWidth = (chart.width() || parseInt(container.style('width')) || 960) - chart.margin().left - chart.margin().right;

However, if the window resizes, you will need to refresh this value:

nv.utils.windowResize(function() {
    availableWidth = (chart.width() || parseInt(container.style('width')) || 960) - chart.margin().left - chart.margin().right;
});

As for getting the number of ticks, this depends solely on your data. In your case, there will be 11 ticks: every year between 2001 and 2011. So we'll go with that. Therefore, the entire scale definition looks like this:

var container = d3.select('#chart svg'),
    availableWidth,
    numTicks = 11,
    xScale = d3.time.scale();

function updateAvailableWidth() {
    availableWidth = (chart.width() || parseInt(container.style('width')) || 960) - chart.margin().left - chart.margin().right;
}
updateAvailableWidth();
nv.utils.windowResize(updateAvailableWidth);

xScale.rangeBands = xScale.range;
xScale.rangeBand = function() { return (1 - chart.groupSpacing()) * availableWidth / numTicks; };

chart.multibar.xScale(xScale);

Finally, you need to set your xDomain manually. If you did this with the ordinal scale it had before, it would fail, but with a linear time scale it will work excellently:

chart.xDomain([new Date().setFullYear('2001'), new Date().setFullYear('2011')]);

Putting it all together, here is your example code (pasteable into http://nvd3.org/livecode/#codemirrorNav):

nv.addGraph(function() {
    var chart = nv.models.multiBarChart(),
        container = d3.select('#chart svg'),
        availableWidth,
        numTicks = 11,
        xScale = d3.time.scale();

    function updateAvailableWidth() {
        availableWidth = (chart.width() || parseInt(container.style('width')) || 960) - chart.margin().left - chart.margin().right;
    }
    updateAvailableWidth();
    nv.utils.windowResize(updateAvailableWidth);

    xScale.rangeBands = xScale.range;
    xScale.rangeBand = function() { return (1 - chart.groupSpacing()) * availableWidth / numTicks; };

    chart.multibar.xScale(xScale);
    chart.xDomain([new Date().setFullYear('2001'), new Date().setFullYear('2011')]);

    chart.xAxis
        .tickFormat(function(d){ return d3.time.format('%y')(new Date(d)); });

    chart.yAxis
        .tickFormat(d3.format(',f'));

    chart.reduceXTicks(false);
    chart.showControls(false);

    var data = [{
      'key': 'GB by year',
      'values': [
        {x: new Date().setFullYear('2001'), y: 0.12},
        {x: new Date().setFullYear('2004'), y: 0.03},
        {x: new Date().setFullYear('2005'), y: 0.53},
        {x: new Date().setFullYear('2006'), y: 0.43},
        {x: new Date().setFullYear('2007'), y: 5.5},
        {x: new Date().setFullYear('2008'), y: 9.9},
        {x: new Date().setFullYear('2009'), y: 26.85},
        {x: new Date().setFullYear('2010'), y: 0.03},
        {x: new Date().setFullYear('2011'), y: 0.12}
      ]
    }];        

    container.datum(data).transition().duration(500).call(chart);

    nv.utils.windowResize(chart.update);

    return chart;
});
Thorley answered 7/12, 2015 at 20:17 Comment(4)
I haven't used nvd3 for some time and haven't tested it, but this sounds like it would work so I'm marking it as the correct answer.Tallent
I was having a similar problem and finally found the solution a while after I first found this question so I thought I'd share ;) Although now I'm banging my head into the wall because I just figured out that the engine that gives me my data (elasticsearch) has options to interpolate zero values. All this trouble for nothing...Thorley
@Thorley would you check why it's not working for my use case please? #53576221Exigent
Hi @meustrus, can you check if there are similar solution for this other problem here?Radarman

© 2022 - 2024 — McMap. All rights reserved.