FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Fix for overlapping datetime intervals by shadidsh · Pull Request #10779 · matplotlib/matplotlib · GitHub

Repository navigation

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .png  (4) .py  (3) .rst  (1) .template  (1) All 4 file types selected
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
5 changes: 5 additions & 0 deletions doc/api/next_api_changes/2018-03-15-SS.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
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.
63 changes: 58 additions & 5 deletions lib/matplotlib/dates.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]

Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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...

shadidsh Mar 23, 2018 •
edited
Loading

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

If we raise exceptions it would break existing tests using mock classes that might have aaxis attributes as none.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Which tests? I don't understand what a test would be doing down in here w/o the axes being set (for instance).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

OK, well I'd just check if whichever one of these is None and set spacing=False if that happens

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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:
Expand All @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

??? I don't understand why you only break if apply_spread=True here.

shadidsh Mar 23, 2018 •
edited
Loading

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

But you need "interval" to be correct for byranges, below.

else:
# We went through the whole loop without breaking, default to
# the last interval in the list and raise a warning
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/matplotlib/rcsetup.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,9 @@ def _validate_linestyle(ls):
'date.autoformatter.second': ['%H:%M:%S', validate_string],
'date.autoformatter.microsecond': ['%M:%S.%f', validate_string],

# To avoid overlapping date invervals, we can set the spacing in advance
# 'generous' is set to avoid overlapping, otherwise 'tight' by default
'autodatelocator.spacing' : ['tight', validate_string],
#legend properties
'legend.fancybox': [True, validate_bool],
'legend.loc': ['best', validate_legend_loc],
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Viewer requires iframe.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Viewer requires iframe.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Viewer requires iframe.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Viewer requires iframe.
58 changes: 51 additions & 7 deletions lib/matplotlib/tests/test_dates.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from matplotlib.testing.decorators import image_comparison
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib import rcParams


def test_date_numpyx():
Expand Down Expand Up @@ -405,12 +406,12 @@ def _create_auto_date_locator(date1, date2):
'2180-01-01 00:00:00+00:00', '2200-01-01 00:00:00+00:00']
],
[datetime.timedelta(weeks=52),
['1997-01-01 00:00:00+00:00', '1997-02-01 00:00:00+00:00',
'1997-03-01 00:00:00+00:00', '1997-04-01 00:00:00+00:00',
'1997-05-01 00:00:00+00:00', '1997-06-01 00:00:00+00:00',
'1997-07-01 00:00:00+00:00', '1997-08-01 00:00:00+00:00',
'1997-09-01 00:00:00+00:00', '1997-10-01 00:00:00+00:00',
'1997-11-01 00:00:00+00:00', '1997-12-01 00:00:00+00:00']
['1997-01-01 00:00:00+00:00', '1997-02-01 00:00:00+00:00',
'1997-03-01 00:00:00+00:00', '1997-04-01 00:00:00+00:00',
'1997-05-01 00:00:00+00:00', '1997-06-01 00:00:00+00:00',
'1997-07-01 00:00:00+00:00', '1997-08-01 00:00:00+00:00',
'1997-09-01 00:00:00+00:00', '1997-10-01 00:00:00+00:00',
'1997-11-01 00:00:00+00:00', '1997-12-01 00:00:00+00:00']
],
[datetime.timedelta(days=141),
['1997-01-01 00:00:00+00:00', '1997-01-22 00:00:00+00:00',
Expand Down Expand Up @@ -453,7 +454,6 @@ def _create_auto_date_locator(date1, date2):
'1997-01-01 00:00:00.001500+00:00']
],
)

for t_delta, expected in results:
d2 = d1 + t_delta
locator = _create_auto_date_locator(d1, d2)
Expand Down Expand Up @@ -612,3 +612,47 @@ def test_tz_utc():
def test_num2timedelta(x, tdelta):
dt = mdates.num2timedelta(x)
assert dt == tdelta


@image_comparison(baseline_images=['datetime_daily_overlap'],
extensions=['png'])
def test_datetime_daily_overlap():
# issue 7712 for overlapping daily dates
plt.rcParams['date.autoformatter.day'] = "%Y-%m-%d"
plt.rcParams["autodatelocator.spacing"] = "generous"
dates = [datetime.datetime(2018, 1, i) for i in range(1, 30)]
values = list(range(1, 30))
plt.plot(dates, values)


@image_comparison(baseline_images=['datetime_monthly_overlap'],
extensions=['png'])
def test_datetime_monthly_overlap():
# issue 7712 for overlapping monthly dates
plt.rcParams['date.autoformatter.month'] = '%Y-%m'
plt.rcParams["autodatelocator.spacing"] = "generous"
dates = [datetime.datetime(2018, i, 1) for i in range(1, 11)]
values = list(range(1, 11))
plt.plot(dates, values)


@image_comparison(baseline_images=['datetime_hourly_overlap'],
extensions=['png'])
def test_datetime_hourly_overlap():
# issue 7712 for overlapping hourly dates
plt.rcParams['date.autoformatter.hour'] = '%m-%d %H'
plt.rcParams["autodatelocator.spacing"] = "generous"
dates = [datetime.datetime(2018, 1, 1, i) for i in range(1, 20)]
values = list(range(1, 20))
plt.plot(dates, values)


@image_comparison(baseline_images=['datetime_minutely_overlap'],
extensions=['png'])
def test_datetime_minutely_overlap():
# issue 7712 for overlapping date ticks in minutely intervals
plt.rcParams['date.autoformatter.minute'] = '%d %H:%M'
plt.rcParams["autodatelocator.spacing"] = "generous"
dates = [datetime.datetime(2018, 1, 1, 1, i) for i in range(1, 55)]
values = list(range(1, 55))
plt.plot(dates, values)
2 changes: 2 additions & 0 deletions matplotlibrc.template
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Maybe autodatelocator.generousspacing : False True/False would be a cleaner API?


#### TICKS
## see http://matplotlib.org/api/axis_api.html#matplotlib.axis.Tick
#xtick.top : False ## draw ticks on the top side
Expand Down

Back | FazBrowse Home | New Git URL