-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
RFC: new function-based API #14058
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
base: main
Are you sure you want to change the base?
RFC: new function-based API #14058
Conversation
def ploting_func(*data_args, ax, **style_kwargs): | ||
... | ||
|
||
The first case has the advantage that it works in both python2 and |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hopefully py2 won't really be a thing anymore by the time this is implemented :p
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed! If you dig into the commits on this, I started writing this in 2016...
and allow libraries to internally organize them selves using either of | ||
the above Axes-is-required API. This avoids bike-shedding over the | ||
API and eliminates the first-party 'special' namespace, but is a bit | ||
magical. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally I find passing the axes as first argument muuuuuuch, much nicer (well, that may be because I essentially never use the pyplot layer which I understand may not be representative of most users).
Either we could use the "magic" decorator, or alternatively we could just have parallel namespaces plt.somefunc(..., ax=None)
(None=gca()) & someothernamespace.somefunc(ax, ...)
which would at least have the advantage of keeping reasonable signatures for all functions (with the "magic" decorator, inspect.signature can't represent the signature; which is not nice). Note that one namespace could be autogenerated from the other, e.g. in mod.py
@gen_pyplotlike # registers to module_level registry
def func(ax, ...): ...
@gen_pyplotlike
def otherfunc(ax, ...): ...
pyplotlike = collect_pyplotlike()
and then one can do import mod; mod.func(ax, ...)
or from mod import pyplotlike as mod; mod.func(..., ax=ax)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#4488 I tried something similar and it got rejected (and I sadly never followed up on making it its own package).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find all three calling patterns valid approaches:
plt.plot([1, 2, 3])
plt.plot(ax, [1, 2, 3])
plt.plot([1, 2, 3], ax=ax)
and I welcome supporting all of them. Each one has it's use:
- simple interactive use
- interactive use with multiple axes (less to type than
ax=ax
) - programmatic use, where the data should be the first arguments for better readability.
I'm against creating multiple namespaces just for the sake of different calling patterns. For one, it's conceptually more difficult to tell people: "Use pyplot.plot()
if, or use posax.plot(ax, ...)
or use kwargs.plot(..., ax=ax)
". Also you would have to create multiple variants of the documentation. While that could be automated, you still have the problem which one to link. It's much easier to once state "axes can be automatically determined, or passed as the first positional arguement, or passed as kwarg."
As @tacaswell has demonstrated that can all be resolved with a decorator.
I'm not quite sure if the actual function definition should be
@ensure_ax
def func(ax, *data_args, **style_kwargs)
or
def func(*data_args, ax=ax, **style_kwargs)
I tend towards the latter because it's the syntactically correct signature for two out of the three cases. And it puts more emphasis on the data_args rather than on the axes. Also it has the big advantage, that it could be build into pyplot
in a backward-compatible way. That way, we wouldn't need any new namespace.
Note also, that an axes context could be a valuable addition:
with plt.sca(ax):
plt.plot([1, 2, 3])
plt.xlabel('The x')
plt.ylabel('The y')
(maybe using a more telling name than sca()
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if I understand the goal here.
Is the goal to have libraries add plt.my_new_plotting_thing(...)
and ax.my_new_plotting_thing(...)
?
That's all about entry points, right? Also: what exactly is the benefit of doing that?
Right now, my pattern is having an axes kwarg and if it's None
I do plt.gca()
.
That's basically a single line, which might be slightly longer than adding the ensure_ax
decorator in terms of characters but not by much, and seems much easier to understand.
Right now I'm reasonably happy to do some_plotting(ax=ax)
. Doing ax.some_plotting
instead might be nice, but I'm not entirely sure if that is the main goal of this proposal? Doing plt.some_plotting(...)
instead of just some_plotting(...)
is just more characters, right? I guess it tells you by convention that if it starts with plt
it'll modify the current axes? Though that's not even really true: plt.matshow creates a new figure.
Generally I prefer thinking about what code looks like when I use it first instead of thinking about the implementation first. Usually implementing whatever API we settle on is possible so it's more a question of what we want user code to look like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the main goal is for matplotlib have plotting libraries that do
import matplotlib.basic as mbasic
import matplotlib.2d as m2d
fig, ax = plt.subplots()
mbasic.scatter(x, y, ax=ax)
m2d.pcolormesh(x, y, z, ax=ax)
so the matplotlib library looks more like what user and third party libraries look like.
I think the goal would then be for ax.scatter
to just be a wrapper around mbasic.scatter
.
But maybe I've completely misunderstood.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jklymak Xo you're saying you want to change the matplotlib api to no longer do ax.scatter
and plt.scatter
but do scatter(ax=ax)
.
That is very different from what I understood, but I'm also very confused ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think its meant to be a third way. Ahem, I don't particularly want this, but I think the point is to make third-party libraries more plug-and-play with the main library. It also would allow us to have more domain-specific sub libraries without polluting the ax.do_something
name space. But maybe it has some deeper advantages as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok yeah I follow your interpretation. Let's see if that's what the others meant ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be made significantly more clear by using ax.scatter
(or your choice) as a concrete example.
I'm not sure if this is discussed somewhere, but is there a suggestion on what to do if a function wants to own a figure? I feel the convention is to call [edit: replaced gca by gcf which is what I meant] |
I don't think this is discussed somewhere. Generally, I discourage using Instead, either create the figure and axes you need inside the function, or pass them in as parameters (depends a bit what you your function should do). |
@timhoffm can you give an example where passing in makes sense? I'm thinking about a function that creates several axes and does other things to the figure. But basically you're saying not to use |
If you're fully controlling the figure, then you can create it within your function. A typical pattern would be:
Whether to pass You are only using pyplot to create a new figure. You don't use it's notion of current figure or axes ( Passing the figure in as an argument makes sense when you don't necessarily control the full layout of the figure. An example is https://matplotlib.org/api/_as_gen/mpl_toolkits.axes_grid1.axes_grid.ImageGrid.html. The other case for passing in a figure is when you cannot rely on pyplot for figure creation. For example if your function should be able to be used for drawing a figure in a GUI Application. Such figures need a different set up for binding to the canvas and backend (pylot figure creation hides these details from you to make simple popup or notebook figures simple). |
But you will tap into it as the newly created figure will be the 'current figure' . |
Actually I think it should not, there should be some way of saying "create a new pyplot-managed figure but don't touch the current figure/axes", otherwise, say your 3rd-party plotting function creates a multi-axes plot; now which axes is the current axes becomes deeply coupled with the internals of the function or that function needs to be careful to set the current axes at the end (or perhaps even place all its axes where it wants in the current-axes stack...) |
@timhoffm @tacaswell @anntzer thank you for your input. In some sense I feel that it might be nice for a library to control the state of the axes stack. But I'm also not sure what the expected behavior of a function should be. |
I briefly thought about this again (due to the mention of function-based APIs in #9629 (comment)) and I think I realized another reason why I'm uncomfortable with function-based APIs which "promote" the use of the current-axes state (even if one can explicitly pass Fairly often, an issue is reported in the tracker of people mixing pyplot and embedding in a GUI, and nearly always, the resolution is "don't mix pyplot and embedding". Which, in fact, is very easy to check: one can just see whether pyplot is imported. On the contrary, with function-based APIs where |
@anntzer this seems like a good argument to me, but it addresses a use-case that's quite far removed from my usual use of matplotlib (which is interactive use in jupyter). Is your preferred solution never to use the current-axes state, so basically make |
I agree the use cases are fairly orthogonal (but I basically end up never using pyplot even for interactive work; |
Basically I think of a notebook in which I call a bunch of pandas plotting functions, like The namespace doesn't really work for methods attached to objects, though, which is very common in pandas and now also present in sklearn. And I have no doubt that this is a real issue and making it easy to check for the use of global state is a goal I'd be totally on-board with. |
Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it. |
This a compangian to https://paper.dropbox.com/doc/Matplotlib-4.0--Ab_mupLexT4JeesRilNoR2LCAQ-WTYwd0NQaSHTjtLUZwkNx