Skip to content

feat: add borderRadius to bar charts. Closes #7701 #7951

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

Merged
merged 8 commits into from
Oct 26, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion docs/docs/charts/bar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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) | <code>number&#124;object</code> | Yes | Yes | `0`
| [`borderRadius`](#borderradius) | <code>number&#124;object</code> | Yes | Yes | `0`
| [`clip`](#general) | <code>number&#124;object</code> | - | - | `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`
Expand Down Expand Up @@ -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.

Expand All @@ -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:
Expand All @@ -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.

Expand Down
147 changes: 147 additions & 0 deletions samples/charts/bar/border-radius.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<!doctype html>
<html>

<head>
<title>Bar Chart</title>
<script src="../../../dist/chart.min.js"></script>
<script src="../../utils.js"></script>
<style>
canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
</head>

<body>
<div id="container" style="width: 75%;">
<canvas id="canvas"></canvas>
</div>
<button id="randomizeData">Randomize Data</button>
<button id="addDataset">Add Dataset</button>
<button id="removeDataset">Remove Dataset</button>
<button id="addData">Add Data</button>
<button id="removeData">Remove Data</button>
<script>
var MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
var color = Chart.helpers.color;
var barChartData = {
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [{
label: 'Fully Rounded',
backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(),
borderColor: window.chartColors.red,
borderWidth: 2,
borderRadius: Number.MAX_VALUE,
borderSkipped: false,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
]
}, {
label: 'Small Radius',
backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(),
borderColor: window.chartColors.blue,
borderWidth: 2,
borderRadius: 5,
data: [
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor(),
randomScalingFactor()
]
}]

};

window.onload = function() {
var ctx = document.getElementById('canvas').getContext('2d');
window.myBar = new Chart(ctx, {
type: 'bar',
data: barChartData,
options: {
responsive: true,
legend: {
position: 'top',
},
title: {
display: true,
text: 'Chart.js Bar Chart'
}
}
});

};

document.getElementById('randomizeData').addEventListener('click', function() {
var zero = Math.random() < 0.2 ? true : false;
barChartData.datasets.forEach(function(dataset) {
dataset.data = dataset.data.map(function() {
return zero ? 0.0 : randomScalingFactor();
});

});
window.myBar.update();
});

var colorNames = Object.keys(window.chartColors);
document.getElementById('addDataset').addEventListener('click', function() {
var colorName = colorNames[barChartData.datasets.length % colorNames.length];
var dsColor = window.chartColors[colorName];
var newDataset = {
label: 'Dataset ' + (barChartData.datasets.length + 1),
backgroundColor: color(dsColor).alpha(0.5).rgbString(),
borderColor: dsColor,
borderWidth: 2,
borderRadius: Math.floor(Math.random() * 20),
data: []
};

for (var index = 0; index < barChartData.labels.length; ++index) {
newDataset.data.push(randomScalingFactor());
}

barChartData.datasets.push(newDataset);
window.myBar.update();
});

document.getElementById('addData').addEventListener('click', function() {
if (barChartData.datasets.length > 0) {
var month = MONTHS[barChartData.labels.length % MONTHS.length];
barChartData.labels.push(month);

for (var index = 0; index < barChartData.datasets.length; ++index) {
barChartData.datasets[index].data.push(randomScalingFactor());
}

window.myBar.update();
}
});

document.getElementById('removeDataset').addEventListener('click', function() {
barChartData.datasets.pop();
window.myBar.update();
});

document.getElementById('removeData').addEventListener('click', function() {
barChartData.labels.splice(-1, 1); // remove the label first

barChartData.datasets.forEach(function(dataset) {
dataset.data.pop();
});

window.myBar.update();
});
</script>
</body>

</html>
3 changes: 3 additions & 0 deletions samples/samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
}, {
title: 'Floating',
path: 'charts/bar/float.html'
}, {
title: 'Border Radius',
path: 'charts/bar/border-radius.html'
}]
}, {
title: 'Line charts',
Expand Down
1 change: 1 addition & 0 deletions src/controllers/controller.bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ BarController.defaults = {
'borderColor',
'borderSkipped',
'borderWidth',
'borderRadius',
'barPercentage',
'barThickness',
'base',
Expand Down
87 changes: 80 additions & 7 deletions src/elements/element.bar.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)),
}
}
};
}
Expand All @@ -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) {
Expand All @@ -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();
}
Expand Down Expand Up @@ -183,7 +255,8 @@ BarElement.id = 'bar';
*/
BarElement.defaults = {
borderSkipped: 'start',
borderWidth: 0
borderWidth: 0,
borderRadius: 0
};

/**
Expand Down
27 changes: 27 additions & 0 deletions src/helpers/helpers.options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading