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

Minor ticklabels are missing at positions of major ticks. #13618

Closed
ImportanceOfBeingErnest opened this issue Mar 7, 2019 · 25 comments · Fixed by #13632
Closed

Minor ticklabels are missing at positions of major ticks. #13618

ImportanceOfBeingErnest opened this issue Mar 7, 2019 · 25 comments · Fixed by #13632
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Milestone

Comments

@ImportanceOfBeingErnest
Copy link
Member

Bug report

Bug summary

Minor ticklabels are missing at positions of major ticks.

Code for reproduction

import numpy as np
import matplotlib.dates as mdates
import matplotlib.pyplot as plt

t = np.arange("2018-11-03", "2018-11-06", dtype="datetime64")
x = np.random.rand(len(t))

fig, ax = plt.subplots()
ax.plot(t,x)

ax.xaxis.set_major_locator(mdates.DayLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter('\n%a'))

ax.xaxis.set_minor_locator(mdates.HourLocator((0,6,12,18)))
ax.xaxis.set_minor_formatter(mdates.DateFormatter('%H:%M'))

plt.show()

Actual outcome

The above code run with current master produces

image

The minor ticklabels showing the 00:00 hours are missing.

Expected outcome

The expected outcome would be the same as when running the code with matplotlib 3.0.2 or below:

image

I would expect to see the hours throughout.

Matplotlib version

  • Operating system: Win8
  • Matplotlib version: master
  • Matplotlib backend (print(matplotlib.get_backend())): any
  • Python version: 3.6
@ImportanceOfBeingErnest ImportanceOfBeingErnest added this to the v3.1.0 milestone Mar 7, 2019
@jklymak
Copy link
Member

jklymak commented Mar 7, 2019

There is no minor tick there anymore so there won’t be a label. What’s wrong w putting the HH:MM in the major label?

@ImportanceOfBeingErnest
Copy link
Member Author

Actually, I don't think there is anything wrong with that. It's more that the previous code suddenly broke. Was this an intentional change?

@jklymak
Copy link
Member

jklymak commented Mar 7, 2019

Yes though I’m on my phone and can’t look up the PRs. Recent ones by @anntzer and or myself. Basically minor ticks no longer include major ticks. So no more over strike on the ticking and no more heuristic guessing if a labeled minor tick is really a major tick.

@anntzer
Copy link
Contributor

anntzer commented Mar 7, 2019

Yes, that comes from #13314. I guess this could have been better documented; on the other hand the issue that #13314 fixed did keep coming up again and again, so trying to play whack-a-mole by fixing it one locator at a time is a bit an endless task.

Note that in the example here, your formatters are actually not really independent from one another (you need to embed the newline in the major formatter), so I think the solution with the new API (ax.xaxis.set_major_formatter(mdates.DateFormatter('%H%M\n%a')) looks just fine. (But yes, I acknowledge it's an API break.)

@ImportanceOfBeingErnest
Copy link
Member Author

I see. Now reading the API change note, "Minor Locator no longer try to avoid overstriking major Locators", it seems to tell me the opposite, because obviously the minor locator does avoid the major locations.

May I suggest to write an additional what's new entry that is understandable by normal people and shows what is changed and why that is?

@anntzer
Copy link
Contributor

anntzer commented Mar 7, 2019

Do you want to give it a try? You are obviously more aware of the cases that have been broken. (If not I'll do it, that's fine too.)

@ImportanceOfBeingErnest
Copy link
Member Author

Is there any way to revert back to the old behaviour?

@anntzer
Copy link
Contributor

anntzer commented Mar 8, 2019

Right now, no. Could perhaps be switched with a new flag (with the note that in that case, even loglocators don't try to avoid crashing minor and major ticks).

@ImportanceOfBeingErnest
Copy link
Member Author

For a what's new entry maybe show the effect as follows:

ax.xaxis.set_major_locator(mticker.MultipleLocator(10))
ax.xaxis.set_minor_locator(mticker.MultipleLocator(2))
ax.xaxis.set_minor_formatter(mticker.ScalarFormatter())
ax.grid(which="both", axis="x")

previously:
majorminorchange_3 0 2

now:
majorminorchange_3 0 2 post1846 gfd40d7d74

I mean this really looks like a great improvement, but maybe someone relies on the major and minor ticks/grids overlapping?

@ImportanceOfBeingErnest
Copy link
Member Author

I think a what's new entry would still be useful, since noone reads API change notes. (Reading through the recent API changes actually a lot of them should have been mentionned in the what's new section?! Or maybe I don't quite understand the difference between what's new and API change notes?)

Also, how do you revert this change? Previously you could still write your own ticker in order not to tick some locations. Arguably, the new behaviour is much better for most standard cases. However for special cases, with this change, you cannot write any ticker to force a tick at a specific location if it happens to be part of the major ticks. Not even a FixedLocator will work, right?

Concrete example:

ax.set_xticks([0.2], minor=True)
ax.grid(which="minor", axis="x")

previously:
image

now:

image

Question: How to get the gridline back?

@jklymak
Copy link
Member

jklymak commented Mar 8, 2019

I’m not opposed to having a way to get all the ticks back, but I’m not clear on what the practical problem is versus a theoretical one. If you need a bunch of vertical lines at arbitrary locations axvline does that for you. This makes all the practical cases much better at the cost of a few obscure cases being a bit harder. I’d need a bit more to convince me that adding API to toggle this behaviour is worth the fuss.

I think what’s new is for new features. API changes is for changes to existing features. At least in my mind. OTOH Id support merging these two under what’s new and just labelling the API changes as such.

@ImportanceOfBeingErnest
Copy link
Member Author

I’m not clear on what the practical problem is versus a theoretical one.

That is a theoretical problem indeed. You type something in (ax.set_xticks(..)) and don't get out what you asked for, like

you > Please give me a tick at position 0.2
interpreter > Na, I don't feel like doing that is a good idea; I will ignore your command.

If you need a bunch of vertical lines at arbitrary locations axvline does that for you.

Sure, there is no need for .grid at all, given that there is a Line2D object available.

I think what’s new is for new features. API changes is for changes to existing features.

I think I would argue that things like "Hey look, we've fixed this long standing bug." or "If you use good old command x your plot will now look like y." are still somehow news people are interested in reading the What's new section.

@jklymak
Copy link
Member

jklymak commented Mar 8, 2019

interpreter > Na, I don't feel like doing that is a good idea; I will ignore your command.

Thats correct - #13314 says that minor ticks are exclusive of major ticks by definition, so if you ask to put a minor tick where a major tick is, you won't get it.

I'm still not clear what the use-case is, but if you need to hack around this definition:

import matplotlib.pyplot as plt

fig, ax =plt.subplots()
ax.set_xticks([0.2001], minor=True)
ax.grid(which="minor", axis="x")
plt.show()

though I note that going more decimal places (0.20001) excludes the tick, which seems a bit too much slop... (well, its rtol=1e-5)

@anntzer
Copy link
Contributor

anntzer commented Mar 8, 2019

On my phone but note that #11575 is close to (though not exactly) the opposite of what @ImportanceOfBeingErnest mentioned above: users were complaining that set_xticks did not cause the minor ticks to be excluded from colliding locations.

@ImportanceOfBeingErnest
Copy link
Member Author

The fact that log scales use major and minor locators is more an implementation detail, #11575 could be solved differently as well. In general, I'm not at all opposing the default Locators to exclude minor ticks at major tick positions.

If the decision is indeed to redefine the notions of major and minor in the sense of "minor ticks are exclusive of major ticks by definition", that is a major change in the semantics and a "What's new" entry is the very least one needs for that.

@anntzer
Copy link
Contributor

anntzer commented Mar 12, 2019

I don't mind moving/duplicating the api_changes to the whatsnew.
If you want to put up an alternate PR to fix issue #11575 and the other similar issues, and revert #13314, I won't block it either.
Having a different behavior for default and nondefault locators (what's even a "default" locator?) seems strange, though.

@ImportanceOfBeingErnest
Copy link
Member Author

By "default" I meant all those cases where the user does not type in .set_minor_locator or .set_xticks; that would in addition to normal plots be e.g. plt.semilogy, plt.plot(<list of datetimes>) etc.
But I fully agree that different behaviour is in general undesired. I also acknowledge that this change is useful for all but a few edge cases.
It's really more a principle thing: major and minor locators are not independent of each other any more. (A use case would be the original issue where in addition you use a different color or font(size) for the major and minor labels.)

The best would be an opt-out option for this behaviour. (But I currently wouldn't know where to put that. In the locators? In the axes?)
If people really think, that is not necessary, adding a note in the what's new/Api change that says something like "We feel this change best reflects how people would use major and minor locators; however if you have a usecase where this is causes problems, please do file a report on the issue tracker." might be the way to go.

@anntzer
Copy link
Contributor

anntzer commented Mar 12, 2019

By "default" I meant all those cases where the user does not type in .set_minor_locator or .set_xticks;

But all #11575 is a case where the user uses set_xticks but wants collision suppression...

A use case would be the original issue where in addition you use a different color or font(size) for the major and minor labels.

The real fix would be to allow text objects with variable color or size (I mean, here you can have two different colors (major/minor) but not three, so that's clearly a hack).


Can you open a PR to add whatever note you want to the api_changes and possibly move it to the whatsnew? I think we should try to keep this as is, and, if there's too much pushback against it, we can consider adding the opt-out in a future release.

@ImportanceOfBeingErnest
Copy link
Member Author

Can you open a PR [...] ?

No sorry, I can't. I did try and it came out too sarcastic to be publishable.

@anntzer
Copy link
Contributor

anntzer commented Mar 24, 2019

Do you want to block 3.1 over that? (That's fine with me, but you need to ask for it :))

@ImportanceOfBeingErnest
Copy link
Member Author

No, I don't want to block 3.1 over this. I gave some arguments above, and if they are not shared by others, I might simply be wrong in my analysis.

@anntzer
Copy link
Contributor

anntzer commented Mar 24, 2019

OK, let's just ping @tacaswell to get his opinion as well then, if he wants to chime in before the 3.1 release.

@jklymak
Copy link
Member

jklymak commented Mar 24, 2019

Suggest we add to tomorrow’s agenda.

@tacaswell tacaswell added the Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions. label Mar 25, 2019
@tacaswell tacaswell reopened this Mar 25, 2019
@tacaswell
Copy link
Member

Discussed on call

https://paper.dropbox.com/doc/Matplotlib-2019-meeting-agenda--AaCmZlKDONJlV5crSSBPDIBjAg-aAmENlkgepgsMeDZtlsYu#:h2=13618:-Minor-tick-supression-w

Primary plan is to try to add a public API for controlling the de-confliction
Backup plan is to revert this and try again for 3.2

tacaswell added a commit to tacaswell/matplotlib that referenced this issue Apr 9, 2019
If Axis.remove_overlapping_locs is set to False the `Axis` object will
not attempt to remove locations in the minor ticker that overlap with
locations in the major ticker.   This is a follow up to matplotlib#13314 which
un-conditionally removed the overlap.

closes matplotlib#13618
@Hoops01
Copy link

Hoops01 commented Nov 18, 2023

Sorry to bring up a long closed thread, but is there a way for the user to reverse this change?
I would actually like the minor ticklabel overstrike to occur so I can set a shifted minor tick labels as a "sub category" for example calendar months, and a shifted major tick label as category say calendar years;
date_ticklabel_shifted

I think this is a much more natural way of displaying time series data, since when people refer something happening in "2023" or in "March 2023", they mean over a time period. Plotting it this way makes that super easy to find (imo).
FYI, I asked this question here https://stackoverflow.com/questions/77505618/place-tick-labels-between-ticks-for-time-series-charts-matplotlib

My code for doing this is;

import datetime as dt
import matplotlib.pyplot as plt
import matplotlib.dates as mdates

fig, ax = plt.subplots(figsize=(16,4))

# set plot date range
startdate = dt.date(2023,1,1)
enddate = dt.date(2025,7,1)
ax.set_xlim([startdate, enddate])

# set grid and x ticks
ax.grid(which='major', axis='x', color='b')
ax.tick_params(axis='x', which='major', length=16, width=1, colors='b')
ax.grid(which='minor', axis='x', color='y')
ax.tick_params(axis='x', which='minor', length=8, width=1, colors='y')

# Set major x ticks
ax.xaxis.set_major_locator(mdates.YearLocator())
ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y'))

# offset major x tick labels
for label in ax.xaxis.get_majorticklabels():
    label.set_transform(transforms.Affine2D().translate(365/2, 0) +label.get_transform())

# Set minor x ticks
ax.xaxis.set_minor_locator(mdates.MonthLocator())
ax.xaxis.set_minor_formatter(mdates.DateFormatter('%b'))
# offset minor x tick labels
for label in ax.xaxis.get_minorticklabels():
    label.set_transform(transforms.Affine2D().translate(15, 0) +label.get_transform())
    
plt.setp(ax.get_xticklabels(which='minor')[-1], visible=False)

Apologies if this is not the right forum for this, but if I can get Matplotlib to show exactly what @ImportanceOfBeingErnest did in the first post, I can get this to work.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Release critical For bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants