]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Add offset option for arc (#7691)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Wed, 5 Aug 2020 11:35:28 +0000 (14:35 +0300)
committerGitHub <noreply@github.com>
Wed, 5 Aug 2020 11:35:28 +0000 (07:35 -0400)
* Add offset option for arc
* Finishing touches

docs/docs/charts/doughnut.mdx
src/controllers/controller.doughnut.js
src/controllers/controller.polarArea.js
src/core/core.datasetController.js
src/elements/element.arc.js

index 346b6ecd1e22da37862f5b017127b2d14905565d..ab1b15d69d1ae28b7198cea21ec19c445074820f 100644 (file)
@@ -10,9 +10,22 @@ Pie and doughnut charts are effectively the same class in Chart.js, but have one
 
 They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same.
 
-import { useEffect } from 'react';
-
-export const ExampleChart = () => {
+import { useEffect, useRef } from 'react';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+<Tabs
+    defaultValue='doughnut'
+    values={[
+        {label: 'Doughnut', value: 'doughnut' },
+        {label: 'Pie', value: 'pie' },
+    ]}
+>
+<TabItem value="doughnut">
+
+```jsx live
+function example() {
+  const canvas = useRef(null);
   useEffect(() => {
     const cfg = {
       type: 'doughnut',
@@ -29,36 +42,53 @@ export const ExampleChart = () => {
             'rgb(255, 99, 132)',
             'rgb(54, 162, 235)',
             'rgb(255, 205, 86)'
-          ]
+          ],
+          hoverOffset: 4
         }]
       }
     };
-    new Chart(document.getElementById('chartjs-0').getContext('2d'), cfg);
+    new Chart(canvas.current.getContext('2d'), cfg);
   });
-  return <div className="chartjs-wrapper"><canvas id="chartjs-0" className="chartjs"></canvas></div>;
+  return <div className="chartjs-wrapper"><canvas ref={canvas} className="chartjs"></canvas></div>;
 }
+```
 
-<ExampleChart/>
+</TabItem>
 
-## Example Usage
+<TabItem value="pie">
 
-```javascript
-// For a pie chart
-var myPieChart = new Chart(ctx, {
-    type: 'pie',
-    data: data,
-    options: options
-});
+```jsx live
+function example() {
+  const canvas = useRef(null);
+  useEffect(() => {
+    const cfg = {
+      type: 'pie',
+      data: {
+        labels: [
+          'Red',
+          'Blue',
+          'Yellow'
+        ],
+        datasets: [{
+          label: 'My First Dataset',
+          data: [300, 50, 100],
+          backgroundColor: [
+            'rgb(255, 99, 132)',
+            'rgb(54, 162, 235)',
+            'rgb(255, 205, 86)'
+          ],
+          hoverOffset: 4
+        }]
+      }
+    };
+    new Chart(canvas.current.getContext('2d'), cfg);
+  });
+  return <div className="chartjs-wrapper"><canvas ref={canvas} className="chartjs"></canvas></div>;
+}
 ```
 
-```javascript
-// And for a doughnut chart
-var myDoughnutChart = new Chart(ctx, {
-    type: 'doughnut',
-    data: data,
-    options: options
-});
-```
+</TabItem>
+</Tabs>
 
 ## Dataset Properties
 
@@ -75,6 +105,8 @@ The doughnut/pie chart allows a number of properties to be specified for each da
 | [`hoverBackgroundColor`](#interations) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
 | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined`
 | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined`
+| [`hoverOffset`](#interactions) | `number` | Yes | Yes | `0`
+| [`offset`](#styling) | `number` | Yes | Yes | `0`
 | [`weight`](#styling) | `number` | - | - | `1`
 
 ### General
@@ -93,6 +125,7 @@ The style of each arc can be controlled with the following properties:
 | `backgroundColor` | arc background color.
 | `borderColor` | arc border color.
 | `borderWidth` | arc border width (in pixels).
+| `offset` | arc offset (in pixels).
 | `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.
@@ -114,6 +147,7 @@ The interaction with each arc can be controlled with the following properties:
 | `hoverBackgroundColor` | arc background color when hovered.
 | `hoverBorderColor` | arc border color when hovered.
 | `hoverBorderWidth` | arc border width when hovered (in pixels).
+| `hoverOffset` | arc offset when hovered (in pixels).
 
 All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options.
 
index a5a107f4f9116233c5b8d1ee52dec31bf0aed642..562ae11afaf429987601efafc6e9f7f93312b264 100644 (file)
@@ -89,9 +89,9 @@ export default class DoughnutController extends DatasetController {
                const cutout = options.cutoutPercentage / 100 || 0;
                const chartWeight = me._getRingWeight(me.index);
                const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(options.rotation, options.circumference, cutout);
-               const borderWidth = me.getMaxBorderWidth();
-               const maxWidth = (chartArea.right - chartArea.left - borderWidth) / ratioX;
-               const maxHeight = (chartArea.bottom - chartArea.top - borderWidth) / ratioY;
+               const spacing = me.getMaxBorderWidth() + me.getMaxOffset(arcs);
+               const maxWidth = (chartArea.right - chartArea.left - spacing) / ratioX;
+               const maxHeight = (chartArea.bottom - chartArea.top - spacing) / ratioY;
                const outerRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0);
                const innerRadius = Math.max(outerRadius * cutout, 0);
                const radiusLength = (outerRadius - innerRadius) / me._getVisibleDatasetWeightTotal();
@@ -219,6 +219,16 @@ export default class DoughnutController extends DatasetController {
                return max;
        }
 
+       getMaxOffset(arcs) {
+               let max = 0;
+
+               for (let i = 0, ilen = arcs.length; i < ilen; ++i) {
+                       const options = this.resolveDataElementOptions(i);
+                       max = Math.max(max, options.offset || 0, options.hoverOffset || 0);
+               }
+               return max;
+       }
+
        /**
         * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly
         * @private
@@ -264,14 +274,12 @@ DoughnutController.defaults = {
                'borderColor',
                'borderWidth',
                'borderAlign',
-               'hoverBackgroundColor',
-               'hoverBorderColor',
-               'hoverBorderWidth',
+               'offset'
        ],
        animation: {
                numbers: {
                        type: 'number',
-                       properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y']
+                       properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth']
                },
                // Boolean - Whether we animate the rotation of the Doughnut
                animateRotate: true,
index 06bc216985c11ca3e16a3e74ddca243511aeb38d..60e5c042b77b529ef52fe7a81d7f77e46ffb203b 100644 (file)
@@ -148,9 +148,7 @@ PolarAreaController.defaults = {
                'borderColor',
                'borderWidth',
                'borderAlign',
-               'hoverBackgroundColor',
-               'hoverBorderColor',
-               'hoverBorderWidth'
+               'offset'
        ],
 
        animation: {
index 9f9213650ce2498daca1ea2d75d8a462964c56af..f79efba872e5c7c63f59e6d2135e4e5310b0c6bc 100644 (file)
@@ -714,7 +714,7 @@ export default class DatasetController {
 
        /**
         * @param {number} index
-        * @param {string} mode
+        * @param {string} [mode]
         * @protected
         */
        resolveDataElementOptions(index, mode) {
index a69415a03bc34d87f18103451af76350ee232ca3..810c9d7a3902c72b4df3a940f8082e71ce3735ad 100644 (file)
@@ -3,17 +3,17 @@ import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math';
 
 const TAU = Math.PI * 2;
 
-function clipArc(ctx, model) {
-       const {startAngle, endAngle, pixelMargin, x, y} = model;
-       let angleMargin = pixelMargin / model.outerRadius;
+function clipArc(ctx, element) {
+       const {startAngle, endAngle, pixelMargin, x, y, outerRadius, innerRadius} = element;
+       let angleMargin = pixelMargin / outerRadius;
 
        // Draw an inner border by cliping the arc and drawing a double-width border
        // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders
        ctx.beginPath();
-       ctx.arc(x, y, model.outerRadius, startAngle - angleMargin, endAngle + angleMargin);
-       if (model.innerRadius > pixelMargin) {
-               angleMargin = pixelMargin / model.innerRadius;
-               ctx.arc(x, y, model.innerRadius - pixelMargin, endAngle + angleMargin, startAngle - angleMargin, true);
+       ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin);
+       if (innerRadius > pixelMargin) {
+               angleMargin = pixelMargin / innerRadius;
+               ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true);
        } else {
                ctx.arc(x, y, pixelMargin, endAngle + Math.PI / 2, startAngle - Math.PI / 2);
        }
@@ -22,60 +22,73 @@ function clipArc(ctx, model) {
 }
 
 
-function pathArc(ctx, model) {
+function pathArc(ctx, element) {
+       const {x, y, startAngle, endAngle, pixelMargin} = element;
+       const outerRadius = Math.max(element.outerRadius - pixelMargin, 0);
+       const innerRadius = element.innerRadius + pixelMargin;
+
        ctx.beginPath();
-       ctx.arc(model.x, model.y, model.outerRadius, model.startAngle, model.endAngle);
-       ctx.arc(model.x, model.y, model.innerRadius, model.endAngle, model.startAngle, true);
+       ctx.arc(x, y, outerRadius, startAngle, endAngle);
+       ctx.arc(x, y, innerRadius, endAngle, startAngle, true);
        ctx.closePath();
 }
 
-function drawArc(ctx, model, circumference) {
-       if (model.fullCircles) {
-               model.endAngle = model.startAngle + TAU;
+function drawArc(ctx, element) {
+       if (element.fullCircles) {
+               element.endAngle = element.startAngle + TAU;
 
-               pathArc(ctx, model);
+               pathArc(ctx, element);
 
-               for (let i = 0; i < model.fullCircles; ++i) {
+               for (let i = 0; i < element.fullCircles; ++i) {
                        ctx.fill();
                }
-               model.endAngle = model.startAngle + circumference % TAU;
+               element.endAngle = element.startAngle + element.circumference % TAU;
        }
 
-       pathArc(ctx, model);
+       pathArc(ctx, element);
        ctx.fill();
 }
 
-function drawFullCircleBorders(ctx, element, model, inner) {
-       const endAngle = model.endAngle;
+function drawFullCircleBorders(ctx, element, inner) {
+       const {x, y, startAngle, endAngle, pixelMargin} = element;
+       const outerRadius = Math.max(element.outerRadius - pixelMargin, 0);
+       const innerRadius = element.innerRadius + pixelMargin;
+
        let i;
 
        if (inner) {
-               model.endAngle = model.startAngle + TAU;
-               clipArc(ctx, model);
-               model.endAngle = endAngle;
-               if (model.endAngle === model.startAngle && model.fullCircles) {
-                       model.endAngle += TAU;
-                       model.fullCircles--;
+               element.endAngle = element.startAngle + TAU;
+               clipArc(ctx, element);
+               element.endAngle = endAngle;
+               if (element.endAngle === element.startAngle) {
+                       element.endAngle += TAU;
+                       element.fullCircles--;
                }
        }
 
        ctx.beginPath();
-       ctx.arc(model.x, model.y, model.innerRadius, model.startAngle + TAU, model.startAngle, true);
-       for (i = 0; i < model.fullCircles; ++i) {
+       ctx.arc(x, y, innerRadius, startAngle + TAU, startAngle, true);
+       for (i = 0; i < element.fullCircles; ++i) {
                ctx.stroke();
        }
 
        ctx.beginPath();
-       ctx.arc(model.x, model.y, element.outerRadius, model.startAngle, model.startAngle + TAU);
-       for (i = 0; i < model.fullCircles; ++i) {
+       ctx.arc(x, y, outerRadius, startAngle, startAngle + TAU);
+       for (i = 0; i < element.fullCircles; ++i) {
                ctx.stroke();
        }
 }
 
-function drawBorder(ctx, element, model) {
-       const options = element.options;
+function drawBorder(ctx, element) {
+       const {x, y, startAngle, endAngle, pixelMargin, options} = element;
+       const outerRadius = element.outerRadius;
+       const innerRadius = element.innerRadius + pixelMargin;
        const inner = options.borderAlign === 'inner';
 
+       if (!options.borderWidth) {
+               return;
+       }
+
        if (inner) {
                ctx.lineWidth = options.borderWidth * 2;
                ctx.lineJoin = 'round';
@@ -84,17 +97,17 @@ function drawBorder(ctx, element, model) {
                ctx.lineJoin = 'bevel';
        }
 
-       if (model.fullCircles) {
-               drawFullCircleBorders(ctx, element, model, inner);
+       if (element.fullCircles) {
+               drawFullCircleBorders(ctx, element, inner);
        }
 
        if (inner) {
-               clipArc(ctx, model);
+               clipArc(ctx, element);
        }
 
        ctx.beginPath();
-       ctx.arc(model.x, model.y, element.outerRadius, model.startAngle, model.endAngle);
-       ctx.arc(model.x, model.y, model.innerRadius, model.endAngle, model.startAngle, true);
+       ctx.arc(x, y, outerRadius, startAngle, endAngle);
+       ctx.arc(x, y, innerRadius, endAngle, startAngle, true);
        ctx.closePath();
        ctx.stroke();
 }
@@ -110,6 +123,8 @@ export default class Arc extends Element {
                this.endAngle = undefined;
                this.innerRadius = undefined;
                this.outerRadius = undefined;
+               this.pixelMargin = 0;
+               this.fullCircles = 0;
 
                if (cfg) {
                        Object.assign(this, cfg);
@@ -167,17 +182,9 @@ export default class Arc extends Element {
        draw(ctx) {
                const me = this;
                const options = me.options;
-               const pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0;
-               const model = {
-                       x: me.x,
-                       y: me.y,
-                       innerRadius: me.innerRadius,
-                       outerRadius: Math.max(me.outerRadius - pixelMargin, 0),
-                       pixelMargin,
-                       startAngle: me.startAngle,
-                       endAngle: me.endAngle,
-                       fullCircles: Math.floor(me.circumference / TAU)
-               };
+               const offset = options.offset || 0;
+               me.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0;
+               me.fullCircles = Math.floor(me.circumference / TAU);
 
                if (me.circumference === 0) {
                        return;
@@ -185,14 +192,16 @@ export default class Arc extends Element {
 
                ctx.save();
 
+               if (offset && me.circumference < TAU) {
+                       const halfAngle = (me.startAngle + me.endAngle) / 2;
+                       ctx.translate(Math.cos(halfAngle) * offset, Math.sin(halfAngle) * offset);
+               }
+
                ctx.fillStyle = options.backgroundColor;
                ctx.strokeStyle = options.borderColor;
 
-               drawArc(ctx, model, me.circumference);
-
-               if (options.borderWidth) {
-                       drawBorder(ctx, me, model);
-               }
+               drawArc(ctx, me);
+               drawBorder(ctx, me);
 
                ctx.restore();
        }
@@ -206,7 +215,8 @@ Arc.id = 'arc';
 Arc.defaults = {
        borderAlign: 'center',
        borderColor: '#fff',
-       borderWidth: 2
+       borderWidth: 2,
+       offset: 0
 };
 
 /**