diff --git a/docs/charts/bar.md b/docs/charts/bar.md index d4d0a271fdf..71473b41dd0 100644 --- a/docs/charts/bar.md +++ b/docs/charts/bar.md @@ -171,6 +171,10 @@ If this value is a number, it is applied to all sides of the rectangle (left, to If this value is a number, it is applied to all corners of the rectangle (topLeft, topRight, bottomLeft, bottomRight), except corners touching the [`borderSkipped`](#borderskipped). If this value is an object, the `topLeft` property defines the top-left corners border radius. Similarly, the `topRight`, `bottomLeft`, and `bottomRight` properties can also be specified. Omitted corners and those touching the [`borderSkipped`](#borderskipped) are skipped. For example if the `top` border is skipped, the border radius for the corners `topLeft` and `topRight` will be skipped as well. +:::tip Stacked Charts +When the border radius is supplied as a number and the chart is stacked, the radius will only be applied to the bars that are at the edges of the stack or where the bar is floating. The object syntax can be used to override this behavior. +::: + ### Interactions The interaction with each bar can be controlled with the following properties: diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js index abe98841c58..0ed99b7ec8b 100644 --- a/src/controllers/controller.bar.js +++ b/src/controllers/controller.bar.js @@ -269,10 +269,12 @@ export default class BarController extends DatasetController { const parsed = me.getParsed(i); const vpixels = reset || isNullOrUndef(parsed[vScale.axis]) ? {base, head: base} : me._calculateBarValuePixels(i); const ipixels = me._calculateBarIndexPixels(i, ruler); + const stack = (parsed._stacks || {})[vScale.axis]; const properties = { horizontal, base: vpixels.base, + enableBorderRadius: !stack || isFloatBar(parsed._custom) || (me.index === stack._top || me.index === stack._bottom), x: horizontal ? vpixels.head : ipixels.center, y: horizontal ? ipixels.center : vpixels.head, height: horizontal ? ipixels.size : undefined, diff --git a/src/core/core.datasetController.js b/src/core/core.datasetController.js index a3882e9e66d..cff882ab865 100644 --- a/src/core/core.datasetController.js +++ b/src/core/core.datasetController.js @@ -127,6 +127,17 @@ function getOrCreateStack(stacks, stackKey, indexValue) { return subStack[indexValue] || (subStack[indexValue] = {}); } +function getLastIndexInStack(stack, vScale, positive) { + for (const meta of vScale.getMatchingVisibleMetas('bar').reverse()) { + const value = stack[meta.index]; + if ((positive && value > 0) || (!positive && value < 0)) { + return meta.index; + } + } + + return null; +} + function updateStacks(controller, parsed) { const {chart, _cachedMeta: meta} = controller; const stacks = chart._stacks || (chart._stacks = {}); // map structure is {stackKey: {datasetIndex: value}} @@ -143,6 +154,9 @@ function updateStacks(controller, parsed) { const itemStacks = item._stacks || (item._stacks = {}); stack = itemStacks[vAxis] = getOrCreateStack(stacks, key, index); stack[datasetIndex] = value; + + stack._top = getLastIndexInStack(stack, vScale, true); + stack._bottom = getLastIndexInStack(stack, vScale, false); } } diff --git a/src/elements/element.bar.js b/src/elements/element.bar.js index 8d900680435..c7e82309ec6 100644 --- a/src/elements/element.bar.js +++ b/src/elements/element.bar.js @@ -1,4 +1,5 @@ import Element from '../core/core.element'; +import {isObject} from '../helpers'; import {addRoundedRectPath} from '../helpers/helpers.canvas'; import {toTRBL, toTRBLCorners} from '../helpers/helpers.options'; @@ -83,16 +84,21 @@ function parseBorderWidth(bar, maxW, maxH) { } function parseBorderRadius(bar, maxW, maxH) { + const {enableBorderRadius} = bar.getProps(['enableBorderRadius']); const value = bar.options.borderRadius; const o = toTRBLCorners(value); const maxR = Math.min(maxW, maxH); const skip = parseBorderSkipped(bar); + // If the value is an object, assume the user knows what they are doing + // and apply as directed. + const enableBorder = enableBorderRadius || isObject(value); + return { - topLeft: skipOrLimit(skip.top || skip.left, o.topLeft, 0, maxR), - topRight: skipOrLimit(skip.top || skip.right, o.topRight, 0, maxR), - bottomLeft: skipOrLimit(skip.bottom || skip.left, o.bottomLeft, 0, maxR), - bottomRight: skipOrLimit(skip.bottom || skip.right, o.bottomRight, 0, maxR) + topLeft: skipOrLimit(!enableBorder || skip.top || skip.left, o.topLeft, 0, maxR), + topRight: skipOrLimit(!enableBorder || skip.top || skip.right, o.topRight, 0, maxR), + bottomLeft: skipOrLimit(!enableBorder || skip.bottom || skip.left, o.bottomLeft, 0, maxR), + bottomRight: skipOrLimit(!enableBorder || skip.bottom || skip.right, o.bottomRight, 0, maxR) }; } @@ -224,6 +230,7 @@ BarElement.defaults = { borderSkipped: 'start', borderWidth: 0, borderRadius: 0, + enableBorderRadius: true, pointStyle: undefined }; diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.js b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.js new file mode 100644 index 00000000000..897037932e9 --- /dev/null +++ b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.js @@ -0,0 +1,42 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + backgroundColor: 'red', + data: [12, 19, 12, 5, 4, 12], + }, + { + backgroundColor: 'green', + data: [12, 19, -4, 5, 8, 3], + type: 'line' + }, + { + backgroundColor: 'blue', + data: [7, 11, -12, 12, 0, -7], + } + ] + }, + options: { + elements: { + bar: { + borderRadius: Number.MAX_VALUE, + borderWidth: 2, + } + }, + scales: { + x: {display: false, stacked: true}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.png b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.png new file mode 100644 index 00000000000..3aff7387b1d Binary files /dev/null and b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.png differ diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.js b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.js new file mode 100644 index 00000000000..886e9c46318 --- /dev/null +++ b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.js @@ -0,0 +1,44 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + backgroundColor: 'red', + data: [12, 19, 12, 5, 4, 12], + order: 2, + }, + { + backgroundColor: 'green', + data: [12, 19, -4, 5, 8, 3], + order: 1, + }, + { + backgroundColor: 'blue', + data: [7, 11, -12, 12, 0, -7], + order: 0, + } + ] + }, + options: { + elements: { + bar: { + borderRadius: Number.MAX_VALUE, + borderWidth: 2, + } + }, + scales: { + x: {display: false, stacked: true}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.png b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.png new file mode 100644 index 00000000000..24eb8e0ea00 Binary files /dev/null and b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.png differ diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.js b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.js new file mode 100644 index 00000000000..1aeac88cb59 --- /dev/null +++ b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.js @@ -0,0 +1,41 @@ +module.exports = { + threshold: 0.01, + config: { + type: 'bar', + data: { + labels: [0, 1, 2, 3, 4, 5], + datasets: [ + { + backgroundColor: 'red', + data: [12, 19, 12, 5, 4, 12], + }, + { + backgroundColor: 'green', + data: [12, 19, -4, 5, 8, 3], + }, + { + backgroundColor: 'blue', + data: [7, 11, -12, 12, 0, -7], + } + ] + }, + options: { + elements: { + bar: { + borderRadius: Number.MAX_VALUE, + borderWidth: 2, + } + }, + scales: { + x: {display: false, stacked: true}, + y: {display: false, stacked: true} + } + } + }, + options: { + canvas: { + height: 256, + width: 512 + } + } +}; diff --git a/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.png b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.png new file mode 100644 index 00000000000..2b8af4bb8d0 Binary files /dev/null and b/test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.png differ diff --git a/test/fixtures/controller.bar/border-radius.js b/test/fixtures/controller.bar/borderRadius/border-radius.js similarity index 100% rename from test/fixtures/controller.bar/border-radius.js rename to test/fixtures/controller.bar/borderRadius/border-radius.js diff --git a/test/fixtures/controller.bar/border-radius.png b/test/fixtures/controller.bar/borderRadius/border-radius.png similarity index 100% rename from test/fixtures/controller.bar/border-radius.png rename to test/fixtures/controller.bar/borderRadius/border-radius.png diff --git a/test/specs/core.datasetController.tests.js b/test/specs/core.datasetController.tests.js index ce7d121b6c9..e89aa0bf98c 100644 --- a/test/specs/core.datasetController.tests.js +++ b/test/specs/core.datasetController.tests.js @@ -540,12 +540,12 @@ describe('Chart.DatasetController', function() { expect(chart._stacks).toEqual({ 'x.y.1': { - 0: {0: 1, 2: 3}, - 1: {0: 10, 2: 30} + 0: {0: 1, 2: 3, _top: 2, _bottom: null}, + 1: {0: 10, 2: 30, _top: 2, _bottom: null} }, 'x.y.2': { - 0: {1: 2}, - 1: {1: 20} + 0: {1: 2, _top: 1, _bottom: null}, + 1: {1: 20, _top: 1, _bottom: null} } }); @@ -554,12 +554,12 @@ describe('Chart.DatasetController', function() { expect(chart._stacks).toEqual({ 'x.y.1': { - 0: {0: 1}, - 1: {0: 10} + 0: {0: 1, _top: 2, _bottom: null}, + 1: {0: 10, _top: 2, _bottom: null} }, 'x.y.2': { - 0: {1: 2, 2: 3}, - 1: {1: 20, 2: 30} + 0: {1: 2, 2: 3, _top: 2, _bottom: null}, + 1: {1: 20, 2: 30, _top: 2, _bottom: null} } }); }); @@ -584,12 +584,12 @@ describe('Chart.DatasetController', function() { expect(chart._stacks).toEqual({ 'x.y.1': { - 0: {0: 1, 2: 3}, - 1: {0: 10, 2: 30} + 0: {0: 1, 2: 3, _top: 2, _bottom: null}, + 1: {0: 10, 2: 30, _top: 2, _bottom: null} }, 'x.y.2': { - 0: {1: 2}, - 1: {1: 20} + 0: {1: 2, _top: 1, _bottom: null}, + 1: {1: 20, _top: 1, _bottom: null} } }); @@ -598,12 +598,12 @@ describe('Chart.DatasetController', function() { expect(chart._stacks).toEqual({ 'x.y.1': { - 0: {0: 1, 2: 4}, - 1: {0: 10} + 0: {0: 1, 2: 4, _top: 2, _bottom: null}, + 1: {0: 10, _top: 2, _bottom: null} }, 'x.y.2': { - 0: {1: 2}, - 1: {1: 20} + 0: {1: 2, _top: 1, _bottom: null}, + 1: {1: 20, _top: 1, _bottom: null} } }); }); @@ -719,7 +719,7 @@ describe('Chart.DatasetController', function() { }); var meta = chart.getDatasetMeta(0); - expect(meta._parsed[0]._stacks).toEqual(jasmine.objectContaining({y: {0: 10, 1: 20}})); + expect(meta._parsed[0]._stacks).toEqual(jasmine.objectContaining({y: {0: 10, 1: 20, _top: null, _bottom: null}})); }); describe('resolveDataElementOptions', function() {