| [`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|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`
| `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.
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
var defaults = require('../core/core.defaults');
var Element = require('../core/core.element');
+var helpers = require('../helpers/index');
var defaultColor = defaults.global.defaultColor;
}
});
-function isVertical(bar) {
- return bar._view.width !== undefined;
+function isVertical(vm) {
+ return vm && vm.width !== undefined;
}
/**
* @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 {
};
}
-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() {
},
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 {
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);
},
{
// option in element (fallback)
data: [0, 5, -10, null],
+ },
+ {
+ // option in dataset
+ data: [0, 5, -10, null],
+ borderSkipped: false
}
]
},
--- /dev/null
+module.exports = {
+ config: {
+ type: 'bar',
+ data: {
+ labels: [0, 1, 2, 3, 4, 5],
+ datasets: [
+ {
+ // option in dataset
+ data: [0, 5, 10, null, -10, -5],
+ borderSkipped: false,
+ borderWidth: [
+ {},
+ {bottom: 1, left: 1, top: 1, right: 1},
+ {bottom: 1, left: 2, top: 1, right: 2},
+ {bottom: 1, left: 3, top: 1, right: 3},
+ {bottom: 1, left: 4, top: 1, right: 4},
+ {bottom: 1, left: 5, top: 1, right: 5}
+ ]
+ },
+ {
+ // option in element (fallback)
+ data: [0, 5, 10, null, -10, -5],
+ }
+ ]
+ },
+ options: {
+ legend: false,
+ title: false,
+ elements: {
+ rectangle: {
+ backgroundColor: 'transparent',
+ borderColor: '#80808080',
+ borderSkipped: false,
+ borderWidth: [
+ {bottom: 1, left: 5, top: 1, right: 5},
+ {bottom: 1, left: 4, top: 1, right: 4},
+ {bottom: 1, left: 3, top: 1, right: 3},
+ {bottom: 1, left: 2, top: 1, right: 2},
+ {bottom: 1, left: 1, top: 1, right: 1},
+ {}
+ ]
+ }
+ },
+ scales: {
+ xAxes: [{display: false}],
+ yAxes: [{display: false}]
+ }
+ }
+ },
+ options: {
+ canvas: {
+ height: 256,
+ width: 512
+ }
+ }
+};
--- /dev/null
+module.exports = {
+ config: {
+ type: 'bar',
+ data: {
+ labels: [0, 1, 2, 3, 4, 5],
+ datasets: [
+ {
+ // option in dataset
+ data: [0, 5, 10, null, -10, -5],
+ borderWidth: -2
+ },
+ {
+ // option in element (fallback)
+ data: [0, 5, 10, null, -10, -5],
+ },
+ {
+ data: [0, 5, 10, null, -10, -5],
+ borderWidth: {left: -5, top: -5, bottom: -5, right: -5},
+ borderSkipped: false
+ },
+ {
+ data: [0, 5, 10, null, -10, -5],
+ borderWidth: {}
+ },
+ ]
+ },
+ options: {
+ legend: false,
+ title: false,
+ elements: {
+ rectangle: {
+ backgroundColor: '#888',
+ borderColor: '#f00',
+ borderWidth: -4
+ }
+ },
+ scales: {
+ xAxes: [{display: false}],
+ yAxes: [{display: false}]
+ }
+ }
+ },
+ options: {
+ canvas: {
+ height: 256,
+ width: 512
+ }
+ }
+};
--- /dev/null
+module.exports = {
+ config: {
+ type: 'bar',
+ data: {
+ labels: [0, 1, 2, 3, 4, 5],
+ datasets: [
+ {
+ // option in dataset
+ data: [0, 5, 10, null, -10, -5],
+ borderSkipped: false,
+ borderWidth: {bottom: 1, left: 2, top: 3, right: 4}
+ },
+ {
+ // option in element (fallback)
+ data: [0, 5, 10, null, -10, -5],
+ }
+ ]
+ },
+ options: {
+ legend: false,
+ title: false,
+ elements: {
+ rectangle: {
+ backgroundColor: 'transparent',
+ borderColor: '#888',
+ borderSkipped: false,
+ borderWidth: {bottom: 4, left: 3, top: 2, right: 1}
+ }
+ },
+ scales: {
+ xAxes: [{display: false}],
+ yAxes: [{display: false}]
+ }
+ }
+ },
+ options: {
+ canvas: {
+ height: 256,
+ width: 512
+ }
+ }
+};
--- /dev/null
+module.exports = {
+ config: {
+ type: 'bar',
+ data: {
+ labels: [0, 1, 2, 3, 4, 5],
+ datasets: [
+ {
+ // option in dataset
+ data: [0, 5, 10, null, -10, -5],
+ borderSkipped: false,
+ borderWidth: function(ctx) {
+ var value = ctx.dataset.data[ctx.dataIndex] || 0;
+ return {top: Math.abs(value)};
+ }
+ },
+ {
+ // option in element (fallback)
+ data: [0, 5, 10, null, -10, -5]
+ }
+ ]
+ },
+ options: {
+ legend: false,
+ title: false,
+ elements: {
+ rectangle: {
+ backgroundColor: 'transparent',
+ borderColor: '#80808080',
+ borderSkipped: false,
+ borderWidth: function(ctx) {
+ return {left: ctx.dataIndex * 2};
+ }
+ }
+ },
+ scales: {
+ xAxes: [{display: false}],
+ yAxes: [
+ {
+ display: false,
+ ticks: {
+ beginAtZero: true
+ }
+ }
+ ]
+ }
+ }
+ },
+ options: {
+ canvas: {
+ height: 256,
+ width: 512
+ }
+ }
+};
--- /dev/null
+module.exports = {
+ config: {
+ type: 'bar',
+ data: {
+ labels: [0, 1, 3, 4],
+ datasets: [
+ {
+ data: [5, 20, -5, -20],
+ borderColor: '#ff0000'
+ }
+ ]
+ },
+ options: {
+ legend: false,
+ title: false,
+ layout: {
+ padding: {
+ left: 0,
+ right: 0,
+ top: 50,
+ bottom: 50
+ }
+ },
+ elements: {
+ rectangle: {
+ backgroundColor: '#00ff00',
+ borderWidth: 8
+ }
+ },
+ scales: {
+ xAxes: [{display: false}],
+ yAxes: [{display: false, ticks: {min: -10, max: 10}}]
+ }
+ }
+ },
+ options: {
+ canvas: {
+ height: 256,
+ width: 512
+ }
+ }
+};
--- /dev/null
+module.exports = {
+ threshold: 0.01,
+ config: {
+ type: 'horizontalBar',
+ data: {
+ labels: [0, 1, 2, 3, 4, 5],
+ datasets: [
+ {
+ // option in dataset
+ data: [0, 5, 10, null, -10, -5],
+ borderWidth: 2
+ },
+ {
+ // option in element (fallback)
+ data: [0, 5, 10, null, -10, -5],
+ borderSkipped: false,
+ }
+ ]
+ },
+ options: {
+ legend: false,
+ title: false,
+ elements: {
+ rectangle: {
+ backgroundColor: '#AAAAAA80',
+ borderColor: '#80808080',
+ borderWidth: {bottom: 6, left: 15, top: 6, right: 15}
+ }
+ },
+ scales: {
+ xAxes: [{display: false}],
+ yAxes: [{display: false}]
+ }
+ }
+ },
+ options: {
+ canvas: {
+ height: 256,
+ width: 512
+ }
+ }
+};
// Test the rectangle element
describe('Rectangle element tests', function() {
- it ('Should be constructed', function() {
+ it('Should be constructed', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
expect(rectangle._index).toBe(1);
});
- it ('Should correctly identify as in range', function() {
+ it('Should correctly identify as in range', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
expect(negativeRectangle.inRange(10, -5)).toBe(true);
});
- it ('should get the correct height', function() {
+ it('should get the correct height', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
expect(negativeRectangle.height()).toBe(5);
});
- it ('should get the correct tooltip position', function() {
+ it('should get the correct tooltip position', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
});
});
- it ('should get the correct vertical area', function() {
+ it('should get the correct vertical area', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
expect(rectangle.getArea()).toEqual(60);
});
- it ('should get the correct horizontal area', function() {
+ it('should get the correct horizontal area', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
expect(rectangle.getArea()).toEqual(40);
});
- it ('should get the center', function() {
+ it('should get the center', function() {
var rectangle = new Chart.elements.Rectangle({
_datasetIndex: 2,
_index: 1
expect(rectangle.getCenterPoint()).toEqual({x: 10, y: 7.5});
});
-
- it ('should draw correctly', function() {
- var mockContext = window.createMockContext();
- var rectangle = new Chart.elements.Rectangle({
- _datasetIndex: 2,
- _index: 1,
- _chart: {
- ctx: mockContext,
- }
- });
-
- // Attach a view object as if we were the controller
- rectangle._view = {
- backgroundColor: 'rgb(255, 0, 0)',
- base: 0,
- borderColor: 'rgb(0, 0, 255)',
- borderWidth: 1,
- ctx: mockContext,
- width: 4,
- x: 10,
- y: 15,
- };
-
- rectangle.draw();
-
- expect(mockContext.getCalls()).toEqual([{
- name: 'beginPath',
- args: [],
- }, {
- name: 'setFillStyle',
- args: ['rgb(255, 0, 0)']
- }, {
- name: 'setStrokeStyle',
- args: ['rgb(0, 0, 255)'],
- }, {
- name: 'setLineWidth',
- args: [1]
- }, {
- name: 'moveTo',
- args: [8.5, 0]
- }, {
- name: 'lineTo',
- args: [8.5, 14.5] // This is a minus bar. Not 15.5
- }, {
- name: 'lineTo',
- args: [11.5, 14.5]
- }, {
- name: 'lineTo',
- args: [11.5, 0]
- }, {
- name: 'fill',
- args: [],
- }, {
- name: 'stroke',
- args: []
- }]);
- });
-
- it ('should draw correctly with no stroke', function() {
- var mockContext = window.createMockContext();
- var rectangle = new Chart.elements.Rectangle({
- _datasetIndex: 2,
- _index: 1,
- _chart: {
- ctx: mockContext,
- }
- });
-
- // Attach a view object as if we were the controller
- rectangle._view = {
- backgroundColor: 'rgb(255, 0, 0)',
- base: 0,
- borderColor: 'rgb(0, 0, 255)',
- ctx: mockContext,
- width: 4,
- x: 10,
- y: 15,
- };
-
- rectangle.draw();
-
- expect(mockContext.getCalls()).toEqual([{
- name: 'beginPath',
- args: [],
- }, {
- name: 'setFillStyle',
- args: ['rgb(255, 0, 0)']
- }, {
- name: 'setStrokeStyle',
- args: ['rgb(0, 0, 255)'],
- }, {
- name: 'setLineWidth',
- args: [undefined]
- }, {
- name: 'moveTo',
- args: [8, 0]
- }, {
- name: 'lineTo',
- args: [8, 15]
- }, {
- name: 'lineTo',
- args: [12, 15]
- }, {
- name: 'lineTo',
- args: [12, 0]
- }, {
- name: 'fill',
- args: [],
- }]);
- });
-
- function testBorderSkipped(borderSkipped, expectedDrawCalls) {
- var mockContext = window.createMockContext();
- var rectangle = new Chart.elements.Rectangle({
- _chart: {ctx: mockContext}
- });
-
- // Attach a view object as if we were the controller
- rectangle._view = {
- borderSkipped: borderSkipped, // set tested 'borderSkipped' parameter
- ctx: mockContext,
- base: 0,
- width: 4,
- x: 10,
- y: 15,
- };
-
- rectangle.draw();
-
- var drawCalls = rectangle._view.ctx.getCalls().splice(4, 4);
- expect(drawCalls).toEqual(expectedDrawCalls);
- }
-
- it ('should draw correctly respecting "borderSkipped" == "bottom"', function() {
- testBorderSkipped ('bottom', [
- {name: 'moveTo', args: [8, 0]},
- {name: 'lineTo', args: [8, 15]},
- {name: 'lineTo', args: [12, 15]},
- {name: 'lineTo', args: [12, 0]},
- ]);
- });
-
- it ('should draw correctly respecting "borderSkipped" == "left"', function() {
- testBorderSkipped ('left', [
- {name: 'moveTo', args: [8, 15]},
- {name: 'lineTo', args: [12, 15]},
- {name: 'lineTo', args: [12, 0]},
- {name: 'lineTo', args: [8, 0]},
- ]);
- });
-
- it ('should draw correctly respecting "borderSkipped" == "top"', function() {
- testBorderSkipped ('top', [
- {name: 'moveTo', args: [12, 15]},
- {name: 'lineTo', args: [12, 0]},
- {name: 'lineTo', args: [8, 0]},
- {name: 'lineTo', args: [8, 15]},
- ]);
- });
-
- it ('should draw correctly respecting "borderSkipped" == "right"', function() {
- testBorderSkipped ('right', [
- {name: 'moveTo', args: [12, 0]},
- {name: 'lineTo', args: [8, 0]},
- {name: 'lineTo', args: [8, 15]},
- {name: 'lineTo', args: [12, 15]},
- ]);
- });
-
});