]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Allow setting a constance spacing between arc elements (#9180)
authorEvert Timberg <evert.timberg+github@gmail.com>
Sat, 29 May 2021 21:47:44 +0000 (17:47 -0400)
committerGitHub <noreply@github.com>
Sat, 29 May 2021 21:47:44 +0000 (17:47 -0400)
docs/charts/doughnut.md
src/controllers/controller.doughnut.js
src/elements/element.arc.js
test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js [new file with mode: 0644]
test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png [new file with mode: 0644]
test/fixtures/controller.doughnut/doughnut-spacing.js [new file with mode: 0644]
test/fixtures/controller.doughnut/doughnut-spacing.png [new file with mode: 0644]
test/specs/element.arc.tests.js
types/index.esm.d.ts

index a4f36794f851d34afd0b5e054483c1b8858ec831..1c1755e92977bc9812913a9ac191c0c2310ddeb8 100644 (file)
@@ -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.
index d6c9c020b80307e937c7034b5bb7148981e748f2..bf472317c07301db5b723ad1e0abfe0ddf3178b9 100644 (file)
@@ -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}
  */
index c283b577a8e2b63cb0d8cdfdb21fd99b8c0561b6..918128cdffcdac4eab7cc4ce80fdc41a402607e4 100644 (file)
@@ -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,
 };
 
diff --git a/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js b/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js
new file mode 100644 (file)
index 0000000..d2e0c59
--- /dev/null
@@ -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],
+    }
+  }
+};
diff --git a/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png b/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png
new file mode 100644 (file)
index 0000000..e78e313
Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-spacing-and-offset.png differ
diff --git a/test/fixtures/controller.doughnut/doughnut-spacing.js b/test/fixtures/controller.doughnut/doughnut-spacing.js
new file mode 100644 (file)
index 0000000..c6c8437
--- /dev/null
@@ -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,
+    }
+  }
+};
diff --git a/test/fixtures/controller.doughnut/doughnut-spacing.png b/test/fixtures/controller.doughnut/doughnut-spacing.png
new file mode 100644 (file)
index 0000000..d586621
Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-spacing.png differ
index e857b97dc102983145f76e33383d819fef5063d3..23380aa250dce5bb0660f79c5ac2bff6ef970e5d 100644 (file)
@@ -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);
index 17b06d78d13f5ef28df93032bd622b49a592d3f5..0821862172ba9d7cfe24079700a30f2f06b5f2ca 100644 (file)
@@ -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;
 }