From bbcedf1bce905730314fba1dbf45ebe3909407e2 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 18 Nov 2025 10:20:03 -0700 Subject: [PATCH 1/7] Fix bug where too many marks could be rendered on slider --- .../src/utils/computeSliderMarkers.ts | 87 +++++++----------- .../integration/sliders/test_marks_density.py | 89 +++++++++++++++++++ .../tests/unit/computeSliderMarkers.test.ts | 76 ++++++++++++++++ 3 files changed, 198 insertions(+), 54 deletions(-) create mode 100644 components/dash-core-components/tests/integration/sliders/test_marks_density.py diff --git a/components/dash-core-components/src/utils/computeSliderMarkers.ts b/components/dash-core-components/src/utils/computeSliderMarkers.ts index a3081e5460..bc094bca3c 100644 --- a/components/dash-core-components/src/utils/computeSliderMarkers.ts +++ b/components/dash-core-components/src/utils/computeSliderMarkers.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-magic-numbers */ import {pickBy, isEmpty, isNil} from 'ramda'; import {formatPrefix} from 'd3-format'; import {SliderMarks} from '../types'; @@ -38,7 +39,6 @@ const estimateBestSteps = ( stepValue: number, sliderWidth?: number | null ) => { - // Base desired count for 330px slider with 0-100 scale (10 marks = 11 total including endpoints) let targetMarkCount = 11; // Default baseline // Scale mark density based on slider width @@ -87,31 +87,30 @@ const estimateBestSteps = ( idealInterval = range / (targetMarkCount - 1); } - // Find the best interval that's a multiple of stepValue - // Start with multiples of stepValue and find the one closest to idealInterval - const stepMultipliers = [ - // eslint-disable-next-line no-magic-numbers - 1, 2, 2.5, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, - ]; - - let bestInterval = stepValue; - let bestDifference = Math.abs(idealInterval - stepValue); - - for (const multiplier of stepMultipliers) { - const candidateInterval = stepValue * multiplier; - const difference = Math.abs(idealInterval - candidateInterval); - - if (difference < bestDifference) { - bestInterval = candidateInterval; - bestDifference = difference; - } - - // Stop if we've gone too far beyond the ideal - if (candidateInterval > idealInterval * 2) { - break; - } + // Calculate the multiplier needed to get close to idealInterval + // Round to a "nice" number for cleaner mark placement + const rawMultiplier = idealInterval / stepValue; + + // Round to nearest nice multiplier (1, 2, 2.5, 5, or power of 10 multiple) + const magnitude = Math.pow(10, Math.floor(Math.log10(rawMultiplier))); + const normalized = rawMultiplier / magnitude; // Now between 1 and 10 + + let niceMultiplier; + if (normalized <= 1.5) { + niceMultiplier = 1; + } else if (normalized <= 2.25) { + niceMultiplier = 2; + } else if (normalized <= 3.5) { + niceMultiplier = 2.5; + } else if (normalized <= 7.5) { + niceMultiplier = 5; + } else { + niceMultiplier = 10; } + const bestMultiplier = niceMultiplier * magnitude; + const bestInterval = stepValue * bestMultiplier; + // All marks must be at valid step positions: minValue + (n * stepValue) // Find the first mark after minValue that fits our desired interval const stepsInInterval = Math.round(bestInterval / stepValue); @@ -208,38 +207,18 @@ export const autoGenerateMarks = ( ); let cursor = start; - // Apply a safety cap to prevent excessive mark generation while preserving existing behavior - // Only restrict when marks would be truly excessive (much higher than the existing UPPER_BOUND) - const MARK_WIDTH_PX = 20; // More generous spacing for width-based calculation - const FALLBACK_MAX_MARKS = 200; // High fallback to preserve existing behavior when no width - const ABSOLUTE_MAX_MARKS = 200; // Safety cap against extreme cases - - const widthBasedMax = sliderWidth - ? Math.max(10, Math.floor(sliderWidth / MARK_WIDTH_PX)) - : FALLBACK_MAX_MARKS; - - const maxAutoGeneratedMarks = Math.min(widthBasedMax, ABSOLUTE_MAX_MARKS); - - // Calculate how many marks would be generated with current interval - const estimatedMarkCount = Math.floor((max - start) / interval) + 1; - - // If we would exceed the limit, increase the interval to fit within the limit - let actualInterval = interval; - if (estimatedMarkCount > maxAutoGeneratedMarks) { - // Recalculate interval to fit exactly within the limit - actualInterval = (max - start) / (maxAutoGeneratedMarks - 1); - // Round to a reasonable step multiple to keep marks clean - const stepMultiple = Math.ceil(actualInterval / chosenStep); - actualInterval = stepMultiple * chosenStep; - } - - if ((max - cursor) / actualInterval > 0) { - do { + if ((max - cursor) / interval > 0) { + while (cursor < max) { marks.push(alignValue(cursor, chosenStep)); - cursor += actualInterval; - } while (cursor < max && marks.length < maxAutoGeneratedMarks); + const prevCursor = cursor; + cursor += interval; + + // Safety check: floating point precision could impact this loop + if (cursor <= prevCursor) { + break; + } + } - // do some cosmetic const discardThreshold = 1.5; if ( marks.length >= 2 && diff --git a/components/dash-core-components/tests/integration/sliders/test_marks_density.py b/components/dash-core-components/tests/integration/sliders/test_marks_density.py new file mode 100644 index 0000000000..1ed956da45 --- /dev/null +++ b/components/dash-core-components/tests/integration/sliders/test_marks_density.py @@ -0,0 +1,89 @@ +from dash import Dash, dcc, html + + +def test_slsl_extreme_range_marks_density(dash_dcc): + """ + Test that extreme ranges don't generate too many overlapping marks. + + With min=-1, max=480256671, and container width ~365px, we should have + no more than ~7 marks to prevent overlap (given the long labels). + """ + app = Dash(__name__) + app.layout = html.Div( + style={"width": "365px"}, + children=[ + dcc.RangeSlider( + id="rangeslider-extreme", + min=-1, + max=480256671, + value=[-1, 480256671], + ) + ], + ) + + dash_dcc.start_server(app) + + # Wait for component to render + dash_dcc.wait_for_element("#rangeslider-extreme") + + # Count the rendered marks + marks = dash_dcc.find_elements(".dash-slider-mark") + mark_count = len(marks) + + print(f"Number of marks rendered: {mark_count}") + + # Get the actual mark text to verify what's rendered + mark_texts = [mark.text for mark in marks] + print(f"Mark labels: {mark_texts}") + + # Should have between 2 and 7 marks (min/max plus a few intermediate) + assert 2 <= mark_count <= 7, ( + f"Expected 2-7 marks for extreme range, but found {mark_count}. " + f"This suggests overlapping marks. Labels: {mark_texts}" + ) + + # Verify min and max are included + assert "-1" in mark_texts, "Min value (-1) should be included in marks" + assert any( + "480" in text or "M" in text for text in mark_texts + ), "Max value should be included in marks" + + assert dash_dcc.get_logs() == [] + + +def test_slsl_extreme_range_no_width(dash_dcc): + """ + Test that extreme ranges work even before width is measured. + + This simulates the initial render state where sliderWidth is null. + """ + app = Dash(__name__) + app.layout = html.Div( + # No explicit width, so ResizeObserver will measure it + children=[ + dcc.RangeSlider( + id="rangeslider-no-width", + min=-1, + max=480256671, + value=[-1, 480256671], + ) + ], + ) + + dash_dcc.start_server(app) + + # Wait for component to render + dash_dcc.wait_for_element("#rangeslider-no-width") + + # Count the rendered marks + marks = dash_dcc.find_elements(".dash-slider-mark") + mark_count = len(marks) + + print(f"Number of marks rendered (no explicit width): {mark_count}") + + # Even without explicit width, should not have too many marks + assert ( + mark_count <= 7 + ), f"Expected <= 7 marks even without explicit width, but found {mark_count}" + + assert dash_dcc.get_logs() == [] diff --git a/components/dash-core-components/tests/unit/computeSliderMarkers.test.ts b/components/dash-core-components/tests/unit/computeSliderMarkers.test.ts index ef46274b0e..31c34b282b 100644 --- a/components/dash-core-components/tests/unit/computeSliderMarkers.test.ts +++ b/components/dash-core-components/tests/unit/computeSliderMarkers.test.ts @@ -338,4 +338,80 @@ describe('Dynamic Slider Mark Density', () => { expect(areAllMarksValidSteps(width660, 0, 5)).toBe(true); }); }); + + describe('Extreme ranges with large numbers', () => { + test('should not create overlapping marks for range -1 to 480256671 WITHOUT width (initial render)', () => { + const marks = sanitizeMarks({ + min: -1, + max: 480256671, + marks: undefined, + step: undefined, // Let it auto-calculate + sliderWidth: null, // Simulates initial render before width is measured + }); + + const positions = getMarkPositions(marks); + + // Should have min and max + expect(positions[0]).toBe(-1); + expect(positions[positions.length - 1]).toBe(480256671); + + // Should have reasonable number of marks to prevent overlap even without width + // With ~9-character labels (480256671), we need substantial spacing + // Labels like "45M", "95M" are ~3-4 chars, so reasonable mark count is 5-7 + expect(positions.length).toBeGreaterThanOrEqual(2); // At least min and max + expect(positions.length).toBeLessThanOrEqual(11); // Not too many to cause overlap + + // Even without explicit width, assume a reasonable default (330px baseline) + // and verify spacing would be sufficient + const estimatedSpacing = 330 / (positions.length - 1); + expect(estimatedSpacing).toBeGreaterThanOrEqual(30); + }); + + test('should not create overlapping marks for range -1 to 480256671 at 365px width', () => { + const marks = sanitizeMarks({ + min: -1, + max: 480256671, + marks: undefined, + step: undefined, // Let it auto-calculate + sliderWidth: 365, + }); + + const positions = getMarkPositions(marks); + + // Should have min and max + expect(positions[0]).toBe(-1); + expect(positions[positions.length - 1]).toBe(480256671); + + // Should have reasonable number of marks to prevent overlap + // With 365px width and ~9-character labels (480256671), we need substantial spacing + // Estimate: 9 chars * 8px/char = 72px per label, so max ~5 marks for 365px + expect(positions.length).toBeGreaterThanOrEqual(2); // At least min and max + expect(positions.length).toBeLessThanOrEqual(7); // Not too many to cause overlap + + // Verify spacing between marks is sufficient + // With 365px width, marks should be at least 50px apart for long labels + const estimatedSpacing = 365 / (positions.length - 1); + expect(estimatedSpacing).toBeGreaterThanOrEqual(50); + }); + + test('should handle very large ranges with appropriate step multipliers', () => { + const marks = sanitizeMarks({ + min: 0, + max: 1000000000, // 1 billion + marks: undefined, + step: undefined, + sliderWidth: 330, + }); + + const positions = getMarkPositions(marks); + + // Should have reasonable mark count + expect(positions.length).toBeGreaterThanOrEqual(2); + expect(positions.length).toBeLessThanOrEqual(15); + + // Should include min and max + expect(positions[0]).toBe(0); + expect(positions[positions.length - 1]).toBe(1000000000); + }); + }); }); From 2ec6d1a76ee555f552d8520a83349136b1f09655 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 18 Nov 2025 10:21:55 -0700 Subject: [PATCH 2/7] Add className and styling for slider markers that fall outside the selected range --- .../src/components/css/sliders.css | 21 ++++++++++ .../src/fragments/RangeSlider.tsx | 2 + .../src/utils/sliderRendering.tsx | 42 ++++++++++++++++++- .../integration/sliders/test_marks_density.py | 15 +++---- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/components/dash-core-components/src/components/css/sliders.css b/components/dash-core-components/src/components/css/sliders.css index 2254054041..bb2335c9da 100644 --- a/components/dash-core-components/src/components/css/sliders.css +++ b/components/dash-core-components/src/components/css/sliders.css @@ -74,6 +74,10 @@ box-shadow: 0 4px 6px -1px var(--Dash-Shading-Weak); } +.dash-slider-thumb:focus { + outline: 1px solid var(--Dash-Fill-Interactive-Strong); +} + .dash-slider-thumb:focus .dash-slider-tooltip, .dash-slider-thumb:hover .dash-slider-tooltip { display: block; @@ -93,6 +97,10 @@ pointer-events: none; } +.dash-slider-mark-outside-selection { + color: var(--Dash-Text-Disabled); +} + .dash-slider-mark:before { content: ''; position: absolute; @@ -119,6 +127,10 @@ width: 8px; height: 8px; border-radius: 50%; + background-color: var(--Dash-Fill-Primary-Active); +} + +.dash-slider-dot-outside-selection { background-color: var(--Dash-Fill-Disabled); } @@ -203,6 +215,15 @@ appearance: textfield; } +.dash-range-slider-input::selection, +.dash-range-slider-input::-webkit-selection { + background: var(--Dash-Fill-Primary-Active); +} + +.dash-range-slider-input:focus { + outline: none; +} + /* Hide the number input spinners */ .dash-range-slider-input::-webkit-inner-spin-button, .dash-range-slider-input::-webkit-outer-spin-button { diff --git a/components/dash-core-components/src/fragments/RangeSlider.tsx b/components/dash-core-components/src/fragments/RangeSlider.tsx index 370269fa91..697e67dfe0 100644 --- a/components/dash-core-components/src/fragments/RangeSlider.tsx +++ b/components/dash-core-components/src/fragments/RangeSlider.tsx @@ -500,6 +500,7 @@ export default function RangeSlider(props: RangeSliderProps) { renderedMarks, !!vertical, minMaxValues, + value, !!dots, !!reverse )} @@ -508,6 +509,7 @@ export default function RangeSlider(props: RangeSliderProps) { renderSliderDots( stepValue, minMaxValues, + value, !!vertical, !!reverse )} diff --git a/components/dash-core-components/src/utils/sliderRendering.tsx b/components/dash-core-components/src/utils/sliderRendering.tsx index 68f16c3d0b..0e8ae37ab9 100644 --- a/components/dash-core-components/src/utils/sliderRendering.tsx +++ b/components/dash-core-components/src/utils/sliderRendering.tsx @@ -58,6 +58,7 @@ export const renderSliderMarks = ( renderedMarks: SliderMarks, vertical: boolean, minMaxValues: {min_mark: number; max_mark: number}, + selectedValues: number[] = [], dots: boolean, reverse = false ) => { @@ -83,10 +84,30 @@ export const renderSliderMarks = ( transform: 'translateX(-50%)', }; + // Determine if mark is outside the selected range + let isOutsideSelection = false; + if (selectedValues.length === 1) { + isOutsideSelection = pos > selectedValues[0]; + } else if (selectedValues.length > 1) { + const [minValue, maxValue] = [ + selectedValues[0], + selectedValues[selectedValues.length - 1], + ]; + isOutsideSelection = pos < minValue || pos > maxValue; + } + + const outsideClassName = isOutsideSelection + ? 'dash-slider-mark-outside-selection' + : ''; + + const className = `dash-slider-mark ${ + dots ? 'with-dots' : '' + } ${outsideClassName}`.trim(); + return (
{ @@ -149,10 +171,26 @@ export const renderSliderDots = ( transform: 'translate(-50%, 50%)', }; + // Determine if dot is outside the selected range + let isOutsideSelection = false; + if (selectedValues.length === 1) { + isOutsideSelection = dotValue > selectedValues[0]; + } else if (selectedValues.length > 1) { + const [minValue, maxValue] = [ + selectedValues[0], + selectedValues[selectedValues.length - 1], + ]; + isOutsideSelection = dotValue < minValue || dotValue > maxValue; + } + + const className = isOutsideSelection + ? 'dash-slider-dot dash-slider-dot-outside-selection' + : 'dash-slider-dot'; + return (
Date: Tue, 18 Nov 2025 11:58:51 -0700 Subject: [PATCH 3/7] empty commit for ci From 6d08b9989bc04f6e10be8610c72e0a9d0c01bcb9 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Tue, 18 Nov 2025 12:49:32 -0700 Subject: [PATCH 4/7] remove set_trace --- .../tests/integration/sliders/test_marks_density.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/components/dash-core-components/tests/integration/sliders/test_marks_density.py b/components/dash-core-components/tests/integration/sliders/test_marks_density.py index c609eb6fdd..71a621eb5f 100644 --- a/components/dash-core-components/tests/integration/sliders/test_marks_density.py +++ b/components/dash-core-components/tests/integration/sliders/test_marks_density.py @@ -68,10 +68,6 @@ def test_slsl_extreme_range_no_width(dash_dcc): dash_dcc.start_server(app) - import pdb - - pdb.set_trace() - # Wait for component to render dash_dcc.wait_for_element("#rangeslider-no-width") From 0ab715ff19b8ee6b1869556469c32c860d52c62c Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 20 Nov 2025 14:26:47 -0700 Subject: [PATCH 5/7] fix test --- .../tests/integration/sliders/test_marks_density.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dash-core-components/tests/integration/sliders/test_marks_density.py b/components/dash-core-components/tests/integration/sliders/test_marks_density.py index 71a621eb5f..3f147c57c5 100644 --- a/components/dash-core-components/tests/integration/sliders/test_marks_density.py +++ b/components/dash-core-components/tests/integration/sliders/test_marks_density.py @@ -75,6 +75,6 @@ def test_slsl_extreme_range_no_width(dash_dcc): marks = dash_dcc.find_elements(".dash-slider-mark") mark_count = len(marks) - assert mark_count == 11, f"Expected default 11 marks, but found {mark_count}" + assert mark_count <= 11, f"Expected default 11 marks, but found {mark_count}" assert dash_dcc.get_logs() == [] From e692726cb377b2d254f39e993392b62a1e4c6be6 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 20 Nov 2025 16:15:17 -0700 Subject: [PATCH 6/7] Further mark density bugfixes --- .../dash-core-components/package-lock.json | 2 +- components/dash-core-components/package.json | 2 +- .../src/utils/computeSliderMarkers.ts | 104 +++++++----------- .../tests/unit/computeSliderMarkers.test.ts | 54 +++++---- 4 files changed, 72 insertions(+), 90 deletions(-) diff --git a/components/dash-core-components/package-lock.json b/components/dash-core-components/package-lock.json index b59b45a427..2b7671e840 100644 --- a/components/dash-core-components/package-lock.json +++ b/components/dash-core-components/package-lock.json @@ -51,7 +51,7 @@ "@plotly/webpack-dash-dynamic-import": "^1.3.0", "@types/d3-format": "^3.0.4", "@types/fast-isnumeric": "^1.1.2", - "@types/jest": "^29.5.0", + "@types/jest": "^29.5.14", "@types/js-search": "^1.4.4", "@types/ramda": "^0.31.0", "@types/react": "^16.14.8", diff --git a/components/dash-core-components/package.json b/components/dash-core-components/package.json index d53cc6cbd6..47b800335c 100644 --- a/components/dash-core-components/package.json +++ b/components/dash-core-components/package.json @@ -78,7 +78,7 @@ "@plotly/webpack-dash-dynamic-import": "^1.3.0", "@types/d3-format": "^3.0.4", "@types/fast-isnumeric": "^1.1.2", - "@types/jest": "^29.5.0", + "@types/jest": "^29.5.14", "@types/js-search": "^1.4.4", "@types/ramda": "^0.31.0", "@types/react": "^16.14.8", diff --git a/components/dash-core-components/src/utils/computeSliderMarkers.ts b/components/dash-core-components/src/utils/computeSliderMarkers.ts index bc094bca3c..fafaac1bd3 100644 --- a/components/dash-core-components/src/utils/computeSliderMarkers.ts +++ b/components/dash-core-components/src/utils/computeSliderMarkers.ts @@ -33,59 +33,50 @@ const alignDecimalValue = (v: number, d: number) => const alignValue = (v: number, d: number) => decimalCount(d) < 1 ? alignIntValue(v, d) : alignDecimalValue(v, d); +export const applyD3Format = (mark: number, min: number, max: number) => { + const mu_ten_factor = -3; + const k_ten_factor = 4; // values < 10000 don't get formatted + + const ten_factor = Math.log10(Math.abs(mark)); + if ( + mark === 0 || + (ten_factor > mu_ten_factor && ten_factor < k_ten_factor) + ) { + return String(mark); + } + const max_min_mean = (Math.abs(max) + Math.abs(min)) / 2; + const si_formatter = formatPrefix(',.0', max_min_mean); + return String(si_formatter(mark)); +}; + const estimateBestSteps = ( minValue: number, maxValue: number, stepValue: number, sliderWidth?: number | null ) => { - let targetMarkCount = 11; // Default baseline - - // Scale mark density based on slider width - if (sliderWidth) { - const baselineWidth = 330; - const baselineMarkCount = 11; // 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 - - // Calculate density multiplier based on width - const widthMultiplier = sliderWidth / baselineWidth; - - // Target mark count scales with width but maintains consistent density - // The range adjustment should be removed - we want consistent mark density based on width - targetMarkCount = Math.round(baselineMarkCount * widthMultiplier); - - // Ensure reasonable bounds - const UPPER_BOUND = 50; - targetMarkCount = Math.max(3, Math.min(targetMarkCount, UPPER_BOUND)); - - // Adjust density based on maximum character width of mark labels - // Estimate the maximum characters in any mark label - const maxValueChars = Math.max( - String(minValue).length, - String(maxValue).length, - String(Math.abs(minValue)).length, - String(Math.abs(maxValue)).length - ); - - // Baseline: 3-4 characters (like "100", "250") work well with baseline density - // For longer labels, reduce density to prevent overlap - const baselineChars = 3.5; - if (maxValueChars > baselineChars) { - const charReductionFactor = baselineChars / maxValueChars; - targetMarkCount = Math.round(targetMarkCount * charReductionFactor); - targetMarkCount = Math.max(2, targetMarkCount); // Ensure minimum of 2 marks - } - } + // Use formatted label length to account for SI formatting + // (e.g. labels that look like "100M" vs "100000000") + const formattedMin = applyD3Format(minValue, minValue, maxValue); + const formattedMax = applyD3Format(maxValue, minValue, maxValue); + const maxValueChars = Math.max(formattedMin.length, formattedMax.length); + + // Calculate required spacing based on label width + // Estimate: 10px per character + 20px margin for spacing between labels + // This provides comfortable spacing to prevent overlap + const pixelsPerChar = 10; + const spacingMargin = 20; + const minPixelsPerMark = maxValueChars * pixelsPerChar + spacingMargin; + + const effectiveWidth = sliderWidth || 330; + + // Calculate maximum number of marks that can fit without overlap + let targetMarkCount = Math.floor(effectiveWidth / minPixelsPerMark) + 1; + targetMarkCount = Math.max(3, Math.min(targetMarkCount, 50)); // Calculate the ideal interval between marks based on target count const range = maxValue - minValue; - let idealInterval = range / (targetMarkCount - 1); - - // Check if the step value is fractional and adjust density - if (stepValue % 1 !== 0) { - // For fractional steps, reduce mark density by half to avoid clutter - targetMarkCount = Math.max(3, Math.round(targetMarkCount / 2)); - idealInterval = range / (targetMarkCount - 1); - } + const idealInterval = range / (targetMarkCount - 1); // Calculate the multiplier needed to get close to idealInterval // Round to a "nice" number for cleaner mark placement @@ -102,13 +93,13 @@ const estimateBestSteps = ( niceMultiplier = 2; } else if (normalized <= 3.5) { niceMultiplier = 2.5; - } else if (normalized <= 7.5) { + } else if (normalized <= 5) { niceMultiplier = 5; } else { niceMultiplier = 10; } - const bestMultiplier = niceMultiplier * magnitude; + const bestMultiplier = Math.max(1, niceMultiplier * magnitude); const bestInterval = stepValue * bestMultiplier; // All marks must be at valid step positions: minValue + (n * stepValue) @@ -174,22 +165,6 @@ export const setUndefined = ( return definedMarks; }; -export const applyD3Format = (mark: number, min: number, max: number) => { - const mu_ten_factor = -3; - const k_ten_factor = 3; - - const ten_factor = Math.log10(Math.abs(mark)); - if ( - mark === 0 || - (ten_factor > mu_ten_factor && ten_factor < k_ten_factor) - ) { - return String(mark); - } - const max_min_mean = (Math.abs(max) + Math.abs(min)) / 2; - const si_formatter = formatPrefix(',.0', max_min_mean); - return String(si_formatter(mark)); -}; - export const autoGenerateMarks = ( min: number, max: number, @@ -197,8 +172,9 @@ export const autoGenerateMarks = ( sliderWidth?: number | null ) => { const marks = []; - // Always use dynamic logic, but pass the provided step as a constraint - const effectiveStep = step || calcStep(min, max, 0); + + const effectiveStep = step ?? 1; + const [start, interval, chosenStep] = estimateBestSteps( min, max, diff --git a/components/dash-core-components/tests/unit/computeSliderMarkers.test.ts b/components/dash-core-components/tests/unit/computeSliderMarkers.test.ts index 31c34b282b..2aa39d00c4 100644 --- a/components/dash-core-components/tests/unit/computeSliderMarkers.test.ts +++ b/components/dash-core-components/tests/unit/computeSliderMarkers.test.ts @@ -42,7 +42,7 @@ const areAllMarksValidSteps = ( describe('Dynamic Slider Mark Density', () => { describe('Baseline behavior (330px width)', () => { - test('should show ~10-11 marks for 0-100 range with step=5', () => { + test('should show appropriate marks for 0-100 range with step=5 at 330px', () => { const marks = sanitizeMarks({ min: 0, max: 100, @@ -54,7 +54,9 @@ describe('Dynamic Slider Mark Density', () => { expect(marks).toBeDefined(); const positions = getMarkPositions(marks); - expect(positions.length).toBe(11); + // With pixel-based algorithm: 3-char labels need ~50px per mark + // 330px / 50px = ~7 marks (plus min/max adjustment gives 8) + expect(positions.length).toBe(8); expect(areAllMarksValidSteps(marks, 0, 5)).toBe(true); expect(positions).toContain(0); expect(positions).toContain(100); @@ -220,36 +222,17 @@ describe('Dynamic Slider Mark Density', () => { }); }); - test('should reduce density by half for fractional steps', () => { - const integerStep = sanitizeMarks({ - min: 0, - max: 10, - marks: undefined, - step: 1, // Integer step - sliderWidth: 330, - }); - + test('should have appropriate density for fractional steps', () => { const fractionalStep = sanitizeMarks({ min: 0, max: 10, marks: undefined, step: 0.5, // Fractional step - sliderWidth: 330, + sliderWidth: 1600, }); - const integerPositions = getMarkPositions(integerStep); const fractionalPositions = getMarkPositions(fractionalStep); - - // Fractional step should have roughly half the marks of integer step - expect(fractionalPositions.length).toBeLessThan( - integerPositions.length - ); - expect(fractionalPositions.length).toBeLessThanOrEqual( - Math.ceil(integerPositions.length / 2) + 1 - ); - - // Both should be valid steps - expect(areAllMarksValidSteps(integerStep, 0, 1)).toBe(true); + expect(fractionalPositions.length).toBe(21); expect(areAllMarksValidSteps(fractionalStep, 0, 0.5)).toBe(true); }); @@ -413,5 +396,28 @@ describe('Dynamic Slider Mark Density', () => { expect(positions[0]).toBe(0); expect(positions[positions.length - 1]).toBe(1000000000); }); + + test('does not have all marks labeled as "2k" for range 1952 to 2007', () => { + const marks = sanitizeMarks({ + min: 1952, + max: 2007, + marks: undefined, + step: undefined, + sliderWidth: 365, + }); + + // Get all the label values (not the keys) + const labels = Object.values(marks); + + // Count unique labels + const uniqueLabels = new Set(labels); + + // Should have more than one unique label + expect(uniqueLabels.size).toBeGreaterThan(1); + + // Should NOT have all labels be "2k" + const allLabels2k = labels.every(label => label === '2k'); + expect(allLabels2k).toBe(false); + }); }); }); From 6d1c5e42ce0b2807d6d03fffa52a8b64013af918 Mon Sep 17 00:00:00 2001 From: Adrian Borrmann Date: Thu, 20 Nov 2025 16:36:36 -0700 Subject: [PATCH 7/7] attempt to fix flaky test --- tests/integration/callbacks/test_basic_callback.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/integration/callbacks/test_basic_callback.py b/tests/integration/callbacks/test_basic_callback.py index 91ba3ca9fc..a67964c1ba 100644 --- a/tests/integration/callbacks/test_basic_callback.py +++ b/tests/integration/callbacks/test_basic_callback.py @@ -420,6 +420,9 @@ def update_text(data): dash_duo.wait_for_text_to_equal("#output-1", "hello world") assert dash_duo.find_element("#output-1").get_attribute("data-cb") == "hello world" + # Wait for all callbacks to complete + time.sleep(0.1) + # an initial call, one for clearing the input # and one for each hello world character assert input_call_count.value == 2 + len("hello world")