From ad693ae0cc2636dc1c46c34e821962e2d162798e Mon Sep 17 00:00:00 2001 From: Michael Waskom Date: Sat, 25 Jun 2022 22:38:12 -0400 Subject: [PATCH] A little more test coverage and documentation --- seaborn/_core/scales.py | 178 ++++++++++++++++++++++--------------- tests/_core/test_scales.py | 27 ++++-- 2 files changed, 129 insertions(+), 76 deletions(-) diff --git a/seaborn/_core/scales.py b/seaborn/_core/scales.py index d4b7aab362..8fd0bb3c3a 100644 --- a/seaborn/_core/scales.py +++ b/seaborn/_core/scales.py @@ -67,10 +67,10 @@ def __post_init__(self): def tick(self): raise NotImplementedError() - def _get_locators(self): + def label(self): raise NotImplementedError() - def label(self): + def _get_locators(self): raise NotImplementedError() def _get_formatter(self): @@ -230,6 +230,14 @@ def tick(self, locator=None): } return new + def label(self, formatter=None): + + new = copy(self) + new._label_params = { + "formatter": formatter, + } + return new + def _get_locators(self, locator): if locator is not None: @@ -239,14 +247,6 @@ def _get_locators(self, locator): return locator, None - def label(self, formatter=None): - - new = copy(self) - new._label_params = { - "formatter": formatter, - } - return new - def _get_formatter(self, locator, formatter): if formatter is not None: @@ -390,7 +390,7 @@ def tick( every: float | None = None, between: tuple[float, float] | None = None, minor: int | None = None, - ) -> Continuous: # TODO type return value as Self + ) -> Continuous: """ Configure the selection of ticks for the scale's axis or legend. @@ -450,6 +450,55 @@ def tick( } return new + def label( + self, + formatter=None, *, + like: str | Callable | None = None, + base: int | None = None, + unit: str | None = None, + ) -> Continuous: + """ + Configure the appearance of tick labels for the scale's axis or legend. + + Parameters + ---------- + formatter : matplotlib Formatter + Pre-configured formatter to use; other parameters will be ignored. + like : str or callable + Either a format pattern (e.g., `".2f"`), a format string with fields named + `x` and/or `pos` (e.g., `"${x:.2f}"`), or a callable that consumes a number + and returns a string. + base : number + Use log formatter (with scientific notation) having this value as the base. + unit : str or (str, str) tuple + Use engineering formatter with SI units (e.g., with `unit="g"`, a tick value + of 5000 will appear as `5 kg`). When a tuple, the first element gives the + seperator between the number and unit. + + Returns + ------- + Copy of self with new label configuration. + + """ + if formatter is not None and not isinstance(formatter, Formatter): + raise TypeError( + f"Label formatter must be an instance of {Formatter!r}, " + f"not {type(formatter)!r}" + ) + + if like is not None and not (isinstance(like, str) or callable(like)): + msg = f"`like` must be a string or callable, not {type(like).__name__}." + raise TypeError(msg) + + new = copy(self) + new._label_params = { + "formatter": formatter, + "like": like, + "base": base, + "unit": unit, + } + return new + def _get_locators(self, locator, at, upto, count, every, between, minor): # TODO what about symlog? @@ -510,55 +559,6 @@ def _get_locators(self, locator, at, upto, count, every, between, minor): return major_locator, minor_locator - def label( - self, - formatter=None, *, - like: str | Callable | None = None, - base: int | None = None, - unit: str | None = None, - ) -> Continuous: - """ - Configure the appearance of tick labels for the scale's axis or legend. - - Parameters - ---------- - formatter : matplotlib Formatter - Pre-configured formatter to use; other parameters will be ignored. - like : str or callable - Either a format pattern (e.g., `".2f"`), a format string with fields named - `x` and/or `pos` (e.g., `"${x:.2f}"`), or a callable that consumes a number - and returns a string. - base : number - Use log formatter (with scientific notation) having this value as the base. - unit : str or (str, str) tuple - Use engineering formatter with SI units (e.g., with `unit="g"`, a tick value - of 5000 will appear as `5 kg`). When a tuple, the first element gives the - seperator between the number and unit. - - Returns - ------- - Copy of self with new label configuration. - - """ - if formatter is not None and not isinstance(formatter, Formatter): - raise TypeError( - f"Label formatter must be an instance of {Formatter!r}, " - f"not {type(formatter)!r}" - ) - - if like is not None and not (isinstance(like, str) or callable(like)): - msg = f"`like` must be a string or callable, not {type(like).__name__}." - raise TypeError(msg) - - new = copy(self) - new._label_params = { - "formatter": formatter, - "like": like, - "base": base, - "unit": unit, - } - return new - def _get_formatter(self, locator, formatter, like, base, unit): # TODO this has now been copied in a few places @@ -617,9 +617,26 @@ class Temporal(ContinuousBase): _priority: ClassVar[int] = 2 def tick( - self, locator: Locator | None = None, *, upto: int | None = None, + self, locator: Locator | None = None, *, + upto: int | None = None, ) -> Temporal: + """ + Configure the selection of ticks for the scale's axis or legend. + + This API is under construction and will be enhanced over time. + + Parameters + ---------- + locator: matplotlib Locator + Pre-configured matplotlib locator; other parameters will not be used. + upto : int + Choose "nice" locations for ticks, but do not exceed this number. + Returns + ------- + Copy of self with new tick configuration. + + """ if locator is not None and not isinstance(locator, Locator): err = ( f"Tick locator must be an instance of {Locator!r}, " @@ -631,12 +648,38 @@ def tick( new._tick_params = {"locator": locator, "upto": upto} return new + def label( + self, + formatter: Formatter | None = None, *, + concise: bool = False, + ) -> Temporal: + """ + Configure the appearance of tick labels for the scale's axis or legend. + + This API is under construction and will be enhanced over time. + + Parameters + ---------- + formatter : matplotlib Formatter + Pre-configured formatter to use; other parameters will be ignored. + concise : bool + If True, use :class:`matplotlib.dates.ConciseDateFormatter` to make + the tick labels as compact as possible. + + Returns + ------- + Copy of self with new label configuration. + + """ + new = copy(self) + new._label_params = {"formatter": formatter, "concise": concise} + return new + def _get_locators(self, locator, upto): if locator is not None: major_locator = locator elif upto is not None: - # TODO atleast for minticks? major_locator = AutoDateLocator(minticks=2, maxticks=upto) else: @@ -647,7 +690,8 @@ def _get_locators(self, locator, upto): def _get_formatter(self, locator, formatter, concise): - # TODO handle formatter + if formatter is not None: + return formatter if concise: # TODO ideally we would have concise coordinate ticks, @@ -658,14 +702,6 @@ def _get_formatter(self, locator, formatter, concise): return formatter - def label( - self, formatter: Formatter | None = None, *, concise: bool = False, - ) -> Temporal: - - new = copy(self) - new._label_params = {"formatter": formatter, "concise": concise} - return new - # ----------------------------------------------------------------------------------- # diff --git a/tests/_core/test_scales.py b/tests/_core/test_scales.py index b69ef553c3..b87e84ec0e 100644 --- a/tests/_core/test_scales.py +++ b/tests/_core/test_scales.py @@ -585,12 +585,13 @@ def test_coordinate_axis(self, t, x): assert isinstance(locator, mpl.dates.AutoDateLocator) assert isinstance(formatter, mpl.dates.AutoDateFormatter) - def test_label_concise(self, t, x): + def test_tick_locator(self, t): - ax = mpl.figure.Figure().subplots() - Temporal().label(concise=True)._setup(t, Coordinate(), ax.xaxis) - formatter = ax.xaxis.get_major_formatter() - assert isinstance(formatter, mpl.dates.ConciseDateFormatter) + locator = mpl.dates.YearLocator(month=3, day=15) + s = Temporal().tick(locator) + a = PseudoAxis(s._setup(t, Coordinate())._matplotlib_scale) + a.set_view_interval(0, 365) + assert 73 in a.major.locator() def test_tick_upto(self, t, x): @@ -599,3 +600,19 @@ def test_tick_upto(self, t, x): Temporal().tick(upto=n)._setup(t, Coordinate(), ax.xaxis) locator = ax.xaxis.get_major_locator() assert set(locator.maxticks.values()) == {n} + + def test_label_concise(self, t, x): + + ax = mpl.figure.Figure().subplots() + Temporal().label(concise=True)._setup(t, Coordinate(), ax.xaxis) + formatter = ax.xaxis.get_major_formatter() + assert isinstance(formatter, mpl.dates.ConciseDateFormatter) + + def test_label_formatter(self, t): + + formatter = mpl.dates.DateFormatter("%Y") + s = Temporal().label(formatter) + a = PseudoAxis(s._setup(t, Coordinate())._matplotlib_scale) + a.set_view_interval(10, 1000) + label, = a.major.formatter.format_ticks([100]) + assert label == "1970"