Skip to content

Commit 495c359

Browse files
authored
Add borderRadius to bar charts. Closes #7701 (#7951)
* Add helper to parse border radius options * feat: Implement borderRadius for bar charts * chore: add demo of bar charts with border radius * chore: document bar borderRadius * chore: update typescript with bar borderRadius property * fix horizontal borders test failing due to antialiasing * chore: Add border-radius visual test
1 parent 4ed650a commit 495c359

File tree

11 files changed

+366
-10
lines changed

11 files changed

+366
-10
lines changed

docs/docs/charts/bar.mdx

+9-1
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,13 @@ the color of the bars is generally set this way.
8383
| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'`
8484
| [`borderSkipped`](#borderskipped) | `string` | Yes | Yes | `'start'`
8585
| [`borderWidth`](#borderwidth) | <code>number&#124;object</code> | Yes | Yes | `0`
86+
| [`borderRadius`](#borderradius) | <code>number&#124;object</code> | Yes | Yes | `0`
8687
| [`clip`](#general) | <code>number&#124;object</code> | - | - | `undefined`
8788
| [`data`](#data-structure) | `object[]` | - | - | **required**
8889
| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
8990
| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
9091
| [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1`
92+
| [`hoverBorderRadius`](#interactions) | `number` | Yes | Yes | `0`
9193
| [`indexAxis`](#general) | `string` | `'x'` | The base axis for the dataset. Use `'y'` for horizontal bar.
9294
| [`label`](#general) | `string` | - | - | `''`
9395
| [`order`](#general) | `number` | - | - | `0`
@@ -116,13 +118,14 @@ The style of each bar can be controlled with the following properties:
116118
| `borderColor` | The bar border color.
117119
| [`borderSkipped`](#borderskipped) | The edge to skip when drawing bar.
118120
| [`borderWidth`](#borderwidth) | The bar border width (in pixels).
121+
| [`borderRadius`](#borderradius) | The bar border radius (in pixels).
119122
| `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}`
120123

121124
All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options.
122125

123126
#### borderSkipped
124127

125-
This setting is used to avoid drawing the bar stroke at the base of the fill.
128+
This setting is used to avoid drawing the bar stroke at the base of the fill, or disable the border radius.
126129
In general, this does not need to be changed except when creating chart types
127130
that derive from a bar chart.
128131

@@ -142,6 +145,10 @@ Options are:
142145

143146
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.
144147

148+
#### borderRadius
149+
150+
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.
151+
145152
### Interactions
146153

147154
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:
151158
| `hoverBackgroundColor` | The bar background color when hovered.
152159
| `hoverBorderColor` | The bar border color when hovered.
153160
| `hoverBorderWidth` | The bar border width when hovered (in pixels).
161+
| `hoverBorderRadius` | The bar border radius when hovered (in pixels).
154162

155163
All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options.
156164

samples/charts/bar/border-radius.html

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<!doctype html>
2+
<html>
3+
4+
<head>
5+
<title>Bar Chart</title>
6+
<script src="../../../dist/chart.min.js"></script>
7+
<script src="../../utils.js"></script>
8+
<style>
9+
canvas {
10+
-moz-user-select: none;
11+
-webkit-user-select: none;
12+
-ms-user-select: none;
13+
}
14+
</style>
15+
</head>
16+
17+
<body>
18+
<div id="container" style="width: 75%;">
19+
<canvas id="canvas"></canvas>
20+
</div>
21+
<button id="randomizeData">Randomize Data</button>
22+
<button id="addDataset">Add Dataset</button>
23+
<button id="removeDataset">Remove Dataset</button>
24+
<button id="addData">Add Data</button>
25+
<button id="removeData">Remove Data</button>
26+
<script>
27+
var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
28+
var color = Chart.helpers.color;
29+
var barChartData = {
30+
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
31+
datasets: [{
32+
label: 'Fully Rounded',
33+
backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(),
34+
borderColor: window.chartColors.red,
35+
borderWidth: 2,
36+
borderRadius: Number.MAX_VALUE,
37+
borderSkipped: false,
38+
data: [
39+
randomScalingFactor(),
40+
randomScalingFactor(),
41+
randomScalingFactor(),
42+
randomScalingFactor(),
43+
randomScalingFactor(),
44+
randomScalingFactor(),
45+
randomScalingFactor()
46+
]
47+
}, {
48+
label: 'Small Radius',
49+
backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(),
50+
borderColor: window.chartColors.blue,
51+
borderWidth: 2,
52+
borderRadius: 5,
53+
data: [
54+
randomScalingFactor(),
55+
randomScalingFactor(),
56+
randomScalingFactor(),
57+
randomScalingFactor(),
58+
randomScalingFactor(),
59+
randomScalingFactor(),
60+
randomScalingFactor()
61+
]
62+
}]
63+
64+
};
65+
66+
window.onload = function() {
67+
var ctx = document.getElementById('canvas').getContext('2d');
68+
window.myBar = new Chart(ctx, {
69+
type: 'bar',
70+
data: barChartData,
71+
options: {
72+
responsive: true,
73+
legend: {
74+
position: 'top',
75+
},
76+
title: {
77+
display: true,
78+
text: 'Chart.js Bar Chart'
79+
}
80+
}
81+
});
82+
83+
};
84+
85+
document.getElementById('randomizeData').addEventListener('click', function() {
86+
var zero = Math.random() < 0.2 ? true : false;
87+
barChartData.datasets.forEach(function(dataset) {
88+
dataset.data = dataset.data.map(function() {
89+
return zero ? 0.0 : randomScalingFactor();
90+
});
91+
92+
});
93+
window.myBar.update();
94+
});
95+
96+
var colorNames = Object.keys(window.chartColors);
97+
document.getElementById('addDataset').addEventListener('click', function() {
98+
var colorName = colorNames[barChartData.datasets.length % colorNames.length];
99+
var dsColor = window.chartColors[colorName];
100+
var newDataset = {
101+
label: 'Dataset ' + (barChartData.datasets.length + 1),
102+
backgroundColor: color(dsColor).alpha(0.5).rgbString(),
103+
borderColor: dsColor,
104+
borderWidth: 2,
105+
borderRadius: Math.floor(Math.random() * 20),
106+
data: []
107+
};
108+
109+
for (var index = 0; index < barChartData.labels.length; ++index) {
110+
newDataset.data.push(randomScalingFactor());
111+
}
112+
113+
barChartData.datasets.push(newDataset);
114+
window.myBar.update();
115+
});
116+
117+
document.getElementById('addData').addEventListener('click', function() {
118+
if (barChartData.datasets.length > 0) {
119+
var month = MONTHS[barChartData.labels.length % MONTHS.length];
120+
barChartData.labels.push(month);
121+
122+
for (var index = 0; index < barChartData.datasets.length; ++index) {
123+
barChartData.datasets[index].data.push(randomScalingFactor());
124+
}
125+
126+
window.myBar.update();
127+
}
128+
});
129+
130+
document.getElementById('removeDataset').addEventListener('click', function() {
131+
barChartData.datasets.pop();
132+
window.myBar.update();
133+
});
134+
135+
document.getElementById('removeData').addEventListener('click', function() {
136+
barChartData.labels.splice(-1, 1); // remove the label first
137+
138+
barChartData.datasets.forEach(function(dataset) {
139+
dataset.data.pop();
140+
});
141+
142+
window.myBar.update();
143+
});
144+
</script>
145+
</body>
146+
147+
</html>

samples/samples.js

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
}, {
2323
title: 'Floating',
2424
path: 'charts/bar/float.html'
25+
}, {
26+
title: 'Border Radius',
27+
path: 'charts/bar/border-radius.html'
2528
}]
2629
}, {
2730
title: 'Line charts',

src/controllers/controller.bar.js

+1
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ BarController.defaults = {
521521
'borderColor',
522522
'borderSkipped',
523523
'borderWidth',
524+
'borderRadius',
524525
'barPercentage',
525526
'barThickness',
526527
'base',

src/elements/element.bar.js

+80-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Element from '../core/core.element';
2-
import {toTRBL} from '../helpers/helpers.options';
2+
import {toTRBL, toTRBLCorners} from '../helpers/helpers.options';
3+
import {PI, HALF_PI} from '../helpers/helpers.math';
34

45
/**
56
* Helper function to get the bounds of the bar regardless of the orientation
@@ -81,24 +82,46 @@ function parseBorderWidth(bar, maxW, maxH) {
8182
};
8283
}
8384

85+
function parseBorderRadius(bar, maxW, maxH) {
86+
const value = bar.options.borderRadius;
87+
const o = toTRBLCorners(value);
88+
const maxR = Math.min(maxW, maxH);
89+
const skip = parseBorderSkipped(bar);
90+
91+
return {
92+
topLeft: skipOrLimit(skip.top || skip.left, o.topLeft, 0, maxR),
93+
topRight: skipOrLimit(skip.top || skip.right, o.topRight, 0, maxR),
94+
bottomLeft: skipOrLimit(skip.bottom || skip.left, o.bottomLeft, 0, maxR),
95+
bottomRight: skipOrLimit(skip.bottom || skip.right, o.bottomRight, 0, maxR)
96+
};
97+
}
98+
8499
function boundingRects(bar) {
85100
const bounds = getBarBounds(bar);
86101
const width = bounds.right - bounds.left;
87102
const height = bounds.bottom - bounds.top;
88103
const border = parseBorderWidth(bar, width / 2, height / 2);
104+
const radius = parseBorderRadius(bar, width / 2, height / 2);
89105

90106
return {
91107
outer: {
92108
x: bounds.left,
93109
y: bounds.top,
94110
w: width,
95-
h: height
111+
h: height,
112+
radius
96113
},
97114
inner: {
98115
x: bounds.left + border.l,
99116
y: bounds.top + border.t,
100117
w: width - border.l - border.r,
101-
h: height - border.t - border.b
118+
h: height - border.t - border.b,
119+
radius: {
120+
topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)),
121+
topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)),
122+
bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)),
123+
bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)),
124+
}
102125
}
103126
};
104127
}
@@ -114,6 +137,52 @@ function inRange(bar, x, y, useFinalPosition) {
114137
&& (skipY || y >= bounds.top && y <= bounds.bottom);
115138
}
116139

140+
function hasRadius(radius) {
141+
return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight;
142+
}
143+
144+
/**
145+
* Add a path of a rectangle with rounded corners to the current sub-path
146+
* @param {CanvasRenderingContext2D} ctx Context
147+
* @param {*} rect Bounding rect
148+
*/
149+
function addRoundedRectPath(ctx, rect) {
150+
const {x, y, w, h, radius} = rect;
151+
152+
// top left arc
153+
ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, -HALF_PI, PI, true);
154+
155+
// line from top left to bottom left
156+
ctx.lineTo(x, y + h - radius.bottomLeft);
157+
158+
// bottom left arc
159+
ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true);
160+
161+
// line from bottom left to bottom right
162+
ctx.lineTo(x + w - radius.bottomRight, y + h);
163+
164+
// bottom right arc
165+
ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true);
166+
167+
// line from bottom right to top right
168+
ctx.lineTo(x + w, y + radius.topRight);
169+
170+
// top right arc
171+
ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true);
172+
173+
// line from top right to top left
174+
ctx.lineTo(x + radius.topLeft, y);
175+
}
176+
177+
/**
178+
* Add a path of a rectangle to the current sub-path
179+
* @param {CanvasRenderingContext2D} ctx Context
180+
* @param {*} rect Bounding rect
181+
*/
182+
function addNormalRectPath(ctx, rect) {
183+
ctx.rect(rect.x, rect.y, rect.w, rect.h);
184+
}
185+
117186
export default class BarElement extends Element {
118187

119188
constructor(cfg) {
@@ -133,20 +202,23 @@ export default class BarElement extends Element {
133202
draw(ctx) {
134203
const options = this.options;
135204
const {inner, outer} = boundingRects(this);
205+
const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath;
136206

137207
ctx.save();
138208

139209
if (outer.w !== inner.w || outer.h !== inner.h) {
140210
ctx.beginPath();
141-
ctx.rect(outer.x, outer.y, outer.w, outer.h);
211+
addRectPath(ctx, outer);
142212
ctx.clip();
143-
ctx.rect(inner.x, inner.y, inner.w, inner.h);
213+
addRectPath(ctx, inner);
144214
ctx.fillStyle = options.borderColor;
145215
ctx.fill('evenodd');
146216
}
147217

218+
ctx.beginPath();
219+
addRectPath(ctx, inner);
148220
ctx.fillStyle = options.backgroundColor;
149-
ctx.fillRect(inner.x, inner.y, inner.w, inner.h);
221+
ctx.fill();
150222

151223
ctx.restore();
152224
}
@@ -183,7 +255,8 @@ BarElement.id = 'bar';
183255
*/
184256
BarElement.defaults = {
185257
borderSkipped: 'start',
186-
borderWidth: 0
258+
borderWidth: 0,
259+
borderRadius: 0
187260
};
188261

189262
/**

src/helpers/helpers.options.js

+27
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,33 @@ export function toTRBL(value) {
6464
};
6565
}
6666

67+
/**
68+
* Converts the given value into a TRBL corners object (similar with css border-radius).
69+
* @param {number|object} value - If a number, set the value to all TRBL corner components,
70+
* else, if an object, use defined properties and sets undefined ones to 0.
71+
* @returns {object} The TRBL corner values (topLeft, topRight, bottomLeft, bottomRight)
72+
* @since 3.0.0
73+
*/
74+
export function toTRBLCorners(value) {
75+
let tl, tr, bl, br;
76+
77+
if (isObject(value)) {
78+
tl = numberOrZero(value.topLeft);
79+
tr = numberOrZero(value.topRight);
80+
bl = numberOrZero(value.bottomLeft);
81+
br = numberOrZero(value.bottomRight);
82+
} else {
83+
tl = tr = bl = br = numberOrZero(value);
84+
}
85+
86+
return {
87+
topLeft: tl,
88+
topRight: tr,
89+
bottomLeft: bl,
90+
bottomRight: br
91+
};
92+
}
93+
6794
/**
6895
* Converts the given value into a padding object with pre-computed width/height.
6996
* @param {number|object} value - If a number, set the value to all TRBL component,

0 commit comments

Comments
 (0)