[Matplotlib-devel] Easier tick formatters

Currently setting up the tick formatting on matplotlib requires you to import a formatter from the ticker module, create an instance of that formatter, then pass that to the axis. This makes sense for complicated formatting. However, there are some obvious cases where we can probably infer the type of formatting someone wants to do if we are just given the formatter content. For example

  • ‘’ is probably going to be the null formatter

  • A callable, besides a formatter instance, is probably going to be a function formatter

  • A sequence or numpy array is probably going to be an index or fixed formatter, depending on the locator

  • Any other string is probably going to be a format string formatter or string method formatter.

So I think we could allow the set_major_formatter and set_minor_formatter to take certain sorts of inputs and create the formatter automatically for the user, making such cases much, much easier. Specifically, I propose the following inputs be accepted:

  • None sets it to the default formatter

  • A callable (besides a formatter instance) is set to be a FuncFormatter

  • An empty string, '', is set to be a NullFormatter

  • Any other string is a StrMethodFormatter

  • An abc.collections.Sequence subclass or numpy ndarray subclass is an IndexFormatter, unless the axis is using a FixedLocator in which case it is a FixedLocator. We could restrict this to just lists and numpy arrays only.

Any thoughts?

Interesting, I am weakly in favor of this.

I am normally very hesitant about adopting heuristics, but these seem well defined. If we do this, we should drop the last rule as IndexFormatter is scheduled to be deprecated in in 3.3 and is a bit problematic in other ways.

I don’t see any back-compatibility concerns (as none of those inputs work now).

Tom

···

Thomas Caswell
tcaswell@gmail.com

I'm very sympathetic to the goal of making smoother and simpler interfaces, but I'm not yet convinced that this proposal is the way to go. An example of a case long ago where a proposal something like this made a big improvement is the specification of a cmap kwarg: originally it had to be a Colormap instance, but then it was changed to accept a string. That worked well because it was an extremely common situation, and only one change in behavior was made--it essentially yielded expected behavior right away.

Using custom formatters is much less common, I presume, and the new behavior is not as simple as supplying a colormap name.

Eric

···

On 2020/03/04 5:03 PM, Todd wrote:

Currently setting up the tick formatting on matplotlib requires you to import a formatter from the ticker module, create an instance of that formatter, then pass that to the axis. This makes sense for complicated formatting. However, there are some obvious cases where we can probably infer the type of formatting someone wants to do if we are just given the formatter content. For example

\* '' is probably going to be the null formatter
\* A callable, besides a formatter instance, is probably going to be a function formatter
\* A sequence or numpy array is probably going to be an index or fixed formatter, depending on the locator
\* Any other string is probably going to be a format string formatter or string method formatter\.

So I think we could allow the `set_major_formatter` and `set_minor_formatter` to take certain sorts of inputs and create the formatter automatically for the user, making such cases much, much easier. Specifically, I propose the following inputs be accepted:

\* \`None\` sets it to the default formatter
\* A callable \(besides a formatter instance\) is set to be a FuncFormatter
\* An empty string, \`''\`, is set to be a NullFormatter
\* Any other string is a StrMethodFormatter
\* An abc\.collections\.Sequence subclass or numpy ndarray subclass is an IndexFormatter, unless the axis is using a FixedLocator in which case it is a FixedLocator\.  We could restrict this to just lists and numpy arrays only\.

Any thoughts?

_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@python.org
https://mail.python.org/mailman/listinfo/matplotlib-devel

_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@python.org
https://mail.python.org/mailman/listinfo/matplotlib-devel

As a frequent user of MultipleLocator, I'd suggest that `set_major_formatter` should also accept a float if this API change is made.

-Mike

···

On 3/4/20 9:57 PM, Eric Firing wrote:

I'm very sympathetic to the goal of making smoother and simpler interfaces, but I'm not yet convinced that this proposal is the way to go. An example of a case long ago where a proposal something like this made a big improvement is the specification of a cmap kwarg: originally it had to be a Colormap instance, but then it was changed to accept a string. That worked well because it was an extremely common situation, and only one change in behavior was made--it essentially yielded expected behavior right away.

Using custom formatters is much less common, I presume, and the new behavior is not as simple as supplying a colormap name.

Eric

On 2020/03/04 5:03 PM, Todd wrote:

Currently setting up the tick formatting on matplotlib requires you to import a formatter from the ticker module, create an instance of that formatter, then pass that to the axis. This makes sense for complicated formatting. However, there are some obvious cases where we can probably infer the type of formatting someone wants to do if we are just given the formatter content. For example

\* '' is probably going to be the null formatter
\* A callable, besides a formatter instance, is probably going to be a function formatter
\* A sequence or numpy array is probably going to be an index or fixed formatter, depending on the locator
\* Any other string is probably going to be a format string formatter or string method formatter\.

So I think we could allow the `set_major_formatter` and `set_minor_formatter` to take certain sorts of inputs and create the formatter automatically for the user, making such cases much, much easier. Specifically, I propose the following inputs be accepted:

\* \`None\` sets it to the default formatter
\* A callable \(besides a formatter instance\) is set to be a FuncFormatter
\* An empty string, \`''\`, is set to be a NullFormatter
\* Any other string is a StrMethodFormatter
\* An abc\.collections\.Sequence subclass or numpy ndarray subclass is an IndexFormatter, unless the axis is using a FixedLocator in which case it is a FixedLocator\.  We could restrict this to just lists and numpy arrays only\.

Any thoughts?

_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@python.org
https://mail.python.org/mailman/listinfo/matplotlib-devel

_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@python.org
https://mail.python.org/mailman/listinfo/matplotlib-devel

_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@python.org
https://mail.python.org/mailman/listinfo/matplotlib-devel

I am sympathetic to the idea. A much simpler rule, though, would be to just additionally accept callables. New-style format string formatters can then be implemented by passing bound methods (set_major_formatter("tick {}".format) – the bound method quietly ignores the second (index) argument), which also makes it explicit that we talking about {}-format, not %s-format. Even set_major_formatter("".format) would already be much shorter to type than importing NullFormatter and using it. In any case, we could always first implement support for callables, which already buys us all these cases, and defer further cases to later.

The only case not covered by callables/bound methods is FixedLocator, but this is already handled by set_x/yticks, and anyways (usually) involves setting both the locator and the formatter at once (and in fact is semantically more associated with setting the locator rather than the formatter), so I think it’s fine to keep it separate for now.

Antony

···

On Thu, Mar 5, 2020 at 4:03 AM Todd toddrjen@gmail.com wrote:

Currently setting up the tick formatting on matplotlib requires you to import a formatter from the ticker module, create an instance of that formatter, then pass that to the axis. This makes sense for complicated formatting. However, there are some obvious cases where we can probably infer the type of formatting someone wants to do if we are just given the formatter content. For example

  • ‘’ is probably going to be the null formatter
  • A callable, besides a formatter instance, is probably going to be a function formatter
  • A sequence or numpy array is probably going to be an index or fixed formatter, depending on the locator
  • Any other string is probably going to be a format string formatter or string method formatter.

So I think we could allow the set_major_formatter and set_minor_formatter to take certain sorts of inputs and create the formatter automatically for the user, making such cases much, much easier. Specifically, I propose the following inputs be accepted:

  • None sets it to the default formatter
  • A callable (besides a formatter instance) is set to be a FuncFormatter
  • An empty string, '', is set to be a NullFormatter
  • Any other string is a StrMethodFormatter
  • An abc.collections.Sequence subclass or numpy ndarray subclass is an IndexFormatter, unless the axis is using a FixedLocator in which case it is a FixedLocator. We could restrict this to just lists and numpy arrays only.

Any thoughts?


Matplotlib-devel mailing list

Matplotlib-devel@python.org

https://mail.python.org/mailman/listinfo/matplotlib-devel

First, I think this is a bit of a chicken and egg problem. Doing this isn’t that common because it requires you jump through more hoops than most plot customization does. I don’t have any hard evidence, but I would think something like adding degrees symbols to polar plots or units to any sort of plot would be a natural thing to do if it was easy. I know I personally have held off on doing it for throw-away plots because the amount of boilerplate needed.

Second, there are other places in matplotlib where simpler inputs are allowed in place of more complicated ones where the inputs are relatively unambiguous. For example Axes.legend doesn’t require that you manually specify which objects you are assigning labels to, you can just provide a sequence of strings. Similarly Axes.set_position can either accept a sequence of floats for a position or a Bbox. And there are lots of places you can either provide a dict of properties instead of a more complicated property object, or set_ functions where you can provide a string label and matplotlib will handle the underlying text object for you, even when the corresponding get_ function returns a text object.

I don’t think the expected behavior is very ambiguous here, and the implementation is very easy.

···

On Wed, Mar 4, 2020 at 11:02 PM Eric Firing efiring@hawaii.edu wrote:

I’m very sympathetic to the goal of making smoother and simpler

interfaces, but I’m not yet convinced that this proposal is the way to

go. An example of a case long ago where a proposal something like this

made a big improvement is the specification of a cmap kwarg: originally

it had to be a Colormap instance, but then it was changed to accept a

string. That worked well because it was an extremely common situation,

and only one change in behavior was made–it essentially yielded

expected behavior right away.

Using custom formatters is much less common, I presume, and the new

behavior is not as simple as supplying a colormap name.

Eric

I thought of that approach, but it seemed like it still requires some unnecessary boilerplate for what seemed to me to by fairly unambiguous situations. And it also doesn’t handle resetting the formatter to the default.

···

On Thu, Mar 5, 2020 at 5:37 AM Antony Lee antony.lee@institutoptique.fr wrote:

I am sympathetic to the idea. A much simpler rule, though, would be to just additionally accept callables. New-style format string formatters can then be implemented by passing bound methods (set_major_formatter("tick {}".format) – the bound method quietly ignores the second (index) argument), which also makes it explicit that we talking about {}-format, not %s-format. Even set_major_formatter("".format) would already be much shorter to type than importing NullFormatter and using it. In any case, we could always first implement support for callables, which already buys us all these cases, and defer further cases to later.

The only case not covered by callables/bound methods is FixedLocator, but this is already handled by set_x/yticks, and anyways (usually) involves setting both the locator and the formatter at once (and in fact is semantically more associated with setting the locator rather than the formatter), so I think it’s fine to keep it separate for now.

Antony

Okay, so then it would be None to reset, '' for NullFormatter, a callable for FuncFormatter, and a string for StrMethodFormatter. That avoids the complexity of determining what exactly constitutes a “sequence” for the purposes of IndexFormatter/FixedFormatter.

···

On Wed, Mar 4, 2020 at 10:15 PM Thomas Caswell tcaswell@gmail.com wrote:

Interesting, I am weakly in favor of this.

I am normally very hesitant about adopting heuristics, but these seem well defined. If we do this, we should drop the last rule as IndexFormatter is scheduled to be deprecated in in 3.3 and is a bit problematic in other ways.

I don’t see any back-compatibility concerns (as none of those inputs work now).

Tom

On Wed, Mar 4, 2020 at 10:03 PM Todd toddrjen@gmail.com wrote:

Currently setting up the tick formatting on matplotlib requires you to import a formatter from the ticker module, create an instance of that formatter, then pass that to the axis. This makes sense for complicated formatting. However, there are some obvious cases where we can probably infer the type of formatting someone wants to do if we are just given the formatter content. For example

  • ‘’ is probably going to be the null formatter
  • A callable, besides a formatter instance, is probably going to be a function formatter
  • A sequence or numpy array is probably going to be an index or fixed formatter, depending on the locator
  • Any other string is probably going to be a format string formatter or string method formatter.

So I think we could allow the set_major_formatter and set_minor_formatter to take certain sorts of inputs and create the formatter automatically for the user, making such cases much, much easier. Specifically, I propose the following inputs be accepted:

  • None sets it to the default formatter
  • A callable (besides a formatter instance) is set to be a FuncFormatter
  • An empty string, '', is set to be a NullFormatter
  • Any other string is a StrMethodFormatter
  • An abc.collections.Sequence subclass or numpy ndarray subclass is an IndexFormatter, unless the axis is using a FixedLocator in which case it is a FixedLocator. We could restrict this to just lists and numpy arrays only.

Any thoughts?


Matplotlib-devel mailing list

Matplotlib-devel@python.org

https://mail.python.org/mailman/listinfo/matplotlib-devel


Thomas Caswell
tcaswell@gmail.com

The point is that if we support callables, we’ll already have all these cases supported, and while appending .format to your string could be considered boilerplate, it’s already considerably shorter than FuncFormatter (or any of the others) + the import. Finally this still doesn’t preclude adding the other cases later.
Supporting None as default seems likely to confuse people who’d think it means NullFormatter…

Antony

···

On Thu, Mar 5, 2020 at 3:55 PM Todd toddrjen@gmail.com wrote:

On Thu, Mar 5, 2020 at 5:37 AM Antony Lee antony.lee@institutoptique.fr wrote:

I am sympathetic to the idea. A much simpler rule, though, would be to just additionally accept callables. New-style format string formatters can then be implemented by passing bound methods (set_major_formatter("tick {}".format) – the bound method quietly ignores the second (index) argument), which also makes it explicit that we talking about {}-format, not %s-format. Even set_major_formatter("".format) would already be much shorter to type than importing NullFormatter and using it. In any case, we could always first implement support for callables, which already buys us all these cases, and defer further cases to later.

The only case not covered by callables/bound methods is FixedLocator, but this is already handled by set_x/yticks, and anyways (usually) involves setting both the locator and the formatter at once (and in fact is semantically more associated with setting the locator rather than the formatter), so I think it’s fine to keep it separate for now.

Antony

I thought of that approach, but it seemed like it still requires some unnecessary boilerplate for what seemed to me to by fairly unambiguous situations. And it also doesn’t handle resetting the formatter to the default.


Matplotlib-devel mailing list

Matplotlib-devel@python.org

https://mail.python.org/mailman/listinfo/matplotlib-devel

Based on the input I have gotten so far, I have made a simplified version of the approach [1]. It only handles the cases for str and function (or rather callable, but I call it a function in the documentation). These seem to handle most use-cases reasonably well. As Antony pointed out, None is potentially confusing, and as Thomas pointed out IndexFormatter is deprecated. That left only the empty NullFormatter (with an empty str), StrMethodFormatter (with any other str), and FuncFormatter (with a callable).

Antony made a good case that we should keep the number of approaches as small as possible. Since the NullFormatter is trivially handled with an empty str, and that is really what it is behind-the-scenes, that was an obvious one to cut.

Antony made the good case that any str could be handled with str.format. I tried that approach initially, but the issue I ended up having is that, although it is obvious to experienced Python users, it isn’t as obvious to newer users. That would mean I would need to properly communicate this workaround to users in the documentation. However, my attempts to do this ended up being longer, more complicated, and harder to understand than simply implementing the str case directly. So I ended up keeping the str and function approaches. Of course this decision is open to discussion.

I also updated the documentation, tests, and examples to explain these new features. Please take a look and let me know what you think.

One open issue is that I did not change any examples currently using any of these formatters, I only added the new approach. I think it might be good to change some of the simpler examples of using formatters to use this new feature in order to make formatters seem more approachable, but I haven’t done that yet.

[1] https://github.com/toddrjen/matplotlib/commit/673e4b32bb8849d1de7f1180d5a48b13d68a6f34

···

On Wed, Mar 4, 2020 at 10:03 PM Todd toddrjen@gmail.com wrote:

Currently setting up the tick formatting on matplotlib requires you to import a formatter from the ticker module, create an instance of that formatter, then pass that to the axis. This makes sense for complicated formatting. However, there are some obvious cases where we can probably infer the type of formatting someone wants to do if we are just given the formatter content. For example

  • ‘’ is probably going to be the null formatter
  • A callable, besides a formatter instance, is probably going to be a function formatter
  • A sequence or numpy array is probably going to be an index or fixed formatter, depending on the locator
  • Any other string is probably going to be a format string formatter or string method formatter.

So I think we could allow the set_major_formatter and set_minor_formatter to take certain sorts of inputs and create the formatter automatically for the user, making such cases much, much easier. Specifically, I propose the following inputs be accepted:

  • None sets it to the default formatter
  • A callable (besides a formatter instance) is set to be a FuncFormatter
  • An empty string, '', is set to be a NullFormatter
  • Any other string is a StrMethodFormatter
  • An abc.collections.Sequence subclass or numpy ndarray subclass is an IndexFormatter, unless the axis is using a FixedLocator in which case it is a FixedLocator. We could restrict this to just lists and numpy arrays only.

Any thoughts?

It looks like something funny is going on with your branch (It looks like there are some extra rebased commits), but other than that it look like a good start.

Could you please open a PR so it can go through the normal code review process / have CI run?

Tom

···

Thomas Caswell
tcaswell@gmail.com

The PR is number 16715 [1]

https://github.com/matplotlib/matplotlib/pull/16715

···

On Mon, Mar 9, 2020 at 2:16 PM Thomas Caswell tcaswell@gmail.com wrote:

It looks like something funny is going on with your branch (It looks like there are some extra rebased commits), but other than that it look like a good start.

Could you please open a PR so it can go through the normal code review process / have CI run?

Tom

On Mon, Mar 9, 2020 at 10:45 AM Todd toddrjen@gmail.com wrote:

On Wed, Mar 4, 2020 at 10:03 PM Todd toddrjen@gmail.com wrote:

Currently setting up the tick formatting on matplotlib requires you to import a formatter from the ticker module, create an instance of that formatter, then pass that to the axis. This makes sense for complicated formatting. However, there are some obvious cases where we can probably infer the type of formatting someone wants to do if we are just given the formatter content. For example

  • ‘’ is probably going to be the null formatter
  • A callable, besides a formatter instance, is probably going to be a function formatter
  • A sequence or numpy array is probably going to be an index or fixed formatter, depending on the locator
  • Any other string is probably going to be a format string formatter or string method formatter.

So I think we could allow the set_major_formatter and set_minor_formatter to take certain sorts of inputs and create the formatter automatically for the user, making such cases much, much easier. Specifically, I propose the following inputs be accepted:

  • None sets it to the default formatter
  • A callable (besides a formatter instance) is set to be a FuncFormatter
  • An empty string, '', is set to be a NullFormatter
  • Any other string is a StrMethodFormatter
  • An abc.collections.Sequence subclass or numpy ndarray subclass is an IndexFormatter, unless the axis is using a FixedLocator in which case it is a FixedLocator. We could restrict this to just lists and numpy arrays only.

Any thoughts?

Based on the input I have gotten so far, I have made a simplified version of the approach [1]. It only handles the cases for str and function (or rather callable, but I call it a function in the documentation). These seem to handle most use-cases reasonably well. As Antony pointed out, None is potentially confusing, and as Thomas pointed out IndexFormatter is deprecated. That left only the empty NullFormatter (with an empty str), StrMethodFormatter (with any other str), and FuncFormatter (with a callable).

Antony made a good case that we should keep the number of approaches as small as possible. Since the NullFormatter is trivially handled with an empty str, and that is really what it is behind-the-scenes, that was an obvious one to cut.

Antony made the good case that any str could be handled with str.format. I tried that approach initially, but the issue I ended up having is that, although it is obvious to experienced Python users, it isn’t as obvious to newer users. That would mean I would need to properly communicate this workaround to users in the documentation. However, my attempts to do this ended up being longer, more complicated, and harder to understand than simply implementing the str case directly. So I ended up keeping the str and function approaches. Of course this decision is open to discussion.

I also updated the documentation, tests, and examples to explain these new features. Please take a look and let me know what you think.

One open issue is that I did not change any examples currently using any of these formatters, I only added the new approach. I think it might be good to change some of the simpler examples of using formatters to use this new feature in order to make formatters seem more approachable, but I haven’t done that yet.

[1] https://github.com/toddrjen/matplotlib/commit/673e4b32bb8849d1de7f1180d5a48b13d68a6f34


Matplotlib-devel mailing list

Matplotlib-devel@python.org

https://mail.python.org/mailman/listinfo/matplotlib-devel


Thomas Caswell
tcaswell@gmail.com

1 Like