diff --git a/docs/charts/doughnut.md b/docs/charts/doughnut.md index 36d7680d561..f04575ee9a2 100644 --- a/docs/charts/doughnut.md +++ b/docs/charts/doughnut.md @@ -149,7 +149,8 @@ The style of each arc can be controlled with the following properties: | `borderJoinStyle` | arc border join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `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. +| `spacing` | Fixed arc (slice) offset (in pixels). Similar to `offset` but applies to all slices. +| `datasetSpacing` | Fixed spacing between datasets (in pixels). This property only applies to multi-dataset doughnut/pie charts. It adjusts the spacing between concentric rings of data, allowing better visual separation between datasets. | `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. diff --git a/src/controllers/controller.doughnut.js b/src/controllers/controller.doughnut.js index a159b42c86b..a893ebde637 100644 --- a/src/controllers/controller.doughnut.js +++ b/src/controllers/controller.doughnut.js @@ -234,8 +234,16 @@ export default class DoughnutController extends DatasetController { meta.total = this.calculateTotal(); - this.outerRadius = outerRadius - radiusLength * this._getRingWeightOffset(this.index); - this.innerRadius = Math.max(this.outerRadius - radiusLength * chartWeight, 0); + // For index 0, 0 spacing + // For index 1, 1 spacing + // For index 2, 2 spacings (account for the spacing for previous datasets) + // These are triangular numbers, so N*(N+1)/2 are the count of spacings we need to add. + const datasetSpacing = this.index > 0 && this.options.datasetSpacing + ? (this.options.datasetSpacing || 0) * (this.index * (this.index + 1) / 2) + : 0; + + this.outerRadius = outerRadius - radiusLength * this._getRingWeightOffset(this.index) - datasetSpacing; + this.innerRadius = Math.max(this.outerRadius - radiusLength * chartWeight - datasetSpacing, 0); this.updateElements(arcs, 0, arcs.length, mode); } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index 32831adc88c..d2e2191ea09 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -336,11 +336,17 @@ export interface DoughnutControllerChartOptions { rotation: number; /** - * Spacing between the arcs + * Spacing between the arcs (slices) * @default 0 */ spacing: number; + /** + * Spacing between the dataSets + * @default 0 + */ + datasetSpacing: number; + /** * Geometry used to apply arc spacing. * - `proportional`: legacy behavior (default for polarArea). diff --git a/test/fixtures/controller.doughnut/doughnut-dataset-spacing.json b/test/fixtures/controller.doughnut/doughnut-dataset-spacing.json new file mode 100644 index 00000000000..908001991bb --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-dataset-spacing.json @@ -0,0 +1,70 @@ +{ + "config": { + "type": "doughnut", + "data": { + "datasets": [ + { + "label": "Dataset 1", + "data": [ + 10, + 20, + 40 + ], + "backgroundColor": [ + "rgba(255, 99, 132, 0.8)", + "rgba(54, 162, 235, 0.8)", + "rgba(255, 206, 86, 0.8)" + ], + "borderWidth": 0, + "weight": 1 + }, + { + "label": "Dataset 2", + "data": [ + 15, + 25, + 35 + ], + "backgroundColor": [ + "rgba(75, 192, 192, 0.8)", + "rgba(153, 102, 255, 0.8)", + "rgba(255, 159, 64, 0.8)" + ], + "borderWidth": 0, + "weight": 1 + }, + { + "label": "Dataset 3", + "data": [ + 20, + 30, + 30 + ], + "backgroundColor": [ + "rgba(255, 99, 132, 0.5)", + "rgba(54, 162, 235, 0.5)", + "rgba(255, 206, 86, 0.5)" + ], + "borderWidth": 0, + "weight": 1 + } + ], + "labels": [ + "Category A", + "Category B", + "Category C" + ] + }, + "options": { + "datasetSpacing": 8, + "responsive": false, + "maintainAspectRatio": true, + "plugins": { + "legend": { + "display": true, + "position": "top" + } + } + } +} +} diff --git a/test/fixtures/controller.doughnut/doughnut-dataset-spacing.png b/test/fixtures/controller.doughnut/doughnut-dataset-spacing.png new file mode 100644 index 00000000000..03df527c4a4 Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-dataset-spacing.png differ diff --git a/test/specs/controller.doughnut.tests.js b/test/specs/controller.doughnut.tests.js index ff45ab83138..e1bd5d08f9d 100644 --- a/test/specs/controller.doughnut.tests.js +++ b/test/specs/controller.doughnut.tests.js @@ -460,4 +460,117 @@ describe('Chart.controllers.doughnut', function() { after: [] }]); }); + + it ('should apply datasetSpacing to multiple datasets', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [10, 20], + label: 'Dataset 1', + backgroundColor: ['red', 'blue'] + }, { + data: [15, 25], + label: 'Dataset 2', + backgroundColor: ['green', 'yellow'] + }, { + data: [20, 30], + label: 'Dataset 3', + backgroundColor: ['orange', 'purple'] + }], + labels: ['label0', 'label1'] + }, + options: { + plugins: { + legend: false, + title: false + }, + datasetSpacing: 10 + } + }); + + chart.update(); + + var controller0 = chart.getDatasetMeta(0).controller; + var controller1 = chart.getDatasetMeta(1).controller; + var controller2 = chart.getDatasetMeta(2).controller; + + // Verify that outer/inner radius decrease by datasetSpacing for each dataset + expect(controller0.outerRadius).toBeGreaterThan(0); + expect(controller1.outerRadius).toBeGreaterThan(0); + expect(controller2.outerRadius).toBeGreaterThan(0); + + expect(controller0.outerRadius).toBeGreaterThan(controller1.outerRadius); + expect(controller1.outerRadius).toBeGreaterThan(controller2.outerRadius); + + // The outer radius should decrease as we move to inner datasets + // Each dataset should have its spacing applied + var spacing0to1 = controller0.outerRadius - controller1.outerRadius; + var spacing1to2 = controller1.outerRadius - controller2.outerRadius; + expect(spacing0to1).toBeGreaterThan(0); + expect(spacing1to2).toBeGreaterThan(0); + }); + + it ('should handle zero datasetSpacing', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [10, 20], + label: 'Dataset 1' + }, { + data: [15, 25], + label: 'Dataset 2' + }], + labels: ['label0', 'label1'] + }, + options: { + plugins: { + legend: false, + title: false + }, + datasetSpacing: 0 + } + }); + + chart.update(); + + var controller0 = chart.getDatasetMeta(0).controller; + var controller1 = chart.getDatasetMeta(1).controller; + + // With zero spacing, the radius difference should be only due to radiusLength + var radiusLength = (controller0.outerRadius - controller0.innerRadius); + expect(controller0.outerRadius - controller1.outerRadius).toBeCloseTo(radiusLength, 0); + }); + + it ('should handle undefined datasetSpacing (default to 0)', function() { + var chart = window.acquireChart({ + type: 'doughnut', + data: { + datasets: [{ + data: [10, 20], + label: 'Dataset 1' + }, { + data: [15, 25], + label: 'Dataset 2' + }], + labels: ['label0', 'label1'] + }, + options: { + plugins: { + legend: false, + title: false + } + } + }); + + chart.update(); + + var controller0 = chart.getDatasetMeta(0).controller; + var controller1 = chart.getDatasetMeta(1).controller; + + // With undefined spacing (defaults to 0), the radius difference should be only due to radiusLength + var radiusLength = (controller0.outerRadius - controller0.innerRadius); + expect(controller0.outerRadius - controller1.outerRadius).toBeCloseTo(radiusLength, 0); + }); }); diff --git a/test/types/options.ts b/test/types/options.ts index b848475df54..d8f8a760529 100644 --- a/test/types/options.ts +++ b/test/types/options.ts @@ -39,6 +39,7 @@ const doughnutOptions: DoughnutControllerChartOptions = { radius: 100, rotation: 0, spacing: 0, + datasetSpacing: 0, animation: false, spacingMode: 'angular', };