@@ -122,12 +122,18 @@ function pathArc(
122122 circular : boolean ,
123123) {
124124 const { x, y, startAngle : start , pixelMargin, innerRadius : innerR } = element ;
125+ const { spacingMode = 'angular' } = element . options ;
125126
126127 const outerRadius = Math . max ( element . outerRadius + spacing + offset - pixelMargin , 0 ) ;
127- const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0 ;
128+ let innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0 ;
128129
129- let spacingOffset = 0 ;
130+ let outerSpacingOffset = 0 ;
131+ let innerSpacingOffset = 0 ;
130132 const alpha = end - start ;
133+ const beta = outerRadius > 0
134+ ? Math . max ( 0.001 , alpha * outerRadius - offset / PI ) / outerRadius
135+ : 0.001 ;
136+ const angleOffset = ( alpha - beta ) / 2 ;
131137
132138 if ( spacing ) {
133139 // When spacing is present, it is the same for all items
@@ -136,29 +142,63 @@ function pathArc(
136142 const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0 ;
137143 const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0 ;
138144 const avgNoSpacingRadius = ( noSpacingInnerRadius + noSpacingOuterRadius ) / 2 ;
139- if ( circular && avgNoSpacingRadius > 0 ) {
140- spacingOffset = Math . asin ( Math . min ( 1 , spacing / avgNoSpacingRadius ) ) ;
141- } else {
145+ const proportionalOffset = ( ( ) => {
142146 const adjustedAngle = avgNoSpacingRadius !== 0 ? ( alpha * avgNoSpacingRadius ) / ( avgNoSpacingRadius + spacing ) : alpha ;
143- spacingOffset = ( alpha - adjustedAngle ) / 2 ;
147+ return ( alpha - adjustedAngle ) / 2 ;
148+ } ) ( ) ;
149+ const angularOffset = avgNoSpacingRadius > 0 ? Math . asin ( Math . min ( 1 , spacing / avgNoSpacingRadius ) ) : 0 ;
150+
151+ // Keep spacing trims below half the available span after base offset trimming.
152+ const maxOffset = Math . max ( 0 , beta / 2 - 0.001 ) ;
153+ const maxOffsetSin = Math . sin ( maxOffset ) ;
154+
155+ if ( spacingMode === 'parallel' ) {
156+ if ( innerRadius === 0 && maxOffsetSin > 0 ) {
157+ // A root radius of zero cannot realize a non-zero parallel separator width.
158+ // Raise the root just enough for the available angular span.
159+ const minInnerRadius = spacing / maxOffsetSin ;
160+ const maxInnerRadius = Math . max ( 0 , outerRadius - 0.001 ) ;
161+ innerRadius = Math . min ( minInnerRadius , maxInnerRadius ) ;
162+ }
163+
164+ // Use one bounded spacing value for both radii so large spacing keeps stable geometry.
165+ const maxParallelSpacing = Math . min (
166+ outerRadius > 0 ? outerRadius * maxOffsetSin : Number . POSITIVE_INFINITY ,
167+ innerRadius > 0 ? innerRadius * maxOffsetSin : Number . POSITIVE_INFINITY
168+ ) ;
169+ const parallelSpacing = Math . min ( spacing , maxParallelSpacing ) ;
170+
171+ outerSpacingOffset = outerRadius > 0
172+ ? Math . asin ( Math . min ( 1 , parallelSpacing / outerRadius ) )
173+ : Math . min ( maxOffset , angularOffset ) ;
174+ innerSpacingOffset = innerRadius > 0
175+ ? Math . asin ( Math . min ( 1 , parallelSpacing / innerRadius ) )
176+ : outerSpacingOffset ;
177+ } else if ( spacingMode === 'proportional' ) {
178+ outerSpacingOffset = Math . min ( maxOffset , proportionalOffset ) ;
179+ innerSpacingOffset = Math . min ( maxOffset , proportionalOffset ) ;
180+ } else {
181+ outerSpacingOffset = Math . min ( maxOffset , angularOffset ) ;
182+ innerSpacingOffset = Math . min ( maxOffset , angularOffset ) ;
144183 }
145184 }
146185
147- const beta = Math . max ( 0.001 , alpha * outerRadius - offset / PI ) / outerRadius ;
148- const angleOffset = ( alpha - beta ) / 2 ;
149- const startAngle = start + angleOffset + spacingOffset ;
150- const endAngle = end - angleOffset - spacingOffset ;
151- const { outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius ( element , innerRadius , outerRadius , endAngle - startAngle ) ;
186+ const outerStartAngle = start + angleOffset + outerSpacingOffset ;
187+ const outerEndAngle = end - angleOffset - outerSpacingOffset ;
188+ const innerStartAngle = start + angleOffset + innerSpacingOffset ;
189+ const innerEndAngle = end - angleOffset - innerSpacingOffset ;
190+ const angleDelta = Math . min ( outerEndAngle - outerStartAngle , innerEndAngle - innerStartAngle ) ;
191+ const { outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius ( element , innerRadius , outerRadius , angleDelta ) ;
152192
153193 const outerStartAdjustedRadius = outerRadius - outerStart ;
154194 const outerEndAdjustedRadius = outerRadius - outerEnd ;
155- const outerStartAdjustedAngle = startAngle + outerStart / outerStartAdjustedRadius ;
156- const outerEndAdjustedAngle = endAngle - outerEnd / outerEndAdjustedRadius ;
195+ const outerStartAdjustedAngle = outerStartAngle + outerStart / outerStartAdjustedRadius ;
196+ const outerEndAdjustedAngle = outerEndAngle - outerEnd / outerEndAdjustedRadius ;
157197
158198 const innerStartAdjustedRadius = innerRadius + innerStart ;
159199 const innerEndAdjustedRadius = innerRadius + innerEnd ;
160- const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius ;
161- const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius ;
200+ const innerStartAdjustedAngle = innerStartAngle + innerStart / innerStartAdjustedRadius ;
201+ const innerEndAdjustedAngle = innerEndAngle - innerEnd / innerEndAdjustedRadius ;
162202
163203 ctx . beginPath ( ) ;
164204
@@ -171,38 +211,38 @@ function pathArc(
171211 // The corner segment from point 2 to point 3
172212 if ( outerEnd > 0 ) {
173213 const pCenter = rThetaToXY ( outerEndAdjustedRadius , outerEndAdjustedAngle , x , y ) ;
174- ctx . arc ( pCenter . x , pCenter . y , outerEnd , outerEndAdjustedAngle , endAngle + HALF_PI ) ;
214+ ctx . arc ( pCenter . x , pCenter . y , outerEnd , outerEndAdjustedAngle , outerEndAngle + HALF_PI ) ;
175215 }
176216
177217 // The line from point 3 to point 4
178- const p4 = rThetaToXY ( innerEndAdjustedRadius , endAngle , x , y ) ;
218+ const p4 = rThetaToXY ( innerEndAdjustedRadius , innerEndAngle , x , y ) ;
179219 ctx . lineTo ( p4 . x , p4 . y ) ;
180220
181221 // The corner segment from point 4 to point 5
182222 if ( innerEnd > 0 ) {
183223 const pCenter = rThetaToXY ( innerEndAdjustedRadius , innerEndAdjustedAngle , x , y ) ;
184- ctx . arc ( pCenter . x , pCenter . y , innerEnd , endAngle + HALF_PI , innerEndAdjustedAngle + Math . PI ) ;
224+ ctx . arc ( pCenter . x , pCenter . y , innerEnd , innerEndAngle + HALF_PI , innerEndAdjustedAngle + Math . PI ) ;
185225 }
186226
187227 // The inner arc from point 5 to point b to point 6
188- const innerMidAdjustedAngle = ( ( endAngle - ( innerEnd / innerRadius ) ) + ( startAngle + ( innerStart / innerRadius ) ) ) / 2 ;
189- ctx . arc ( x , y , innerRadius , endAngle - ( innerEnd / innerRadius ) , innerMidAdjustedAngle , true ) ;
190- ctx . arc ( x , y , innerRadius , innerMidAdjustedAngle , startAngle + ( innerStart / innerRadius ) , true ) ;
228+ const innerMidAdjustedAngle = ( ( innerEndAngle - ( innerEnd / innerRadius ) ) + ( innerStartAngle + ( innerStart / innerRadius ) ) ) / 2 ;
229+ ctx . arc ( x , y , innerRadius , innerEndAngle - ( innerEnd / innerRadius ) , innerMidAdjustedAngle , true ) ;
230+ ctx . arc ( x , y , innerRadius , innerMidAdjustedAngle , innerStartAngle + ( innerStart / innerRadius ) , true ) ;
191231
192232 // The corner segment from point 6 to point 7
193233 if ( innerStart > 0 ) {
194234 const pCenter = rThetaToXY ( innerStartAdjustedRadius , innerStartAdjustedAngle , x , y ) ;
195- ctx . arc ( pCenter . x , pCenter . y , innerStart , innerStartAdjustedAngle + Math . PI , startAngle - HALF_PI ) ;
235+ ctx . arc ( pCenter . x , pCenter . y , innerStart , innerStartAdjustedAngle + Math . PI , innerStartAngle - HALF_PI ) ;
196236 }
197237
198238 // The line from point 7 to point 8
199- const p8 = rThetaToXY ( outerStartAdjustedRadius , startAngle , x , y ) ;
239+ const p8 = rThetaToXY ( outerStartAdjustedRadius , outerStartAngle , x , y ) ;
200240 ctx . lineTo ( p8 . x , p8 . y ) ;
201241
202242 // The corner segment from point 8 to point 1
203243 if ( outerStart > 0 ) {
204244 const pCenter = rThetaToXY ( outerStartAdjustedRadius , outerStartAdjustedAngle , x , y ) ;
205- ctx . arc ( pCenter . x , pCenter . y , outerStart , startAngle - HALF_PI , outerStartAdjustedAngle ) ;
245+ ctx . arc ( pCenter . x , pCenter . y , outerStart , outerStartAngle - HALF_PI , outerStartAdjustedAngle ) ;
206246 }
207247 } else {
208248 ctx . moveTo ( x , y ) ;
@@ -315,6 +355,7 @@ export default class ArcElement extends Element<ArcProps, ArcOptions> {
315355 borderWidth : 2 ,
316356 offset : 0 ,
317357 spacing : 0 ,
358+ spacingMode : 'angular' ,
318359 angle : undefined ,
319360 circular : true ,
320361 selfJoin : false ,
0 commit comments