]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Enable per-dataset circumference and rotation for pie/doughnut charts (#7833)
authorEvert Timberg <evert.timberg+github@gmail.com>
Sat, 3 Oct 2020 20:47:39 +0000 (16:47 -0400)
committerGitHub <noreply@github.com>
Sat, 3 Oct 2020 20:47:39 +0000 (16:47 -0400)
* Enable per-dataset circumference and rotation for pie/doughnut charts
* Convert `circumference` and `rotation` options to degrees

12 files changed:
docs/docs/charts/doughnut.mdx
docs/docs/getting-started/v3-migration.md
src/controllers/controller.doughnut.js
test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json
test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.js [new file with mode: 0644]
test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.png [new file with mode: 0644]
test/fixtures/controller.doughnut/doughnut-circumference.json
test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.js [new file with mode: 0644]
test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.png [new file with mode: 0644]
test/fixtures/controller.doughnut/pie-circumference.json
test/specs/controller.doughnut.tests.js
types/controllers/index.d.ts

index ab1b15d69d1ae28b7198cea21ec19c445074820f..451fb52fc78b5ffa8587b8630b9c93fdc3bce808 100644 (file)
@@ -100,6 +100,7 @@ The doughnut/pie chart allows a number of properties to be specified for each da
 | [`borderAlign`](#border-alignment) | `string` | Yes | Yes | `'center'`
 | [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'#fff'`
 | [`borderWidth`](#styling) | `number` | Yes | Yes | `2`
+| ['circumference`](#general) | `number` | - | - | `undefined`
 | [`clip`](#general) | <code>number&#124;object</code> | - | - | `undefined`
 | [`data`](#data-structure) | `number[]` | - | - | **required**
 | [`hoverBackgroundColor`](#interations) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
@@ -107,13 +108,16 @@ The doughnut/pie chart allows a number of properties to be specified for each da
 | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined`
 | [`hoverOffset`](#interactions) | `number` | Yes | Yes | `0`
 | [`offset`](#styling) | `number` | Yes | Yes | `0`
+| [`rotation`](#general) | `number` | - | - | `undefined`
 | [`weight`](#styling) | `number` | - | - | `1`
 
 ### General
 
 | Name | Description
 | ---- | ----
+| `circumference` | Per-dataset override for the sweep that the arcs cover
 | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}`
+| `rotation` | Per-dataset override for the starting angle to draw arcs from
 
 
 ### Styling
index 09a3dd11875de95655a2a857e5cedf3494f57581..d80c078e0007fe24dd3c07b9d67fbb70960f7018 100644 (file)
@@ -66,6 +66,8 @@ A number of changes were made to the configuration options passed to the `Chart`
 * `hover.animationDuration` is now configured in `animation.active.duration`
 * `responsiveAnimationDuration` is now configured in `animation.resize.duration`
 * Polar area `startAngle` option is now consistent with `Radar`, 0 is at top and value is in degrees. Default is changed from `-½π` to  `0`.
+* Doughnut `rotation` option is now in degrees and 0 is at top. Default is changed from `-½π` to  `0`.
+* Doughnut `circumference` option is now in degrees. Default is changed from `2π` to `0`.
 * `scales.[x/y]Axes` arrays were removed. Scales are now configured directly to `options.scales` object with the object key being the scale Id.
 * `scales.[x/y]Axes.barPercentage` was moved to dataset option `barPercentage`
 * `scales.[x/y]Axes.barThickness` was moved to dataset option `barThickness`
index 389e4697900e7e7e12be3ad32957693d16974243..8856ceef9b4d69efab679456561fc8f1d647001c 100644 (file)
@@ -1,5 +1,6 @@
 import DatasetController from '../core/core.datasetController';
 import {isArray, valueOrDefault} from '../helpers/helpers.core';
+import {toRadians} from '../helpers/helpers.math';
 
 /**
  * @typedef { import("../core/core.controller").default } Chart
@@ -78,6 +79,35 @@ export default class DoughnutController extends DatasetController {
                return ringIndex;
        }
 
+       /**
+        * Get the maximal rotation & circumference extents
+        * across all visible datasets.
+        */
+       _getRotationExtents() {
+               let min = DOUBLE_PI;
+               let max = -DOUBLE_PI;
+
+               const me = this;
+               const opts = me.chart.options;
+
+               for (let i = 0; i < me.chart.data.datasets.length; ++i) {
+                       if (me.chart.isDatasetVisible(i)) {
+                               const dataset = me.chart.data.datasets[i];
+                               const rotation = toRadians(valueOrDefault(dataset.rotation, opts.rotation) - 90);
+                               const circumference = toRadians(valueOrDefault(dataset.circumference, opts.circumference));
+
+                               min = Math.min(min, rotation);
+                               max = Math.max(max, rotation + circumference);
+                       }
+               }
+
+
+               return {
+                       rotation: min,
+                       circumference: max - min,
+               };
+       }
+
        /**
         * @param {string} mode
         */
@@ -89,7 +119,12 @@ export default class DoughnutController extends DatasetController {
                const arcs = meta.data;
                const cutout = options.cutoutPercentage / 100 || 0;
                const chartWeight = me._getRingWeight(me.index);
-               const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(options.rotation, options.circumference, cutout);
+
+               // Compute the maximal rotation & circumference limits.
+               // If we only consider our dataset, this can cause problems when two datasets
+               // are both less than a circle with different rotations (starting angles)
+               const {circumference, rotation} = me._getRotationExtents();
+               const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout);
                const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs);
                const maxWidth = (chartArea.right - chartArea.left - spacing) / ratioX;
                const maxHeight = (chartArea.bottom - chartArea.top - spacing) / ratioY;
@@ -114,7 +149,8 @@ export default class DoughnutController extends DatasetController {
                const me = this;
                const opts = me.chart.options;
                const meta = me._cachedMeta;
-               return reset && opts.animation.animateRotate ? 0 : this.chart.getDataVisibility(i) ? me.calculateCircumference(meta._parsed[i] * opts.circumference / DOUBLE_PI) : 0;
+               const circumference = toRadians(valueOrDefault(me._config.circumference, opts.circumference));
+               return reset && opts.animation.animateRotate ? 0 : this.chart.getDataVisibility(i) ? me.calculateCircumference(meta._parsed[i] * circumference / DOUBLE_PI) : 0;
        }
 
        updateElements(arcs, start, count, mode) {
@@ -132,7 +168,7 @@ export default class DoughnutController extends DatasetController {
                const firstOpts = me.resolveDataElementOptions(start, mode);
                const sharedOptions = me.getSharedOptions(firstOpts);
                const includeOptions = me.includeOptions(mode, sharedOptions);
-               let startAngle = opts.rotation;
+               let startAngle = toRadians(valueOrDefault(me._config.rotation, opts.rotation) - 90);
                let i;
 
                for (i = 0; i < start; ++i) {
@@ -334,10 +370,10 @@ DoughnutController.defaults = {
        cutoutPercentage: 50,
 
        // The rotation of the chart, where the first data arc begins.
-       rotation: -HALF_PI,
+       rotation: 0,
 
        // The total circumference of the chart.
-       circumference: DOUBLE_PI,
+       circumference: 360,
 
        // Need to override these to give a nice default
        tooltips: {
index e2c42751740f861e6c82af5deca5c3450c8c1198..c7f37d4854e7d5e973427e7f99cd0bf54ea2c399 100644 (file)
@@ -15,7 +15,7 @@
             }]
         },
         "options": {
-            "circumference": 7,
+            "circumference": 400,
             "responsive": false,
             "legend": false,
             "title": false
diff --git a/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.js b/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.js
new file mode 100644 (file)
index 0000000..677c14a
--- /dev/null
@@ -0,0 +1,33 @@
+module.exports = {
+    config: {
+        type: 'doughnut',
+        data: {
+            labels: ['A', 'B', 'C', 'D', 'E'],
+            datasets: [{
+                data: [1, 5, 10, 50, 100],
+                backgroundColor: [
+                    'rgba(255, 99, 132, 0.8)',
+                    'rgba(54, 162, 235, 0.8)',
+                    'rgba(255, 206, 86, 0.8)',
+                    'rgba(75, 192, 192, 0.8)',
+                    'rgba(153, 102, 255, 0.8)'
+                ],
+                borderWidth: 1,
+                borderColor: [
+                    'rgb(255, 99, 132)',
+                    'rgb(54, 162, 235)',
+                    'rgb(255, 206, 86)',
+                    'rgb(75, 192, 192)',
+                    'rgb(153, 102, 255)'
+                ],
+                circumference: 180
+            }]
+        },
+        options: {
+            circumference: 57.32,
+            responsive: false,
+            legend: false,
+            title: false
+        }
+    }
+}
diff --git a/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.png b/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.png
new file mode 100644 (file)
index 0000000..7965426
Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.png differ
index 427079f505b3f794eebdcec52d5ee107db4e3e60..c6073a4f9889976d7073824142595d93fd4a3792 100644 (file)
@@ -23,7 +23,7 @@
             }]
         },
         "options": {
-            "circumference": 1,
+            "circumference": 57.32,
             "responsive": false,
             "legend": false,
             "title": false
diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.js b/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.js
new file mode 100644 (file)
index 0000000..03ddaeb
--- /dev/null
@@ -0,0 +1,51 @@
+module.exports = {
+    config: {
+        type: 'doughnut',
+        data: {
+            labels: ['A', 'B', 'C', 'D', 'E'],
+            datasets: [{
+                data: [1, 5, 10, 50, 100],
+                backgroundColor: [
+                    'rgba(255, 99, 132, 0.8)',
+                    'rgba(54, 162, 235, 0.8)',
+                    'rgba(255, 206, 86, 0.8)',
+                    'rgba(75, 192, 192, 0.8)',
+                    'rgba(153, 102, 255, 0.8)'
+                ],
+                borderWidth: 1,
+                borderColor: [
+                    'rgb(255, 99, 132)',
+                    'rgb(54, 162, 235)',
+                    'rgb(255, 206, 86)',
+                    'rgb(75, 192, 192)',
+                    'rgb(153, 102, 255)'
+                ],
+                rotation: -90
+            }, {
+                data: [1, 5, 10, 50, 100],
+                backgroundColor: [
+                    'rgba(255, 99, 132, 0.8)',
+                    'rgba(54, 162, 235, 0.8)',
+                    'rgba(255, 206, 86, 0.8)',
+                    'rgba(75, 192, 192, 0.8)',
+                    'rgba(153, 102, 255, 0.8)'
+                ],
+                borderWidth: 1,
+                borderColor: [
+                    'rgb(255, 99, 132)',
+                    'rgb(54, 162, 235)',
+                    'rgb(255, 206, 86)',
+                    'rgb(75, 192, 192)',
+                    'rgb(153, 102, 255)'
+                ],
+                rotation: 0
+            }]
+        },
+        options: {
+            circumference: 180,
+            responsive: false,
+            legend: false,
+            title: false
+        }
+    }
+}
diff --git a/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.png b/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.png
new file mode 100644 (file)
index 0000000..081367d
Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.png differ
index b892707fe34659a92c8d1e67b06429f658bb4d3a..3287186456cc32f7c2316e8d03972faa262fab29 100644 (file)
@@ -23,7 +23,7 @@
             }]
         },
         "options": {
-            "circumference": 1,
+            "circumference": 57.32,
             "responsive": false,
             "legend": false,
             "title": false
index 32c992dcd012ba75749333675dfdb224eec20752..c6bd73ab7693b52203e7d1105c90787b9c751333 100644 (file)
@@ -69,8 +69,8 @@ describe('Chart.controllers.doughnut', function() {
                                        animateScale: false
                                },
                                cutoutPercentage: 50,
-                               rotation: Math.PI * -0.5,
-                               circumference: Math.PI * 2.0,
+                               rotation: 0,
+                               circumference: 360,
                                elements: {
                                        arc: {
                                                backgroundColor: 'rgb(255, 0, 0)',
@@ -160,14 +160,14 @@ describe('Chart.controllers.doughnut', function() {
                                }, {
                                        data: [1, 0]
                                }],
-                               labels: ['label0', 'label1']
+                               labels: ['label0', 'label1', 'label2']
                        },
                        options: {
                                legend: false,
                                title: false,
                                cutoutPercentage: 50,
-                               rotation: Math.PI,
-                               circumference: Math.PI * 0.5,
+                               rotation: 270,
+                               circumference: 90,
                                elements: {
                                        arc: {
                                                backgroundColor: 'rgb(255, 0, 0)',
@@ -210,8 +210,8 @@ describe('Chart.controllers.doughnut', function() {
                                legend: false,
                                title: false,
                                cutoutPercentage: 50,
-                               rotation: Math.PI,
-                               circumference: Math.PI * 0.5,
+                               rotation: 270,
+                               circumference: 90,
                                elements: {
                                        arc: {
                                                backgroundColor: 'rgb(255, 0, 0)',
index 6482d11c87c6b85cb25f8a8beccb86a693b50d87..b24728cd8d29a024589e2e5cd8e19a4f8331081b 100644 (file)
@@ -177,6 +177,19 @@ export interface IDoughnutControllerDatasetOptions
   extends IControllerDatasetOptions,
     ScriptableAndArrayOptions<IArcOptions>,
     ScriptableAndArrayOptions<IArcHoverOptions> {
+  
+  /**
+   * Sweep to allow arcs to cover.
+   * @default 2 * Math.PI
+   */
+  circumference: number;
+
+  /**
+   * Starting angle to draw this dataset from.
+   * @default -0.5 * Math.PI
+   */
+  rotation: number;
+
   /**
    * 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.
    * @default 1