[Chartjs]-Chart.js v2 is there a way to draw bar chart horizontally?

15πŸ‘

βœ…

This is part of Chart.js 2.1 now! The chart type is horizontalBar (with a lower case h).

var config = {
    type: 'horizontalBar',
    data: {
        ...

Fiddle – http://jsfiddle.net/gpxjohup/


Original Answer

Preview

enter image description here


Script

Most of this code is a copy of the code for bar from the Chart.js library with x / y and width / height swapped. Thanks to @ZachPanzarino for helping with the axis labels!

Chart.defaults.HorizontalBar = {
    hover: {
        mode: "single"
    },
    scales: {
        yAxes: [{
            position: 'left',
            type: "category",
            categoryPercentage: 0.8,
            barPercentage: 1,
            gridLines: { offsetGridLines: true }
        }],
        xAxes: [{
            ticks: {
                beginAtZero: true
            },
            position: 'bottom',
            type: "linear",
        }],
    },
};

Chart.controllers.HorizontalBar = Chart.controllers.bar.extend({
    updateElement: function updateElement(rectangle, index, reset, numBars) {

        var xScale = this.getScaleForId(this.getDataset().xAxisID);
        var yScale = this.getScaleForId(this.getDataset().yAxisID);

        var xScalePoint;

        if (xScale.min < 0 && xScale.max < 0) {
            xScalePoint = xScale.getPixelForValue(xScale.max);
        } else if (xScale.min > 0 && xScale.max > 0) {
            xScalePoint = xScale.getPixelForValue(xScale.min);
        } else {
            xScalePoint = xScale.getPixelForValue(0);
        }

        Chart.helpers.extend(rectangle, {
            _chart: this.chart.chart,
            _xScale: xScale,
            _yScale: yScale,
            _datasetIndex: this.index,
            _index: index,

            _model: {
                x: reset ? xScalePoint : this.calculateBarX(index, this.index),
                y: this.calculateBarY(index, this.index),
                label: this.chart.data.labels[index],
                datasetLabel: this.getDataset().label,
                base: this.calculateBarBase(this.index, index),
                height: this.calculateBarHeight(numBars),
                backgroundColor: rectangle.custom && rectangle.custom.backgroundColor ? rectangle.custom.backgroundColor : Chart.helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.rectangle.backgroundColor),
                borderColor: rectangle.custom && rectangle.custom.borderColor ? rectangle.custom.borderColor : Chart.helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.rectangle.borderColor),
                borderWidth: rectangle.custom && rectangle.custom.borderWidth ? rectangle.custom.borderWidth : Chart.helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.rectangle.borderWidth),
            },

            // override the draw and inRange functions because the one in the library needs width (we only have height)

            draw: function() {
                var ctx = this._chart.ctx;
                ctx.fillStyle = this._view.backgroundColor;
                ctx.fillRect(this._view.base, this._view.y - this._view.height / 2, this._view.x - this._view.base, this._view.height);

                ctx.strokeStyle = this._view.borderColor;
                ctx.strokeWidth = this._view.borderWidth;
                ctx.strokeRect(this._view.base, this._view.y - this._view.height / 2, this._view.x - this._view.base, this._view.height);
            },

            inRange: function (mouseX, mouseY) {
                var vm = this._view;
                var inRange = false;

                if (vm) {
                    if (vm.x < vm.base) {
                        inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.x && mouseX <= vm.base);
                    } else {
                        inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.base && mouseX <= vm.x);
                    }
                }

                return inRange;
            }
        });

        rectangle.pivot();

        // the animation progresses _view values from their current value to the _model value
        rectangle._view.x = rectangle._model.base;
    },

    calculateBarBase: function (datasetIndex, index) {
        var xScale = this.getScaleForId(this.getDataset().xAxisID);
        var yScale = this.getScaleForId(this.getDataset().yAxisID);

        var base = 0;

        if (xScale.options.stacked) {
            var value = this.chart.data.datasets[datasetIndex].data[index];

            if (value < 0) {
                for (var i = 0; i < datasetIndex; i++) {
                    var negDS = this.chart.data.datasets[i];
                    if (Chart.helpers.isDatasetVisible(negDS) && negDS.xAxisID === xScale.id) {
                        base += negDS.data[index] < 0 ? negDS.data[index] : 0;
                    }
                }
            } else {
                for (var j = 0; j < datasetIndex; j++) {
                    var posDS = this.chart.data.datasets[j];
                    if (Chart.helpers.isDatasetVisible(posDS) && posDS.xAxisID === xScale.id) {
                        base += posDS.data[index] > 0 ? posDS.data[index] : 0;
                    }
                }
            }

            return xScale.getPixelForValue(base);
        }

        base = xScale.getPixelForValue(xScale.min);

        if (xScale.beginAtZero || ((xScale.min <= 0 && xScale.max >= 0) || (xScale.min >= 0 && xScale.max <= 0))) {
            base = xScale.getPixelForValue(0, 0);
        } else if (xScale.min < 0 && xScale.max < 0) {
            base = xScale.getPixelForValue(xScale.max);
        }

        return base;
    },

    getRuler: function () {
        var xScale = this.getScaleForId(this.getDataset().xAxisID);
        var yScale = this.getScaleForId(this.getDataset().yAxisID);
        var datasetCount = this.getBarCount();

        var tickHeight = (function () {
            var min = yScale.getPixelForTick(1) - yScale.getPixelForTick(0);
            for (var i = 2; i < this.getDataset().data.length; i++) {
                min = Math.min(yScale.getPixelForTick(i) - yScale.getPixelForTick(i - 1), min);
            }
            return min;
        }).call(this);
        var categoryHeight = tickHeight * yScale.options.categoryPercentage;
        var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2;
        var fullBarHeight = categoryHeight / datasetCount;
        var barHeight = fullBarHeight * yScale.options.barPercentage;
        var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage);

        return {
            datasetCount: datasetCount,
            tickHeight: tickHeight,
            categoryHeight: categoryHeight,
            categorySpacing: categorySpacing,
            fullBarHeight: fullBarHeight,
            barHeight: barHeight,
            barSpacing: barSpacing,
        };
    },

    calculateBarHeight: function () {
        var yScale = this.getScaleForId(this.getDataset().yAxisID);
        var ruler = this.getRuler();

        if (yScale.options.stacked) {
            return ruler.categoryHeight;
        }

        return ruler.barHeight;
    },

    calculateBarY: function (index, datasetIndex) {
        var yScale = this.getScaleForId(this.getDataset().yAxisID);
        var xScale = this.getScaleForId(this.getDataset().xAxisID);
        var barIndex = this.getBarIndex(datasetIndex);

        var ruler = this.getRuler();
        var leftTick = yScale.getPixelForValue(null, index, datasetIndex, this.chart.isCombo);
        leftTick -= this.chart.isCombo ? (ruler.tickHeight / 2) : 0;

        if (yScale.options.stacked) {
            return leftTick + (ruler.categoryHeight / 2) + ruler.categorySpacing;
        }

        return leftTick +
            (ruler.barHeight / 2) +
            ruler.categorySpacing +
            (ruler.barHeight * barIndex) +
            (ruler.barSpacing / 2) +
            (ruler.barSpacing * barIndex);
    },

    calculateBarX: function (index, datasetIndex) {
        var xScale = this.getScaleForId(this.getDataset().xAxisID);
        var yScale = this.getScaleForId(this.getDataset().yAxisID);

        var value = this.getDataset().data[index];

        if (xScale.options.stacked) {
            var sumPos = 0,
                sumNeg = 0;

            for (var i = 0; i < datasetIndex; i++) {
                var ds = this.chart.data.datasets[i];
                if (Chart.helpers.isDatasetVisible(ds)) {
                    if (ds.data[index] < 0) {
                        sumNeg += ds.data[index] || 0;
                    } else {
                        sumPos += ds.data[index] || 0;
                    }
                }
            }

            if (value < 0) {
                return xScale.getPixelForValue(sumNeg + value);
            } else {
                return xScale.getPixelForValue(sumPos + value);
            }

            return xScale.getPixelForValue(value);
        }

        return xScale.getPixelForValue(value);
    }
});

and then

var config = {
  type: 'HorizontalBar',
  ...

Fiddle – http://jsfiddle.net/0y8mnma1/

0πŸ‘

It is a temporary hack but for now you could rotate the entire graph 90 degrees making it horizontal.

Demo

#my-chart {
  width: 100%;
  transform: rotate(-90deg);
}

.column {
  width: 100%;
  height: 50px;
}

.column-1 {
  background: orange;
}

.column-2 {
  background: green;
}
<div id="my-chart">
  <div class="column column-1">Column 1</div>
  <div class="column column-2">Column 2</div>
</div>

0πŸ‘

Am not sure if it will work, but you can try. I am working on Charts.js now and found some limitations and stumbled upon this library that is a custom version of Charts.js

https://github.com/FVANCOP/ChartNew.js

You dont need to change any old format you were using to pull data.
Just replace your scripts to use :

<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
    <script src="/pathTo/ChartNew.js"></script>
    <script src="/pathTo/shapesInChart.js"></script>

In shapesInChart.js files you will see customized shapes and cases. It has one for HorizontalBar and HorizontalStackedBar

Let me know if it works for you πŸ™‚

0πŸ‘

I edited @potatopeelings original example a bit, so now you can have offsetGridLines set to your liking and working tooltip!

Chart.defaults.HorizontalBar = {
  hover: {
    mode: "label"
  },
  tooltips: {
    mode: "label",
    callbacks: {
        title: function(tooltipItems, data) {
            label = [];
            for (index in tooltipItems) {          
               if (label.indexOf(tooltipItems[index].yLabel) < 0) label.push(tooltipItems[index].yLabel)
             }
             return label.join(", ");
        },
        label: function(tooltipItem, data) {
            return data.datasets[tooltipItem.datasetIndex].label + ': ' + tooltipItem.xLabel;
        }
    }
  },
  scales: {
    yAxes: [{
      position: 'left',
      type: "category",
      categoryPercentage: 0.8,
      barPercentage: 1,
      gridLines: {
            offsetGridLines: true
      }
    }],
    xAxes: [{
      ticks: {
        beginAtZero: true
      },
      position: 'bottom',
      type: "linear",
    }],
  },
};

Chart.controllers.HorizontalBar = Chart.controllers.bar.extend({
  initialize: function(chart, datasetIndex) {
    // insert dummy labels for proper vertical positioning
    if (!datasetIndex && !chart.config.options.scales.yAxes[0].gridLines.offsetGridLines) {
      chart.config.data.labels.unshift('')
      chart.config.data.labels.push('')
    }

    Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex);

    this.getDataset().bar = true;
  },
  updateElement: function updateElement(rectangle, index, reset, numBars) {

    var xScale = this.getScaleForId(this.getDataset().xAxisID);
    var yScale = this.getScaleForId(this.getDataset().yAxisID);

    var xScalePoint;

    if (xScale.min < 0 && xScale.max < 0) {
      xScalePoint = xScale.getPixelForValue(xScale.max);
    } else if (xScale.min > 0 && xScale.max > 0) {
      xScalePoint = xScale.getPixelForValue(xScale.min);
    } else {
      xScalePoint = xScale.getPixelForValue(0);
    }

    Chart.helpers.extend(rectangle, {
      _chart: this.chart.chart,
      _xScale: xScale,
      _yScale: yScale,
      _datasetIndex: this.index,
      _index: index,

      _model: {
        x: reset ? xScalePoint : this.calculateBarX(index, this.index),
        y: this.calculateBarY(index, this.index),
        label: this.chart.data.labels[index],
        datasetLabel: this.getDataset().label,
        base: this.calculateBarBase(this.index, index),
        height: this.calculateBarHeight(numBars),
        backgroundColor: rectangle.custom && rectangle.custom.backgroundColor ? rectangle.custom.backgroundColor : Chart.helpers.getValueAtIndexOrDefault(this.getDataset().backgroundColor, index, this.chart.options.elements.rectangle.backgroundColor),
        borderColor: rectangle.custom && rectangle.custom.borderColor ? rectangle.custom.borderColor : Chart.helpers.getValueAtIndexOrDefault(this.getDataset().borderColor, index, this.chart.options.elements.rectangle.borderColor),
        borderWidth: rectangle.custom && rectangle.custom.borderWidth ? rectangle.custom.borderWidth : Chart.helpers.getValueAtIndexOrDefault(this.getDataset().borderWidth, index, this.chart.options.elements.rectangle.borderWidth),
      },

      // override the inRange function because the one in the library needs width (we have only height)
      inRange: function(mouseX, mouseY) {
        var vm = this._view;
        var inRange = false;

        if (vm) {
          if (vm.x < vm.base) {
            inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.x && mouseX <= vm.base);
          } else {
            inRange = (mouseY >= vm.y - vm.height / 2 && mouseY <= vm.y + vm.height / 2) && (mouseX >= vm.base && mouseX <= vm.x);
          }
        }

        return inRange;
      }
    });

    rectangle.pivot();

    // the animation progresses _view values from their current value to the _model value
    rectangle._view.x = rectangle._model.base;
  },

  calculateBarBase: function(datasetIndex, index) {
    var xScale = this.getScaleForId(this.getDataset().xAxisID);
    var yScale = this.getScaleForId(this.getDataset().yAxisID);

    var base = 0;

    if (xScale.options.stacked) {
      var value = this.chart.data.datasets[datasetIndex].data[index];

      if (value < 0) {
        for (var i = 0; i < datasetIndex; i++) {
          var negDS = this.chart.data.datasets[i];
          if (Chart.helpers.isDatasetVisible(negDS) && negDS.xAxisID === xScale.id) {
            base += negDS.data[index] < 0 ? negDS.data[index] : 0;
          }
        }
      } else {
        for (var j = 0; j < datasetIndex; j++) {
          var posDS = this.chart.data.datasets[j];
          if (Chart.helpers.isDatasetVisible(posDS) && posDS.xAxisID === xScale.id) {
            base += posDS.data[index] > 0 ? posDS.data[index] : 0;
          }
        }
      }

      return xScale.getPixelForValue(base);
    }

    base = xScale.getPixelForValue(xScale.min);

    if (xScale.beginAtZero || ((xScale.min <= 0 && xScale.max >= 0) || (xScale.min >= 0 && xScale.max <= 0))) {
      base = xScale.getPixelForValue(0, 0);
    } else if (xScale.min < 0 && xScale.max < 0) {
      base = xScale.getPixelForValue(xScale.max);
    }

    return base;
  },

  draw: function(ease) {
    var easingDecimal = ease || 1;
    var ctx = this.chart.chart.ctx;
    ctx.save();
    Chart.helpers.each(this.getDataset().metaData, function(rectangle, index) {
      ctx.fillStyle = rectangle._view.backgroundColor;
      ctx.fillRect(rectangle._view.base, rectangle._view.y - rectangle._view.height / 2, rectangle._view.x - rectangle._view.base, rectangle._view.height);

      ctx.strokeStyle = rectangle._view.borderColor;
      ctx.strokeWidth = rectangle._view.borderWidth;
      ctx.strokeRect(rectangle._view.base, rectangle._view.y - rectangle._view.height / 2, rectangle._view.x - rectangle._view.base, rectangle._view.height);

      rectangle.transition(easingDecimal).draw();
    }, this);
    ctx.restore();
  },

  getRuler: function() {
    var xScale = this.getScaleForId(this.getDataset().xAxisID);
    var yScale = this.getScaleForId(this.getDataset().yAxisID);
    var datasetCount = this.getBarCount();

    var tickHeight = (function() {
      var min = yScale.getPixelForTick(1) - yScale.getPixelForTick(0);
      for (var i = 2; i < this.getDataset().data.length; i++) {
        min = Math.min(yScale.getPixelForTick(i) - yScale.getPixelForTick(i - 1), min);
      }
      return min;
    }).call(this);
    var categoryHeight = tickHeight * yScale.options.categoryPercentage;
    var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2;
    var fullBarHeight = categoryHeight / datasetCount;
    var barHeight = fullBarHeight * yScale.options.barPercentage;
    var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage);

    return {
      datasetCount: datasetCount,
      tickHeight: tickHeight,
      categoryHeight: categoryHeight,
      categorySpacing: categorySpacing,
      fullBarHeight: fullBarHeight,
      barHeight: barHeight,
      barSpacing: barSpacing,
    };
  },

  calculateBarHeight: function() {
    var yScale = this.getScaleForId(this.getDataset().yAxisID);
    var ruler = this.getRuler();

    console.debug(yScale.options.stacked);
    if (yScale.options.stacked) {
      return ruler.categoryHeight;
    }

    return ruler.barHeight;
  },

  calculateBarY: function(index, datasetIndex) {
    var yScale = this.getScaleForId(this.getDataset().yAxisID);
    var xScale = this.getScaleForId(this.getDataset().xAxisID);
    var barIndex = this.getBarIndex(datasetIndex);

    var ruler = this.getRuler();
    var leftTick = yScale.getPixelForValue(null, index, datasetIndex, this.chart.isCombo);
    leftTick -= this.chart.isCombo ? (ruler.tickHeight / 2) : 0;
    leftTick = (!yScale.options.gridLines.offsetGridLines) ? leftTick + (ruler.tickHeight / 2) : leftTick;

    if (yScale.options.stacked) {
      return leftTick + (ruler.categoryHeight / 2) + ruler.categorySpacing;
    }

    return leftTick + 1 +
      (ruler.barHeight / 2) +
      ruler.categorySpacing +
      (ruler.barHeight * barIndex) +
      (ruler.barSpacing / 2) +
      (ruler.barSpacing * barIndex);
  },

  calculateBarX: function(index, datasetIndex) {
    var xScale = this.getScaleForId(this.getDataset().xAxisID);
    var yScale = this.getScaleForId(this.getDataset().yAxisID);

    var value = this.getDataset().data[index];

    if (xScale.options.stacked) {
      var sumPos = 0,
          sumNeg = 0;

      for (var i = 0; i < datasetIndex; i++) {
        var ds = this.chart.data.datasets[i];
        if (Chart.helpers.isDatasetVisible(ds)) {
          if (ds.data[index] < 0) {
            sumNeg += ds.data[index] || 0;
          } else {
            sumPos += ds.data[index] || 0;
          }
        }
      }

      if (value < 0) {
        return xScale.getPixelForValue(sumNeg + value);
      } else {
        return xScale.getPixelForValue(sumPos + value);
      }

      return xScale.getPixelForValue(value);
    }
});

jsfiddle example

cheers
c_bb

Leave a comment