Allow setting a constance spacing between arc elements (#9180)

This commit is contained in:
Evert Timberg
2021-05-29 17:47:44 -04:00
committed by GitHub
parent 5082d13d5a
commit c853ca6942
9 changed files with 183 additions and 21 deletions

View File

@@ -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)
@@ -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.

View File

@@ -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);
@@ -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.
@@ -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}
*/

View File

@@ -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;
@@ -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();
@@ -176,7 +190,7 @@ function drawArc(ctx, element, offset) {
}
}
pathArc(ctx, element, offset, endAngle);
pathArc(ctx, element, offset, spacing, endAngle);
ctx.fill();
return endAngle;
}
@@ -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';
@@ -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();
}
@@ -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);
}
@@ -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
@@ -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;
@@ -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();
}
@@ -345,6 +362,7 @@ ArcElement.defaults = {
borderRadius: 0,
borderWidth: 2,
offset: 0,
spacing: 0,
angle: undefined,
};

View File

@@ -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],
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

View File

@@ -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,
}
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -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);
@@ -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({
@@ -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);
@@ -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();
@@ -59,6 +90,10 @@ describe('Arc element tests', function() {
y: 0,
innerRadius: 0,
outerRadius: Math.sqrt(2),
options: {
spacing: 0,
offset: 0,
}
});
var center = arc.getCenterPoint();
@@ -66,6 +101,26 @@ describe('Arc element tests', function() {
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({
@@ -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();
@@ -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);
@@ -114,7 +175,10 @@ describe('Arc element tests', function() {
y: 0,
innerRadius: 0,
outerRadius: -1,
options: {}
options: {
spacing: 0,
offset: 0,
}
});
arc.draw(ctx);

13
types/index.esm.d.ts vendored
View File

@@ -252,6 +252,13 @@ export interface DoughnutControllerDatasetOptions
* @default 1
*/
weight: number;
/**
* Similar to the `offset` option, but applies to all arcs. This can be used to to add spaces
* between arcs
* @default 0
*/
spacing: number;
}
export interface DoughnutAnimationOptions {
@@ -294,6 +301,12 @@ export interface DoughnutControllerChartOptions {
*/
rotation: number;
/**
* Spacing between the arcs
* @default 0
*/
spacing: number;
animation: DoughnutAnimationOptions;
}