diff --git a/src/elements/element.arc.js b/src/elements/element.arc.js index 1d5f9aede4d..e3b8875b4dd 100644 --- a/src/elements/element.arc.js +++ b/src/elements/element.arc.js @@ -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 index 00000000000..e2c42751740 --- /dev/null +++ b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json @@ -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 index 00000000000..b918ceb921b Binary files /dev/null and b/test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png differ diff --git a/test/specs/element.arc.tests.js b/test/specs/element.arc.tests.js index dd373643519..40d52533d5e 100644 --- a/test/specs/element.arc.tests.js +++ b/test/specs/element.arc.tests.js @@ -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: [] - }]); - }); });