+
+ it('should use angular spacing mode independently from proportional mode', function() {
+ function createArc(spacingMode) {
+ return new Chart.elements.ArcElement({
+ startAngle: 0,
+ endAngle: Math.PI / 2,
+ x: 0,
+ y: 0,
+ innerRadius: 0,
+ outerRadius: 100,
+ options: {
+ circular: true,
+ spacingMode: spacingMode,
+ spacing: 20,
+ offset: 0,
+ borderWidth: 0,
+ borderRadius: 0,
+ backgroundColor: 'red',
+ borderColor: 'black'
+ }
+ });
+ }
+
+ function firstOuterArcStartAngle(arc) {
+ var ctx = window.createMockContext();
+ arc.draw(ctx);
+
+ var arcCall = ctx.getCalls().filter(function(x) {
+ return x.name === 'arc';
+ })[0];
+ if (arcCall) {
+ return arcCall.args[3];
+ }
+
+ var lineToCall = ctx.getCalls().filter(function(x) {
+ return x.name === 'lineTo';
+ })[0];
+ var dx = lineToCall.args[0] - arc.x;
+ var dy = lineToCall.args[1] - arc.y;
+ return Math.atan2(dy, dx);
+ }
+
+ var proportionalStart = firstOuterArcStartAngle(createArc('proportional'));
+ var angularStart = firstOuterArcStartAngle(createArc('angular'));
+ var alpha = Math.PI / 2;
+ var spacing = 10; // draw() passes spacing / 2 to pathArc
+ var avgNoSpacingRadius = 50;
+ var adjustedAngle = (alpha * avgNoSpacingRadius) / (avgNoSpacingRadius + spacing);
+ var proportionalSpacingOffset = (alpha - adjustedAngle) / 2;
+ var angularSpacingOffset = Math.asin(Math.min(1, spacing / avgNoSpacingRadius));
+
+ expect(angularStart - proportionalStart).toBeCloseTo(angularSpacingOffset - proportionalSpacingOffset, 6);
+ });
+
+ it('should keep valid arc direction with large parallel spacing', function() {
+ var arc = new Chart.elements.ArcElement({
+ startAngle: 0,
+ endAngle: Math.PI / 2,
+ x: 0,
+ y: 0,
+ innerRadius: 40,
+ outerRadius: 100,
+ options: {
+ circular: true,
+ spacingMode: 'parallel',
+ spacing: 40,
+ offset: 0,
+ borderWidth: 0,
+ borderRadius: 0,
+ backgroundColor: 'red',
+ borderColor: 'black'
+ }
+ });
+
+ var ctx = window.createMockContext();
+ arc.draw(ctx);
+
+ var arcCalls = ctx.getCalls().filter(function(x) {
+ return x.name === 'arc';
+ });
+
+ // First two calls are the split outer arc segments; ensure they keep forward direction.
+ expect(arcCalls[0].args[3]).toBeLessThan(arcCalls[0].args[4]);
+ expect(arcCalls[1].args[3]).toBeLessThan(arcCalls[1].args[4]);
+ });
+
+ it('should not reverse end separator angle in parallel mode with large spacing', function() {
+ var arc = new Chart.elements.ArcElement({
+ startAngle: 0,
+ endAngle: Math.PI / 3,
+ x: 0,
+ y: 0,
+ innerRadius: 40,
+ outerRadius: 100,
+ options: {
+ circular: true,
+ spacingMode: 'parallel',
+ spacing: 20,
+ offset: 0,
+ borderWidth: 0,
+ borderRadius: 0,
+ backgroundColor: 'red',
+ borderColor: 'black'
+ }
+ });
+
+ var ctx = window.createMockContext();
+ arc.draw(ctx);
+
+ var arcCalls = ctx.getCalls().filter(function(x) {
+ return x.name === 'arc';
+ });
+ var lineToCalls = ctx.getCalls().filter(function(x) {
+ return x.name === 'lineTo';
+ });
+
+ var outerEndAngle = arcCalls[1].args[4];
+ var innerEndAngle = Math.atan2(lineToCalls[0].args[1] - arc.y, lineToCalls[0].args[0] - arc.x);
+
+ expect(innerEndAngle).toBeLessThan(outerEndAngle);
+ });
+
+ it('should create a non-zero root for parallel spacing when innerRadius is zero', function() {
+ var arc = new Chart.elements.ArcElement({
+ startAngle: 0,
+ endAngle: Math.PI / 3,
+ x: 0,
+ y: 0,
+ innerRadius: 0,
+ outerRadius: 100,
+ options: {
+ circular: true,
+ spacingMode: 'parallel',
+ spacing: 20,
+ offset: 0,
+ borderWidth: 0,
+ borderRadius: 0,
+ backgroundColor: 'red',
+ borderColor: 'black'
+ }
+ });
+
+ var ctx = window.createMockContext();
+ arc.draw(ctx);
+
+ var lineToCalls = ctx.getCalls().filter(function(x) {
+ return x.name === 'lineTo';
+ });
+ var rootDistance = Math.hypot(lineToCalls[0].args[0] - arc.x, lineToCalls[0].args[1] - arc.y);
+
+ expect(rootDistance).toBeGreaterThan(0);
+ });
+
+ it('should render full-circle geometry for selfJoin with borderRadius', function() {
+ var arc = new Chart.elements.ArcElement({
+ startAngle: 0,
+ endAngle: Math.PI * 2,
+ x: 0,
+ y: 0,
+ innerRadius: 50,
+ outerRadius: 100,
+ options: {
+ circular: true,
+ selfJoin: true,
+ spacing: 0,
+ offset: 0,
+ borderWidth: 6,
+ borderRadius: 12,
+ backgroundColor: 'red',
+ borderColor: 'black'
+ }
+ });
+
+ var ctx = window.createMockContext();
+ arc.draw(ctx);
+
+ var arcCalls = ctx.getCalls().filter(function(x) {
+ return x.name === 'arc';
+ });
+ var radii = Array.from(new Set(arcCalls.map(function(x) {
+ return x.args[2].toFixed(3);
+ })));
+
+ expect(radii.length).toBe(2);
+ expect(Math.abs(arcCalls[0].args[4] - arcCalls[0].args[3])).toBeCloseTo(Math.PI * 2, 3);
+ });
+
+ it('should apply spacing to a full circle when selfJoin is false', function() {
+ var arc = new Chart.elements.ArcElement({
+ startAngle: 0,
+ endAngle: Math.PI * 2,
+ x: 0,
+ y: 0,
+ innerRadius: 40,
+ outerRadius: 100,
+ options: {
+ circular: true,
+ selfJoin: false,
+ spacingMode: 'angular',
+ spacing: 20,
+ offset: 0,
+ borderWidth: 0,
+ borderRadius: 0,
+ backgroundColor: 'red',
+ borderColor: 'black'
+ }
+ });
+
+ var ctx = window.createMockContext();
+ arc.draw(ctx);
+
+ var firstOuterArc = ctx.getCalls().filter(function(x) {
+ return x.name === 'arc';
+ })[0];
+
+ expect(firstOuterArc.args[4] - firstOuterArc.args[3]).toBeLessThan(Math.PI * 2);
+ });
+
+ it('should not clip full-circle selfJoin borders with evenodd clipping', function() {
+ var arc = new Chart.elements.ArcElement({
+ startAngle: 0,
+ endAngle: Math.PI * 2,
+ x: 0,
+ y: 0,
+ innerRadius: 40,
+ outerRadius: 100,
+ options: {
+ circular: true,
+ selfJoin: true,
+ spacing: 20,
+ offset: 0,
+ borderWidth: 2,
+ borderRadius: 0,
+ borderJoinStyle: 'round',
+ backgroundColor: 'red',
+ borderColor: 'black'
+ }
+ });
+
+ var ctx = window.createMockContext();
+ arc.draw(ctx);
+
+ var evenOddClipCall = ctx.getCalls().find(function(x) {
+ return x.name === 'clip' && x.args[0] === 'evenodd';
+ });
+
+ expect(evenOddClipCall).toBeUndefined();
+ });
+
+ it('should not draw a radial seam for full-circle selfJoin pie', function() {
+ var arc = new Chart.elements.ArcElement({
+ startAngle: 0,
+ endAngle: Math.PI * 2,
+ x: 10,
+ y: 20,
+ innerRadius: 0,
+ outerRadius: 100,
+ options: {
+ circular: true,
+ selfJoin: true,
+ spacing: 0,
+ offset: 0,
+ borderWidth: 10,
+ borderRadius: 1,
+ borderJoinStyle: 'round',
+ backgroundColor: 'red',
+ borderColor: 'black'
+ }
+ });
+
+ var ctx = window.createMockContext();
+ arc.draw(ctx);
+
+ var radialLineToCenter = ctx.getCalls().find(function(x) {
+ return x.name === 'lineTo' && x.args[0] === arc.x && x.args[1] === arc.y;
+ });
+
+ expect(radialLineToCenter).toBeUndefined();
+ });