@@ -33,59 +33,50 @@ const alignDecimalValue = (v: number, d: number) =>
3333const alignValue = ( v : number , d : number ) =>
3434 decimalCount ( d ) < 1 ? alignIntValue ( v , d ) : alignDecimalValue ( v , d ) ;
3535
36+ export const applyD3Format = ( mark : number , min : number , max : number ) => {
37+ const mu_ten_factor = - 3 ;
38+ const k_ten_factor = 4 ; // values < 10000 don't get formatted
39+
40+ const ten_factor = Math . log10 ( Math . abs ( mark ) ) ;
41+ if (
42+ mark === 0 ||
43+ ( ten_factor > mu_ten_factor && ten_factor < k_ten_factor )
44+ ) {
45+ return String ( mark ) ;
46+ }
47+ const max_min_mean = ( Math . abs ( max ) + Math . abs ( min ) ) / 2 ;
48+ const si_formatter = formatPrefix ( ',.0' , max_min_mean ) ;
49+ return String ( si_formatter ( mark ) ) ;
50+ } ;
51+
3652const estimateBestSteps = (
3753 minValue : number ,
3854 maxValue : number ,
3955 stepValue : number ,
4056 sliderWidth ?: number | null
4157) => {
42- let targetMarkCount = 11 ; // Default baseline
43-
44- // Scale mark density based on slider width
45- if ( sliderWidth ) {
46- const baselineWidth = 330 ;
47- const baselineMarkCount = 11 ; // 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
48-
49- // Calculate density multiplier based on width
50- const widthMultiplier = sliderWidth / baselineWidth ;
51-
52- // Target mark count scales with width but maintains consistent density
53- // The range adjustment should be removed - we want consistent mark density based on width
54- targetMarkCount = Math . round ( baselineMarkCount * widthMultiplier ) ;
55-
56- // Ensure reasonable bounds
57- const UPPER_BOUND = 50 ;
58- targetMarkCount = Math . max ( 3 , Math . min ( targetMarkCount , UPPER_BOUND ) ) ;
59-
60- // Adjust density based on maximum character width of mark labels
61- // Estimate the maximum characters in any mark label
62- const maxValueChars = Math . max (
63- String ( minValue ) . length ,
64- String ( maxValue ) . length ,
65- String ( Math . abs ( minValue ) ) . length ,
66- String ( Math . abs ( maxValue ) ) . length
67- ) ;
68-
69- // Baseline: 3-4 characters (like "100", "250") work well with baseline density
70- // For longer labels, reduce density to prevent overlap
71- const baselineChars = 3.5 ;
72- if ( maxValueChars > baselineChars ) {
73- const charReductionFactor = baselineChars / maxValueChars ;
74- targetMarkCount = Math . round ( targetMarkCount * charReductionFactor ) ;
75- targetMarkCount = Math . max ( 2 , targetMarkCount ) ; // Ensure minimum of 2 marks
76- }
77- }
58+ // Use formatted label length to account for SI formatting
59+ // (e.g. labels that look like "100M" vs "100000000")
60+ const formattedMin = applyD3Format ( minValue , minValue , maxValue ) ;
61+ const formattedMax = applyD3Format ( maxValue , minValue , maxValue ) ;
62+ const maxValueChars = Math . max ( formattedMin . length , formattedMax . length ) ;
63+
64+ // Calculate required spacing based on label width
65+ // Estimate: 10px per character + 20px margin for spacing between labels
66+ // This provides comfortable spacing to prevent overlap
67+ const pixelsPerChar = 10 ;
68+ const spacingMargin = 20 ;
69+ const minPixelsPerMark = maxValueChars * pixelsPerChar + spacingMargin ;
70+
71+ const effectiveWidth = sliderWidth || 330 ;
72+
73+ // Calculate maximum number of marks that can fit without overlap
74+ let targetMarkCount = Math . floor ( effectiveWidth / minPixelsPerMark ) + 1 ;
75+ targetMarkCount = Math . max ( 3 , Math . min ( targetMarkCount , 50 ) ) ;
7876
7977 // Calculate the ideal interval between marks based on target count
8078 const range = maxValue - minValue ;
81- let idealInterval = range / ( targetMarkCount - 1 ) ;
82-
83- // Check if the step value is fractional and adjust density
84- if ( stepValue % 1 !== 0 ) {
85- // For fractional steps, reduce mark density by half to avoid clutter
86- targetMarkCount = Math . max ( 3 , Math . round ( targetMarkCount / 2 ) ) ;
87- idealInterval = range / ( targetMarkCount - 1 ) ;
88- }
79+ const idealInterval = range / ( targetMarkCount - 1 ) ;
8980
9081 // Calculate the multiplier needed to get close to idealInterval
9182 // Round to a "nice" number for cleaner mark placement
@@ -102,13 +93,13 @@ const estimateBestSteps = (
10293 niceMultiplier = 2 ;
10394 } else if ( normalized <= 3.5 ) {
10495 niceMultiplier = 2.5 ;
105- } else if ( normalized <= 7. 5) {
96+ } else if ( normalized <= 5 ) {
10697 niceMultiplier = 5 ;
10798 } else {
10899 niceMultiplier = 10 ;
109100 }
110101
111- const bestMultiplier = niceMultiplier * magnitude ;
102+ const bestMultiplier = Math . max ( 1 , niceMultiplier * magnitude ) ;
112103 const bestInterval = stepValue * bestMultiplier ;
113104
114105 // All marks must be at valid step positions: minValue + (n * stepValue)
@@ -174,31 +165,16 @@ export const setUndefined = (
174165 return definedMarks ;
175166} ;
176167
177- export const applyD3Format = ( mark : number , min : number , max : number ) => {
178- const mu_ten_factor = - 3 ;
179- const k_ten_factor = 3 ;
180-
181- const ten_factor = Math . log10 ( Math . abs ( mark ) ) ;
182- if (
183- mark === 0 ||
184- ( ten_factor > mu_ten_factor && ten_factor < k_ten_factor )
185- ) {
186- return String ( mark ) ;
187- }
188- const max_min_mean = ( Math . abs ( max ) + Math . abs ( min ) ) / 2 ;
189- const si_formatter = formatPrefix ( ',.0' , max_min_mean ) ;
190- return String ( si_formatter ( mark ) ) ;
191- } ;
192-
193168export const autoGenerateMarks = (
194169 min : number ,
195170 max : number ,
196171 step ?: number | null ,
197172 sliderWidth ?: number | null
198173) => {
199174 const marks = [ ] ;
200- // Always use dynamic logic, but pass the provided step as a constraint
201- const effectiveStep = step || calcStep ( min , max , 0 ) ;
175+
176+ const effectiveStep = step ?? 1 ;
177+
202178 const [ start , interval , chosenStep ] = estimateBestSteps (
203179 min ,
204180 max ,
0 commit comments