8000 Add explicit converter setting to Axis (#28970) · matplotlib/matplotlib@32ec60d · GitHub
[go: up one dir, main page]

Skip to content

Commit 32ec60d

Browse files
authored
Add explicit converter setting to Axis (#28970)
* Add explicit converter setting to Axis Closes #19229 The replacement is the get/set_converter method. This includes changes to use the getter and the private setter in the qt figure options dialog menu. The choice to use the private setter was a defensive one as the public setter prevents being called multiple times (though does short circuit if an identical input is provided, which I think is actually true here, therefore the public one is probably functional (and a no-op).) It is not clear to me on analysis how the unit information is or was lost. A quick test commenting out the two lines which reset converter/units displayed no obvious detrimental effect to removing those, suggesting that even if once they were necessary, they may no longer be. These lines were last touched in #24141, though that really only generalized the code into a loop rather than copy/pasted x and y behavior. The original inclusion of resetting was in #4909, which indicated that the dialog reset unit info. AFAICT, that is no longer true, though I have not rigorously proved that.
1 parent c52cb41 commit 32ec60d

File tree

6 files changed

+100
-24
lines changed

6 files changed

+100
-24
lines changed

doc/api/axis_api.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ Units
169169
Axis.convert_units
170170
Axis.set_units
171171
Axis.get_units
172+
Axis.set_converter
173+
Axis.get_converter
172174
Axis.update_units
173175

174176

lib/matplotlib/axis.py

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -600,6 +600,10 @@ class Axis(martist.Artist):
600600
# The class used in _get_tick() to create tick instances. Must either be
601601
# overwritten in subclasses, or subclasses must reimplement _get_tick().
602602
_tick_class = None
603+
converter = _api.deprecate_privatize_attribute(
604+
"3.10",
605+
alternative="get_converter and set_converter methods"
606+
)
603607

604608
def __str__(self):
605609
return "{}({},{})".format(
@@ -656,7 +660,8 @@ def __init__(self, axes, *, pickradius=15, clear=True):
656660
if clear:
657661
self.clear()
658662
else:
659-
self.converter = None
663+
self._converter = None
664+
self._converter_is_explicit = False
660665
self.units = None
661666

662667
self._autoscale_on = True
@@ -886,7 +891,8 @@ def clear(self):
886891
mpl.rcParams['axes.grid.which'] in ('both', 'minor'))
887892
self.reset_ticks()
888893

889-
self.converter = None
894+
self._converter = None
895+
self._converter_is_explicit = False
890896
self.units = None
891897
self.stale = True
892898

@@ -1738,16 +1744,20 @@ def grid(self, visible=None, which='major', **kwargs):
17381744
def update_units(self, data):
17391745
"""
17401746
Introspect *data* for units converter and update the
1741-
``axis.converter`` instance if necessary. Return *True*
1747+
``axis.get_converter`` instance if necessary. Return *True*
17421748
if *data* is registered for unit conversion.
17431749
"""
1744-
converter = munits.registry.get_converter(data)
1750+
if not self._converter_is_explicit:
1751+
converter = munits.registry.get_converter(data)
1752+
else:
1753+
converter = self._converter
1754+
17451755
if converter is None:
17461756
return False
17471757

1748-
neednew = self.converter != converter
1749-
self.converter = converter
1750-
default = self.converter.default_units(data, self)
1758+
neednew = self._converter != converter
1759+
self._set_converter(converter)
1760+
default = self._converter.default_units(data, self)
17511761
if default is not None and self.units is None:
17521762
self.set_units(default)
17531763

@@ -1761,10 +1771,10 @@ def _update_axisinfo(self):
17611771
Check the axis converter for the stored units to see if the
17621772
axis info needs to be updated.
17631773
"""
1764-
if self.converter is None:
1774+
if self._converter is None:
17651775
return
17661776

1767-
info = self.converter.axisinfo(self.units, self)
1777+
info = self._converter.axisinfo(self.units, self)
17681778

17691779
if info is None:
17701780
return
@@ -1791,25 +1801,58 @@ def _update_axisinfo(self):
17911801
self.set_default_intervals()
17921802

17931803
def have_units(self):
1794-
return self.converter is not None or self.units is not None
1804+
return self._converter is not None or self.units is not None
17951805

17961806
def convert_units(self, x):
17971807
# If x is natively supported by Matplotlib, doesn't need converting
17981808
if munits._is_natively_supported(x):
17991809
return x
18001810

1801-
if self.converter is None:
1802-
self.converter = munits.registry.get_converter(x)
1811+
if self._converter is None:
1812+
self._set_converter(munits.registry.get_converter(x))
18031813

1804-
if self.converter is None:
1814+
if self._converter is None:
18051815
return x
18061816
try:
1807-
ret = self.converter.convert(x, self.units, self)
1817+
ret = self._converter.convert(x, self.units, self)
18081818
except Exception as e:
18091819
raise munits.ConversionError('Failed to convert value(s) to axis '
18101820
f'units: {x!r}') from e
18111821
return ret
18121822

1823+
def get_converter(self):
1824+
"""
1825+
Get the unit converter for axis.
1826+
1827+
Returns
1828+
-------
1829+
`~matplotlib.units.ConversionInterface` or None
1830+
"""
1831+
return self._converter
1832+
1833+
def set_converter(self, converter):
1834+
"""
1835+
Set the unit converter for axis.
1836+
1837+
Parameters
1838+
----------
1839+
converter : `~matplotlib.units.ConversionInterface`
1840+
"""
1841+
self._set_converter(converter)
1842+
self._converter_is_explicit = True
1843+
1844+
def _set_converter(self, converter):
1845+
if self._converter == converter:
1846+
return
1847+
if self._converter_is_explicit:
1848+
raise RuntimeError("Axis already has an explicit converter set")
1849+
elif self._converter is not None:
1850+
_api.warn_external(
1851+
"This axis already has a converter set and "
1852+
"is updating to a potentially incompatible converter"
1853+
)
1854+
self._converter = converter
1855+
18131856
def set_units(self, u):
18141857
"""
18151858
Set the units for axis.
@@ -2529,8 +2572,8 @@ def set_default_intervals(self):
25292572
# not changed the view:
25302573
if (not self.axes.dataLim.mutatedx() and
25312574
not self.axes.viewLim.mutatedx()):
2532-
if self.converter is not None:
2533-
info = self.converter.axisinfo(self.units, self)
2575+
if self._converter is not None:
2576+
info = self._converter.axisinfo(self.units, self)
25342577
if info.default_limits is not None:
25352578
xmin, xmax = self.convert_units(info.default_limits)
25362579
self.axes.viewLim.intervalx = xmin, xmax
@@ -2759,8 +2802,8 @@ def set_default_intervals(self):
27592802
# not changed the view:
27602803
if (not self.axes.dataLim.mutatedy() and
27612804
not self.axes.viewLim.mutatedy()):
2762-
if self.converter is not None:
2763-
info = self.converter.axisinfo(self.units, self)
2805+
if self._converter is not None:
2806+
info = self._converter.axisinfo(self.units, self)
27642807
if info.default_limits is not None:
27652808
ymin, ymax = self.convert_units(info.default_limits)
27662809
self.axes.viewLim.intervaly = ymin, ymax

lib/matplotlib/axis.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ from matplotlib.text import Text
1515
from matplotlib.ticker import Locator, Formatter
1616
from matplotlib.transforms import Transform, Bbox
1717
from matplotlib.typing import ColorType
18+
from matplotlib.units import ConversionInterface
1819

1920

2021
GRIDLINE_INTERPOLATION_STEPS: int
@@ -207,6 +208,8 @@ class Axis(martist.Artist):
207208
def update_units(self, data): ...
208209
def have_units(self) -> bool: ...
209210
def convert_units(self, x): ...
211+
def get_converter(self) -> ConversionInterface | None: ...
212+
def set_converter(self, converter: ConversionInterface) -> None: ...
210213
def set_units(self, u) -> None: ...
211214
def get_units(self): ...
212215
def set_label_text(

lib/matplotlib/backends/qt_editor/figureoptions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def convert_limits(lim, converter):
4242
axis_map = axes._axis_map
4343
axis_limits = {
4444
name: tuple(convert_limits(
45-
getattr(axes, f'get_{name}lim')(), axis.converter
45+
getattr(axes, f'get_{name}lim')(), axis.get_converter()
4646
))
4747
for name, axis in axis_map.items()
4848
}
@@ -66,7 +66,7 @@ def convert_limits(lim, converter):
6666

6767
# Save the converter and unit data
6868
axis_converter = {
69-
name: axis.converter
69+
name: axis.get_converter()
7070
for name, axis in axis_map.items()
7171
}
7272
axis_units = {
@@ -209,7 +209,7 @@ def apply_callback(data):
209209
axis.set_label_text(axis_label)
210210

211211
# Restore the unit data
212-
axis.converter = axis_converter[name]
212+
axis._set_converter(axis_converter[name])
213213
axis.set_units(axis_units[name])
214214

215215
# Set / Curves

lib/matplotlib/tests/test_dates.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -668,10 +668,12 @@ def test_concise_converter_stays():
668668
fig, ax = plt.subplots()
669669
ax.plot(x, y)
670670
# Bypass Switchable date converter
671-
ax.xaxis.converter = conv = mdates.ConciseDateConverter()
671+
conv = mdates.ConciseDateConverter()
672+
with pytest.warns(UserWarning, match="already has a converter"):
673+
ax.xaxis.set_converter(conv)
672674
assert ax.xaxis.units is None
673675
ax.set_xlim(*x)
674-
assert ax.xaxis.converter == conv
676+
assert ax.xaxis.get_converter() == conv
675677

676678

677679
def test_offset_changes():

lib/matplotlib/tests/test_units.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import matplotlib.pyplot as plt
66
from matplotlib.testing.decorators import check_figures_equal, image_comparison
77
import matplotlib.units as munits
8-
from matplotlib.category import UnitData
8+
from matplotlib.category import StrCategoryConverter, UnitData
99
import numpy as np
1010
import pytest
1111

@@ -236,6 +236,32 @@ def test_shared_axis_categorical():
236236
assert "c" in ax2.xaxis.get_units()._mapping.keys()
237237

238238

239+
def test_explicit_converter():
240+
d1 = {"a": 1, "b": 2}
241+
str_cat_converter = StrCategoryConverter()
242+
str_cat_converter_2 = StrCategoryConverter()
243+
244+
# Explicit is set
245+
fig1, ax1 = plt.subplots()
246+
ax1.xaxis.set_converter(str_cat_converter)
247+
assert ax1.xaxis.get_converter() == str_cat_converter
248+
# Explicit not overridden by implicit
249+
ax1.plot(d1.keys(), d1.values())
250+
assert ax1.xaxis.get_converter() == str_cat_converter
251+
# No error when called twice with equivalent input
252+
ax1.xaxis.set_converter(str_cat_converter)
253+
# Error when explicit called twice
254+
with pytest.raises(RuntimeError):
255+
ax1.xaxis.set_converter(str_cat_converter_2)
256+
257+
# Warn when implicit overridden
258+
fig2, ax2 = plt.subplots()
259+
ax2.plot(d1.keys(), d1.values())
260+
261+
with pytest.warns():
262+
ax2.xaxis.set_converter(str_cat_converter)
263+
264+
239265
def test_empty_default_limits(quantity_converter):
240266
munits.registry[Quantity] = quantity_converter
241267
fig, ax1 = plt.subplots()

0 commit comments

Comments
 (0)
0