mirror of
https://github.com/chartjs/Chart.js.git
synced 2026-03-21 23:56:52 +01:00
Allow setting a constance spacing between arc elements (#9180)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
29
test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js
vendored
Normal file
29
test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js
vendored
Normal 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],
|
||||
}
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png
vendored
Normal file
BIN
test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
28
test/fixtures/controller.doughnut/doughnut-spacing.js
vendored
Normal file
28
test/fixtures/controller.doughnut/doughnut-spacing.js
vendored
Normal 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,
|
||||
}
|
||||
}
|
||||
};
|
||||
BIN
test/fixtures/controller.doughnut/doughnut-spacing.png
vendored
Normal file
BIN
test/fixtures/controller.doughnut/doughnut-spacing.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
@@ -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
13
types/index.esm.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user