Skip to content

Fix floating point drift in linear scale ticks - #12308

Open
aoright wants to merge 1 commit into
chartjs:masterfrom
aoright:fix/12281-linear-ticks-precision-drift
Open

aoright wants to merge 1 commit into
chartjs:masterfrom
aoright:fix/12281-linear-ticks-precision-drift

Conversation

@aoright

@aoright aoright commented Sep 14, 2026

Copy link
Copy Markdown

Fixes #12281

Motivation and Root Cause

When generating linear scale ticks with bounds === 'ticks' on non-round bounds (e.g., min: 49.894, max: 51.5264, spacing: 0.2), niceMin = Math.floor(rmin / spacing) * spacing evaluates to 49.800000000000004 due to IEEE-754 floating-point multiplication.
Because _decimalPlaces(niceMin) counts the decimal digits needed to write that value, it evaluates to 16 instead of 1.
Consequently, decimalPlaces = Math.max(_decimalPlaces(spacing), _decimalPlaces(niceMin)) becomes 16, resulting in factor = 1e16.
The subsequent tick rounding safeguard Math.round((niceMin + j * spacing) * factor) / factor then rounds to 16 decimal places, preserving floating-point drift in intermediate ticks (e.g., producing 50.00000000000001 instead of 50).
This causes custom ticks.callback functions checking integer values (e.g., value => Number.isInteger(value) ? value : '') to silently fail to match integer ticks and discard axis labels.

Changes Summary

  • In src/scales/scale.linearbase.js: Normalized niceMin and niceMax to _decimalPlaces(spacing) using spacingFactor = Math.pow(10, _decimalPlaces(spacing) || 0) immediately after computing floor and ceil multiples in the bounds === 'ticks' branch. Since niceMin and niceMax are integer multiples of spacing by definition, their mathematical precision cannot exceed _decimalPlaces(spacing).
  • In test/specs/scale.linear.tests.js: Added regression unit tests for positive non-round bounds (min: 49.894, max: 51.5264, stepSize: 0.2) and negative non-round bounds (min: -51.5264, max: -49.894, stepSize: 0.2) to verify that generated tick values are clean and integer ticks match Number.isInteger().

Verification Evidence

  • Native typecheck passed: pnpm lint-types completed with 0 errors.
  • Native linter passed: pnpm lint-js completed with 0 errors.
  • Native Karma tests in Chrome passed: ./node_modules/.bin/karma start ./karma.conf.cjs --single-run --browsers chrome --grep scale.linear executed 75 of 75 tests with SUCCESS.

Normalize niceMin and niceMax to _decimalPlaces(spacing) when bounds === 'ticks'. Prevents IEEE-754 precision artifacts from inflating decimalPlaces and preserving floating point drift in generated tick values.

Fixes chartjs#12281

Signed-off-by: aoright <102943475+aoright@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ticks may carry floating point drift

1 participant