Skip to content

Commit

Permalink
Add support for per side border width for rectangle (chartjs#6077)
Browse files Browse the repository at this point in the history
  • Loading branch information
kurkle authored and simonbrunel committed Feb 25, 2019
1 parent 2d64e4d commit 41bbede
Show file tree
Hide file tree
Showing 19 changed files with 420 additions and 303 deletions.
11 changes: 9 additions & 2 deletions docs/charts/bar.md
Expand Up @@ -71,7 +71,7 @@ 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`
| [`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 +97,7 @@ 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).

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

Expand All @@ -107,11 +107,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.

### Interactions

Expand Down
239 changes: 114 additions & 125 deletions src/elements/element.rectangle.js
Expand Up @@ -2,6 +2,7 @@

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

var defaultColor = defaults.global.defaultColor;

Expand All @@ -16,8 +17,8 @@ defaults._set('global', {
}
});

function isVertical(bar) {
return bar._view.width !== undefined;
function isVertical(vm) {
return vm && vm.width !== undefined;
}

/**
Expand All @@ -26,24 +27,21 @@ function isVertical(bar) {
* @return {Bounds} bounds of the bar
* @private
*/
function getBarBounds(bar) {
var vm = bar._view;
var x1, x2, y1, y2;

if (isVertical(bar)) {
// vertical
var halfWidth = vm.width / 2;
x1 = vm.x - halfWidth;
x2 = vm.x + halfWidth;
function getBarBounds(vm) {
var x1, x2, y1, y2, half;

if (isVertical(vm)) {
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,96 +52,107 @@ 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;

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';
}
function swap(orig, v1, v2) {
return orig === v1 ? v2 : orig === v2 ? v1 : orig;
}

// 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;
}
// not become a horizontal line?
if (borderTop !== borderBottom) {
left = borderLeft;
right = borderRight;
}
}
function parseBorderSkipped(vm) {
var edge = vm.borderSkipped;
var res = {};

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;
}
if (!edge) {
return res;
}

function cornerAt(index) {
return corners[(startCorner + index) % 4];
if (vm.horizontal) {
if (vm.base > vm.x) {
edge = swap(edge, 'left', 'right');
}
} else if (vm.base < vm.y) {
edge = swap(edge, 'bottom', 'top');
}

res[edge] = true;
return res;
}

function parseBorderWidth(vm, maxW, maxH) {
var value = vm.borderWidth;
var skip = parseBorderSkipped(vm);
var t, r, b, l;

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 {
t: skip.top || (t < 0) ? 0 : t > maxH ? maxH : t,
r: skip.right || (r < 0) ? 0 : r > maxW ? maxW : r,
b: skip.bottom || (b < 0) ? 0 : b > maxH ? maxH : b,
l: skip.left || (l < 0) ? 0 : l > maxW ? maxW : l
};
}

// Draw rectangle from 'startCorner'
var corner = cornerAt(0);
ctx.moveTo(corner[0], corner[1]);
function boundingRects(vm) {
var bounds = getBarBounds(vm);
var width = bounds.right - bounds.left;
var height = bounds.bottom - bounds.top;
var border = parseBorderWidth(vm, width / 2, height / 2);

for (var i = 1; i < 4; i++) {
corner = cornerAt(i);
ctx.lineTo(corner[0], corner[1]);
return {
outer: {
x: bounds.left,
y: bounds.top,
w: width,
h: height
},
inner: {
x: bounds.left + border.l,
y: bounds.top + border.t,
w: width - border.l - border.r,
h: height - border.t - border.b
}
};
}

function inRange(vm, x, y) {
var skipX = x === null;
var skipY = y === null;
var bounds = !vm || (skipX && skipY) ? false : getBarBounds(vm);

return bounds
&& (skipX || x >= bounds.left && x <= bounds.right)
&& (skipY || y >= bounds.top && y <= bounds.bottom);
}

module.exports = Element.extend({
draw: function() {
var ctx = this._chart.ctx;
var vm = this._view;
var rects = boundingRects(vm);
var outer = rects.outer;
var inner = rects.inner;

ctx.fillStyle = vm.backgroundColor;
ctx.fillRect(outer.x, outer.y, outer.w, outer.h);

ctx.fill();
if (borderWidth) {
ctx.stroke();
if (outer.w === inner.w && outer.h === inner.h) {
return;
}

ctx.save();
ctx.beginPath();
ctx.rect(outer.x, outer.y, outer.w, outer.h);
ctx.clip();
ctx.fillStyle = vm.borderColor;
ctx.rect(inner.x, inner.y, inner.w, inner.h);
ctx.fill('evenodd');
ctx.restore();
},

height: function() {
Expand All @@ -152,48 +161,28 @@ module.exports = Element.extend({
},

inRange: function(mouseX, mouseY) {
var inRange = false;

if (this._view) {
var bounds = getBarBounds(this);
inRange = mouseX >= bounds.left && mouseX <= bounds.right && mouseY >= bounds.top && mouseY <= bounds.bottom;
}

return inRange;
return inRange(this._view, mouseX, mouseY);
},

inLabelRange: function(mouseX, mouseY) {
var me = this;
if (!me._view) {
return false;
}

var inRange = false;
var bounds = getBarBounds(me);

if (isVertical(me)) {
inRange = mouseX >= bounds.left && mouseX <= bounds.right;
} else {
inRange = mouseY >= bounds.top && mouseY <= bounds.bottom;
}

return inRange;
var vm = this._view;
return isVertical(vm)
? inRange(vm, mouseX, null)
: inRange(vm, null, mouseY);
},

inXRange: function(mouseX) {
var bounds = getBarBounds(this);
return mouseX >= bounds.left && mouseX <= bounds.right;
return inRange(this._view, mouseX, null);
},

inYRange: function(mouseY) {
var bounds = getBarBounds(this);
return mouseY >= bounds.top && mouseY <= bounds.bottom;
return inRange(this._view, null, mouseY);
},

getCenterPoint: function() {
var vm = this._view;
var x, y;
if (isVertical(this)) {
if (isVertical(vm)) {
x = vm.x;
y = (vm.y + vm.base) / 2;
} else {
Expand All @@ -207,7 +196,7 @@ module.exports = Element.extend({
getArea: function() {
var vm = this._view;

return isVertical(this)
return isVertical(vm)
? vm.width * Math.abs(vm.y - vm.base)
: vm.height * Math.abs(vm.x - vm.base);
},
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/controller.bar/borderSkipped/value.js
Expand Up @@ -22,6 +22,11 @@ module.exports = {
{
// option in element (fallback)
data: [0, 5, -10, null],
},
{
// option in dataset
data: [0, 5, -10, null],
borderSkipped: false
}
]
},
Expand Down
Binary file modified test/fixtures/controller.bar/borderSkipped/value.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 41bbede

Please sign in to comment.