diff --git a/docs/docs/charts/bar.mdx b/docs/docs/charts/bar.mdx
index eeb39d9284f..2a96460a1fe 100644
--- a/docs/docs/charts/bar.mdx
+++ b/docs/docs/charts/bar.mdx
@@ -83,11 +83,13 @@ the color of the bars is generally set this way.
| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'`
| [`borderSkipped`](#borderskipped) | `string` | Yes | Yes | `'start'`
| [`borderWidth`](#borderwidth) | number|object | Yes | Yes | `0`
+| [`borderRadius`](#borderradius) | number|object | Yes | Yes | `0`
| [`clip`](#general) | number|object | - | - | `undefined`
| [`data`](#data-structure) | `object[]` | - | - | **required**
| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
| [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1`
+| [`hoverBorderRadius`](#interactions) | `number` | Yes | Yes | `0`
| [`indexAxis`](#general) | `string` | `'x'` | The base axis for the dataset. Use `'y'` for horizontal bar.
| [`label`](#general) | `string` | - | - | `''`
| [`order`](#general) | `number` | - | - | `0`
@@ -116,13 +118,14 @@ The style of each bar can be controlled with the following properties:
| `borderColor` | The bar border color.
| [`borderSkipped`](#borderskipped) | The edge to skip when drawing bar.
| [`borderWidth`](#borderwidth) | The bar border width (in pixels).
+| [`borderRadius`](#borderradius) | The bar border radius (in pixels).
| `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}`
All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options.
#### borderSkipped
-This setting is used to avoid drawing the bar stroke at the base of the fill.
+This setting is used to avoid drawing the bar stroke at the base of the fill, or disable the border radius.
In general, this does not need to be changed except when creating chart types
that derive from a bar chart.
@@ -142,6 +145,10 @@ Options are:
If this value is a number, it is applied to all sides of the rectangle (left, top, right, bottom), except [`borderSkipped`](#borderskipped). If this value is an object, the `left` property defines the left border width. Similarly, the `right`, `top`, and `bottom` properties can also be specified. Omitted borders and [`borderSkipped`](#borderskipped) are skipped.
+#### borderRadius
+
+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.
+
### Interactions
The interaction with each bar can be controlled with the following properties:
@@ -151,6 +158,7 @@ The interaction with each bar can be controlled with the following properties:
| `hoverBackgroundColor` | The bar background color when hovered.
| `hoverBorderColor` | The bar border color when hovered.
| `hoverBorderWidth` | The bar border width when hovered (in pixels).
+| `hoverBorderRadius` | The bar border radius when hovered (in pixels).
All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options.
diff --git a/samples/charts/bar/border-radius.html b/samples/charts/bar/border-radius.html
new file mode 100644
index 00000000000..b2abcfa16eb
--- /dev/null
+++ b/samples/charts/bar/border-radius.html
@@ -0,0 +1,147 @@
+
+
+
+
+ Bar Chart
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/samples.js b/samples/samples.js
index 57cac6b3adf..eb569df52e7 100644
--- a/samples/samples.js
+++ b/samples/samples.js
@@ -22,6 +22,9 @@
}, {
title: 'Floating',
path: 'charts/bar/float.html'
+ }, {
+ title: 'Border Radius',
+ path: 'charts/bar/border-radius.html'
}]
}, {
title: 'Line charts',
diff --git a/src/controllers/controller.bar.js b/src/controllers/controller.bar.js
index 6ca1e4f6795..128f0fc033d 100644
--- a/src/controllers/controller.bar.js
+++ b/src/controllers/controller.bar.js
@@ -521,6 +521,7 @@ BarController.defaults = {
'borderColor',
'borderSkipped',
'borderWidth',
+ 'borderRadius',
'barPercentage',
'barThickness',
'base',
diff --git a/src/elements/element.bar.js b/src/elements/element.bar.js
index 5e4f04ab284..1375e9276ea 100644
--- a/src/elements/element.bar.js
+++ b/src/elements/element.bar.js
@@ -1,5 +1,6 @@
import Element from '../core/core.element';
-import {toTRBL} from '../helpers/helpers.options';
+import {toTRBL, toTRBLCorners} from '../helpers/helpers.options';
+import {PI, HALF_PI} from '../helpers/helpers.math';
/**
* Helper function to get the bounds of the bar regardless of the orientation
@@ -81,24 +82,46 @@ function parseBorderWidth(bar, maxW, maxH) {
};
}
+function parseBorderRadius(bar, maxW, maxH) {
+ const value = bar.options.borderRadius;
+ const o = toTRBLCorners(value);
+ const maxR = Math.min(maxW, maxH);
+ const skip = parseBorderSkipped(bar);
+
+ 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)
+ };
+}
+
function boundingRects(bar) {
const bounds = getBarBounds(bar);
const width = bounds.right - bounds.left;
const height = bounds.bottom - bounds.top;
const border = parseBorderWidth(bar, width / 2, height / 2);
+ const radius = parseBorderRadius(bar, width / 2, height / 2);
return {
outer: {
x: bounds.left,
y: bounds.top,
w: width,
- h: height
+ h: height,
+ radius
},
inner: {
x: bounds.left + border.l,
y: bounds.top + border.t,
w: width - border.l - border.r,
- h: height - border.t - border.b
+ h: height - border.t - border.b,
+ radius: {
+ topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
+ topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)),
+ bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
+ bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),
+ }
}
};
}
@@ -114,6 +137,52 @@ function inRange(bar, x, y, useFinalPosition) {
&& (skipY || y >= bounds.top && y <= bounds.bottom);
}
+function hasRadius(radius) {
+ return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
+}
+
+/**
+ * Add a path of a rectangle with rounded corners to the current sub-path
+ * @param {CanvasRenderingContext2D} ctx Context
+ * @param {*} rect Bounding rect
+ */
+function addRoundedRectPath(ctx, rect) {
+ const {x, y, w, h, radius} = rect;
+
+ // top left arc
+ ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, -HALF_PI, PI, true);
+
+ // line from top left to bottom left
+ ctx.lineTo(x, y + h - radius.bottomLeft);
+
+ // bottom left arc
+ ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true);
+
+ // line from bottom left to bottom right
+ ctx.lineTo(x + w - radius.bottomRight, y + h);
+
+ // bottom right arc
+ ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true);
+
+ // line from bottom right to top right
+ ctx.lineTo(x + w, y + radius.topRight);
+
+ // top right arc
+ ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true);
+
+ // line from top right to top left
+ ctx.lineTo(x + radius.topLeft, y);
+}
+
+/**
+ * Add a path of a rectangle to the current sub-path
+ * @param {CanvasRenderingContext2D} ctx Context
+ * @param {*} rect Bounding rect
+ */
+function addNormalRectPath(ctx, rect) {
+ ctx.rect(rect.x, rect.y, rect.w, rect.h);
+}
+
export default class BarElement extends Element {
constructor(cfg) {
@@ -133,20 +202,23 @@ export default class BarElement extends Element {
draw(ctx) {
const options = this.options;
const {inner, outer} = boundingRects(this);
+ const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
ctx.save();
if (outer.w !== inner.w || outer.h !== inner.h) {
ctx.beginPath();
- ctx.rect(outer.x, outer.y, outer.w, outer.h);
+ addRectPath(ctx, outer);
ctx.clip();
- ctx.rect(inner.x, inner.y, inner.w, inner.h);
+ addRectPath(ctx, inner);
ctx.fillStyle = options.borderColor;
ctx.fill('evenodd');
}
+ ctx.beginPath();
+ addRectPath(ctx, inner);
ctx.fillStyle = options.backgroundColor;
- ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
+ ctx.fill();
ctx.restore();
}
@@ -183,7 +255,8 @@ BarElement.id = 'bar';
*/
BarElement.defaults = {
borderSkipped: 'start',
- borderWidth: 0
+ borderWidth: 0,
+ borderRadius: 0
};
/**
diff --git a/src/helpers/helpers.options.js b/src/helpers/helpers.options.js
index ba61f5e5249..980ca15634c 100644
--- a/src/helpers/helpers.options.js
+++ b/src/helpers/helpers.options.js
@@ -64,6 +64,33 @@ export function toTRBL(value) {
};
}
+/**
+ * Converts the given value into a TRBL corners object (similar with css border-radius).
+ * @param {number|object} value - If a number, set the value to all TRBL corner components,
+ * else, if an object, use defined properties and sets undefined ones to 0.
+ * @returns {object} The TRBL corner values (topLeft, topRight, bottomLeft, bottomRight)
+ * @since 3.0.0
+ */
+export function toTRBLCorners(value) {
+ let tl, tr, bl, br;
+
+ if (isObject(value)) {
+ tl = numberOrZero(value.topLeft);
+ tr = numberOrZero(value.topRight);
+ bl = numberOrZero(value.bottomLeft);
+ br = numberOrZero(value.bottomRight);
+ } else {
+ tl = tr = bl = br = numberOrZero(value);
+ }
+
+ return {
+ topLeft: tl,
+ topRight: tr,
+ bottomLeft: bl,
+ bottomRight: br
+ };
+}
+
/**
* Converts the given value into a padding object with pre-computed width/height.
* @param {number|object} value - If a number, set the value to all TRBL component,
diff --git a/test/fixtures/controller.bar/border-radius.js b/test/fixtures/controller.bar/border-radius.js
new file mode 100644
index 00000000000..67c579ff09a
--- /dev/null
+++ b/test/fixtures/controller.bar/border-radius.js
@@ -0,0 +1,45 @@
+module.exports = {
+ threshold: 0.01,
+ config: {
+ type: 'bar',
+ data: {
+ labels: [0, 1, 2, 3, 4, 5],
+ datasets: [
+ {
+ // option in dataset
+ data: [0, 5, 10, null, -10, -5],
+ borderWidth: 2,
+ borderRadius: 5
+ },
+ {
+ // option in element (fallback)
+ data: [0, 5, 10, null, -10, -5],
+ borderSkipped: false,
+ borderRadius: Number.MAX_VALUE
+ }
+ ]
+ },
+ options: {
+ legend: false,
+ title: false,
+ indexAxis: 'y',
+ elements: {
+ bar: {
+ backgroundColor: '#AAAAAA80',
+ borderColor: '#80808080',
+ borderWidth: {bottom: 6, left: 15, top: 6, right: 15}
+ }
+ },
+ scales: {
+ x: {display: false},
+ y: {display: false}
+ }
+ }
+ },
+ options: {
+ canvas: {
+ height: 256,
+ width: 512
+ }
+ }
+};
diff --git a/test/fixtures/controller.bar/border-radius.png b/test/fixtures/controller.bar/border-radius.png
new file mode 100644
index 00000000000..68e7c0dd291
Binary files /dev/null and b/test/fixtures/controller.bar/border-radius.png differ
diff --git a/test/fixtures/controller.bar/horizontal-borders.png b/test/fixtures/controller.bar/horizontal-borders.png
index 1cd6913acfc..73adeead561 100644
Binary files a/test/fixtures/controller.bar/horizontal-borders.png and b/test/fixtures/controller.bar/horizontal-borders.png differ
diff --git a/test/specs/helpers.options.tests.js b/test/specs/helpers.options.tests.js
index f742b1bf548..6bad385c122 100644
--- a/test/specs/helpers.options.tests.js
+++ b/test/specs/helpers.options.tests.js
@@ -1,4 +1,4 @@
-const {toLineHeight, toPadding, toFont, resolve} = Chart.helpers; // from '../../src/helpers/helpers.options';
+const {toLineHeight, toPadding, toFont, resolve, toTRBLCorners} = Chart.helpers; // from '../../src/helpers/helpers.options';
describe('Chart.helpers.options', function() {
describe('toLineHeight', function() {
@@ -23,6 +23,43 @@ describe('Chart.helpers.options', function() {
});
});
+ describe('toTRBLCorners', function() {
+ it('should support number values', function() {
+ expect(toTRBLCorners(4)).toEqual(
+ {topLeft: 4, topRight: 4, bottomLeft: 4, bottomRight: 4});
+ expect(toTRBLCorners(4.5)).toEqual(
+ {topLeft: 4.5, topRight: 4.5, bottomLeft: 4.5, bottomRight: 4.5});
+ });
+ it('should support string values', function() {
+ expect(toTRBLCorners('4')).toEqual(
+ {topLeft: 4, topRight: 4, bottomLeft: 4, bottomRight: 4});
+ expect(toTRBLCorners('4.5')).toEqual(
+ {topLeft: 4.5, topRight: 4.5, bottomLeft: 4.5, bottomRight: 4.5});
+ });
+ it('should support object values', function() {
+ expect(toTRBLCorners({topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4})).toEqual(
+ {topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4});
+ expect(toTRBLCorners({topLeft: 1.5, topRight: 2.5, bottomLeft: 3.5, bottomRight: 4.5})).toEqual(
+ {topLeft: 1.5, topRight: 2.5, bottomLeft: 3.5, bottomRight: 4.5});
+ expect(toTRBLCorners({topLeft: '1', topRight: '2', bottomLeft: '3', bottomRight: '4'})).toEqual(
+ {topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4});
+ });
+ it('should fallback to 0 for invalid values', function() {
+ expect(toTRBLCorners({topLeft: 'foo', topRight: 'foo', bottomLeft: 'foo', bottomRight: 'foo'})).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners({topLeft: null, topRight: null, bottomLeft: null, bottomRight: null})).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners({})).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners('foo')).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners(null)).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ expect(toTRBLCorners(undefined)).toEqual(
+ {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0});
+ });
+ });
+
describe('toPadding', function() {
it ('should support number values', function() {
expect(toPadding(4)).toEqual(
diff --git a/types/elements/index.d.ts b/types/elements/index.d.ts
index ce3c200a517..2558c4d1c69 100644
--- a/types/elements/index.d.ts
+++ b/types/elements/index.d.ts
@@ -263,9 +263,24 @@ export interface IBarOptions extends ICommonOptions {
* @default 'start'
*/
borderSkipped: 'start' | 'end' | 'left' | 'right' | 'bottom' | 'top';
+
+ /**
+ * Border radius
+ * @default 0
+ */
+ borderRadius: number | IBorderRadius;
}
-export interface IBarHoverOptions extends ICommonHoverOptions {}
+export interface IBorderRadius {
+ topLeft: number;
+ topRight: number;
+ bottomLeft: number;
+ bottomRight: number;
+}
+
+export interface IBarHoverOptions extends ICommonHoverOptions {
+ hoverBorderRadius: number | IBorderRadius;
+}
export interface BarElement<
T extends IBarProps = IBarProps,