mirror of
https://github.com/chartjs/Chart.js.git
synced 2024-10-07 20:59:08 +02:00
36ccf40946
* 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
559 lines
18 KiB
JavaScript
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);
|
|
}
|
|
});
|
|
};
|