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);
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.
// 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}
*/
* @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;
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();
}
}
- pathArc(ctx, element, offset, endAngle);
+ pathArc(ctx, element, offset, spacing, endAngle);
ctx.fill();
return endAngle;
}
}
}
-function drawBorder(ctx, element, offset, endAngle) {
+function drawBorder(ctx, element, offset, spacing, endAngle) {
const {options} = element;
const inner = options.borderAlign === 'inner';
clipArc(ctx, element, endAngle);
}
- pathArc(ctx, element, offset, endAngle);
+ pathArc(ctx, element, offset, spacing, endAngle);
ctx.stroke();
}
'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);
}
'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
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;
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();
}
borderRadius: 0,
borderWidth: 2,
offset: 0,
+ spacing: 0,
angle: undefined,
};
y: 0,
innerRadius: 5,
outerRadius: 10,
+ options: {
+ spacing: 0,
+ offset: 0,
+ }
});
expect(arc.inRange(2, 2)).toBe(false);
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({
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);
y: 0,
innerRadius: 0,
outerRadius: Math.sqrt(2),
+ options: {
+ spacing: 0,
+ offset: 0,
+ }
});
var pos = arc.tooltipPosition();
y: 0,
innerRadius: 0,
outerRadius: Math.sqrt(2),
+ options: {
+ spacing: 0,
+ offset: 0,
+ }
});
var center = arc.getCenterPoint();
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({
y: 2,
innerRadius: 0,
outerRadius: 2,
- options: {}
+ options: {
+ spacing: 0,
+ offset: 0,
+ }
});
var center = arc.getCenterPoint();
y: 0,
innerRadius: -0.1,
outerRadius: Math.sqrt(2),
- options: {}
+ options: {
+ spacing: 0,
+ offset: 0,
+ }
});
arc.draw(ctx);
y: 0,
innerRadius: 0,
outerRadius: -1,
- options: {}
+ options: {
+ spacing: 0,
+ offset: 0,
+ }
});
arc.draw(ctx);