From 7e0ca217bda61f1f856f55d343b1880b1515162b Mon Sep 17 00:00:00 2001 From: Simon Brunel Date: Sun, 26 Nov 2017 14:54:24 +0100 Subject: [PATCH] Implement equally sized bars When `barThickness: undefined|null` (new default), we compute an optimal sample size based on the smallest tick interval and reduced to prevent any bar to overlap (bar equally sized). Also added support for the special `barThickness: 'flex'` value (previous default) that globally arranges bars side by side to prevent any gap when percentage options are 1 (variable bar sizes). --- src/controllers/controller.bar.js | 149 +++++++++++++++++++++--------- 1 file changed, 105 insertions(+), 44 deletions(-) diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index b811c6f05b1..9c135792148 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -95,6 +95,92 @@ defaults._set('horizontalBar', { } }); +/** + * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. + * @private + */ +function computeMinSampleSize(scale, pixels) { + var min = scale.isHorizontal() ? scale.width : scale.height; + var ticks = scale.getTicks(); + var prev, curr, i, ilen; + + for (i = 0, ilen = pixels.length; i < ilen; ++i) { + min = i > 0 ? Math.min(min, pixels[i] - pixels[i - 1]) : min; + } + + for (i = 0, ilen = ticks.length; i < ilen; ++i) { + curr = scale.getPixelForTick(i); + min = i > 0 ? Math.min(min, curr - prev) : min; + prev = curr; + } + + return min; +} + +/** + * Computes the "ideal" sample range based on the absolute bar thickness or, if undefined or + * null, uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. + * @private + */ +function computeFitSampleRange(index, ruler, options) { + var thickness = options.barThickness; + var count = ruler.stackCount; + var pixels = ruler.pixels; + var curr = pixels[index]; + var size, ratio; + + if (helpers.isNullOrUndef(thickness)) { + size = ruler.min * options.categoryPercentage; + ratio = options.barPercentage; + } else { + // When bar thickness is enforced, category and bar percentages are ignored. + // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') + // and deprecate barPercentage since this value is ignored when thickness is absolute. + size = thickness * count; + ratio = 1; + } + + return { + chunk: size / count, + ratio: ratio, + start: curr - (size / 2) + }; +} + +/** + * Computes a "dynamic" sample range that globally arranges bars side by side (no + * gap when percentage options are 1), based on the previous and following range. + * @private + */ +function computeFlexSampleRange(index, ruler, options) { + var pixels = ruler.pixels; + var curr = pixels[index]; + var prev = index > 0 ? pixels[index - 1] : null; + var next = index < pixels.length - 1 ? pixels[index + 1] : null; + var percent = options.categoryPercentage; + var start, size; + + if (prev === null) { + // first data: its size is double based on the next point or, + // if it's also the last data, we use the scale end extremity. + prev = curr - (next === null ? ruler.end - curr : next - curr); + } + + if (next === null) { + // last data: its size is also double based on the previous point. + next = curr + curr - prev; + } + console.log(ruler.start, prev, curr, next, ruler.end); + start = curr - ((curr - prev) / 2) * percent; + size = ((next - prev) / 2) * percent; + + return { + chunk: size / ruler.stackCount, + ratio: options.barPercentage, + start: start + }; +} + module.exports = function(Chart) { Chart.controllers.bar = Chart.DatasetController.extend({ @@ -262,17 +348,22 @@ module.exports = function(Chart) { var scale = me.getIndexScale(); var stackCount = me.getStackCount(); var datasetIndex = me.index; - var pixels = []; var isHorizontal = scale.isHorizontal(); var start = isHorizontal ? scale.left : scale.top; var end = start + (isHorizontal ? scale.width : scale.height); - var i, ilen; + var pixels = []; + var i, ilen, min; for (i = 0, ilen = me.getMeta().data.length; i < ilen; ++i) { pixels.push(scale.getPixelForValue(null, i, datasetIndex)); } + min = helpers.isNullOrUndef(scale.options.barThickness) + ? computeMinSampleSize(scale, pixels) + : -1; + return { + min: min, pixels: pixels, start: start, end: end, @@ -332,51 +423,21 @@ module.exports = function(Chart) { calculateBarIndexPixels: function(datasetIndex, index, ruler) { var me = this; var options = ruler.scale.options; - var meta = me.getMeta(); - var stackIndex = me.getStackIndex(datasetIndex, meta.stack); - var pixels = ruler.pixels; - var base = pixels[index]; - var length = pixels.length; - var start = ruler.start; - var end = ruler.end; - var leftSampleSize, rightSampleSize, leftCategorySize, rightCategorySize, fullBarSize, size; - - if (length === 1) { - leftSampleSize = base > start ? base - start : end - base; - rightSampleSize = base < end ? end - base : base - start; - } else { - if (index > 0) { - leftSampleSize = (base - pixels[index - 1]) / 2; - if (index === length - 1) { - rightSampleSize = leftSampleSize; - } - } - if (index < length - 1) { - rightSampleSize = (pixels[index + 1] - base) / 2; - if (index === 0) { - leftSampleSize = rightSampleSize; - } - } - } - - leftCategorySize = leftSampleSize * options.categoryPercentage; - rightCategorySize = rightSampleSize * options.categoryPercentage; - fullBarSize = (leftCategorySize + rightCategorySize) / ruler.stackCount; - size = fullBarSize * options.barPercentage; + var range = options.barThickness === 'flex' + ? computeFlexSampleRange(index, ruler, options) + : computeFitSampleRange(index, ruler, options); - size = Math.min( - helpers.valueOrDefault(options.barThickness, size), - helpers.valueOrDefault(options.maxBarThickness, Infinity)); - - base -= leftCategorySize; - base += fullBarSize * stackIndex; - base += (fullBarSize - size) / 2; + var stackIndex = me.getStackIndex(datasetIndex, me.getMeta().stack); + var center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); + var size = Math.min( + helpers.valueOrDefault(options.maxBarThickness, Infinity), + range.chunk * range.ratio); return { - size: size, - base: base, - head: base + size, - center: base + size / 2 + base: center - size / 2, + head: center + size / 2, + center: center, + size: size }; },