@@ -209,14 +209,32 @@ export function calculateFontSize(
209209 outerRadius : number ,
210210 arcAngle : number
211211) : number {
212- const arcLength = ( ( outerRadius + innerRadius ) / 2 ) * arcAngle ;
212+ const avgRadius = ( outerRadius + innerRadius ) / 2 ;
213+ const arcLength = avgRadius * arcAngle ;
213214 const arcHeight = outerRadius - innerRadius ;
214215
215- // Use the smaller dimension to constrain font size
216- const constraint = Math . min ( arcLength / 10 , arcHeight / 2 ) ;
216+ // Height constrained by radial band, must fit ≥2 chars along arc
217+ const heightConstraint = arcHeight * 0.55 ;
218+ const lengthConstraint = arcLength / 2 ;
219+ const constraint = Math . min ( heightConstraint , lengthConstraint ) ;
220+
221+ // No minimum — CSS scale handles sub-pixel rendering for zoom
222+ return Math . min ( 14 , constraint ) ;
223+ }
217224
218- // Clamp between reasonable bounds
219- return Math . max ( 8 , Math . min ( 14 , constraint ) ) ;
225+ /**
226+ * Calculate the font size needed to fit a name without truncation.
227+ * For radial text: name must fit in the radial height.
228+ * For arc text: name must fit along the arc length.
229+ */
230+ export function fitFontSizeToName (
231+ name : string ,
232+ availableSpace : number ,
233+ maxFontSize : number
234+ ) : number {
235+ // Estimate: each char is ~0.55em wide
236+ const neededSize = availableSpace / ( name . length * 0.55 ) ;
237+ return Math . min ( maxFontSize , neededSize ) ;
220238}
221239
222240/**
@@ -256,6 +274,57 @@ export function truncateNameForArc(
256274 return name . slice ( 0 , maxLength - 1 ) + '\u2026' ;
257275}
258276
277+ /**
278+ * Check if an arc is in the bottom half of the circle (needs text flip).
279+ * In SVG coords (y-down): angles 0°-180° are the bottom half.
280+ */
281+ export function isArcInBottomHalf ( startAngle : number , endAngle : number ) : boolean {
282+ const midAngleDeg = ( ( ( startAngle + endAngle ) / 2 ) * 180 ) / Math . PI ;
283+ const normalized = ( ( midAngleDeg % 360 ) + 360 ) % 360 ;
284+ return normalized > 0 && normalized < 180 ;
285+ }
286+
287+ /**
288+ * Generate a curved text path along an arc segment.
289+ * For bottom-half arcs (flipped), reverses path direction for readability.
290+ * Caller should offset the radius for flipped arcs to keep text outward-facing.
291+ */
292+ export function generateTextArcPath (
293+ cx : number ,
294+ cy : number ,
295+ radius : number ,
296+ startAngle : number ,
297+ endAngle : number ,
298+ flipped : boolean
299+ ) : string {
300+ const largeArc = endAngle - startAngle > Math . PI ? 1 : 0 ;
301+
302+ if ( flipped ) {
303+ // Reverse direction so text reads correctly in the bottom half
304+ const start = polarToCartesian ( cx , cy , radius , endAngle ) ;
305+ const end = polarToCartesian ( cx , cy , radius , startAngle ) ;
306+ return `M ${ start . x } ${ start . y } A ${ radius } ${ radius } 0 ${ largeArc } 0 ${ end . x } ${ end . y } ` ;
307+ }
308+
309+ const start = polarToCartesian ( cx , cy , radius , startAngle ) ;
310+ const end = polarToCartesian ( cx , cy , radius , endAngle ) ;
311+ return `M ${ start . x } ${ start . y } A ${ radius } ${ radius } 0 ${ largeArc } 1 ${ end . x } ${ end . y } ` ;
312+ }
313+
314+ /**
315+ * Get rotation angle for radial text (reading from center outward).
316+ * Flips text on the left half so it's always readable.
317+ * Input: angle in degrees (0=right, 90=down, 180=left, 270=up).
318+ */
319+ export function getRadialTextRotation ( angleDeg : number ) : number {
320+ const a = ( ( angleDeg % 360 ) + 360 ) % 360 ;
321+ // Left half: flip so text reads inward (visually right-side-up)
322+ if ( a > 90 && a < 270 ) {
323+ return a + 180 ;
324+ }
325+ return a ;
326+ }
327+
259328/**
260329 * Generate a root circle path for the center of the fan chart
261330 */
0 commit comments