|
| 1 | +/* eslint-disable no-magic-numbers */ |
1 | 2 | import {pickBy, isEmpty, isNil} from 'ramda'; |
2 | 3 | import {formatPrefix} from 'd3-format'; |
3 | 4 | import {SliderMarks} from '../types'; |
@@ -38,7 +39,6 @@ const estimateBestSteps = ( |
38 | 39 | stepValue: number, |
39 | 40 | sliderWidth?: number | null |
40 | 41 | ) => { |
41 | | - // Base desired count for 330px slider with 0-100 scale (10 marks = 11 total including endpoints) |
42 | 42 | let targetMarkCount = 11; // Default baseline |
43 | 43 |
|
44 | 44 | // Scale mark density based on slider width |
@@ -87,31 +87,30 @@ const estimateBestSteps = ( |
87 | 87 | idealInterval = range / (targetMarkCount - 1); |
88 | 88 | } |
89 | 89 |
|
90 | | - // Find the best interval that's a multiple of stepValue |
91 | | - // Start with multiples of stepValue and find the one closest to idealInterval |
92 | | - const stepMultipliers = [ |
93 | | - // eslint-disable-next-line no-magic-numbers |
94 | | - 1, 2, 2.5, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, |
95 | | - ]; |
96 | | - |
97 | | - let bestInterval = stepValue; |
98 | | - let bestDifference = Math.abs(idealInterval - stepValue); |
99 | | - |
100 | | - for (const multiplier of stepMultipliers) { |
101 | | - const candidateInterval = stepValue * multiplier; |
102 | | - const difference = Math.abs(idealInterval - candidateInterval); |
103 | | - |
104 | | - if (difference < bestDifference) { |
105 | | - bestInterval = candidateInterval; |
106 | | - bestDifference = difference; |
107 | | - } |
108 | | - |
109 | | - // Stop if we've gone too far beyond the ideal |
110 | | - if (candidateInterval > idealInterval * 2) { |
111 | | - break; |
112 | | - } |
| 90 | + // Calculate the multiplier needed to get close to idealInterval |
| 91 | + // Round to a "nice" number for cleaner mark placement |
| 92 | + const rawMultiplier = idealInterval / stepValue; |
| 93 | + |
| 94 | + // Round to nearest nice multiplier (1, 2, 2.5, 5, or power of 10 multiple) |
| 95 | + const magnitude = Math.pow(10, Math.floor(Math.log10(rawMultiplier))); |
| 96 | + const normalized = rawMultiplier / magnitude; // Now between 1 and 10 |
| 97 | + |
| 98 | + let niceMultiplier; |
| 99 | + if (normalized <= 1.5) { |
| 100 | + niceMultiplier = 1; |
| 101 | + } else if (normalized <= 2.25) { |
| 102 | + niceMultiplier = 2; |
| 103 | + } else if (normalized <= 3.5) { |
| 104 | + niceMultiplier = 2.5; |
| 105 | + } else if (normalized <= 7.5) { |
| 106 | + niceMultiplier = 5; |
| 107 | + } else { |
| 108 | + niceMultiplier = 10; |
113 | 109 | } |
114 | 110 |
|
| 111 | + const bestMultiplier = niceMultiplier * magnitude; |
| 112 | + const bestInterval = stepValue * bestMultiplier; |
| 113 | + |
115 | 114 | // All marks must be at valid step positions: minValue + (n * stepValue) |
116 | 115 | // Find the first mark after minValue that fits our desired interval |
117 | 116 | const stepsInInterval = Math.round(bestInterval / stepValue); |
@@ -208,38 +207,18 @@ export const autoGenerateMarks = ( |
208 | 207 | ); |
209 | 208 | let cursor = start; |
210 | 209 |
|
211 | | - // Apply a safety cap to prevent excessive mark generation while preserving existing behavior |
212 | | - // Only restrict when marks would be truly excessive (much higher than the existing UPPER_BOUND) |
213 | | - const MARK_WIDTH_PX = 20; // More generous spacing for width-based calculation |
214 | | - const FALLBACK_MAX_MARKS = 200; // High fallback to preserve existing behavior when no width |
215 | | - const ABSOLUTE_MAX_MARKS = 200; // Safety cap against extreme cases |
216 | | - |
217 | | - const widthBasedMax = sliderWidth |
218 | | - ? Math.max(10, Math.floor(sliderWidth / MARK_WIDTH_PX)) |
219 | | - : FALLBACK_MAX_MARKS; |
220 | | - |
221 | | - const maxAutoGeneratedMarks = Math.min(widthBasedMax, ABSOLUTE_MAX_MARKS); |
222 | | - |
223 | | - // Calculate how many marks would be generated with current interval |
224 | | - const estimatedMarkCount = Math.floor((max - start) / interval) + 1; |
225 | | - |
226 | | - // If we would exceed the limit, increase the interval to fit within the limit |
227 | | - let actualInterval = interval; |
228 | | - if (estimatedMarkCount > maxAutoGeneratedMarks) { |
229 | | - // Recalculate interval to fit exactly within the limit |
230 | | - actualInterval = (max - start) / (maxAutoGeneratedMarks - 1); |
231 | | - // Round to a reasonable step multiple to keep marks clean |
232 | | - const stepMultiple = Math.ceil(actualInterval / chosenStep); |
233 | | - actualInterval = stepMultiple * chosenStep; |
234 | | - } |
235 | | - |
236 | | - if ((max - cursor) / actualInterval > 0) { |
237 | | - do { |
| 210 | + if ((max - cursor) / interval > 0) { |
| 211 | + while (cursor < max) { |
238 | 212 | marks.push(alignValue(cursor, chosenStep)); |
239 | | - cursor += actualInterval; |
240 | | - } while (cursor < max && marks.length < maxAutoGeneratedMarks); |
| 213 | + const prevCursor = cursor; |
| 214 | + cursor += interval; |
| 215 | + |
| 216 | + // Safety check: floating point precision could impact this loop |
| 217 | + if (cursor <= prevCursor) { |
| 218 | + break; |
| 219 | + } |
| 220 | + } |
241 | 221 |
|
242 | | - // do some cosmetic |
243 | 222 | const discardThreshold = 1.5; |
244 | 223 | if ( |
245 | 224 | marks.length >= 2 && |
|
0 commit comments