Chart.js/src/controllers/controller.bar.js
Evert Timberg 36ccf40946 Fix for stacked bar charts with log axes (#4010)
* Undo fix for #3585 since it has broken the stacked bar charts when the axis has a user defined minimum value.

* When a value of 0 is requested for a vertical logarithmic axis, return the bottom point
2017-03-20 20:38:15 -04:00

559 lines
18 KiB
JavaScript

'use strict';
module.exports = function(Chart) {
var helpers = Chart.helpers;
Chart.defaults.bar = {
hover: {
mode: 'label'
},
scales: {
xAxes: [{
type: 'category',
// Specific to Bar Controller
categoryPercentage: 0.8,
barPercentage: 0.9,
// grid line settings
gridLines: {
offsetGridLines: true
}
}],
yAxes: [{
type: 'linear'
}]
}
};
Chart.controllers.bar = Chart.DatasetController.extend({
dataElementType: Chart.elements.Rectangle,
initialize: function(chart, datasetIndex) {
Chart.DatasetController.prototype.initialize.call(this, chart, datasetIndex);
var me = this;
var meta = me.getMeta();
var dataset = me.getDataset();
meta.stack = dataset.stack;
// Use this to indicate that this is a bar dataset.
meta.bar = true;
},
// Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible
getStackCount: function() {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var stacks = [];
helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) {
var dsMeta = me.chart.getDatasetMeta(datasetIndex);
if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) &&
(yScale.options.stacked === false ||
(yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) ||
(yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) {
stacks.push(dsMeta.stack);
}
}, me);
return stacks.length;
},
update: function(reset) {
var me = this;
helpers.each(me.getMeta().data, function(rectangle, index) {
me.updateElement(rectangle, index, reset);
}, me);
},
updateElement: function(rectangle, index, reset) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var yScale = me.getScaleForId(meta.yAxisID);
var scaleBase = yScale.getBasePixel();
var rectangleElementOptions = me.chart.options.elements.rectangle;
var custom = rectangle.custom || {};
var dataset = me.getDataset();
rectangle._xScale = xScale;
rectangle._yScale = yScale;
rectangle._datasetIndex = me.index;
rectangle._index = index;
var ruler = me.getRuler(index); // The index argument for compatible
rectangle._model = {
x: me.calculateBarX(index, me.index, ruler),
y: reset ? scaleBase : me.calculateBarY(index, me.index),
// Tooltip
label: me.chart.data.labels[index],
datasetLabel: dataset.label,
// Appearance
horizontal: false,
base: reset ? scaleBase : me.calculateBarBase(me.index, index),
width: me.calculateBarWidth(ruler),
backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor),
borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped,
borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor),
borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth)
};
rectangle.pivot();
},
calculateBarBase: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var base = 0;
if (yScale.options.stacked === true ||
(yScale.options.stacked === undefined && meta.stack !== undefined)) {
var chart = me.chart;
var datasets = chart.data.datasets;
var value = Number(datasets[datasetIndex].data[index]);
for (var i = 0; i < datasetIndex; i++) {
var currentDs = datasets[i];
var currentDsMeta = chart.getDatasetMeta(i);
if (currentDsMeta.bar && currentDsMeta.yAxisID === yScale.id && chart.isDatasetVisible(i) &&
meta.stack === currentDsMeta.stack) {
var currentVal = Number(currentDs.data[index]);
base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0);
}
}
return yScale.getPixelForValue(base);
}
return yScale.getBasePixel();
},
getRuler: function() {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var stackCount = me.getStackCount();
var tickWidth = xScale.width / xScale.ticks.length;
var categoryWidth = tickWidth * xScale.options.categoryPercentage;
var categorySpacing = (tickWidth - (tickWidth * xScale.options.categoryPercentage)) / 2;
var fullBarWidth = categoryWidth / stackCount;
var barWidth = fullBarWidth * xScale.options.barPercentage;
var barSpacing = fullBarWidth - (fullBarWidth * xScale.options.barPercentage);
return {
stackCount: stackCount,
tickWidth: tickWidth,
categoryWidth: categoryWidth,
categorySpacing: categorySpacing,
fullBarWidth: fullBarWidth,
barWidth: barWidth,
barSpacing: barSpacing
};
},
calculateBarWidth: function(ruler) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var options = xScale.options;
var maxBarThickness = options.maxBarThickness || Infinity;
var barWidth;
if (options.barThickness) {
return options.barThickness;
}
barWidth = options.stacked ? ruler.categoryWidth * options.barPercentage : ruler.barWidth;
return Math.min(barWidth, maxBarThickness);
},
// Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible
getStackIndex: function(datasetIndex) {
var me = this;
var meta = me.chart.getDatasetMeta(datasetIndex);
var yScale = me.getScaleForId(meta.yAxisID);
var dsMeta, j;
var stacks = [meta.stack];
for (j = 0; j < datasetIndex; ++j) {
dsMeta = this.chart.getDatasetMeta(j);
if (dsMeta.bar && this.chart.isDatasetVisible(j) &&
(yScale.options.stacked === false ||
(yScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) ||
(yScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) {
stacks.push(dsMeta.stack);
}
}
return stacks.length - 1;
},
calculateBarX: function(index, datasetIndex, ruler) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var stackIndex = me.getStackIndex(datasetIndex);
var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);
leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0;
if (xScale.options.stacked) {
return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing;
}
return leftTick +
(ruler.barWidth / 2) +
ruler.categorySpacing +
(ruler.barWidth * stackIndex) +
(ruler.barSpacing / 2) +
(ruler.barSpacing * stackIndex);
},
calculateBarY: function(index, datasetIndex) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var value = Number(me.getDataset().data[index]);
if (yScale.options.stacked ||
(yScale.options.stacked === undefined && meta.stack !== undefined)) {
var sumPos = 0,
sumNeg = 0;
for (var i = 0; i < datasetIndex; i++) {
var ds = me.chart.data.datasets[i];
var dsMeta = me.chart.getDatasetMeta(i);
if (dsMeta.bar && dsMeta.yAxisID === yScale.id && me.chart.isDatasetVisible(i) &&
meta.stack === dsMeta.stack) {
var stackedVal = Number(ds.data[index]);
if (stackedVal < 0) {
sumNeg += stackedVal || 0;
} else {
sumPos += stackedVal || 0;
}
}
}
if (value < 0) {
return yScale.getPixelForValue(sumNeg + value);
}
return yScale.getPixelForValue(sumPos + value);
}
return yScale.getPixelForValue(value);
},
draw: function() {
var me = this;
var chart = me.chart;
var elements = me.getMeta().data;
var dataset = me.getDataset();
var ilen = elements.length;
var i = 0;
var d;
Chart.canvasHelpers.clipArea(chart.ctx, chart.chartArea);
for (; i<ilen; ++i) {
d = dataset.data[i];
if (d !== null && d !== undefined && !isNaN(d)) {
elements[i].draw();
}
}
Chart.canvasHelpers.unclipArea(chart.ctx);
},
setHoverStyle: function(rectangle) {
var dataset = this.chart.data.datasets[rectangle._datasetIndex];
var index = rectangle._index;
var custom = rectangle.custom || {};
var model = rectangle._model;
model.backgroundColor = custom.hoverBackgroundColor ? custom.hoverBackgroundColor : helpers.getValueAtIndexOrDefault(dataset.hoverBackgroundColor, index, helpers.getHoverColor(model.backgroundColor));
model.borderColor = custom.hoverBorderColor ? custom.hoverBorderColor : helpers.getValueAtIndexOrDefault(dataset.hoverBorderColor, index, helpers.getHoverColor(model.borderColor));
model.borderWidth = custom.hoverBorderWidth ? custom.hoverBorderWidth : helpers.getValueAtIndexOrDefault(dataset.hoverBorderWidth, index, model.borderWidth);
},
removeHoverStyle: function(rectangle) {
var dataset = this.chart.data.datasets[rectangle._datasetIndex];
var index = rectangle._index;
var custom = rectangle.custom || {};
var model = rectangle._model;
var rectangleElementOptions = this.chart.options.elements.rectangle;
model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor);
model.borderColor = custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor);
model.borderWidth = custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth);
}
});
// including horizontalBar in the bar file, instead of a file of its own
// it extends bar (like pie extends doughnut)
Chart.defaults.horizontalBar = {
hover: {
mode: 'label'
},
scales: {
xAxes: [{
type: 'linear',
position: 'bottom'
}],
yAxes: [{
position: 'left',
type: 'category',
// Specific to Horizontal Bar Controller
categoryPercentage: 0.8,
barPercentage: 0.9,
// grid line settings
gridLines: {
offsetGridLines: true
}
}]
},
elements: {
rectangle: {
borderSkipped: 'left'
}
},
tooltips: {
callbacks: {
title: function(tooltipItems, data) {
// Pick first xLabel for now
var title = '';
if (tooltipItems.length > 0) {
if (tooltipItems[0].yLabel) {
title = tooltipItems[0].yLabel;
} else if (data.labels.length > 0 && tooltipItems[0].index < data.labels.length) {
title = data.labels[tooltipItems[0].index];
}
}
return title;
},
label: function(tooltipItem, data) {
var datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';
return datasetLabel + ': ' + tooltipItem.xLabel;
}
}
}
};
Chart.controllers.horizontalBar = Chart.controllers.bar.extend({
// Correctly calculate the bar width accounting for stacks and the fact that not all bars are visible
getStackCount: function() {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var stacks = [];
helpers.each(me.chart.data.datasets, function(dataset, datasetIndex) {
var dsMeta = me.chart.getDatasetMeta(datasetIndex);
if (dsMeta.bar && me.chart.isDatasetVisible(datasetIndex) &&
(xScale.options.stacked === false ||
(xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) ||
(xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) {
stacks.push(dsMeta.stack);
}
}, me);
return stacks.length;
},
updateElement: function(rectangle, index, reset) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var yScale = me.getScaleForId(meta.yAxisID);
var scaleBase = xScale.getBasePixel();
var custom = rectangle.custom || {};
var dataset = me.getDataset();
var rectangleElementOptions = me.chart.options.elements.rectangle;
rectangle._xScale = xScale;
rectangle._yScale = yScale;
rectangle._datasetIndex = me.index;
rectangle._index = index;
var ruler = me.getRuler(index); // The index argument for compatible
rectangle._model = {
x: reset ? scaleBase : me.calculateBarX(index, me.index),
y: me.calculateBarY(index, me.index, ruler),
// Tooltip
label: me.chart.data.labels[index],
datasetLabel: dataset.label,
// Appearance
horizontal: true,
base: reset ? scaleBase : me.calculateBarBase(me.index, index),
height: me.calculateBarHeight(ruler),
backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleElementOptions.backgroundColor),
borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleElementOptions.borderSkipped,
borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleElementOptions.borderColor),
borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleElementOptions.borderWidth)
};
rectangle.pivot();
},
calculateBarBase: function(datasetIndex, index) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var base = xScale.getBaseValue();
if (xScale.options.stacked ||
(xScale.options.stacked === undefined && meta.stack !== undefined)) {
var chart = me.chart;
var datasets = chart.data.datasets;
var value = Number(datasets[datasetIndex].data[index]);
for (var i = 0; i < datasetIndex; i++) {
var currentDs = datasets[i];
var currentDsMeta = chart.getDatasetMeta(i);
if (currentDsMeta.bar && currentDsMeta.xAxisID === xScale.id && chart.isDatasetVisible(i) &&
meta.stack === currentDsMeta.stack) {
var currentVal = Number(currentDs.data[index]);
base += value < 0 ? Math.min(currentVal, 0) : Math.max(currentVal, 0);
}
}
return xScale.getPixelForValue(base);
}
return xScale.getBasePixel();
},
getRuler: function() {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var stackCount = me.getStackCount();
var tickHeight = yScale.height / yScale.ticks.length;
var categoryHeight = tickHeight * yScale.options.categoryPercentage;
var categorySpacing = (tickHeight - (tickHeight * yScale.options.categoryPercentage)) / 2;
var fullBarHeight = categoryHeight / stackCount;
var barHeight = fullBarHeight * yScale.options.barPercentage;
var barSpacing = fullBarHeight - (fullBarHeight * yScale.options.barPercentage);
return {
stackCount: stackCount,
tickHeight: tickHeight,
categoryHeight: categoryHeight,
categorySpacing: categorySpacing,
fullBarHeight: fullBarHeight,
barHeight: barHeight,
barSpacing: barSpacing
};
},
calculateBarHeight: function(ruler) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var options = yScale.options;
var maxBarThickness = options.maxBarThickness || Infinity;
var barHeight;
if (options.barThickness) {
return options.barThickness;
}
barHeight = options.stacked ? ruler.categoryHeight * options.barPercentage : ruler.barHeight;
return Math.min(barHeight, maxBarThickness);
},
// Get stack index from the given dataset index accounting for stacks and the fact that not all bars are visible
getStackIndex: function(datasetIndex) {
var me = this;
var meta = me.chart.getDatasetMeta(datasetIndex);
var xScale = me.getScaleForId(meta.xAxisID);
var dsMeta, j;
var stacks = [meta.stack];
for (j = 0; j < datasetIndex; ++j) {
dsMeta = this.chart.getDatasetMeta(j);
if (dsMeta.bar && this.chart.isDatasetVisible(j) &&
(xScale.options.stacked === false ||
(xScale.options.stacked === true && stacks.indexOf(dsMeta.stack) === -1) ||
(xScale.options.stacked === undefined && (dsMeta.stack === undefined || stacks.indexOf(dsMeta.stack) === -1)))) {
stacks.push(dsMeta.stack);
}
}
return stacks.length - 1;
},
calculateBarX: function(index, datasetIndex) {
var me = this;
var meta = me.getMeta();
var xScale = me.getScaleForId(meta.xAxisID);
var value = Number(me.getDataset().data[index]);
if (xScale.options.stacked ||
(xScale.options.stacked === undefined && meta.stack !== undefined)) {
var sumPos = 0,
sumNeg = 0;
for (var i = 0; i < datasetIndex; i++) {
var ds = me.chart.data.datasets[i];
var dsMeta = me.chart.getDatasetMeta(i);
if (dsMeta.bar && dsMeta.xAxisID === xScale.id && me.chart.isDatasetVisible(i) &&
meta.stack === dsMeta.stack) {
var stackedVal = Number(ds.data[index]);
if (stackedVal < 0) {
sumNeg += stackedVal || 0;
} else {
sumPos += stackedVal || 0;
}
}
}
if (value < 0) {
return xScale.getPixelForValue(sumNeg + value);
}
return xScale.getPixelForValue(sumPos + value);
}
return xScale.getPixelForValue(value);
},
calculateBarY: function(index, datasetIndex, ruler) {
var me = this;
var meta = me.getMeta();
var yScale = me.getScaleForId(meta.yAxisID);
var stackIndex = me.getStackIndex(datasetIndex);
var topTick = yScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo);
topTick -= me.chart.isCombo ? (ruler.tickHeight / 2) : 0;
if (yScale.options.stacked) {
return topTick + (ruler.categoryHeight / 2) + ruler.categorySpacing;
}
return topTick +
(ruler.barHeight / 2) +
ruler.categorySpacing +
(ruler.barHeight * stackIndex) +
(ruler.barSpacing / 2) +
(ruler.barSpacing * stackIndex);
}
});
};