Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement equally sized bars #4994

Merged
merged 3 commits into from Dec 2, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -12,3 +12,4 @@
bower.json
*.log
*.swp
*.stackdump
150 changes: 106 additions & 44 deletions src/controllers/controller.bar.js
Expand Up @@ -95,6 +95,93 @@ 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 = 1, ilen = pixels.length; i < ilen; ++i) {
min = Math.min(min, pixels[i] - pixels[i - 1]);
}

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 an "ideal" category based on the absolute bar thickness or, if undefined or null,
* uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This
* mode currently always generates bars equally sized (until we introduce scriptable options?).
* @private
*/
function computeFitCategoryTraits(index, ruler, options) {
var thickness = options.barThickness;
var count = ruler.stackCount;
var curr = ruler.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 an "optimal" category that globally arranges bars side by side (no gap when
* percentage options are 1), based on the previous and following categories. This mode
* generates bars with different widths when data are not evenly spaced.
* @private
*/
function computeFlexCategoryTraits(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;
}

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 +349,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 +424,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'
? computeFlexCategoryTraits(index, ruler, options)
: computeFitCategoryTraits(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
1 change: 1 addition & 0 deletions test/.eslintrc
Expand Up @@ -11,3 +11,4 @@ globals:
rules:
# Best Practices
complexity: 0
max-statements: 0
42 changes: 42 additions & 0 deletions test/fixtures/controller.bar/bar-thickness-absolute.json
@@ -0,0 +1,42 @@
{
"config": {
"type": "bar",
"data": {
"labels": ["2017", "2018", "2019", "2024", "2025"],
"datasets": [{
"backgroundColor": "rgba(255, 99, 132, 0.5)",
"data": [1, null, 3, 4, 5]
}]
},
"options": {
"responsive": false,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"type": "time",
"offset": true,
"display": false,
"barPercentage": 1,
"categoryPercentage": 1,
"barThickness": 128,
"ticks": {
"source": "labels"
}
}],
"yAxes": [{
"display": false,
"ticks": {
"beginAtZero": true
}
}]
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions test/fixtures/controller.bar/bar-thickness-flex-offset.json
@@ -0,0 +1,42 @@
{
"config": {
"type": "bar",
"data": {
"labels": ["2017", "2018", "2020", "2024", "2038"],
"datasets": [{
"backgroundColor": "#FF6384",
"data": [1, null, 3, 4, 5]
}]
},
"options": {
"responsive": false,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"type": "time",
"offset": true,
"display": false,
"barPercentage": 1,
"categoryPercentage": 1,
"barThickness": "flex",
"ticks": {
"source": "labels"
}
}],
"yAxes": [{
"display": false,
"ticks": {
"beginAtZero": true
}
}]
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions test/fixtures/controller.bar/bar-thickness-flex.json
@@ -0,0 +1,41 @@
{
"config": {
"type": "bar",
"data": {
"labels": ["2017", "2018", "2020", "2024", "2038"],
"datasets": [{
"backgroundColor": "#FF6384",
"data": [1, null, 3, 4, 5]
}]
},
"options": {
"responsive": false,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"type": "time",
"display": false,
"barPercentage": 1,
"categoryPercentage": 1,
"barThickness": "flex",
"ticks": {
"source": "labels"
}
}],
"yAxes": [{
"display": false,
"ticks": {
"beginAtZero": true
}
}]
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions test/fixtures/controller.bar/bar-thickness-max.json
@@ -0,0 +1,41 @@
{
"config": {
"type": "bar",
"data": {
"labels": ["2016", "2018", "2020", "2024", "2030"],
"datasets": [{
"backgroundColor": "#FF6384",
"data": [1, null, 3, 4, 5]
}]
},
"options": {
"responsive": false,
"legend": false,
"title": false,
"scales": {
"xAxes": [{
"type": "time",
"display": false,
"barPercentage": 1,
"categoryPercentage": 1,
"maxBarThickness": 8,
"ticks": {
"source": "labels"
}
}],
"yAxes": [{
"display": false,
"ticks": {
"beginAtZero": true
}
}]
}
}
},
"options": {
"canvas": {
"height": 256,
"width": 512
}
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.