From c3df0050cfafa199a1563722ac65173285caa46c Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sun, 15 Mar 2026 04:52:23 +0530 Subject: [PATCH 01/13] scale: add `ScaleBase.val_in_range` for domain validation Introduce a val_in_range method in ScaleBase to explicitly check whether values lie within the valid domain of a scale. The default implementation falls back to limit_range_for_scale for compatibility with existing scales. Specific scales (e.g., LogScale, LogitScale) override this method with more efficient checks. --- lib/matplotlib/scale.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 480ce59e34ec..540c42dd9f56 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -113,6 +113,17 @@ def limit_range_for_scale(self, vmin, vmax, minpos): This is used by log scales to determine a minimum value. """ return vmin, vmax + + def val_in_range(self, val): + """ + Return whether the value(s) are within the valid range for this scale. + """ + if np.isscalar(val): + vmin, vmax = self.limit_range_for_scale(val, val, minpos=1e-300) + return (vmax == val) and (vmin == val) + + val = np.asanyarray(val) + return np.array([self.val_in_range(v) for v in val]) def _make_axis_parameter_optional(init_func): @@ -195,6 +206,13 @@ def get_transform(self): `~matplotlib.transforms.IdentityTransform`. """ return IdentityTransform() + + def val_in_range(self, val): + """ + Return `True` for all values. + """ + val = np.asanyarray(val) + return np.ones(val.shape, dtype=bool) class FuncTransform(Transform): @@ -399,6 +417,10 @@ def limit_range_for_scale(self, vmin, vmax, minpos): return (minpos if vmin <= 0 else vmin, minpos if vmax <= 0 else vmax) + + def val_in_range(self, val): + """Return `True` for positive values.""" + return np.asanyarray(val) > 0 class FuncScaleLog(LogScale): @@ -819,6 +841,13 @@ def limit_range_for_scale(self, vmin, vmax, minpos): minpos = 1e-7 # Should rarely (if ever) have a visible effect. return (minpos if vmin <= 0 else vmin, 1 - minpos if vmax >= 1 else vmax) + + def val_in_range(self, val): + """ + Return `True` if value(s) lie between 0 and 1 (excluded) + """ + val = np.asanyarray(val) + return (val > 0) & (val < 1) _scale_mapping = { From f91d2b4a541d4d9e0440b07b8bc4b6c7970e87c8 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sun, 15 Mar 2026 04:53:11 +0530 Subject: [PATCH 02/13] typing: add `ScaleBase.val_in_range` to `scale.pyi` --- lib/matplotlib/scale.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/scale.pyi b/lib/matplotlib/scale.pyi index 7cef8a3438a8..186b33a453bf 100644 --- a/lib/matplotlib/scale.pyi +++ b/lib/matplotlib/scale.pyi @@ -3,6 +3,7 @@ from matplotlib.transforms import Transform from collections.abc import Callable, Iterable from typing import Literal +import numpy as np from numpy.typing import ArrayLike class ScaleBase: @@ -12,6 +13,7 @@ class ScaleBase: def limit_range_for_scale( self, vmin: float, vmax: float, minpos: float ) -> tuple[float, float]: ... + def val_in_range(self, val: ArrayLike) -> bool | np.ndarray: ... class LinearScale(ScaleBase): name: str From 926c2592b8c6f15626adbfef0104287833c49c9d Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sun, 15 Mar 2026 04:58:54 +0530 Subject: [PATCH 03/13] axis: add `Axis.val_in_range` to delgate the domain check --- lib/matplotlib/axis.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 2cd07f869060..7d4f52549cbb 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -824,6 +824,13 @@ def limit_range_for_scale(self, vmin, vmax): current scale. """ return self._scale.limit_range_for_scale(vmin, vmax, self.get_minpos()) + + def val_in_range(self, val): + """ + Return `True` if the value(s) lie within the domain supported by the + current scale. + """ + return self._scale.val_in_range(val) def _get_autoscale_on(self): """Return whether this Axis is autoscaled.""" From e3dc7b7ae1e909d5a4e85c71d7e3c4bf62552b92 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sun, 15 Mar 2026 04:59:28 +0530 Subject: [PATCH 04/13] typing: add `Axis.val_in_range` to `axis.pyi` --- lib/matplotlib/axis.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/axis.pyi b/lib/matplotlib/axis.pyi index 4bcfb1e1cfb7..9c632ebc689d 100644 --- a/lib/matplotlib/axis.pyi +++ b/lib/matplotlib/axis.pyi @@ -144,6 +144,7 @@ class Axis(martist.Artist): def limit_range_for_scale( self, vmin: float, vmax: float ) -> tuple[float, float]: ... + def val_in_range(self, val: ArrayLike) -> bool | np.ndarray: ... def get_children(self) -> list[martist.Artist]: ... # TODO units converter: Any From 6feeec12180fa68edf024bf59668a33770fa6885 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sun, 15 Mar 2026 05:00:18 +0530 Subject: [PATCH 05/13] axes: modify `_point_in_data_domain` to use the new `Axis.val_in_range` --- lib/matplotlib/axes/_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 947457100730..c9eadb4663a4 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2479,8 +2479,7 @@ def _point_in_data_domain(self, x, y): (e.g. negative coordinates with a log scale). """ for val, axis in zip([x, y], self._axis_map.values()): - vmin, vmax = axis.limit_range_for_scale(val, val) - if vmin != val or vmax != val: + if not axis._scale.val_in_range(val): return False return True From 631a52f6cb95f120debb0af88d373ee96083fd7f Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sun, 15 Mar 2026 14:27:20 +0530 Subject: [PATCH 06/13] scale: refactor `val_in_range` to accept scalar values and return a single bool --- lib/matplotlib/scale.py | 45 +++++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 540c42dd9f56..a2aa44f00c88 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -113,17 +113,19 @@ def limit_range_for_scale(self, vmin, vmax, minpos): This is used by log scales to determine a minimum value. """ return vmin, vmax - + def val_in_range(self, val): """ Return whether the value(s) are within the valid range for this scale. + + This method is a generic implementation. Subclasses may implement more + efficient solutions for their domain. """ - if np.isscalar(val): + try: vmin, vmax = self.limit_range_for_scale(val, val, minpos=1e-300) - return (vmax == val) and (vmin == val) - - val = np.asanyarray(val) - return np.array([self.val_in_range(v) for v in val]) + return vmin == val and vmax == val + except (TypeError, ValueError): + return False def _make_axis_parameter_optional(init_func): @@ -206,13 +208,14 @@ def get_transform(self): `~matplotlib.transforms.IdentityTransform`. """ return IdentityTransform() - + def val_in_range(self, val): """ - Return `True` for all values. + Return whether the value is within the valid range for this scale. + + This is True for all values. """ - val = np.asanyarray(val) - return np.ones(val.shape, dtype=bool) + return True class FuncTransform(Transform): @@ -417,10 +420,17 @@ def limit_range_for_scale(self, vmin, vmax, minpos): return (minpos if vmin <= 0 else vmin, minpos if vmax <= 0 else vmax) - + def val_in_range(self, val): - """Return `True` for positive values.""" - return np.asanyarray(val) > 0 + """ + Return whether the value is within the valid range for this scale. + + This is True for value(s) > 0 + """ + if np.isnan(val): + return False + else: + return val > 0 class FuncScaleLog(LogScale): @@ -841,13 +851,14 @@ def limit_range_for_scale(self, vmin, vmax, minpos): minpos = 1e-7 # Should rarely (if ever) have a visible effect. return (minpos if vmin <= 0 else vmin, 1 - minpos if vmax >= 1 else vmax) - + def val_in_range(self, val): """ - Return `True` if value(s) lie between 0 and 1 (excluded) + Return whether the value is within the valid range for this scale. + + This is True for value(s) which are between 0 and 1 (excluded). """ - val = np.asanyarray(val) - return (val > 0) & (val < 1) + return (val > 0) and (val < 1) _scale_mapping = { From d62a69f28c6fea4576db7b66a6910cc5df3da729 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sun, 15 Mar 2026 14:28:07 +0530 Subject: [PATCH 07/13] typing: refactor `Scale.val_in_range` to reflect the type changes --- lib/matplotlib/scale.pyi | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/matplotlib/scale.pyi b/lib/matplotlib/scale.pyi index 186b33a453bf..866509ee020d 100644 --- a/lib/matplotlib/scale.pyi +++ b/lib/matplotlib/scale.pyi @@ -3,7 +3,6 @@ from matplotlib.transforms import Transform from collections.abc import Callable, Iterable from typing import Literal -import numpy as np from numpy.typing import ArrayLike class ScaleBase: @@ -13,7 +12,7 @@ class ScaleBase: def limit_range_for_scale( self, vmin: float, vmax: float, minpos: float ) -> tuple[float, float]: ... - def val_in_range(self, val: ArrayLike) -> bool | np.ndarray: ... + def val_in_range(self, val: float) -> bool: ... class LinearScale(ScaleBase): name: str From 59bd52b5bd1a83450a32c820bcf3aeb7660d3e6f Mon Sep 17 00:00:00 2001 From: null-dreams Date: Wed, 18 Mar 2026 16:40:46 +0530 Subject: [PATCH 08/13] tests: add test cases for `val_in_range` for different scales --- lib/matplotlib/tests/test_scale.py | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index f98e083d84a0..6c2e0d2e6056 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -371,3 +371,63 @@ def set_default_locators_and_formatters(self, axis): # cleanup - there's no public unregister_scale() del mscale._scale_mapping["custom"] del mscale._scale_has_axis_parameter["custom"] + + +def test_val_in_range(): + + test_cases = [ + # LinearScale: Always True (even for Inf/NaN) + ('linear', 10.0, True), + ('linear', -10.0, True), + ('linear', 0.0, True), + ('linear', np.inf, True), + ('linear', np.nan, True), + + # LogScale: Only positive values (> 0) + ('log', 1.0, True), + ('log', 1e-300, True), + ('log', 0.0, False), + ('log', -1.0, False), + ('log', np.inf, True), + ('log', np.nan, False), + + # LogitScale: Strictly between 0 and 1 + ('logit', 0.5, True), + ('logit', 0.0, False), + ('logit', 1.0, False), + ('logit', -0.1, False), + ('logit', 1.1, False), + ('logit', np.inf, False), + ('logit', np.nan, False), + + # SymmetricalLogScale: Valid for all real numbers + # Uses ScaleBase fallback. NaN returns False since NaN != NaN + ('symlog', 10.0, True), + ('symlog', -10.0, True), + ('symlog', 0.0, True), + ('symlog', np.inf, True), + ('symlog', np.nan, False), + ] + + for name, val, expected in test_cases: + scale_cls = mscale._scale_mapping[name] + s = scale_cls(axis=None) + + result = s.val_in_range(val) + assert result is expected, ( + f"Failed {name}.val_in_range({val})." + f"Expected {expected}, got {result}" + ) + + +def test_val_in_range_base_fallback(): + # Directly test the ScaleBase fallback for custom scales. + # ScaleBase.limit_range_for_scale returns values unchanged by default + s = mscale.ScaleBase(axis=None) + + # Normal values should be True + assert s.val_in_range(1.0) is True + assert s.val_in_range(-5.5) is True + + # NaN check: fallback uses 'vmin == vmax' + assert s.val_in_range(np.nan) is False From 6d03949171f74038d1359f12b2a90b5457750206 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Wed, 18 Mar 2026 16:52:23 +0530 Subject: [PATCH 09/13] refactor: remove 'val_in_range' from axis.py and the corressponding pyi file --- lib/matplotlib/axis.py | 7 ------- lib/matplotlib/axis.pyi | 1 - 2 files changed, 8 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 7d4f52549cbb..2cd07f869060 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -824,13 +824,6 @@ def limit_range_for_scale(self, vmin, vmax): current scale. """ return self._scale.limit_range_for_scale(vmin, vmax, self.get_minpos()) - - def val_in_range(self, val): - """ - Return `True` if the value(s) lie within the domain supported by the - current scale. - """ - return self._scale.val_in_range(val) def _get_autoscale_on(self): """Return whether this Axis is autoscaled.""" diff --git a/lib/matplotlib/axis.pyi b/lib/matplotlib/axis.pyi index 9c632ebc689d..4bcfb1e1cfb7 100644 --- a/lib/matplotlib/axis.pyi +++ b/lib/matplotlib/axis.pyi @@ -144,7 +144,6 @@ class Axis(martist.Artist): def limit_range_for_scale( self, vmin: float, vmax: float ) -> tuple[float, float]: ... - def val_in_range(self, val: ArrayLike) -> bool | np.ndarray: ... def get_children(self) -> list[martist.Artist]: ... # TODO units converter: Any From b78b00361a053e14e2424e6740b7186de26596cd Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 19 Mar 2026 01:56:19 +0530 Subject: [PATCH 10/13] refactor: add finite bound check for base class and implemented classes --- lib/matplotlib/scale.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index a2aa44f00c88..64fffc0c0c84 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -34,6 +34,7 @@ from functools import wraps import numpy as np +import math import matplotlib as mpl from matplotlib import _api, _docstring @@ -122,8 +123,11 @@ def val_in_range(self, val): efficient solutions for their domain. """ try: - vmin, vmax = self.limit_range_for_scale(val, val, minpos=1e-300) - return vmin == val and vmax == val + if not math.isfinite(val): + return False + else: + vmin, vmax = self.limit_range_for_scale(val, val, minpos=1e-300) + return vmin == val and vmax == val except (TypeError, ValueError): return False @@ -213,9 +217,9 @@ def val_in_range(self, val): """ Return whether the value is within the valid range for this scale. - This is True for all values. + This is True for all values, except +-inf and NaN. """ - return True + return math.isfinite(val) class FuncTransform(Transform): @@ -425,9 +429,9 @@ def val_in_range(self, val): """ Return whether the value is within the valid range for this scale. - This is True for value(s) > 0 + This is True for value(s) > 0 except +inf. """ - if np.isnan(val): + if not math.isfinite(val): return False else: return val > 0 @@ -613,6 +617,14 @@ def get_transform(self): """Return the `.SymmetricalLogTransform` associated with this scale.""" return self._transform + def val_in_range(self, val): + """ + Return whether the value is within the valid range for this scale. + + This is True for all values, except +-inf and NaN. + """ + return math.isfinite(val) + class AsinhTransform(Transform): """Inverse hyperbolic-sine transformation used by `.AsinhScale`""" @@ -739,6 +751,14 @@ def set_default_locators_and_formatters(self, axis): else: axis.set_major_formatter('{x:.3g}') + def val_in_range(self, val): + """ + Return whether the value is within the valid range for this scale. + + This is True for all values, except +-inf and NaN. + """ + return math.isfinite(val) + class LogitTransform(Transform): input_dims = output_dims = 1 From 3de6b1f9622fc05de9d215520c74b7299e5f8499 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Thu, 19 Mar 2026 01:57:02 +0530 Subject: [PATCH 11/13] tests: edited test cases to fit the new requirements --- lib/matplotlib/tests/test_scale.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 6c2e0d2e6056..9f882103967e 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -380,15 +380,15 @@ def test_val_in_range(): ('linear', 10.0, True), ('linear', -10.0, True), ('linear', 0.0, True), - ('linear', np.inf, True), - ('linear', np.nan, True), + ('linear', np.inf, False), + ('linear', np.nan, False), # LogScale: Only positive values (> 0) ('log', 1.0, True), ('log', 1e-300, True), ('log', 0.0, False), ('log', -1.0, False), - ('log', np.inf, True), + ('log', np.inf, False), ('log', np.nan, False), # LogitScale: Strictly between 0 and 1 @@ -405,7 +405,7 @@ def test_val_in_range(): ('symlog', 10.0, True), ('symlog', -10.0, True), ('symlog', 0.0, True), - ('symlog', np.inf, True), + ('symlog', np.inf, False), ('symlog', np.nan, False), ] @@ -429,5 +429,7 @@ def test_val_in_range_base_fallback(): assert s.val_in_range(1.0) is True assert s.val_in_range(-5.5) is True - # NaN check: fallback uses 'vmin == vmax' + # NaN and Inf returns False since they cannot be drawn in a plot assert s.val_in_range(np.nan) is False + assert s.val_in_range(np.inf) is False + assert s.val_in_range(-np.inf) is False From c35268fb9e516ee9091f545b745d89999eab28b9 Mon Sep 17 00:00:00 2001 From: null-dreams Date: Fri, 20 Mar 2026 08:17:09 +0530 Subject: [PATCH 12/13] refactor: compact function body in LogScale --- lib/matplotlib/scale.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 64fffc0c0c84..7cc01e0c7f24 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -431,10 +431,7 @@ def val_in_range(self, val): This is True for value(s) > 0 except +inf. """ - if not math.isfinite(val): - return False - else: - return val > 0 + return math.isfinite(val) and val > 0 class FuncScaleLog(LogScale): From 6b820a8b53b38fb9913243be768f3657adc7b6fe Mon Sep 17 00:00:00 2001 From: null-dreams Date: Sat, 21 Mar 2026 02:00:14 +0530 Subject: [PATCH 13/13] refactor: made changes as suggested by reviewer --- lib/matplotlib/scale.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 7cc01e0c7f24..0793bb31e566 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -30,11 +30,11 @@ """ # noqa: E501 import inspect +import math import textwrap from functools import wraps import numpy as np -import math import matplotlib as mpl from matplotlib import _api, _docstring @@ -429,7 +429,7 @@ def val_in_range(self, val): """ Return whether the value is within the valid range for this scale. - This is True for value(s) > 0 except +inf. + This is True for value(s) > 0 except +inf and NaN. """ return math.isfinite(val) and val > 0 @@ -875,7 +875,7 @@ def val_in_range(self, val): This is True for value(s) which are between 0 and 1 (excluded). """ - return (val > 0) and (val < 1) + return 0 < val < 1 _scale_mapping = {