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 setting a constance spacing between arc elements #9180

Merged
merged 5 commits into from May 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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',
etimberg marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* @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