-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Fix for overlapping datetime intervals #10779
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
Changes from all commits
2e8184b
36dd933
19bb1f0
4159670
0af2379
9b65519
cc2c074
ad15851
5325ff2
242b4b2
c5afe20
a7a0a3f
202ed25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
``AutoDateLocator.get_locator`` now contains reduced datetime overlaps | ||
```````````````````````````````````````````````````````````````````````` | ||
|
||
Due to issue #7712, the interval frequency of datetime ticks gets reduced due to | ||
avoid overlapping tick labels. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1208,12 +1208,20 @@ def __init__(self, tz=None, minticks=5, maxticks=None, | |
The interval is used to specify multiples that are appropriate for | ||
the frequency of ticking. For instance, every 7 days is sensible | ||
for daily ticks, but for minutes/seconds, 15 or 30 make sense. | ||
You can customize this dictionary by doing:: | ||
You can customize this dictionary by doing: | ||
|
||
locator = AutoDateLocator() | ||
locator.intervald[HOURLY] = [3] # only show every 3 hours | ||
|
||
In order to avoid overlapping dates, another dictionary was | ||
created to map date intervals to the format of the date used in | ||
rcParams. In addition, the figsize and font is used from the axis, | ||
and a new setting in rcparams['autodatelocator.spacing'] is added | ||
and used to let the user decide when spacing should be used. This | ||
was done because rotation at this point in runtime is not known. | ||
""" | ||
DateLocator.__init__(self, tz) | ||
|
||
self._locator = YearLocator() | ||
self._freq = YEARLY | ||
self._freqs = [YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY, | ||
|
@@ -1242,6 +1250,16 @@ def __init__(self, tz=None, minticks=5, maxticks=None, | |
MICROSECONDLY: [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, | ||
5000, 10000, 20000, 50000, 100000, 200000, 500000, | ||
1000000]} | ||
|
||
self.eachtick = { | ||
YEARLY: rcParams['date.autoformatter.year'], | ||
MONTHLY: rcParams['date.autoformatter.month'], | ||
DAILY: rcParams['date.autoformatter.day'], | ||
HOURLY: rcParams['date.autoformatter.hour'], | ||
MINUTELY: rcParams['date.autoformatter.minute'], | ||
SECONDLY: rcParams['date.autoformatter.second'], | ||
MICROSECONDLY: rcParams['date.autoformatter.microsecond']} | ||
|
||
self._byranges = [None, range(1, 13), range(1, 32), | ||
range(0, 24), range(0, 60), range(0, 60), None] | ||
|
||
|
@@ -1314,11 +1332,34 @@ def get_locator(self, dmin, dmax): | |
# bysecond, unused (for microseconds)] | ||
byranges = [None, 1, 1, 0, 0, 0, None] | ||
|
||
# Required attributes to get width from figure's normal points | ||
axis_name = getattr(self.axis, 'axis_name', '') | ||
axes = getattr(self.axis, 'axes', None) | ||
label = getattr(self.axis, 'label', None) | ||
figure = getattr(self.axis, 'figure', None) | ||
transfig = getattr(figure, 'transFigure', None) | ||
maxwid = rcParams['figure.figsize'][0] | ||
|
||
# estimated font ratio on font size 10 | ||
if label is not None: | ||
font_ratio = label.get_fontsize() / 10 | ||
else: | ||
font_ratio = rcParams['font.size'] / 10 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure the ticks have a fontsize associated with them beyond the label fontsize. THats the fontsize you should querry, and I'm pretty sure it exists. |
||
|
||
# a ratio of 8 date characters per inch is 'estimated' | ||
if (axes is not None and transfig is not None): | ||
bbox = axes.get_position(original=False) | ||
figwidth = transfig.transform(bbox.get_points())[1][0] | ||
dpi = figure.get_dpi() | ||
maxwid = (figwidth / dpi) * 8 | ||
|
||
spacing = (rcParams["autodatelocator.spacing"] == "generous") | ||
|
||
# Loop over all the frequencies and try to find one that gives at | ||
# least a minticks tick positions. Once this is found, look for | ||
# an interval from an list specific to that frequency that gives no | ||
# more than maxticks tick positions. Also, set up some ranges | ||
# (bymonth, etc.) as appropriate to be passed to rrulewrapper. | ||
# (bymonth, etc.) as appropriate to be passed to rrulewrapper | ||
for i, (freq, num) in enumerate(zip(self._freqs, nums)): | ||
# If this particular frequency doesn't give enough ticks, continue | ||
if num < self.minticks: | ||
|
@@ -1328,11 +1369,24 @@ def get_locator(self, dmin, dmax): | |
byranges[i] = None | ||
continue | ||
|
||
# Compute at runtime the size of date label with given format | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again, this should only happen if spacing is on. |
||
if (axis_name == 'x'): | ||
datelen_ratio = \ | ||
len(dmin.strftime(self.eachtick[freq])) * font_ratio | ||
else: | ||
datelen_ratio = 1 * font_ratio | ||
|
||
# Find the first available interval that doesn't give too many | ||
# ticks | ||
for interval in self.intervald[freq]: | ||
if num <= interval * (self.maxticks[freq] - 1): | ||
break | ||
|
||
# Using an estmation of characters per inch, reduce | ||
# intervals untill we get no overlaps | ||
apply_spread = (not spacing or | ||
((num/interval) * datelen_ratio) <= maxwid) | ||
if (num <= interval * (self.maxticks[freq] - 1)): | ||
if (apply_spread): | ||
break | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ??? I don't understand why you only break if apply_spread=True here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When users have rotated labels we shouldn't apply spread, it applies too much spacing. Unfortunately this is why i added to Rcparams in the first place There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But you need "interval" to be correct for |
||
else: | ||
# We went through the whole loop without breaking, default to | ||
# the last interval in the list and raise a warning | ||
|
@@ -1344,7 +1398,6 @@ def get_locator(self, dmin, dmax): | |
|
||
# Set some parameters as appropriate | ||
self._freq = freq | ||
|
||
if self._byranges[i] and self.interval_multiples: | ||
byranges[i] = self._byranges[i][::interval] | ||
interval = 1 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -352,6 +352,8 @@ backend : $TEMPLATE_BACKEND | |
#date.autoformatter.second : %H:%M:%S | ||
#date.autoformatter.microsecond : %M:%S.%f | ||
|
||
#autodatelocator.spacing : tight ## if generous, add extra spacing to avoid overlap, otherwise tight | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe |
||
|
||
#### TICKS | ||
## see http://matplotlib.org/api/axis_api.html#matplotlib.axis.Tick | ||
#xtick.top : False ## draw ticks on the top side | ||
|
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 this should all be wrapped in
if spacing:
I don't get why you are using getattr for all these. If these attributes don't exist, you might as well error here because you weren't passed an axis...
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.
If we raise exceptions it would break existing tests using mock classes that might have aaxis attributes as none.
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.
Which tests? I don't understand what a test would be doing down in here w/o the axes being set (for instance).
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.
for example test_dates.py::def test_auto_date_locator(), this uses dummy axis to specifically check values returned from autodatelocator. It has a few none axis attributes
https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/tests/test_dates.py#L320
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, well I'd just check if whichever one of these is None and set spacing=False if that happens