From bd2b0e16733deacb85545549897ea4eb98a9078a Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 19 Mar 2017 17:51:18 +0100 Subject: [PATCH] Fix and refactor bar controllers Merge most of the horizontalBar controller into the bar one and fix stack groups and bar positioning when scales are stacked but also when a min and/or max tick values are explicitly defined. --- src/controllers/controller.bar.js | 587 ++++++++++++----------------- test/specs/controller.bar.tests.js | 40 +- 2 files changed, 252 insertions(+), 375 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index 8f7ba3fa2b5..02996af5023 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -32,40 +32,20 @@ module.exports = function(Chart) { 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() { + initialize: function() { var me = this; - var meta = me.getMeta(); - var yScale = me.getScaleForId(meta.yAxisID); + var meta; - 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); + Chart.DatasetController.prototype.initialize.apply(me, arguments); - return stacks.length; + meta = me.getMeta(); + meta.stack = me.getDataset().stack; + meta.bar = true; }, update: function(reset) { var me = this; + helpers.each(me.getMeta().data, function(rectangle, index) { me.updateElement(rectangle, index, reset); }, me); @@ -73,185 +53,213 @@ module.exports = function(Chart) { updateElement: function(rectangle, index, reset) { var me = this; + var chart = me.chart; 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 ruler = me.getRuler(); var dataset = me.getDataset(); + var custom = rectangle.custom || {}; + var rectangleOptions = chart.options.elements.rectangle; - rectangle._xScale = xScale; - rectangle._yScale = yScale; + rectangle._xScale = me.getScaleForId(meta.xAxisID); + rectangle._yScale = me.getScaleForId(meta.yAxisID); 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], + // Labeling + label: 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) + // Style + borderSkipped: custom.borderSkipped ? custom.borderSkipped : rectangleOptions.borderSkipped, + backgroundColor: custom.backgroundColor ? custom.backgroundColor : helpers.getValueAtIndexOrDefault(dataset.backgroundColor, index, rectangleOptions.backgroundColor), + borderColor: custom.borderColor ? custom.borderColor : helpers.getValueAtIndexOrDefault(dataset.borderColor, index, rectangleOptions.borderColor), + borderWidth: custom.borderWidth ? custom.borderWidth : helpers.getValueAtIndexOrDefault(dataset.borderWidth, index, rectangleOptions.borderWidth) }; + helpers.extend(rectangle._model, me.getBarGeometry(index, reset, ruler)); + rectangle.pivot(); }, - calculateBarBase: function(datasetIndex, index) { + /** + * @private + */ + getValueScaleId: function() { + return this.getMeta().yAxisID; + }, + + /** + * @private + */ + getIndexScaleId: function() { + return this.getMeta().xAxisID; + }, + + /** + * @private + */ + getValueScale: function() { + return this.getScaleForId(this.getValueScaleId()); + }, + + /** + * @private + */ + getIndexScale: function() { + return this.getScaleForId(this.getIndexScaleId()); + }, + + /** + * Returns the effective number of stacks based on groups and bar visibility. + * @private + */ + getStackCount: function(last) { var me = this; - var meta = me.getMeta(); - var yScale = me.getScaleForId(meta.yAxisID); - var base = yScale.getBaseValue(); - var original = base; - - 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, original) : Math.max(currentVal, original); - } + var chart = me.chart; + var scale = me.getIndexScale(); + var stacked = scale.options.stacked; + var ilen = last === undefined? chart.data.datasets.length : last + 1; + var stacks = []; + var i, meta; + + for (i = 0; i < ilen; ++i) { + meta = chart.getDatasetMeta(i); + if (meta.bar && chart.isDatasetVisible(i) && + (stacked === false || + (stacked === true && stacks.indexOf(meta.stack) === -1) || + (stacked === undefined && (meta.stack === undefined || stacks.indexOf(meta.stack) === -1)))) { + stacks.push(meta.stack); } - - return yScale.getPixelForValue(base); } - return yScale.getBasePixel(); + return stacks.length; + }, + + /** + * Returns the stack index for the given dataset based on groups and bar visibility. + * @private + */ + getStackIndex: function(datasetIndex) { + return this.getStackCount(datasetIndex) - 1; }, + /** + * @private + */ getRuler: function() { var me = this; - var meta = me.getMeta(); - var xScale = me.getScaleForId(meta.xAxisID); + var scale = me.getIndexScale(); + var options = scale.options; var stackCount = me.getStackCount(); + var fullSize = scale.isHorizontal()? scale.width : scale.height; + var tickSize = fullSize / scale.ticks.length; + var categorySize = tickSize * options.categoryPercentage; + var fullBarSize = categorySize / stackCount; + var barSize = fullBarSize * options.barPercentage; - 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); + barSize = Math.min( + helpers.getValueOrDefault(options.barThickness, barSize), + helpers.getValueOrDefault(options.maxBarThickness, Infinity)); return { stackCount: stackCount, - tickWidth: tickWidth, - categoryWidth: categoryWidth, - categorySpacing: categorySpacing, - fullBarWidth: fullBarWidth, - barWidth: barWidth, - barSpacing: barSpacing + tickSize: tickSize, + categorySize: categorySize, + categorySpacing: tickSize - categorySize, + fullBarSize: fullBarSize, + barSize: barSize, + barSpacing: fullBarSize - barSize, + scale: scale }; }, - calculateBarWidth: function(ruler) { + /** + * Note: pixel values are not clamped to the scale area. + * @private + */ + calculateBarValuePixels: function(datasetIndex, index) { var me = this; + var chart = me.chart; 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); + var scale = me.getValueScale(); + var datasets = chart.data.datasets; + var value = Number(datasets[datasetIndex].data[index]); + var stacked = scale.options.stacked; + var stack = meta.stack; + var start = 0; + var i, imeta, ivalue, base, head, size; + + if (stacked || (stacked === undefined && stack !== undefined)) { + for (i = 0; i < datasetIndex; ++i) { + imeta = chart.getDatasetMeta(i); + + if (imeta.bar && + imeta.stack === stack && + imeta.controller.getValueScaleId() === scale.id && + chart.isDatasetVisible(i)) { + + ivalue = Number(datasets[i].data[index]); + if ((value < 0 && ivalue < 0) || (value >= 0 && ivalue > 0)) { + start += ivalue; + } + } } } - return stacks.length - 1; + base = scale.getPixelForValue(start); + head = scale.getPixelForValue(start + value); + size = (head - base) / 2; + + return { + size: size, + base: base, + head: head, + center: head + size / 2 + }; }, - calculateBarX: function(index, datasetIndex, ruler) { + /** + * @private + */ + calculateBarIndexPixels: function(datasetIndex, index, ruler) { var me = this; - var meta = me.getMeta(); - var xScale = me.getScaleForId(meta.xAxisID); + var scale = ruler.scale; var stackIndex = me.getStackIndex(datasetIndex); - var leftTick = xScale.getPixelForValue(null, index, datasetIndex, me.chart.isCombo); - leftTick -= me.chart.isCombo ? (ruler.tickWidth / 2) : 0; + var base = scale.getPixelForValue(null, index, datasetIndex); + var size = ruler.barSize; - if (xScale.options.stacked) { - return leftTick + (ruler.categoryWidth / 2) + ruler.categorySpacing; - } + base += ruler.fullBarSize * stackIndex; + base += ruler.categorySpacing / 2; + base += ruler.barSpacing / 2; - return leftTick + - (ruler.barWidth / 2) + - ruler.categorySpacing + - (ruler.barWidth * stackIndex) + - (ruler.barSpacing / 2) + - (ruler.barSpacing * stackIndex); + return { + size: size, + base: base, + head: base + size, + center: base + size / 2 + }; }, - calculateBarY: function(index, datasetIndex) { + /** + * @private + */ + getBarGeometry: function(index, reset, ruler) { 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 base = yScale.getBaseValue(); - var sumPos = base, - sumNeg = base; - - 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); - } + var vscale = me.getValueScale(); + var base = vscale.getBasePixel(); + var horizontal = vscale.isHorizontal(); + var vpixels = me.calculateBarValuePixels(me.index, index); + var ipixels = me.calculateBarIndexPixels(me.index, index, ruler); - return yScale.getPixelForValue(value); + return { + horizontal: horizontal, + base: reset? base : vpixels.base, + x: horizontal? reset? base : vpixels.head : ipixels.center, + y: horizontal? ipixels.center : reset? base : vpixels.head, + height: horizontal? ipixels.size : undefined, + width: horizontal? undefined : ipixels.size + }; }, draw: function() { @@ -263,22 +271,24 @@ module.exports = function(Chart) { var i = 0; var d; - Chart.canvasHelpers.clipArea(chart.ctx, chart.chartArea); + helpers.canvas.clipArea(chart.ctx, chart.chartArea); + for (; i