]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Fix arc border with circumference over 2*PI (#6215)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Tue, 30 Apr 2019 09:34:10 +0000 (12:34 +0300)
committerSimon Brunel <simonbrunel@users.noreply.github.com>
Tue, 30 Apr 2019 09:34:10 +0000 (11:34 +0200)
src/elements/element.arc.js
test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json [new file with mode: 0644]
test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png [new file with mode: 0644]
test/specs/element.arc.tests.js

index 1d5f9aede4d6806bcd6d7d945f13b9e74630bee4..e3b8875b4ddcc7a99b6b4d5e6a1e09f9d2376bd1 100644 (file)
@@ -3,6 +3,7 @@
 var defaults = require('../core/core.defaults');
 var Element = require('../core/core.element');
 var helpers = require('../helpers/index');
+var TAU = Math.PI * 2;
 
 defaults._set('global', {
        elements: {
@@ -15,6 +16,81 @@ defaults._set('global', {
        }
 });
 
+function clipArc(ctx, arc) {
+       var startAngle = arc.startAngle;
+       var endAngle = arc.endAngle;
+       var pixelMargin = arc.pixelMargin;
+       var angleMargin = pixelMargin / arc.outerRadius;
+       var x = arc.x;
+       var y = arc.y;
+
+       // 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, arc.outerRadius, startAngle - angleMargin, endAngle + angleMargin);
+       if (arc.innerRadius > pixelMargin) {
+               angleMargin = pixelMargin / arc.innerRadius;
+               ctx.arc(x, y, arc.innerRadius - pixelMargin, endAngle + angleMargin, startAngle - angleMargin, true);
+       } else {
+               ctx.arc(x, y, pixelMargin, endAngle + Math.PI / 2, startAngle - Math.PI / 2);
+       }
+       ctx.closePath();
+       ctx.clip();
+}
+
+function drawFullCircleBorders(ctx, vm, arc, inner) {
+       var endAngle = arc.endAngle;
+       var i;
+
+       if (inner) {
+               arc.endAngle = arc.startAngle + TAU;
+               clipArc(ctx, arc);
+               arc.endAngle = endAngle;
+               if (arc.endAngle === arc.startAngle && arc.fullCircles) {
+                       arc.endAngle += TAU;
+                       arc.fullCircles--;
+               }
+       }
+
+       ctx.beginPath();
+       ctx.arc(arc.x, arc.y, arc.innerRadius, arc.startAngle + TAU, arc.startAngle, true);
+       for (i = 0; i < arc.fullCircles; ++i) {
+               ctx.stroke();
+       }
+
+       ctx.beginPath();
+       ctx.arc(arc.x, arc.y, vm.outerRadius, arc.startAngle, arc.startAngle + TAU);
+       for (i = 0; i < arc.fullCircles; ++i) {
+               ctx.stroke();
+       }
+}
+
+function drawBorder(ctx, vm, arc) {
+       var inner = vm.borderAlign === 'inner';
+
+       if (inner) {
+               ctx.lineWidth = vm.borderWidth * 2;
+               ctx.lineJoin = 'round';
+       } else {
+               ctx.lineWidth = vm.borderWidth;
+               ctx.lineJoin = 'bevel';
+       }
+
+       if (arc.fullCircles) {
+               drawFullCircleBorders(ctx, vm, arc, inner);
+       }
+
+       if (inner) {
+               clipArc(ctx, arc);
+       }
+
+       ctx.beginPath();
+       ctx.arc(arc.x, arc.y, vm.outerRadius, arc.startAngle, arc.endAngle);
+       ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true);
+       ctx.closePath();
+       ctx.stroke();
+}
+
 module.exports = Element.extend({
        inLabelRange: function(mouseX) {
                var vm = this._view;
@@ -30,20 +106,20 @@ module.exports = Element.extend({
 
                if (vm) {
                        var pointRelativePosition = helpers.getAngleFromPoint(vm, {x: chartX, y: chartY});
-                       var     angle = pointRelativePosition.angle;
+                       var angle = pointRelativePosition.angle;
                        var distance = pointRelativePosition.distance;
 
                        // Sanitise angle range
                        var startAngle = vm.startAngle;
                        var endAngle = vm.endAngle;
                        while (endAngle < startAngle) {
-                               endAngle += 2.0 * Math.PI;
+                               endAngle += TAU;
                        }
                        while (angle > endAngle) {
-                               angle -= 2.0 * Math.PI;
+                               angle -= TAU;
                        }
                        while (angle < startAngle) {
-                               angle += 2.0 * Math.PI;
+                               angle += TAU;
                        }
 
                        // Check if within the range of the open/close angle
@@ -84,51 +160,44 @@ module.exports = Element.extend({
        draw: function() {
                var ctx = this._chart.ctx;
                var vm = this._view;
-               var sA = vm.startAngle;
-               var eA = vm.endAngle;
                var pixelMargin = (vm.borderAlign === 'inner') ? 0.33 : 0;
-               var angleMargin;
+               var arc = {
+                       x: vm.x,
+                       y: vm.y,
+                       innerRadius: vm.innerRadius,
+                       outerRadius: Math.max(vm.outerRadius - pixelMargin, 0),
+                       pixelMargin: pixelMargin,
+                       startAngle: vm.startAngle,
+                       endAngle: vm.endAngle,
+                       fullCircles: Math.floor(vm.circumference / TAU)
+               };
+               var i;
 
                ctx.save();
 
+               ctx.fillStyle = vm.backgroundColor;
+               ctx.strokeStyle = vm.borderColor;
+
+               if (arc.fullCircles) {
+                       arc.endAngle = arc.startAngle + TAU;
+                       ctx.beginPath();
+                       ctx.arc(arc.x, arc.y, arc.outerRadius, arc.startAngle, arc.endAngle);
+                       ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true);
+                       ctx.closePath();
+                       for (i = 0; i < arc.fullCircles; ++i) {
+                               ctx.fill();
+                       }
+                       arc.endAngle = arc.startAngle + vm.circumference % TAU;
+               }
+
                ctx.beginPath();
-               ctx.arc(vm.x, vm.y, Math.max(vm.outerRadius - pixelMargin, 0), sA, eA);
-               ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true);
+               ctx.arc(arc.x, arc.y, arc.outerRadius, arc.startAngle, arc.endAngle);
+               ctx.arc(arc.x, arc.y, arc.innerRadius, arc.endAngle, arc.startAngle, true);
                ctx.closePath();
-
-               ctx.fillStyle = vm.backgroundColor;
                ctx.fill();
 
                if (vm.borderWidth) {
-                       if (vm.borderAlign === 'inner') {
-                               // 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();
-                               angleMargin = pixelMargin / vm.outerRadius;
-                               ctx.arc(vm.x, vm.y, vm.outerRadius, sA - angleMargin, eA + angleMargin);
-                               if (vm.innerRadius > pixelMargin) {
-                                       angleMargin = pixelMargin / vm.innerRadius;
-                                       ctx.arc(vm.x, vm.y, vm.innerRadius - pixelMargin, eA + angleMargin, sA - angleMargin, true);
-                               } else {
-                                       ctx.arc(vm.x, vm.y, pixelMargin, eA + Math.PI / 2, sA - Math.PI / 2);
-                               }
-                               ctx.closePath();
-                               ctx.clip();
-
-                               ctx.beginPath();
-                               ctx.arc(vm.x, vm.y, vm.outerRadius, sA, eA);
-                               ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true);
-                               ctx.closePath();
-
-                               ctx.lineWidth = vm.borderWidth * 2;
-                               ctx.lineJoin = 'round';
-                       } else {
-                               ctx.lineWidth = vm.borderWidth;
-                               ctx.lineJoin = 'bevel';
-                       }
-
-                       ctx.strokeStyle = vm.borderColor;
-                       ctx.stroke();
+                       drawBorder(ctx, vm, arc);
                }
 
                ctx.restore();
diff --git a/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json
new file mode 100644 (file)
index 0000000..e2c4275
--- /dev/null
@@ -0,0 +1,24 @@
+{
+    "config": {
+        "type": "doughnut",
+        "data": {
+            "labels": ["A"],
+            "datasets": [{
+                "data": [100],
+                "backgroundColor": [
+                    "rgba(153, 102, 255, 0.8)"
+                ],
+                "borderWidth": 20,
+                "borderColor": [
+                    "rgb(153, 102, 255)"
+                ]
+            }]
+        },
+        "options": {
+            "circumference": 7,
+            "responsive": false,
+            "legend": false,
+            "title": false
+        }
+    }
+}
diff --git a/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png
new file mode 100644 (file)
index 0000000..b918ceb
Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png differ
index dd3736435194192d35c11eed3e04e8e6f4afdb0e..40d52533d5ec050ead3bcf3a4b0908bc65fac518 100644 (file)
@@ -99,214 +99,4 @@ describe('Arc element tests', function() {
                expect(center.x).toBeCloseTo(0.5, 6);
                expect(center.y).toBeCloseTo(0.5, 6);
        });
-
-       it ('should draw correctly with no border', function() {
-               var mockContext = window.createMockContext();
-               var arc = new Chart.elements.Arc({
-                       _datasetIndex: 2,
-                       _index: 1,
-                       _chart: {
-                               ctx: mockContext,
-                       }
-               });
-
-               // Mock out the view as if the controller put it there
-               arc._view = {
-                       startAngle: 0,
-                       endAngle: Math.PI / 2,
-                       x: 10,
-                       y: 5,
-                       innerRadius: 1,
-                       outerRadius: 3,
-
-                       backgroundColor: 'rgb(0, 0, 255)',
-                       borderColor: 'rgb(255, 0, 0)',
-               };
-
-               arc.draw();
-
-               expect(mockContext.getCalls()).toEqual([{
-                       name: 'save',
-                       args: []
-               }, {
-                       name: 'beginPath',
-                       args: []
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 3, 0, Math.PI / 2]
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 1, Math.PI / 2, 0, true]
-               }, {
-                       name: 'closePath',
-                       args: []
-               }, {
-                       name: 'setFillStyle',
-                       args: ['rgb(0, 0, 255)']
-               }, {
-                       name: 'fill',
-                       args: []
-               }, {
-                       name: 'restore',
-                       args: []
-               }]);
-       });
-
-       it ('should draw correctly with a border', function() {
-               var mockContext = window.createMockContext();
-               var arc = new Chart.elements.Arc({
-                       _datasetIndex: 2,
-                       _index: 1,
-                       _chart: {
-                               ctx: mockContext,
-                       }
-               });
-
-               // Mock out the view as if the controller put it there
-               arc._view = {
-                       startAngle: 0,
-                       endAngle: Math.PI / 2,
-                       x: 10,
-                       y: 5,
-                       innerRadius: 1,
-                       outerRadius: 3,
-
-                       backgroundColor: 'rgb(0, 0, 255)',
-                       borderColor: 'rgb(255, 0, 0)',
-                       borderWidth: 5
-               };
-
-               arc.draw();
-
-               expect(mockContext.getCalls()).toEqual([{
-                       name: 'save',
-                       args: []
-               }, {
-                       name: 'beginPath',
-                       args: []
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 3, 0, Math.PI / 2]
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 1, Math.PI / 2, 0, true]
-               }, {
-                       name: 'closePath',
-                       args: []
-               }, {
-                       name: 'setFillStyle',
-                       args: ['rgb(0, 0, 255)']
-               }, {
-                       name: 'fill',
-                       args: []
-               }, {
-                       name: 'setLineWidth',
-                       args: [5]
-               }, {
-                       name: 'setLineJoin',
-                       args: ['bevel']
-               }, {
-                       name: 'setStrokeStyle',
-                       args: ['rgb(255, 0, 0)']
-               }, {
-                       name: 'stroke',
-                       args: []
-               }, {
-                       name: 'restore',
-                       args: []
-               }]);
-       });
-
-       it ('should draw correctly with an inner border', function() {
-               var mockContext = window.createMockContext();
-               var arc = new Chart.elements.Arc({
-                       _datasetIndex: 2,
-                       _index: 1,
-                       _chart: {
-                               ctx: mockContext,
-                       }
-               });
-
-               // Mock out the view as if the controller put it there
-               arc._view = {
-                       startAngle: 0,
-                       endAngle: Math.PI / 2,
-                       x: 10,
-                       y: 5,
-                       innerRadius: 1,
-                       outerRadius: 3,
-
-                       backgroundColor: 'rgb(0, 0, 255)',
-                       borderColor: 'rgb(255, 0, 0)',
-                       borderWidth: 5,
-                       borderAlign: 'inner'
-               };
-
-               arc.draw();
-
-               expect(mockContext.getCalls()).toEqual([{
-                       name: 'save',
-                       args: []
-               }, {
-                       name: 'beginPath',
-                       args: []
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 2.67, 0, Math.PI / 2]
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 1, Math.PI / 2, 0, true]
-               }, {
-                       name: 'closePath',
-                       args: []
-               }, {
-                       name: 'setFillStyle',
-                       args: ['rgb(0, 0, 255)']
-               }, {
-                       name: 'fill',
-                       args: []
-               }, {
-                       name: 'beginPath',
-                       args: []
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 3, -0.11, Math.PI / 2 + 0.11]
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 1 - 0.33, Math.PI / 2 + 0.33, -0.33, true]
-               }, {
-                       name: 'closePath',
-                       args: []
-               }, {
-                       name: 'clip',
-                       args: []
-               }, {
-                       name: 'beginPath',
-                       args: []
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 3, 0, Math.PI / 2]
-               }, {
-                       name: 'arc',
-                       args: [10, 5, 1, Math.PI / 2, 0, true]
-               }, {
-                       name: 'closePath',
-                       args: []
-               }, {
-                       name: 'setLineWidth',
-                       args: [10]
-               }, {
-                       name: 'setLineJoin',
-                       args: ['round']
-               }, {
-                       name: 'setStrokeStyle',
-                       args: ['rgb(255, 0, 0)']
-               }, {
-                       name: 'stroke',
-                       args: []
-               }, {
-                       name: 'restore',
-                       args: []
-               }]);
-       });
 });