Proposed change to default log scale tick formatting #5161

Merged
merged 9 commits into from Aug 22, 2016

Conversation

Projects
None yet
7 participants
Member

zblz commented Oct 1, 2015

Following #4730 and #4867, this is a proposal to change the defaults for the upcoming 2.0 release regarding the default tick label formatting for log-scaled axes. This is not a finished PR, but mean to initiate discussion on whether this is desired as default (if not I still think a similar thing could be worth having as an option).

  • A new default formatter (LogFormatterSciNotation) is used as default for all log-scaled axes. This formatter always prints all labels for the bases of the log scale (major ticks) and adds labels to the minor ticks as the axis view limits are made smaller (with a similar logic to LogLocator with subs=None):
    • For less than 3 decades, (1, 3) * base**i are labeled.
    • For less than 2 decades, (1, 2, 5) * base**i are labeled.
    • For less than 1 decade, (1, 2, 4, 7) * base**i are labeled.
  • The minor ticks are formatted in scientific notation: $ 3 \times 10^{-2} $
  • The base and exponent are omitted if the exponent is between -1 and 1: i.e., 0.1, 0.7, 1, 8, will be formatted as shown (as in #4730, this could be split as an option for all LogFormatter).

Example of this formatter in action:
log_labels

The main motivation is to improve readability of the axis ticks when log-scaled axis are zoomed-in to less than a few decades, which under the current Formatters leaves only very few labels in the axis. Note that this does not change the formatting whenever more than 3 decades are spanned by the axis view limits.

jenshnielsen added this to the next major release (2.0) milestone Oct 1, 2015

Owner

mdboom commented Oct 6, 2015

It seems like you've combined the logic of whether to display a tick (which is usually the job of a Locator subclass) with the logic of how to display a tick (which is usually the job of a Formatter subclass). Any reason why you didn't separate concerns like all the other Locator/Formatters?

Other than that, I see this as very useful, though we'll have to determine whether or not it's a good idea to change the default behavior.

Member

zblz commented Oct 6, 2015

@mdboom: For the case of log-scaled axis, it is nice to show all minor ticks to make it clear that the scaling is logarithmic, but we obviously do not want to label all minor ticks. The current default is show all minor ticks and label none. I had to combine them so that all minor ticks are shown, but not all of them labeled. I understand its a departure from how other Locator/Formatter work, but I did not see a different way to do it.

EDIT: I could not find it, but is there any other Locator/Formatter where this is done (i.e., only some of the shown minor or major ticks are labeled)?

Owner

mdboom commented Oct 6, 2015

@zblz: I understand the problem better now. I think it could lead to confusion that the formatter chooses not to format some ticks, particularly if using an explicit locator or something. However, as you say, I can't think of a better way right now. That's all the more argument to keep the default for log scales as something that formats everything. Let me think on it for a bit -- maybe there is a better way, perhaps by adding the ability to have separate locators for ticks and text...

Member

zblz commented Oct 6, 2015

Separate locators would be ideal for this case, but it would increase the complexity of the tick logic in Axis. In addition, the process to change tick format and location is not the easiest thing for a user (needing additional imports from ticker, and discovering the names and options of the Locator/Formatter a bit difficult), so adding another Locator might complicate things even further. I don't really have a better option, though.

By the way, I adapted this logic of not showing some labels from the only way I knew how to produce this previously, which was to use horrible, horrible ad-hoc labels like this:

ax.xaxis.set_ticklabels([r'$2\times10^4$','','',r'$5\times10^4$','','','','', 
        r'$2\times 10^4$','','',r'$5\times10^4$','','','','',
        r'$2\times 10^5$','','',r'$5\times10^5$','','','','',
        ],minor=True)
Member

WeatherGod commented Oct 6, 2015

What if a locator could signal whether the location is intended for
tickline, ticklabel, or both (default)?

On Tue, Oct 6, 2015 at 1:58 PM, Victor Zabalza notifications@github.com
wrote:

Separate locators would be ideal for this case, but it would increase the
complexity of the tick logic in Axis and might be quite confusing for the
user. In addition, the process to change tick format and location is not
the easiest thing for a user (needing additional imports from ticker, and
discovering the names and options of the Locator/Formatter a bit
difficult), so adding another Locator might complicate things even further.
I don't really have a better option, though.


Reply to this email directly or view it on GitHub
#5161 (comment)
.

Owner

mdboom commented Oct 6, 2015

@WeatherGod: I like that idea.

Member

zblz commented Oct 6, 2015

@WeatherGod: It sounds good, but I have no idea of how to even begin to implement that from a glance at ticker.py and axis.py. In Axis all ticks have an associated location and label, so if we dissociate the tick location and the label location we might need a lot of refactoring.

Member

WeatherGod commented Oct 6, 2015

If one works from the assumption that all ticklabels must have a tickline,
then it is possible to simply have a blank ticklabel where a location is
only intended for a tickline. Then, it just becomes a matter of how to
signal that from the locator to Axis. That is the hard part, but probably
doable.

On Tue, Oct 6, 2015 at 2:15 PM, Victor Zabalza notifications@github.com
wrote:

@WeatherGod https://github.com/WeatherGod: It sounds good, but I have
no idea of how to even begin to implement that from a glance at ticker.py
and axis.py. In Axis all ticks have an associated location and label, so
if we dissociate the tick location and the label location we might need a
lot of refactoring.


Reply to this email directly or view it on GitHub
#5161 (comment)
.

Owner

tacaswell commented Oct 6, 2015

The Formatter objects are called with the signature (tick_location, tick_number) where iirc tick_number is either None or the integer count of ticks along the axis (with possibly some off-by-one bugs due to a clipped left most tick). It may be possible to write a clever enough Formatter to take this into account.

Member

zblz commented Oct 6, 2015

@tacaswell: But if whether we return a label depends on tick_number then we have the same problem as looking at the parent axis: the decision of whether to show a label is left to the Formatter rather than the Locator, which is what @mdboom objected to initially.

Member

zblz commented Oct 7, 2015

Ok, I tried to move the print logic from the formatter to the locator following @WeatherGod's suggestion, and now the locator must return two arrays: the location for the ticks and whether each tick should be labeled. This means a change to all the __call__ functions of the locators: I have modified it only for LogLocator as a proof-of-concept, but they could also be wrapped so that if they only return one array, the second defaults to all True.

Owner

tacaswell commented Oct 7, 2015

re-milestoned this for 2.1 as this is not just changing some parameters.

Member

zblz commented Oct 7, 2015

Change of mind right after pushing: I moved the logic on whether to show the labels to an different function than the __call__ so that this new function (show_tick_label) can be defined in the parent Locator class. In this way, locator classes that want labels on all their ticks don't have to be modified.

Member

zblz commented Oct 8, 2015

To keep the focus of this PR, I have removed the functionality to plot (1e-1, 1e0, 1e1) as 0.1 1 10, because this should be covered by #4730 in LogFormatterMathtext. This allows LogFormatterSciNotation to be a subclass of LogFormatterMathtext with only the non-decade ticks formatted differently (and any changes to LogFormatterMathtext in #4730 will propagate here).

Owner

mdboom commented Oct 9, 2015

Thanks. I think the separation of Locator/Formatter is much better now. One further refinement though -- I don't think we necessarily require that a Locator inherits from LocatorBase (matplotlib's built-in ones all do, but third-party ones may not). So we should be robust to the possibility that show_tick_label may not be present and handle the default behavior accordingly.

Owner

mdboom commented Oct 15, 2015

Thanks. I'm happy to merge this once the test failure is resolved.

Owner

efiring commented Oct 15, 2015

  1. I presume this needs a "what's new" entry.
  2. Is this still a default change, or is it now just a new Formatter that one can choose? I think it is the latter. Can the PR title be changed accordingly, before or after a merge?
  3. I can see the usefulness of this, particularly for interactive use when zooming, as noted at the start of the PR, but it could also cause clutter and messiness, particularly when using multiple subplots. One thing that might help would be to make the thresholds for labeling minor ticks into settable parameters.
    Suggestion 3 need not hold up this PR; it could be a future improvement, if needed. I suspect it would fit in with the move to traitlets.
Member

zblz commented Oct 16, 2015

@efiring:

  1. It is both a new Formatter and a change in how Locators can specify where to put labels. I think it would make a good default, but if you think it should be considered separately, I can make this PR only about adding the functionality and another PR about whether to make it default (i.e., chaning the default minor tick formatter from NullFormatter to LogFormatterSciNotation) when setting a log scale.
  2. Yes, it does add a bit of horizontal width to the y-axis labels as compared to labeling only the bases, so could be a problem in close multiple plots. It can always be avoided by setting the minor formatter to NullFormatter.
Owner

mdboom commented Oct 19, 2015

We could consider the default change as part of 2.0. Not promising anything, but that's a good window of opportunity for it.

Contributor

anntzer commented Jun 26, 2016 edited

I would like to suggest re-milestoning this (or the original issue, #4867), for 2.0. When round-number limits were the default, a log-scale axis would by default be expanded to initially cover at least one (integer) decade, and thus have at least two labeled ticks (at the two ends). With the switch to margins-based limits, it is now possible for axes to default to having no labels at all (e.g. semilogy([.2, .9])) which is (IMO) very bad.

Edit: Changed the milestone, but feel free to discuss if you disagree.

Member

WeatherGod commented Jul 16, 2016

power-cycling to trigger Travis on latest master...

WeatherGod closed this Jul 16, 2016

mdboom removed the needs_review label Jul 16, 2016

WeatherGod reopened this Jul 16, 2016

mdboom added the needs_review label Jul 16, 2016

@efiring efiring and 1 other commented on an outdated diff Jul 20, 2016

lib/matplotlib/mathtext.py
@@ -1117,7 +1117,7 @@ def _get_font(self, font):
cached_font = self.fonts.get(basename)
if cached_font is None:
fname = os.path.join(self.basepath, basename + ".afm")
- with open(fname, 'r') as fd:
+ with open(fname, 'rb') as fd:
@efiring

efiring Jul 20, 2016

Owner

Is this a bugfix that should go in as a separate PR?

@zblz

zblz Jul 21, 2016

Member

Yes, I ran into this line failing while testing stuff, corrected and commited it by mistake. I'll take it out from this PR.

Owner

tacaswell commented Jul 20, 2016

@zblz Would you be able to add a test + an example?

@anntzer anntzer added a commit to anntzer/matplotlib that referenced this pull request Jul 24, 2016

@zblz @anntzer zblz + anntzer undo SciNotation as default
Closes #5161
cdfdee9
Member

zblz commented Jul 24, 2016

I am working on a test and example. In the meantime, what is the consensus on whether to make this a default or not?

In addition, the PR in #4730 has not been developed further, but I think this PR and that one complement each other nicely in that having several labels that look like 2x10^0, 5x10^0, 10^1 is a lot of clutter compared to 2, 5, 10. Should I reinclude that functionality in this PR?

Member

zblz commented Jul 24, 2016

Tests added, example still missing.

Owner

efiring commented Jul 24, 2016

I think it would be better to keep #4730 as a separate PR; you could take it over if @astrofrog doesn't have the time or inclination to finish it.

@jenshnielsen jenshnielsen commented on an outdated diff Aug 1, 2016

lib/matplotlib/tests/test_ticker.py
@@ -63,6 +72,31 @@ def test_LogLocator():
test_value = np.array([0.5, 1., 2., 4., 8., 16., 32., 64., 128., 256.])
assert_almost_equal(loc.tick_values(1, 100), test_value)
+ # test label locator
@jenshnielsen

jenshnielsen Aug 1, 2016

Owner

This should have a @cleanup decorator to make sure that the plot is closed correctly after the test

Owner

tacaswell commented Aug 1, 2016

👍 to adding this and having it turned on by default.

Can you please move the logic if a tick label or an empty string should be emited into the formatter logic (and not add new public API). If there is a performance bottle neck, we should try to deal with that via private caching/memorized private methods.

Member

zblz commented Aug 2, 2016

Can you please move the logic if a tick label or an empty string should be emited into the formatter logic (and not add new public API). If there is a performance bottle neck, we should try to deal with that via private caching/memorized private methods.

I agree that it would be better to keep this sort of logic private, but @mdboom argued some time ago that a Formatter should only format, not decide whether to format or not, which is a fair point.

We could also keep it in Locator and make it private: show_tick_labels -> _show_tick_labels.

Owner

efiring commented Aug 2, 2016

Locators place ticks, and a formatter either labels all of them, or none of them (NullFormatter), presently with one exception: the FixedFormatter could be given empty strings. Furthermore, a formatter is called once per tick in Axis.iter_ticks, but that series of calls is preceded by a single call to the formatter's set_locs method, giving it the opportunity to calculate axis-wide parameters once at the start, depending on view interval and the tick locations. ScalarFormatter uses this call to calculate the scale and offset. Logically, LogFormatter should be using this to calculate its d parameter, but instead it is doing it repeatedly in the __call__() method. In practice there will be negligible difference, so leaving it as-is is fine.

My conclusion is that the decision as to which tick locations to label is properly the function of the formatter, and it should be done via the set_locs method. Then the __call__ method checks its pos arg against the sequence calculated in set_locs to decide whether to return an empty string.

Contributor

anntzer commented Aug 2, 2016

Nice writeup of the behavior of the formatter class.

I feel like this suggests that the API for the Formatter should be rethought. Fundamentally, there should just be one main method (let's call it .format_labels for now) supported by the class: take a list of tick positions returned by a Locator, and return a list of (possibly empty) strings corresponding to the labels. (FuncFormatter, e.g., would call the passed-in function internally.)

I said one main method because there's actually a second method needed: formatting for the cursor (currently .format_data_short), which may or may not depend on the currently labeled ticks as well. Fortunately, we can ensure that .format_labels is always called first, and thus the Formatter can save whatever information it needs at that point.

Member

zblz commented Aug 2, 2016

@efiring: Thanks for the perspective on the formatter classes, very useful! Considering this, I'll move the logic back into the Fomatter, with as much of it as possible in set_locs.

zblz added some commits Oct 1, 2015

@zblz zblz add LogFormatterSciNotation
move print logic from fromatter to locator

move label logic to independent function

remove redundancy

LogFormatterSciNotation is now a subclass of LogFormatterMathtext, and the
differences (how labels are formatted for non-decade ticks) have been factored
out in a `_non_decade_format` function.

fix default showLabel bool
1320261
@zblz zblz manage locators not deriving from LocatorBase 56e2871
@zblz zblz generalize label location for all bases
fix pep8

fix close to decade coefficients
a6e954d
@zblz zblz use _mathdefault in non-usetex mathtext 4ce70e9
@zblz zblz add tests
add cleanup for test
d1a5aff
@zblz zblz move labeling logic into LogFormatter
4e83e9a
Member

zblz commented Aug 2, 2016

I have moved the logic back into the formatter classes, choosing to add it into LogFormatter rather than LogFormatterSciNotation as all log formatters could benefit from it (note though that all of them except for LogFormatterSciNotation have labelOnlyBase=True as default, so this change will not affect them if used with default parameters).

I have also added back LogFormatterSciNotation as the default formatter for log-scaled axes.

Member

WeatherGod commented Aug 3, 2016

Getting a bunch of errors. Somehow, zeros are getting down into math.log(). Got some image comparison failures, too.

zblz added some commits Aug 3, 2016

@zblz zblz fix numdec computation for symlog axes ad0a751
@zblz zblz update baseline images for log_scales test
f04bff0
Member

zblz commented Aug 3, 2016

I have fixed the error on symlog axes, which where sending values vmin <= 0 to math.log. I also updated the baseline images for the test_axes.log_scale test.

@zblz zblz do not fail on _DummyAxes
5c28e95
Member

zblz commented Aug 3, 2016

The remaining error on matplotlib.tests.test_backend_ps.test_savefig_to_stringio_eps_afm will be fixed by PR #6898, so that should be merged before this PR.

Member

WeatherGod commented Aug 4, 2016

power-cycling to test against the latest master.

WeatherGod closed this Aug 4, 2016

mdboom removed the needs_revision label Aug 4, 2016

WeatherGod reopened this Aug 4, 2016

mdboom added the needs_review label Aug 4, 2016

Member

WeatherGod commented Aug 22, 2016

This is passing and is milestoned for v2.0. I don't know who is the one to approve the remaining style changes.

@tacaswell tacaswell merged commit 947e6eb into matplotlib:master Aug 22, 2016

3 checks passed

continuous-integration/appveyor/pr AppVeyor build succeeded
Details
continuous-integration/travis-ci/pr The Travis CI build passed
Details
coverage/coveralls Coverage increased (+0.1%) to 70.941%
Details

tacaswell removed the needs_review label Aug 22, 2016

@tacaswell tacaswell added a commit that referenced this pull request Aug 22, 2016

@tacaswell tacaswell Merge pull request #5161 from zblz/log-ticks
API: Changes to default log scale tick formatting
fb45c4a
Owner

tacaswell commented Aug 22, 2016

backported to v2.x as fb45c4a

@tacaswell tacaswell added a commit to tacaswell/matplotlib that referenced this pull request Aug 29, 2016

@tacaswell tacaswell REV: remove default formatter on log minor ticks
Reverts part of #5161

original commit merged to master as 947e6eb
the merge was backported to v2.x as fb45c4a
3a03595

@efiring efiring added a commit to efiring/matplotlib that referenced this pull request Nov 7, 2016

@efiring efiring ENH: restore default ability to label some minor log ticks.
This partly restores the functionality that was added in
PR #5161 and partly removed in #7000.  The "partly" is because
now the labeling of minor log ticks is turned on only when
numdecs (the axis range in powers of the log base) is less
than or equal to one, rather than 3.

This also fixes a bug that was causing double labeling with a
base of 2; minor ticks were coinciding with major ticks, and
both were being labeled.
129a32b

QuLogic changed the title from [WIP] Proposed change to default log scale tick formatting to Proposed change to default log scale tick formatting Jan 26, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment