When `barThickness: undefined|null` (default), we compute an optimal sample size based on the smallest tick interval reduced to prevent any bar to overlap (bar equally sized). Also added support for a 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).
bower.json
*.log
*.swp
+*.stackdump
}
});
+/**
+ * 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({
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,
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
};
},
rules:
# Best Practices
complexity: 0
+ max-statements: 0
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+{
+ "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
+ }
+ }
+}
--- /dev/null
+{
+ "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,
+ "ticks": {
+ "source": "labels"
+ }
+ }],
+ "yAxes": [{
+ "display": false,
+ "ticks": {
+ "beginAtZero": true
+ }
+ }]
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "bar",
+ "data": {
+ "labels": ["2016", "2018", "2020", "2024", "2030"],
+ "datasets": [{
+ "backgroundColor": "#FF6384",
+ "data": [1, null, 3, 4, 5]
+ }, {
+ "backgroundColor": "#36A2EB",
+ "data": [5, 4, 3, null, 1]
+ }, {
+ "backgroundColor": "#FFCE56",
+ "data": [3, 5, 2, null, 4]
+ }]
+ },
+ "options": {
+ "responsive": false,
+ "legend": false,
+ "title": false,
+ "scales": {
+ "xAxes": [{
+ "type": "time",
+ "display": false,
+ "barPercentage": 1,
+ "categoryPercentage": 1,
+ "ticks": {
+ "source": "labels"
+ }
+ }],
+ "yAxes": [{
+ "display": false,
+ "ticks": {
+ "beginAtZero": true
+ }
+ }]
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "bar",
+ "data": {
+ "labels": ["2016", "2018", "2020", "2024", "2030"],
+ "datasets": [{
+ "backgroundColor": "#FF6384",
+ "data": [
+ {"y": "1", "t": "2016"},
+ {"y": "2", "t": "2017"},
+ {"y": "3", "t": "2017-08"},
+ {"y": "4", "t": "2024"},
+ {"y": "5", "t": "2030"}
+ ]
+ }]
+ },
+ "options": {
+ "responsive": false,
+ "legend": false,
+ "title": false,
+ "scales": {
+ "xAxes": [{
+ "type": "time",
+ "display": false,
+ "barPercentage": 1,
+ "categoryPercentage": 1,
+ "ticks": {
+ "source": "labels"
+ }
+ }],
+ "yAxes": [{
+ "display": false,
+ "ticks": {
+ "beginAtZero": true
+ }
+ }]
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "bar",
+ "data": {
+ "labels": ["2016", "2018", "2020", "2024", "2030"],
+ "datasets": [{
+ "backgroundColor": "#FF6384",
+ "data": [1, null, 3, 4, 5]
+ }, {
+ "backgroundColor": "#36A2EB",
+ "data": [5, 4, 3, null, 1]
+ }, {
+ "backgroundColor": "#FFCE56",
+ "data": [3, 5, 2, null, 4]
+ }]
+ },
+ "options": {
+ "responsive": false,
+ "legend": false,
+ "title": false,
+ "scales": {
+ "xAxes": [{
+ "type": "time",
+ "offset": true,
+ "display": false,
+ "barPercentage": 1,
+ "categoryPercentage": 1,
+ "ticks": {
+ "source": "labels"
+ }
+ }],
+ "yAxes": [{
+ "display": false,
+ "ticks": {
+ "beginAtZero": true
+ }
+ }]
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "bar",
+ "data": {
+ "labels": ["2016", "2018", "2020", "2024", "2030"],
+ "datasets": [{
+ "backgroundColor": "#FF6384",
+ "data": [{"x": "2022", "y": 42}]
+ }]
+ },
+ "options": {
+ "responsive": false,
+ "legend": false,
+ "title": false,
+ "scales": {
+ "xAxes": [{
+ "type": "time",
+ "display": false,
+ "barPercentage": 1,
+ "categoryPercentage": 1,
+ "ticks": {
+ "source": "labels"
+ }
+ }],
+ "yAxes": [{
+ "display": false,
+ "ticks": {
+ "beginAtZero": true
+ }
+ }]
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "bar",
+ "data": {
+ "labels": ["2016", "2018", "2020", "2024", "2030"],
+ "datasets": [{
+ "backgroundColor": "#FF6384",
+ "data": [1]
+ }]
+ },
+ "options": {
+ "responsive": false,
+ "legend": false,
+ "title": false,
+ "scales": {
+ "xAxes": [{
+ "type": "time",
+ "display": false,
+ "barPercentage": 1,
+ "categoryPercentage": 1,
+ "ticks": {
+ "source": "labels"
+ },
+ "time": {
+ "min": "2013"
+ }
+ }],
+ "yAxes": [{
+ "display": false,
+ "ticks": {
+ "beginAtZero": true
+ }
+ }]
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
--- /dev/null
+{
+ "config": {
+ "type": "bar",
+ "data": {
+ "labels": ["2016", "2018", "2020", "2024", "2030"],
+ "datasets": [{
+ "backgroundColor": "#FF6384",
+ "data": [1, null, 3, 4, 5]
+ }, {
+ "backgroundColor": "#36A2EB",
+ "data": [5, 4, 3, null, 1]
+ }, {
+ "backgroundColor": "#FFCE56",
+ "data": [3, 5, 2, null, 4]
+ }]
+ },
+ "options": {
+ "responsive": false,
+ "legend": false,
+ "title": false,
+ "scales": {
+ "xAxes": [{
+ "type": "time",
+ "stacked": true,
+ "display": false,
+ "barPercentage": 1,
+ "categoryPercentage": 1,
+ "ticks": {
+ "source": "labels"
+ }
+ }],
+ "yAxes": [{
+ "display": false,
+ "stacked": true,
+ "ticks": {
+ "beginAtZero": true
+ }
+ }]
+ }
+ }
+ },
+ "options": {
+ "canvas": {
+ "height": 256,
+ "width": 512
+ }
+ }
+}
var chart = acquireChart(json.config, json.options);
if (!inputs.png) {
fail('Missing PNG comparison file for ' + inputs.json);
- if (!json.debug) {
- releaseChart(chart);
- }
done();
}
describe('Chart.controllers.bar', function() {
+ describe('auto', jasmine.specsFromFixtures('controller.bar'));
+
it('should be constructed', function() {
var chart = window.acquireChart({
type: 'bar',
});
});
});
-
- describe('Bar thickness with a time scale', function() {
- ['auto', 'data', 'labels'].forEach(function(source) {
- ['series', 'linear'].forEach(function(distribution) {
- describe('When ticks.source is "' + source + '", distribution is "' + distribution + '"', function() {
- beforeEach(function() {
- this.chart = window.acquireChart({
- type: 'bar',
- data: {
- datasets: [{
- data: [1, 2, 3]
- }, {
- data: [1, 2, 3]
- }],
- labels: ['2017', '2018', '2020']
- },
- options: {
- legend: false,
- title: false,
- scales: {
- xAxes: [{
- id: 'x',
- type: 'time',
- time: {
- unit: 'year',
- parser: 'YYYY'
- },
- ticks: {
- source: source
- },
- offset: true,
- distribution: distribution
- }],
- yAxes: [{
- type: 'linear'
- }]
- }
- }
- });
- });
-
- it('should correctly set bar width', function() {
- var chart = this.chart;
- var scale = chart.scales.x;
- var options = chart.options.scales.xAxes[0];
- var categoryPercentage = options.categoryPercentage;
- var barPercentage = options.barPercentage;
- var firstInterval = scale.getPixelForValue('2018') - scale.getPixelForValue('2017');
- var firstExpected = firstInterval * categoryPercentage / 2 * barPercentage;
- var lastInterval = scale.getPixelForValue('2020') - scale.getPixelForValue('2018');
- var lastExpected = lastInterval * categoryPercentage / 2 * barPercentage;
- var i, ilen, meta;
-
- for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
- meta = chart.getDatasetMeta(i);
- expect(meta.data[0]._model.width).toBeCloseToPixel(firstExpected);
- expect(meta.data[1]._model.width).toBeCloseToPixel((firstExpected + lastExpected) / 2);
- expect(meta.data[2]._model.width).toBeCloseToPixel(lastExpected);
- }
- });
-
- it('should correctly set bar width if maxBarThickness is specified', function() {
- var chart = this.chart;
- var options = chart.options.scales.xAxes[0];
- var i, ilen, meta;
-
- options.maxBarThickness = 10;
- chart.update();
-
- for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
- meta = chart.getDatasetMeta(i);
- expect(meta.data[0]._model.width).toBeCloseToPixel(10);
- expect(meta.data[1]._model.width).toBeCloseToPixel(10);
- expect(meta.data[2]._model.width).toBeCloseToPixel(10);
- }
- });
- });
- });
- });
- });
});