mirror of
https://github.com/chartjs/Chart.js.git
synced 2025-12-08 20:36:08 +00:00
Fix arc border with circumference over 2*PI (#6215)
This commit is contained in:
parent
0de9fad2b0
commit
1a2a87be3b
@ -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.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.closePath();
|
||||
|
||||
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(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.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();
|
||||
|
||||
24
test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json
vendored
Normal file
24
test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json
vendored
Normal file
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png
vendored
Normal file
BIN
test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@ -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: []
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user