@@ -19,6 +19,8 @@ const LABEL_FONT_SIZE = 12;
1919const VALUE_FONT_SIZE = 14 ;
2020const VALUE_FONT_WEIGHT = 650 ;
2121const TREND_INDICATOR_SPACING = 8 ;
22+ const VERTICAL_STACK_SPACING = 3 ;
23+ const MIN_CHART_WIDTH_FOR_RULE_3_PRIORITY = 400 ;
2224export const LABEL_VERTICAL_OFFSET = 2 ;
2325
2426const TEXT_COLOR = 'rgba(31, 33, 36, 1)' ;
@@ -38,6 +40,12 @@ export interface FunnelChartLabelsProps {
3840 renderScaleIconTooltipContent ?: ( ) => ReactNode ;
3941}
4042
43+ const LAYOUT_STRATEGY = {
44+ ONE_LINE_ALL : 'one_line_all' ,
45+ ONE_LINE_COUNTS_AND_TRENDS : 'one_line_counts_and_trends' ,
46+ VERTICAL_STACKING : 'vertical_stacking' ,
47+ } as const ;
48+
4149export function FunnelChartLabels ( {
4250 formattedValues,
4351 labels,
@@ -49,28 +57,24 @@ export function FunnelChartLabels({
4957 shouldApplyScaling,
5058 renderScaleIconTooltipContent,
5159} : FunnelChartLabelsProps ) {
52- const { characterWidths} = useChartContext ( ) ;
60+ const { characterWidths, containerBounds} = useChartContext ( ) ;
61+ const chartContainerWidth = containerBounds ?. width ?? 0 ;
5362 const [ showTooltip , setShowTooltip ] = useState ( false ) ;
5463
5564 const labelFontSize = useMemo ( ( ) => {
5665 const maxLabelWidth = Math . max (
5766 ...labels . map ( ( label ) => estimateStringWidth ( label , characterWidths ) ) ,
5867 ) ;
59-
6068 return maxLabelWidth > labelWidth ? REDUCED_FONT_SIZE : LABEL_FONT_SIZE ;
6169 } , [ labels , characterWidths , labelWidth ] ) ;
6270
6371 const { layoutStrategy} = useMemo ( ( ) => {
64- let allCanFitRule1 = true ;
65- let allCanFitRule2 = true ;
66- let anyTrendIndicatorExists = false ;
67-
68- for ( let i = 0 ; i < labels . length ; i ++ ) {
72+ // Check if all items can fit in one Main Percentage, Counts, and TI (if present) on a single line.
73+ const canAllItemsFitRule1 = labels . every ( ( _ , i ) => {
6974 const isLast = i === labels . length - 1 ;
7075 const currentTargetWidth = isLast
7176 ? barWidth - GROUP_OFFSET * 2
7277 : labelWidth - GROUP_OFFSET * 2 ;
73-
7478 const currentPercentWidth = estimateStringWidthWithOffset (
7579 percentages [ i ] ,
7680 VALUE_FONT_SIZE ,
@@ -85,34 +89,70 @@ export function FunnelChartLabels({
8589 trendIndicatorWidth : currentTrendWidth ,
8690 trendIndicatorProps : currentTrendProps ,
8791 } = getTrendIndicatorData ( trends ?. [ i ] ?. reached ) ;
88- if ( currentTrendProps ) anyTrendIndicatorExists = true ;
8992
90- // Check Rule 1 for current item
91- const canItemFitRule1 =
93+ return (
9294 currentPercentWidth +
9395 LINE_PADDING +
9496 currentCountStringWidth +
9597 ( currentTrendProps
9698 ? TREND_INDICATOR_SPACING + currentTrendWidth
9799 : 0 ) <
98- currentTargetWidth ;
100+ currentTargetWidth
101+ ) ;
102+ } ) ;
103+
104+ if ( canAllItemsFitRule1 ) {
105+ // All elements on one line
106+ return { layoutStrategy : LAYOUT_STRATEGY . ONE_LINE_ALL } ;
107+ }
99108
100- if ( ! canItemFitRule1 ) allCanFitRule1 = false ;
109+ // If chart width is very narrow, prioritize full stacking.
110+ if ( chartContainerWidth < MIN_CHART_WIDTH_FOR_RULE_3_PRIORITY ) {
111+ return { layoutStrategy : LAYOUT_STRATEGY . VERTICAL_STACKING } ;
112+ }
113+
114+ // Check if all items can fit Rule 2: Main Percentage (L1), Counts + TI (L2, side-by-side with space).
115+ const canAllItemsFitRule2 = labels . every ( ( _ , i ) => {
116+ const isLast = i === labels . length - 1 ;
117+ const currentTargetWidth = isLast
118+ ? barWidth - GROUP_OFFSET * 2
119+ : labelWidth - GROUP_OFFSET * 2 ;
120+ const currentCountStringWidth = estimateStringWidthWithOffset (
121+ formattedValues [ i ] ,
122+ VALUE_FONT_SIZE ,
123+ VALUE_FONT_WEIGHT ,
124+ ) ;
125+ const {
126+ trendIndicatorWidth : currentTrendWidth ,
127+ trendIndicatorProps : currentTrendProps ,
128+ } = getTrendIndicatorData ( trends ?. [ i ] ?. reached ) ;
101129
102- // Check Rule 2 for current item (only relevant if Rule 1 might fail for this item or others)
103- const canItemFitRule2 =
104- currentCountStringWidth + ( currentTrendProps ? currentTrendWidth : 0 ) < // No TREND_INDICATOR_SPACING for Rule 2 adjacency
105- currentTargetWidth ; // Rule 2 checks fit on a *new line*, so full currentTargetWidth is available
130+ return (
131+ currentCountStringWidth +
132+ ( currentTrendProps
133+ ? TREND_INDICATOR_SPACING + currentTrendWidth
134+ : 0 ) <
135+ currentTargetWidth
136+ ) ;
137+ } ) ;
106138
107- if ( ! canItemFitRule2 ) allCanFitRule2 = false ;
139+ if ( canAllItemsFitRule2 ) {
140+ // Main% (L1), Counts + TI (L2, side-by-side with space)
141+ return { layoutStrategy : LAYOUT_STRATEGY . ONE_LINE_COUNTS_AND_TRENDS } ;
108142 }
109143
110- if ( allCanFitRule1 ) return { layoutStrategy : 'rule1' } ;
111- if ( allCanFitRule2 ) return { layoutStrategy : 'rule2' } ;
112- // If neither Rule 1 nor Rule 2 can be applied globally
113- // Rule 3: MainPerc on L1, Counts on L2, TI on L3 (if exists)
114- return { layoutStrategy : 'rule3plusCounts' } ;
115- } , [ labels , percentages , formattedValues , trends , labelWidth , barWidth ] ) ;
144+ // Fall back to vertical stacking.
145+ // Main% (L1), Counts (L2), TI (L3)
146+ return { layoutStrategy : LAYOUT_STRATEGY . VERTICAL_STACKING } ;
147+ } , [
148+ labels ,
149+ percentages ,
150+ formattedValues ,
151+ trends ,
152+ labelWidth ,
153+ barWidth ,
154+ chartContainerWidth ,
155+ ] ) ;
116156
117157 return (
118158 < Fragment >
@@ -172,20 +212,23 @@ export function FunnelChartLabels({
172212 x = { showScaleIcon ? 20 : 0 }
173213 />
174214
215+ { /* Group for Main Percentage, Counts, and Trend Indicator */ }
175216 < g transform = { `translate(0,${ LINE_HEIGHT + LINE_GAP } )` } >
176217 < SingleTextLine
177218 color = { TEXT_COLOR }
178219 text = { percentages [ index ] }
179220 targetWidth = {
180- layoutStrategy === 'rule1' ? percentWidth : currentTargetWidth
221+ layoutStrategy === LAYOUT_STRATEGY . ONE_LINE_ALL
222+ ? percentWidth
223+ : currentTargetWidth
181224 }
182225 textAnchor = "start"
183226 fontSize = { VALUE_FONT_SIZE }
184227 fontWeight = { VALUE_FONT_WEIGHT }
185228 />
186229
187230 { ( ( ) => {
188- if ( layoutStrategy === 'rule1' ) {
231+ if ( layoutStrategy === LAYOUT_STRATEGY . ONE_LINE_ALL ) {
189232 return (
190233 < Fragment >
191234 < SingleTextLine
@@ -214,9 +257,15 @@ export function FunnelChartLabels({
214257 ) }
215258 </ Fragment >
216259 ) ;
217- } else if ( layoutStrategy === 'rule2' ) {
260+ } else if (
261+ layoutStrategy === LAYOUT_STRATEGY . ONE_LINE_COUNTS_AND_TRENDS
262+ ) {
218263 return (
219- < g transform = { `translate(0, ${ LINE_HEIGHT } )` } >
264+ < g
265+ transform = { `translate(0, ${
266+ LINE_HEIGHT + VERTICAL_STACK_SPACING
267+ } )`}
268+ >
220269 < SingleTextLine
221270 color = { VALUE_COLOR }
222271 text = { formattedValues [ index ] }
@@ -232,17 +281,25 @@ export function FunnelChartLabels({
232281 />
233282 { trendIndicatorProps && (
234283 < g
235- transform = { `translate(${ countStringWidth } , ${ - LABEL_VERTICAL_OFFSET } )` }
284+ transform = { `translate(${
285+ countStringWidth + TREND_INDICATOR_SPACING
286+ } , ${ - LABEL_VERTICAL_OFFSET } )`}
236287 >
237288 < TrendIndicator { ...trendIndicatorProps } />
238289 </ g >
239290 ) }
240291 </ g >
241292 ) ;
242- } else if ( layoutStrategy === 'rule3plusCounts' ) {
293+ } else if (
294+ layoutStrategy === LAYOUT_STRATEGY . VERTICAL_STACKING
295+ ) {
243296 return (
244297 < Fragment >
245- < g transform = { `translate(0, ${ LINE_HEIGHT } )` } >
298+ < g
299+ transform = { `translate(0, ${
300+ LINE_HEIGHT + VERTICAL_STACK_SPACING
301+ } )`}
302+ >
246303 < SingleTextLine
247304 color = { VALUE_COLOR }
248305 text = { formattedValues [ index ] }
@@ -254,7 +311,11 @@ export function FunnelChartLabels({
254311 />
255312 </ g >
256313 { trendIndicatorProps && (
257- < g transform = { `translate(0, ${ LINE_HEIGHT * 2 } )` } >
314+ < g
315+ transform = { `translate(0, ${
316+ LINE_HEIGHT * 2 + VERTICAL_STACK_SPACING * 2
317+ } )`}
318+ >
258319 < g
259320 transform = { `translate(0, ${ - LABEL_VERTICAL_OFFSET } )` }
260321 >
0 commit comments