Skip to content

Commit

Permalink
Add support for 'inner' border for arc elements (chartjs#5841)
Browse files Browse the repository at this point in the history
  • Loading branch information
nagix authored and simonbrunel committed Dec 18, 2018
1 parent 3e29d7d commit 48a125f
Show file tree
Hide file tree
Showing 21 changed files with 511 additions and 68 deletions.
21 changes: 15 additions & 6 deletions docs/charts/doughnut.md
Expand Up @@ -55,12 +55,21 @@ The doughnut/pie chart allows a number of properties to be specified for each da

| Name | Type | Description
| ---- | ---- | -----------
| `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors).
| `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors).
| `borderWidth` | `Number[]` | The border width of the arcs in the dataset.
| `hoverBackgroundColor` | `Color[]` | The fill colour of the arcs when hovered.
| `hoverBorderColor` | `Color[]` | The stroke colour of the arcs when hovered.
| `hoverBorderWidth` | `Number[]` | The stroke width of the arcs when hovered.
| `backgroundColor` | `Color/Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors).
| `borderColor` | `Color/Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors).
| `borderWidth` | `Number/Number[]` | The border width of the arcs in the dataset.
| `borderAlign` | `String/String[]` | The border alignment of the arcs in the dataset. [more...](#border-alignment)
| `hoverBackgroundColor` | `Color/Color[]` | The fill colour of the arcs when hovered.
| `hoverBorderColor` | `Color/Color[]` | The stroke colour of the arcs when hovered.
| `hoverBorderWidth` | `Number/Number[]` | The stroke width of the arcs when hovered.

### Border Alignment

The following values are supported for `borderAlign`.
* `'center'` (default)
* `'inner'`

When `'center'` is set, the borders of arcs next to each other will overlap. When `'inner'` is set, it is guaranteed that all the borders are not overlap.

## Config Options

Expand Down
21 changes: 15 additions & 6 deletions docs/charts/polar.md
Expand Up @@ -46,12 +46,21 @@ The following options can be included in a polar area chart dataset to configure

| Name | Type | Description
| ---- | ---- | -----------
| `backgroundColor` | `Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors).
| `borderColor` | `Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors).
| `borderWidth` | `Number[]` | The border width of the arcs in the dataset.
| `hoverBackgroundColor` | `Color[]` | The fill colour of the arcs when hovered.
| `hoverBorderColor` | `Color[]` | The stroke colour of the arcs when hovered.
| `hoverBorderWidth` | `Number[]` | The stroke width of the arcs when hovered.
| `backgroundColor` | `Color/Color[]` | The fill color of the arcs in the dataset. See [Colors](../general/colors.md#colors).
| `borderColor` | `Color/Color[]` | The border color of the arcs in the dataset. See [Colors](../general/colors.md#colors).
| `borderWidth` | `Number/Number[]` | The border width of the arcs in the dataset.
| `borderAlign` | `String/String[]` | The border alignment of the arcs in the dataset. [more...](#border-alignment)
| `hoverBackgroundColor` | `Color/Color[]` | The fill colour of the arcs when hovered.
| `hoverBorderColor` | `Color/Color[]` | The stroke colour of the arcs when hovered.
| `hoverBorderWidth` | `Number/Number[]` | The stroke width of the arcs when hovered.

### Border Alignment

The following values are supported for `borderAlign`.
* `'center'` (default)
* `'inner'`

When `'center'` is set, the borders of arcs next to each other will overlap. When `'inner'` is set, it is guaranteed that all the borders are not overlap.

## Config Options

Expand Down
92 changes: 66 additions & 26 deletions src/controllers/controller.doughnut.js
Expand Up @@ -143,14 +143,15 @@ module.exports = DatasetController.extend({
var chart = me.chart;
var chartArea = chart.chartArea;
var opts = chart.options;
var arcOpts = opts.elements.arc;
var availableWidth = chartArea.right - chartArea.left - arcOpts.borderWidth;
var availableHeight = chartArea.bottom - chartArea.top - arcOpts.borderWidth;
var availableWidth = chartArea.right - chartArea.left;
var availableHeight = chartArea.bottom - chartArea.top;
var minSize = Math.min(availableWidth, availableHeight);
var offset = {x: 0, y: 0};
var meta = me.getMeta();
var arcs = meta.data;
var cutoutPercentage = opts.cutoutPercentage;
var circumference = opts.circumference;
var i, ilen;

// If the chart's circumference isn't a full circle, calculate minSize as a ratio of the width/height of the arc
if (circumference < Math.PI * 2.0) {
Expand All @@ -171,7 +172,11 @@ module.exports = DatasetController.extend({
offset = {x: (max.x + min.x) * -0.5, y: (max.y + min.y) * -0.5};
}

chart.borderWidth = me.getMaxBorderWidth(meta.data);
for (i = 0, ilen = arcs.length; i < ilen; ++i) {
arcs[i]._options = me._resolveElementOptions(arcs[i], i, reset);
}

chart.borderWidth = me.getMaxBorderWidth();
chart.outerRadius = Math.max((minSize - chart.borderWidth) / 2, 0);
chart.innerRadius = Math.max(cutoutPercentage ? (chart.outerRadius / 100) * (cutoutPercentage) : 0, 0);
chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount();
Expand All @@ -183,9 +188,9 @@ module.exports = DatasetController.extend({
me.outerRadius = chart.outerRadius - (chart.radiusLength * me.getRingIndex(me.index));
me.innerRadius = Math.max(me.outerRadius - chart.radiusLength, 0);

helpers.each(meta.data, function(arc, index) {
me.updateElement(arc, index, reset);
});
for (i = 0, ilen = arcs.length; i < ilen; ++i) {
me.updateElement(arcs[i], i, reset);
}
},

updateElement: function(arc, index, reset) {
Expand All @@ -202,7 +207,7 @@ module.exports = DatasetController.extend({
var circumference = reset && animationOpts.animateRotate ? 0 : arc.hidden ? 0 : me.calculateCircumference(dataset.data[index]) * (opts.circumference / (2.0 * Math.PI));
var innerRadius = reset && animationOpts.animateScale ? 0 : me.innerRadius;
var outerRadius = reset && animationOpts.animateScale ? 0 : me.outerRadius;
var valueAtIndexOrDefault = helpers.valueAtIndexOrDefault;
var options = arc._options || {};

helpers.extend(arc, {
// Utility
Expand All @@ -211,27 +216,23 @@ module.exports = DatasetController.extend({

// Desired view properties
_model: {
backgroundColor: options.backgroundColor,
borderColor: options.borderColor,
borderWidth: options.borderWidth,
borderAlign: options.borderAlign,
x: centerX + chart.offsetX,
y: centerY + chart.offsetY,
startAngle: startAngle,
endAngle: endAngle,
circumference: circumference,
outerRadius: outerRadius,
innerRadius: innerRadius,
label: valueAtIndexOrDefault(dataset.label, index, chart.data.labels[index])
label: helpers.valueAtIndexOrDefault(dataset.label, index, chart.data.labels[index])
}
});

var model = arc._model;

// Resets the visual styles
var custom = arc.custom || {};
var valueOrDefault = helpers.valueAtIndexOrDefault;
var elementOpts = this.chart.options.elements.arc;
model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor);
model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor);
model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth);

// Set correct angles if not resetting
if (!reset || !animationOpts.animateRotate) {
if (index === 0) {
Expand Down Expand Up @@ -276,19 +277,58 @@ module.exports = DatasetController.extend({

// gets the max border or hover width to properly scale pie charts
getMaxBorderWidth: function(arcs) {
var me = this;
var max = 0;
var index = this.index;
var length = arcs.length;
var borderWidth;
var hoverWidth;
var chart = me.chart;
var i, ilen, meta, arc, controller, options, borderWidth, hoverWidth;

if (!arcs) {
// Find the outmost visible dataset
for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) {
if (chart.isDatasetVisible(i)) {
meta = chart.getDatasetMeta(i);
arcs = meta.data;
if (i !== me.index) {
controller = meta.controller;
}
break;
}
}
}

if (!arcs) {
return 0;
}

for (var i = 0; i < length; i++) {
borderWidth = arcs[i]._model ? arcs[i]._model.borderWidth : 0;
hoverWidth = arcs[i]._chart ? arcs[i]._chart.config.data.datasets[index].hoverBorderWidth : 0;
for (i = 0, ilen = arcs.length; i < ilen; ++i) {
arc = arcs[i];
options = controller ? controller._resolveElementOptions(arc, i) : arc._options;
if (options.borderAlign !== 'inner') {
borderWidth = options.borderWidth;
hoverWidth = options.hoverBorderWidth;

max = borderWidth > max ? borderWidth : max;
max = hoverWidth > max ? hoverWidth : max;
max = borderWidth > max ? borderWidth : max;
max = hoverWidth > max ? hoverWidth : max;
}
}
return max;
},

/**
* @private
*/
_resolveElementOptions: function(arc, index) {
var me = this;
var dataset = me.getDataset();
var custom = arc.custom || {};
var options = me.chart.options.elements.arc;
var valueAtIndexOrDefault = helpers.valueAtIndexOrDefault;

return {
backgroundColor: custom.backgroundColor ? custom.backgroundColor : valueAtIndexOrDefault(dataset.backgroundColor, index, options.backgroundColor),
borderColor: custom.borderColor ? custom.borderColor : valueAtIndexOrDefault(dataset.borderColor, index, options.borderColor),
borderWidth: custom.borderWidth ? custom.borderWidth : valueAtIndexOrDefault(dataset.borderWidth, index, options.borderWidth),
borderAlign: custom.borderAlign ? custom.borderAlign : valueAtIndexOrDefault(dataset.borderAlign, index, options.borderAlign)
};
}
});
4 changes: 2 additions & 2 deletions src/controllers/controller.polarArea.js
Expand Up @@ -148,10 +148,9 @@ module.exports = DatasetController.extend({
var chart = me.chart;
var chartArea = chart.chartArea;
var opts = chart.options;
var arcOpts = opts.elements.arc;
var minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top);

chart.outerRadius = Math.max((minSize - arcOpts.borderWidth / 2) / 2, 0);
chart.outerRadius = Math.max(minSize / 2, 0);
chart.innerRadius = Math.max(opts.cutoutPercentage ? (chart.outerRadius / 100) * (opts.cutoutPercentage) : 1, 0);
chart.radiusLength = (chart.outerRadius - chart.innerRadius) / chart.getVisibleDatasetCount();

Expand Down Expand Up @@ -206,6 +205,7 @@ module.exports = DatasetController.extend({
model.backgroundColor = custom.backgroundColor ? custom.backgroundColor : valueOrDefault(dataset.backgroundColor, index, elementOpts.backgroundColor);
model.borderColor = custom.borderColor ? custom.borderColor : valueOrDefault(dataset.borderColor, index, elementOpts.borderColor);
model.borderWidth = custom.borderWidth ? custom.borderWidth : valueOrDefault(dataset.borderWidth, index, elementOpts.borderWidth);
model.borderAlign = custom.borderAlign ? custom.borderAlign : valueOrDefault(dataset.borderAlign, index, elementOpts.borderAlign);

arc.pivot();
},
Expand Down
45 changes: 37 additions & 8 deletions src/elements/element.arc.js
Expand Up @@ -9,7 +9,8 @@ defaults._set('global', {
arc: {
backgroundColor: defaults.global.defaultColor,
borderColor: '#fff',
borderWidth: 2
borderWidth: 2,
borderAlign: 'center'
}
}
});
Expand Down Expand Up @@ -85,23 +86,51 @@ module.exports = Element.extend({
var vm = this._view;
var sA = vm.startAngle;
var eA = vm.endAngle;
var pixelMargin = (vm.borderAlign === 'inner') ? 0.33 : 0;
var angleMargin;

ctx.beginPath();
ctx.save();

ctx.arc(vm.x, vm.y, vm.outerRadius, sA, eA);
ctx.beginPath();
ctx.arc(vm.x, vm.y, vm.outerRadius - pixelMargin, sA, eA);
ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true);

ctx.closePath();
ctx.strokeStyle = vm.borderColor;
ctx.lineWidth = vm.borderWidth;

ctx.fillStyle = vm.backgroundColor;

ctx.fill();
ctx.lineJoin = 'bevel';

if (vm.borderWidth) {
if (vm.borderAlign === 'inner') {
// Draw an inner border by cliping the arc and drawing a double-width border
// Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders
ctx.beginPath();
angleMargin = pixelMargin / vm.outerRadius;
ctx.arc(vm.x, vm.y, vm.outerRadius, sA - angleMargin, eA + angleMargin);
if (vm.innerRadius > pixelMargin) {
angleMargin = pixelMargin / vm.innerRadius;
ctx.arc(vm.x, vm.y, vm.innerRadius - pixelMargin, eA + angleMargin, sA - angleMargin, true);
} else {
ctx.arc(vm.x, vm.y, pixelMargin, eA + Math.PI / 2, sA - Math.PI / 2);
}
ctx.closePath();
ctx.clip();

ctx.beginPath();
ctx.arc(vm.x, vm.y, vm.outerRadius, sA, eA);
ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true);
ctx.closePath();

ctx.lineWidth = vm.borderWidth * 2;
ctx.lineJoin = 'round';
} else {
ctx.lineWidth = vm.borderWidth;
ctx.lineJoin = 'bevel';
}

ctx.strokeStyle = vm.borderColor;
ctx.stroke();
}

ctx.restore();
}
});
1 change: 1 addition & 0 deletions test/context.js
Expand Up @@ -79,6 +79,7 @@ Context.prototype._initMethods = function() {
beginPath: function() {},
bezierCurveTo: function() {},
clearRect: function() {},
clip: function() {},
closePath: function() {},
fill: function() {},
fillRect: function() {},
Expand Down
@@ -0,0 +1,31 @@
{
"config": {
"type": "doughnut",
"data": {
"labels": ["A", "B", "C", "D", "E"],
"datasets": [{
"data": [1, 5, 10, 50, 100],
"backgroundColor": [
"rgba(255, 99, 132, 0.8)",
"rgba(54, 162, 235, 0.8)",
"rgba(255, 206, 86, 0.8)",
"rgba(75, 192, 192, 0.8)",
"rgba(153, 102, 255, 0.8)"
],
"borderWidth": 20,
"borderColor": [
"rgb(255, 99, 132)",
"rgb(54, 162, 235)",
"rgb(255, 206, 86)",
"rgb(75, 192, 192)",
"rgb(153, 102, 255)"
]
}]
},
"options": {
"responsive": false,
"legend": false,
"title": false
}
}
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions test/fixtures/controller.doughnut/doughnut-border-align-inner.json
@@ -0,0 +1,32 @@
{
"config": {
"type": "doughnut",
"data": {
"labels": ["A", "B", "C", "D", "E"],
"datasets": [{
"data": [1, 5, 10, 50, 100],
"backgroundColor": [
"rgba(255, 99, 132, 0.8)",
"rgba(54, 162, 235, 0.8)",
"rgba(255, 206, 86, 0.8)",
"rgba(75, 192, 192, 0.8)",
"rgba(153, 102, 255, 0.8)"
],
"borderWidth": 20,
"borderColor": [
"rgb(255, 99, 132)",
"rgb(54, 162, 235)",
"rgb(255, 206, 86)",
"rgb(75, 192, 192)",
"rgb(153, 102, 255)"
],
"borderAlign": "inner"
}]
},
"options": {
"responsive": false,
"legend": false,
"title": false
}
}
}
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 48a125f

Please sign in to comment.