@@ -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 canAllFitInOneLine = 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,68 @@ 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 ( canAllFitInOneLine ) {
105+ // All elements in 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 counts and trends can fit in one line.
115+ const canCountsAndTrendsFitOneLine = 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 ( canCountsAndTrendsFitOneLine ) {
140+ return { layoutStrategy : LAYOUT_STRATEGY . ONE_LINE_COUNTS_AND_TRENDS } ;
108141 }
109142
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 ] ) ;
143+ // Fall back to vertical stacking.
144+ return { layoutStrategy : LAYOUT_STRATEGY . VERTICAL_STACKING } ;
145+ } , [
146+ labels ,
147+ percentages ,
148+ formattedValues ,
149+ trends ,
150+ labelWidth ,
151+ barWidth ,
152+ chartContainerWidth ,
153+ ] ) ;
116154
117155 return (
118156 < Fragment >
@@ -172,20 +210,23 @@ export function FunnelChartLabels({
172210 x = { showScaleIcon ? 20 : 0 }
173211 />
174212
213+ { /* Group for Main Percentage, Counts, and Trend Indicator */ }
175214 < g transform = { `translate(0,${ LINE_HEIGHT + LINE_GAP } )` } >
176215 < SingleTextLine
177216 color = { TEXT_COLOR }
178217 text = { percentages [ index ] }
179218 targetWidth = {
180- layoutStrategy === 'rule1' ? percentWidth : currentTargetWidth
219+ layoutStrategy === LAYOUT_STRATEGY . ONE_LINE_ALL
220+ ? percentWidth
221+ : currentTargetWidth
181222 }
182223 textAnchor = "start"
183224 fontSize = { VALUE_FONT_SIZE }
184225 fontWeight = { VALUE_FONT_WEIGHT }
185226 />
186227
187228 { ( ( ) => {
188- if ( layoutStrategy === 'rule1' ) {
229+ if ( layoutStrategy === LAYOUT_STRATEGY . ONE_LINE_ALL ) {
189230 return (
190231 < Fragment >
191232 < SingleTextLine
@@ -214,9 +255,15 @@ export function FunnelChartLabels({
214255 ) }
215256 </ Fragment >
216257 ) ;
217- } else if ( layoutStrategy === 'rule2' ) {
258+ } else if (
259+ layoutStrategy === LAYOUT_STRATEGY . ONE_LINE_COUNTS_AND_TRENDS
260+ ) {
218261 return (
219- < g transform = { `translate(0, ${ LINE_HEIGHT } )` } >
262+ < g
263+ transform = { `translate(0, ${
264+ LINE_HEIGHT + VERTICAL_STACK_SPACING
265+ } )`}
266+ >
220267 < SingleTextLine
221268 color = { VALUE_COLOR }
222269 text = { formattedValues [ index ] }
@@ -232,17 +279,25 @@ export function FunnelChartLabels({
232279 />
233280 { trendIndicatorProps && (
234281 < g
235- transform = { `translate(${ countStringWidth } , ${ - LABEL_VERTICAL_OFFSET } )` }
282+ transform = { `translate(${
283+ countStringWidth + TREND_INDICATOR_SPACING
284+ } , ${ - LABEL_VERTICAL_OFFSET } )`}
236285 >
237286 < TrendIndicator { ...trendIndicatorProps } />
238287 </ g >
239288 ) }
240289 </ g >
241290 ) ;
242- } else if ( layoutStrategy === 'rule3plusCounts' ) {
291+ } else if (
292+ layoutStrategy === LAYOUT_STRATEGY . VERTICAL_STACKING
293+ ) {
243294 return (
244295 < Fragment >
245- < g transform = { `translate(0, ${ LINE_HEIGHT } )` } >
296+ < g
297+ transform = { `translate(0, ${
298+ LINE_HEIGHT + VERTICAL_STACK_SPACING
299+ } )`}
300+ >
246301 < SingleTextLine
247302 color = { VALUE_COLOR }
248303 text = { formattedValues [ index ] }
@@ -254,7 +309,11 @@ export function FunnelChartLabels({
254309 />
255310 </ g >
256311 { trendIndicatorProps && (
257- < g transform = { `translate(0, ${ LINE_HEIGHT * 2 } )` } >
312+ < g
313+ transform = { `translate(0, ${
314+ LINE_HEIGHT * 2 + VERTICAL_STACK_SPACING * 2
315+ } )`}
316+ >
258317 < g
259318 transform = { `translate(0, ${ - LABEL_VERTICAL_OFFSET } )` }
260319 >
0 commit comments