Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Axes.format features with individual setters #89

Closed
wants to merge 1 commit into from

Conversation

lukelbd
Copy link
Collaborator

@lukelbd lukelbd commented Dec 16, 2019

Starting this PR to keep #63 on everyone's radar. Along with #50 and #45 this is one of the major (but mostly internal) changes I am considering for the version 1.0 release.

ProPlot adds "batch" setter methods called format to every Axes subclass. The idea is it's quicker to call 1 method with 10 keyword arguments rather than 10 methods on 10 lines. Interestingly, it looks like the Plots.jl julia package follows a similar philosophy (see the "Lorenz attractor" example).

However:

  1. It seems kind of sloppy / unpythonic to me to have individual settings that can only be modified with a batch setter. It also makes the setter methods really long and difficult to maintain. (no longer valid; see below) I want to encourage batch setting but permit individual one-liner setting if the user so chooses.
  2. One of my goals for version 1 is to incorporate ProPlot internals more closely with matplotlib, and matplotlib apparently has its own batch setter, Artist.set. However nobody uses it, probably because (1) it seldom appears in the online examples and (2) the valid arguments are not explicitly documented, which is confusing for new userss.

With this PR, I'd like to combine the features of Axes.format with Artist.update and Artist.set by overriding the latter for axes artists. I will deprecate format and break up the format tasks into individual setters, but encourage using the batch setters by using them in all of the examples and documenting the acceptable keyword args.

Here is some pseudocode to outline the general idea.

# The batch setters
@cbook.deprecated("1.0.0", alternative="Axes.set(...)")  # use matplotlib's deprecation system?
def format(self, **kwargs):
    _warn_proplot(
        'Axes.format() was deprecated in version 1.0.0. Please use `Axes.set()` instead.'
    )  # or "deprecation" with a simple warning
    return self.set(*args, **kwargs)

def set(self, **kwargs):
    # Just like Artist.set(), process aliases here, e.g. `xticklabels` for `xformatter`
    # May just add `_alias_map` and `_prop_order` attributes to Axes!
    self._alias_map = ...  # make sure these hidden props are stable!
    self._prop_order = ...
    # then filter out rc props here, and maybe we can have some sort of
    # rcupdate() method that updates *any* rc property that has changed
    rcprops, kwargs = _process_kwargs(kwargs)
    self.applyrc(rcprops)
    super().set(**kwargs)

def update(self, props):
    # *Superset* of internal matplotlib.artist.Artist.update()
    # Permit passing *keyword arguments* to each setter
    # Example: If we get `xlim` and `xlim_kw`, call `Axes.set_xlim(xlim, **xlim_kw)`
    def _update_property(self, k, v, **kwargs):
        k = k.lower()
        if k in {'axes'}:
            return setattr(self, k, v)
        else:
            func = getattr(self, 'set_' + k, None)
            if not callable(func):
                raise AttributeError(
                    f'{type(self).__name__!r} object has no property {k!r}.')
            return func(v)
    with cbook._setattr_cm(self, eventson=False):
        ret = []
        for key, value in _kwargs_not_ending_in_kw(kwargs).items():
            kw = _kwarg_ending_in_kw(key)
            ret.append(_update_property(self, k, v, **kw))
    return ret

# Helper method that looks for rc setting names passed
# to the bulk setters
def applyrc(self, **kwargs):
    # perform various actions currently spread around Axes.format()
    with plot.rc.context(**kwargs):
        # apply "cached" props (i.e. props changed in this block)
        # plot.rc.fill(props, cached=True)
        # plot.rc.get(props, cached=True)
        # etc.
        pass

# The individual setters
# These replace features originally coded in Axes.format
def set_abcstyle(self, style):
    if not isinstance(style, str) or (style.count('a') != 1 and style.count('A') != 1):
        raise ValueError
    abc = _abc(self.number - 1)
    if 'A' in style:
        abc = abc.upper()
    abc = re.sub('[aA]', abc, abcstyle)
    self.abc.set_text(abc)

def set_xticks(self, locator, **kwargs):
    locator = axistools.Locator(locator, **kwargs)
    self.xaxis.set_major_locator(locator)

def set_xminorticks(self, locator, **kwargs):
    locator = axistools.Locator(locator, **kwargs)
    self.xaxis.set_minor_locator(locator)

def set_xticklabels(self, formatter, **kwargs):
    formatter = axistools.Formatter(formatter, **kwargs)
    self.xaxis.set_major_formatter(formatter)

...

Then to encourage using the batch setters, we can concatenate docstrings from each individual setter:

# Documentation builder
def _get_setter_docs(self):
    # Maybe just retrieve first sentence of each docstring,
    # and look at `Axes._alias_map` for aliases!
    ....

Axes.set.__doc__ = f"""
Bulk update the axes.

Parameters
-----------
{_get_setter_docs(Axes)}
"""

# Results in the following docstring
# (not sure how we can consistently get argument types though)
"""
Parameters
-----------
xlim : (float, float), optional
    Call `~Axes.set_xlim`. Set the x-axis view limits.
ylim : (float, float), optional
    Call `~Axes.set_ylim`. Set the y-axis view limits.
... etc ...
"""

@lukelbd lukelbd changed the title Implement Axes.format features with matplotlib setters Implement Axes.format features with individual setters Dec 16, 2019
@lukelbd lukelbd added this to the Version 1 milestone Dec 16, 2019
@lukelbd
Copy link
Collaborator Author

lukelbd commented Jul 30, 2021

This PR will still be useful but no longer high priority. On the main branch I have split up the format() tasks into separate _update_xyz utilities, which already makes things much cleaner/easier to maintain. So half of point 1 is no longer valid.

@lukelbd
Copy link
Collaborator Author

lukelbd commented Aug 22, 2021

...on second thought I really don't think this is necessary anymore.

Matplotlib has enough setters and getters, and in the latest proplot version, if you use mpl setters set_title, set_xlabel, set_ylabel, the labels still get synced to the appropriate shared axes/panels just as if you called format(). So, for almost all settings, users already have the option of using "individual setters" from the native matplotlib API vs. format().

In the future I might make public some of the currently-private functions used for a handful of proplot-specific features (e.g. suplabels(), set_abc(), and making the GeoAxes lonaxis and lataxis used to control lon/lat ticks public in analogy with xaxis and yaxis). But the broader goal of this PR is probably not worth my time beyond these very specific cases and (possibly) the goal of adding generalized artist "properties" (#168). So, I'm closing this PR.

@lukelbd lukelbd closed this Aug 22, 2021
@lukelbd lukelbd deleted the format-with-setters branch August 22, 2021 21:49
@lukelbd lukelbd removed this from the Version 0.9 milestone Aug 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant