Skip to content

Commit

Permalink
Implement equally sized bars
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
simonbrunel committed Nov 26, 2017
1 parent e2dd448 commit 7e0ca21
Showing 1 changed file with 105 additions and 44 deletions.
149 changes: 105 additions & 44 deletions src/controllers/controller.bar.js
Expand Up @@ -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({
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
};
},

Expand Down

0 comments on commit 7e0ca21

Please sign in to comment.