Skip to content
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

Allow configuration of borderWidth as object #6047

Closed
wants to merge 16 commits into from
Closed
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
13 changes: 11 additions & 2 deletions docs/charts/bar.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ the color of the bars is generally set this way.
| [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'`
| [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'`
| [`borderSkipped`](#borderskipped) | `string` | Yes | Yes | `'bottom'`
| [`borderWidth`](#styling) | `number` | Yes | Yes | `0`
| [`borderWidth`](#borderwidth) | <code>number&#124;object</code> | Yes | Yes | `0`
| [`borderRadius`](#styling) | `number` | Yes | Yes | `0`
| [`data`](#data-structure) | `object[]` | - | - | **required**
| [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | - | Yes | `undefined`
| [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | - | Yes | `undefined`
Expand All @@ -97,7 +98,8 @@ The style of each bar can be controlled with the following properties:
| `backgroundColor` | The bar background color.
| `borderColor` | The bar border color.
| [`borderSkipped`](#borderskipped) | The edge to skip when drawing bar.
| `borderWidth` | The bar border width (in pixels).
| [`borderWidth`](#borderwidth) | The bar border width (in pixels).
| `borderRadius` | Rounding corners of outer border edge.

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

Expand All @@ -107,11 +109,18 @@ This setting is used to avoid drawing the bar stroke at the base of the fill.
In general, this does not need to be changed except when creating chart types
that derive from a bar chart.

**Note:** for negative bars in vertical chart, `top` and `bottom` are flipped. Same goes for `left` and `right` in horizontal chart.

Options are:
* `'bottom'`
* `'left'`
* `'top'`
* `'right'`
* `false`

#### borderWidth

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.
benmccann marked this conversation as resolved.
Show resolved Hide resolved

### Interactions

Expand Down
4 changes: 3 additions & 1 deletion src/controllers/controller.bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ module.exports = DatasetController.extend({
borderColor: options.borderColor,
borderSkipped: options.borderSkipped,
borderWidth: options.borderWidth,
borderRadius: options.borderRadius,
datasetLabel: dataset.label,
label: me.chart.data.labels[index]
};
Expand Down Expand Up @@ -399,7 +400,8 @@ module.exports = DatasetController.extend({
'backgroundColor',
'borderColor',
'borderSkipped',
'borderWidth'
'borderWidth',
'borderRadius'
];

for (i = 0, ilen = keys.length; i < ilen; ++i) {
Expand Down
275 changes: 190 additions & 85 deletions src/elements/element.rectangle.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

var defaults = require('../core/core.defaults');
var Element = require('../core/core.element');
var helpers = require('../helpers/index');

var defaultColor = defaults.global.defaultColor;
var valueOrDefault = helpers.valueOrDefault;

defaults._set('global', {
elements: {
Expand All @@ -28,22 +30,20 @@ function isVertical(bar) {
*/
function getBarBounds(bar) {
var vm = bar._view;
var x1, x2, y1, y2;
var x1, x2, y1, y2, half;

if (isVertical(bar)) {
// vertical
var halfWidth = vm.width / 2;
x1 = vm.x - halfWidth;
x2 = vm.x + halfWidth;
half = vm.width / 2;
x1 = vm.x - half;
x2 = vm.x + half;
y1 = Math.min(vm.y, vm.base);
y2 = Math.max(vm.y, vm.base);
} else {
// horizontal bar
var halfHeight = vm.height / 2;
half = vm.height / 2;
x1 = Math.min(vm.x, vm.base);
x2 = Math.max(vm.x, vm.base);
y1 = vm.y - halfHeight;
y2 = vm.y + halfHeight;
y1 = vm.y - half;
y2 = vm.y + half;
}

return {
Expand All @@ -54,95 +54,200 @@ function getBarBounds(bar) {
};
}

module.exports = Element.extend({
draw: function() {
var ctx = this._chart.ctx;
var vm = this._view;
var left, right, top, bottom, signX, signY, borderSkipped;
var borderWidth = vm.borderWidth;
function parseBorderWidth(value, skipped, maxWidth, maxHeight) {
var t, r, b, l;

if (!vm.horizontal) {
// bar
left = vm.x - vm.width / 2;
right = vm.x + vm.width / 2;
top = vm.y;
bottom = vm.base;
signX = 1;
signY = bottom > top ? 1 : -1;
borderSkipped = vm.borderSkipped || 'bottom';
} else {
// horizontal bar
left = vm.base;
right = vm.x;
top = vm.y - vm.height / 2;
bottom = vm.y + vm.height / 2;
signX = right > left ? 1 : -1;
signY = 1;
borderSkipped = vm.borderSkipped || 'left';
if (helpers.isObject(value)) {
t = +value.top || 0;
r = +value.right || 0;
b = +value.bottom || 0;
l = +value.left || 0;
} else {
t = r = b = l = +value || 0;
}

return {
top: Math.min(maxHeight, skipped === 'top' ? 0 : t),
right: Math.min(maxWidth, skipped === 'right' ? 0 : r),
bottom: Math.min(maxHeight, skipped === 'bottom' ? 0 : b),
left: Math.min(maxWidth, skipped === 'left' ? 0 : l)
};
}

function flip(orig, v1, v2) {
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
}

function parseBorderSkipped(bar) {
var vm = bar._view;
var vertical = isVertical(bar);
var borderSkipped = valueOrDefault(vm.borderSkipped, vertical ? 'bottom' : 'left');

if (vertical && vm.base < vm.y) {
borderSkipped = flip(borderSkipped, 'bottom', 'top');
}
if (!vertical && vm.base > vm.x) {
borderSkipped = flip(borderSkipped, 'left', 'right');
}
return borderSkipped;
}

// border can be in 1 or 2 sections. build corners for each border in each section
function buildBorderSections(bounds, inner, border) {
var top = 'top';
var left = 'left';
var right = 'right';
var bottom = 'bottom';
var borderKeys = [top, right, bottom, left];
var corners = [[left, top], [right, top], [right, bottom], [left, bottom]];
var sections = [];
var current = [];
var borderCount = 0;
var startIdx = 0;
var i, idx, nextIdx, c1, c2;

// start from a skipped border, so we can close the path
for (i = 0; i < 4; ++i) {
if (!border[borderKeys[i]]) {
startIdx = i;
break;
}
}

// Canvas doesn't allow us to stroke inside the width so we can
// adjust the sizes to fit if we're setting a stroke on the line
if (borderWidth) {
// borderWidth shold be less than bar width and bar height.
var barSize = Math.min(Math.abs(left - right), Math.abs(top - bottom));
borderWidth = borderWidth > barSize ? barSize : borderWidth;
var halfStroke = borderWidth / 2;
// Adjust borderWidth when bar top position is near vm.base(zero).
var borderLeft = left + (borderSkipped !== 'left' ? halfStroke * signX : 0);
var borderRight = right + (borderSkipped !== 'right' ? -halfStroke * signX : 0);
var borderTop = top + (borderSkipped !== 'top' ? halfStroke * signY : 0);
var borderBottom = bottom + (borderSkipped !== 'bottom' ? -halfStroke * signY : 0);
// not become a vertical line?
if (borderLeft !== borderRight) {
top = borderTop;
bottom = borderBottom;
for (i = 0; i < 4; ++i) {
idx = (i + startIdx) % 4;
if (border[borderKeys[idx]]) {
nextIdx = (i + startIdx + 1) % 4;
c1 = corners[idx];
c2 = corners[nextIdx];
// if this is first border, or previous border is skipped, add start corner
// previous index is found by adding 3 to keep the index positive in all cases
if (i === 0 || !border[borderKeys[(i + startIdx + 3) % 4]]) {
if (current.length) {
sections.push({count: borderCount, corners: current});
current = [];
borderCount = 0;
}
current.push({x1: bounds[c1[0]], y1: bounds[c1[1]], x2: inner[c1[0]], y2: inner[c1[1]]});
}
// not become a horizontal line?
if (borderTop !== borderBottom) {
left = borderLeft;
right = borderRight;
// if this is not last border or next border is skipped, add end corner
if (i < 3 || !border[borderKeys[nextIdx]]) {
current.push({x1: bounds[c2[0]], y1: bounds[c2[1]], x2: inner[c2[0]], y2: inner[c2[1]]});
}
borderCount++;
}
}
if (current.length) {
sections.push({count: borderCount, corners: current});
}
return sections;
}

ctx.beginPath();
ctx.fillStyle = vm.backgroundColor;
ctx.strokeStyle = vm.borderColor;
ctx.lineWidth = borderWidth;

// Corner points, from bottom-left to bottom-right clockwise
// | 1 2 |
// | 0 3 |
var corners = [
[left, bottom],
[left, top],
[right, top],
[right, bottom]
];

// Find first (starting) corner with fallback to 'bottom'
var borders = ['bottom', 'left', 'top', 'right'];
var startCorner = borders.indexOf(borderSkipped, 0);
if (startCorner === -1) {
startCorner = 0;
function drawBorderSection(ctx, section, borderRadius) {
ctx.beginPath();

var corners = section.corners;
var borderCount = section.count;
var cornerCount = corners.length;
var startCorner = corners[corners.length - 1];
var endCorner = corners[0];
var ilen = borderCount === 4 ? 5 : borderCount;
var i;

// if all borders are drawn and we have a border radius,
// move starting point out ouf the rounded section
if (borderRadius && borderCount === 4) {
// clone so original corner stays intact
endCorner = {x1: endCorner.x1, y1: endCorner.y1, x2: endCorner.x2, y2: endCorner.y2};
if (endCorner.x1 === startCorner.x1) {
endCorner.x1 += endCorner.x2 > endCorner.x1 ? borderRadius : -borderRadius;
}

function cornerAt(index) {
return corners[(startCorner + index) % 4];
if (endCorner.y1 === startCorner.y1) {
endCorner.y1 += endCorner.y2 > endCorner.y1 ? borderRadius : -borderRadius;
}
}

// draw outer edge first
ctx.moveTo(endCorner.x1, endCorner.y1);
for (i = 0; i < ilen; i++) {
startCorner = endCorner;
endCorner = corners[(i + 1) % cornerCount];
ctx.arcTo(startCorner.x1, startCorner.y1, endCorner.x1, endCorner.y1, borderRadius);
}

// in between edges
if (borderCount < 4) {
ctx.lineTo(endCorner.x1, endCorner.y1);
ctx.lineTo(endCorner.x2, endCorner.y2);
} else {
// all corners are drawn, close the outer edge
startCorner = endCorner;
endCorner = corners[0];
ctx.closePath();
ctx.moveTo(endCorner.x2, endCorner.y2);
}

// draw the inner edge in reverse direction
for (; i > 0; --i) {
startCorner = endCorner;
endCorner = corners[(i - 1) % cornerCount];
ctx.arcTo(startCorner.x2, startCorner.y2, endCorner.x2, endCorner.y2, 0);
}
ctx.lineTo(endCorner.x2, endCorner.y2);
ctx.closePath();
ctx.fill('evenodd');
}
kurkle marked this conversation as resolved.
Show resolved Hide resolved

// Draw rectangle from 'startCorner'
var corner = cornerAt(0);
ctx.moveTo(corner[0], corner[1]);
module.exports = Element.extend({
draw: function() {
var me = this;
var ctx = me._chart.ctx;
var vm = me._view;
var borderWidth = vm.borderWidth;
var bounds = getBarBounds(me);
var left = bounds.left;
var top = bounds.top;
var right = bounds.right;
var bottom = bounds.bottom;
var width = right - left;
var height = bottom - top;
var borderRadius = vm.borderRadius || 0;
var inner, i, sections;

for (var i = 1; i < 4; i++) {
corner = cornerAt(i);
ctx.lineTo(corner[0], corner[1]);
ctx.fillStyle = vm.backgroundColor;

if (!borderWidth) {
ctx.fillRect(left, top, width, height);
return;
}

ctx.fill();
if (borderWidth) {
ctx.stroke();
borderWidth = parseBorderWidth(
borderWidth,
parseBorderSkipped(me),
width / 2,
height / 2);

// limit borderRadius to smallest non zero borderWidth
borderRadius = Math.min(
borderWidth.left || borderRadius,
borderWidth.top || borderRadius,
borderWidth.right || borderRadius,
borderWidth.bottom || borderRadius
);

inner = {
left: left + borderWidth.left,
top: top + borderWidth.top,
right: right - borderWidth.right,
bottom: bottom - borderWidth.bottom
};

ctx.fillRect(inner.left, inner.top, inner.right - inner.left, inner.bottom - inner.top);

ctx.fillStyle = vm.borderColor;
sections = buildBorderSections(bounds, inner, borderWidth);
for (i = 0; i < sections.length; i++) {
drawBorderSection(ctx, sections[i], borderRadius);
}
},

Expand Down
Loading