Skip to content

Commit

Permalink
Allow setting a constance spacing between arc elements (#9180)
Browse files Browse the repository at this point in the history
  • Loading branch information
etimberg committed May 29, 2021
1 parent 5082d13 commit c853ca6
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 21 deletions.
2 changes: 2 additions & 0 deletions docs/charts/doughnut.md
Expand Up @@ -116,6 +116,7 @@ The doughnut/pie chart allows a number of properties to be specified for each da
| [`hoverOffset`](#interactions) | `number` | Yes | Yes | `0`
| [`offset`](#styling) | `number` | Yes | Yes | `0`
| [`rotation`](#general) | `number` | - | - | `undefined`
| [`spacing](#styling) | `number` | - | - | `0`
| [`weight`](#styling) | `number` | - | - | `1`

All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options)
Expand All @@ -138,6 +139,7 @@ The style of each arc can be controlled with the following properties:
| `borderColor` | arc border color.
| `borderWidth` | arc border width (in pixels).
| `offset` | arc offset (in pixels).
| `spacing` | Fixed arc offset (in pixels). Similar to `offset` but applies to all arcs.
| `weight` | The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values.

All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options.
Expand Down
12 changes: 10 additions & 2 deletions src/controllers/controller.doughnut.js
Expand Up @@ -110,7 +110,7 @@ export default class DoughnutController extends DatasetController {
const {chartArea} = chart;
const meta = me._cachedMeta;
const arcs = meta.data;
const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs);
const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs) + me.options.spacing;
const maxSize = Math.max((Math.min(chartArea.width, chartArea.height) - spacing) / 2, 0);
const cutout = Math.min(toPercentage(me.options.cutout, maxSize), 1);
const chartWeight = me._getRingWeight(me.index);
Expand Down Expand Up @@ -325,7 +325,7 @@ DoughnutController.defaults = {
animations: {
numbers: {
type: 'number',
properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth']
properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth', 'spacing']
},
},
// The percentage of the chart that we cut out of the middle.
Expand All @@ -340,9 +340,17 @@ DoughnutController.defaults = {
// The outr radius of the chart
radius: '100%',

// Spacing between arcs
spacing: 0,

indexAxis: 'r',
};

DoughnutController.descriptors = {
_scriptable: (name) => name !== 'spacing',
_indexable: (name) => name !== 'spacing',
};

/**
* @type {any}
*/
Expand Down
48 changes: 33 additions & 15 deletions src/elements/element.arc.js
Expand Up @@ -93,16 +93,30 @@ function rThetaToXY(r, theta, x, y) {
* @param {CanvasRenderingContext2D} ctx
* @param {ArcElement} element
*/
function pathArc(ctx, element, offset, end) {
function pathArc(ctx, element, offset, spacing, end) {
const {x, y, startAngle: start, pixelMargin, innerRadius: innerR} = element;

const outerRadius = Math.max(element.outerRadius + offset - pixelMargin, 0);
const innerRadius = innerR > 0 ? innerR + offset + pixelMargin : 0;
const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0);
const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0;

let spacingOffset = 0;
const alpha = end - start;

if (spacing) {
// When spacing is present, it is the same for all items
// So we adjust the start and end angle of the arc such that
// the distance is the same as it would be without the spacing
const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0;
const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0;
const avNogSpacingRadius = (noSpacingInnerRadius + noSpacingOuterRadius) / 2;
const adjustedAngle = avNogSpacingRadius !== 0 ? (alpha * avNogSpacingRadius) / (avNogSpacingRadius + spacing) : alpha;
spacingOffset = (alpha - adjustedAngle) / 2;
}

const beta = Math.max(0.001, alpha * outerRadius - offset / PI) / outerRadius;
const angleOffset = (alpha - beta) / 2;
const startAngle = start + angleOffset;
const endAngle = end - angleOffset;
const startAngle = start + angleOffset + spacingOffset;
const endAngle = end - angleOffset - spacingOffset;
const {outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius(element, innerRadius, outerRadius, endAngle - startAngle);

const outerStartAdjustedRadius = outerRadius - outerStart;
Expand Down Expand Up @@ -158,11 +172,11 @@ function pathArc(ctx, element, offset, end) {
ctx.closePath();
}

function drawArc(ctx, element, offset) {
function drawArc(ctx, element, offset, spacing) {
const {fullCircles, startAngle, circumference} = element;
let endAngle = element.endAngle;
if (fullCircles) {
pathArc(ctx, element, offset, startAngle + TAU);
pathArc(ctx, element, offset, spacing, startAngle + TAU);

for (let i = 0; i < fullCircles; ++i) {
ctx.fill();
Expand All @@ -176,7 +190,7 @@ function drawArc(ctx, element, offset) {
}
}

pathArc(ctx, element, offset, endAngle);
pathArc(ctx, element, offset, spacing, endAngle);
ctx.fill();
return endAngle;
}
Expand Down Expand Up @@ -205,7 +219,7 @@ function drawFullCircleBorders(ctx, element, inner) {
}
}

function drawBorder(ctx, element, offset, endAngle) {
function drawBorder(ctx, element, offset, spacing, endAngle) {
const {options} = element;
const inner = options.borderAlign === 'inner';

Expand All @@ -229,7 +243,7 @@ function drawBorder(ctx, element, offset, endAngle) {
clipArc(ctx, element, endAngle);
}

pathArc(ctx, element, offset, endAngle);
pathArc(ctx, element, offset, spacing, endAngle);
ctx.stroke();
}

Expand Down Expand Up @@ -267,8 +281,9 @@ export default class ArcElement extends Element {
'outerRadius',
'circumference'
], useFinalPosition);
const rAdjust = this.options.spacing / 2;
const betweenAngles = circumference >= TAU || _angleBetween(angle, startAngle, endAngle);
const withinRadius = (distance >= innerRadius && distance <= outerRadius);
const withinRadius = (distance >= innerRadius + rAdjust && distance <= outerRadius + rAdjust);

return (betweenAngles && withinRadius);
}
Expand All @@ -284,10 +299,11 @@ export default class ArcElement extends Element {
'endAngle',
'innerRadius',
'outerRadius',
'circumference'
'circumference',
], useFinalPosition);
const {offset, spacing} = this.options;
const halfAngle = (startAngle + endAngle) / 2;
const halfRadius = (innerRadius + outerRadius) / 2;
const halfRadius = (innerRadius + outerRadius + spacing + offset) / 2;
return {
x: x + Math.cos(halfAngle) * halfRadius,
y: y + Math.sin(halfAngle) * halfRadius
Expand All @@ -305,6 +321,7 @@ export default class ArcElement extends Element {
const me = this;
const {options, circumference} = me;
const offset = (options.offset || 0) / 2;
const spacing = (options.spacing || 0) / 2;
me.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0;
me.fullCircles = circumference > TAU ? Math.floor(circumference / TAU) : 0;

Expand All @@ -327,8 +344,8 @@ export default class ArcElement extends Element {
ctx.fillStyle = options.backgroundColor;
ctx.strokeStyle = options.borderColor;

const endAngle = drawArc(ctx, me, radiusOffset);
drawBorder(ctx, me, radiusOffset, endAngle);
const endAngle = drawArc(ctx, me, radiusOffset, spacing);
drawBorder(ctx, me, radiusOffset, spacing, endAngle);

ctx.restore();
}
Expand All @@ -345,6 +362,7 @@ ArcElement.defaults = {
borderRadius: 0,
borderWidth: 2,
offset: 0,
spacing: 0,
angle: undefined,
};

Expand Down
29 changes: 29 additions & 0 deletions test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js
@@ -0,0 +1,29 @@
module.exports = {
config: {
type: 'doughnut',
data: {
datasets: [{
data: [10, 20, 40, 50, 5],
label: 'Dataset 1',
backgroundColor: [
'red',
'orange',
'yellow',
'green',
'blue'
]
}],
labels: [
'Item 1',
'Item 2',
'Item 3',
'Item 4',
'Item 5'
],
},
options: {
spacing: 50,
offset: [0, 50, 0, 0, 0],
}
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions test/fixtures/controller.doughnut/doughnut-spacing.js
@@ -0,0 +1,28 @@
module.exports = {
config: {
type: 'doughnut',
data: {
datasets: [{
data: [10, 20, 40, 50, 5],
label: 'Dataset 1',
backgroundColor: [
'red',
'orange',
'yellow',
'green',
'blue'
]
}],
labels: [
'Item 1',
'Item 2',
'Item 3',
'Item 4',
'Item 5'
],
},
options: {
spacing: 50,
}
}
};
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
72 changes: 68 additions & 4 deletions test/specs/element.arc.tests.js
Expand Up @@ -10,6 +10,10 @@ describe('Arc element tests', function() {
y: 0,
innerRadius: 5,
outerRadius: 10,
options: {
spacing: 0,
offset: 0,
}
});

expect(arc.inRange(2, 2)).toBe(false);
Expand All @@ -19,6 +23,25 @@ describe('Arc element tests', function() {
expect(arc.inRange(-1.0 * Math.sqrt(7), Math.sqrt(7))).toBe(false);
});

it ('should include spacing for in range check', function() {
// Mock out the arc as if the controller put it there
var arc = new Chart.elements.ArcElement({
startAngle: 0,
endAngle: Math.PI / 2,
x: 0,
y: 0,
innerRadius: 5,
outerRadius: 10,
options: {
spacing: 10,
offset: 0,
}
});

expect(arc.inRange(7, 0)).toBe(false);
expect(arc.inRange(15, 0)).toBe(true);
});

it ('should determine if in range, when full circle', function() {
// Mock out the arc as if the controller put it there
var arc = new Chart.elements.ArcElement({
Expand All @@ -28,7 +51,11 @@ describe('Arc element tests', function() {
y: 0,
innerRadius: 0,
outerRadius: 10,
circumference: Math.PI * 2
circumference: Math.PI * 2,
options: {
spacing: 0,
offset: 0,
}
});

expect(arc.inRange(7, 7)).toBe(true);
Expand All @@ -43,6 +70,10 @@ describe('Arc element tests', function() {
y: 0,
innerRadius: 0,
outerRadius: Math.sqrt(2),
options: {
spacing: 0,
offset: 0,
}
});

var pos = arc.tooltipPosition();
Expand All @@ -59,13 +90,37 @@ describe('Arc element tests', function() {
y: 0,
innerRadius: 0,
outerRadius: Math.sqrt(2),
options: {
spacing: 0,
offset: 0,
}
});

var center = arc.getCenterPoint();
expect(center.x).toBeCloseTo(0.5, 6);
expect(center.y).toBeCloseTo(0.5, 6);
});

it ('should get the center with offset and spacing', function() {
// Mock out the arc as if the controller put it there
var arc = new Chart.elements.ArcElement({
startAngle: 0,
endAngle: Math.PI / 2,
x: 0,
y: 0,
innerRadius: 0,
outerRadius: Math.sqrt(2),
options: {
spacing: 10,
offset: 10,
}
});

var center = arc.getCenterPoint();
expect(center.x).toBeCloseTo(7.57, 1);
expect(center.y).toBeCloseTo(7.57, 1);
});

it ('should get the center of full circle before and after draw', function() {
// Mock out the arc as if the controller put it there
var arc = new Chart.elements.ArcElement({
Expand All @@ -75,7 +130,10 @@ describe('Arc element tests', function() {
y: 2,
innerRadius: 0,
outerRadius: 2,
options: {}
options: {
spacing: 0,
offset: 0,
}
});

var center = arc.getCenterPoint();
Expand All @@ -100,7 +158,10 @@ describe('Arc element tests', function() {
y: 0,
innerRadius: -0.1,
outerRadius: Math.sqrt(2),
options: {}
options: {
spacing: 0,
offset: 0,
}
});

arc.draw(ctx);
Expand All @@ -114,7 +175,10 @@ describe('Arc element tests', function() {
y: 0,
innerRadius: 0,
outerRadius: -1,
options: {}
options: {
spacing: 0,
offset: 0,
}
});

arc.draw(ctx);
Expand Down

0 comments on commit c853ca6

Please sign in to comment.