| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'`
| [`borderSkipped`](#borderskipped) | `string` | Yes | Yes | `'start'`
| [`borderWidth`](#borderwidth) | <code>number|object</code> | Yes | Yes | `0`
+| [`borderRadius`](#borderradius) | <code>number|object</code> | Yes | Yes | `0`
| [`clip`](#general) | <code>number|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`
| `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.
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:
| `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.
--- /dev/null
+<!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>
}, {
title: 'Floating',
path: 'charts/bar/float.html'
+ }, {
+ title: 'Border Radius',
+ path: 'charts/bar/border-radius.html'
}]
}, {
title: 'Line charts',
'borderColor',
'borderSkipped',
'borderWidth',
+ 'borderRadius',
'barPercentage',
'barThickness',
'base',
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
};
}
+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)),
+ }
}
};
}
&& (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) {
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();
}
*/
BarElement.defaults = {
borderSkipped: 'start',
- borderWidth: 0
+ borderWidth: 0,
+ borderRadius: 0
};
/**
};
}
+/**
+ * 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,
--- /dev/null
+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
+ }
+ }
+};
-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() {
});
});
+ 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(
* @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,