Skip to content

Commit 061897d

Browse files
nmartensenNo-Stream
authored andcommitted
Fix automatic xlims in line plots (pandas-dev#16600)
* BUG: set correct xlims for lines (pandas-dev#11471, pandas-dev#11310) * Do not assume that xdata is sorted. * Use numpy.nanmin() and numpy.nanmax() instead. * BUG: Let new MPL automatically determine xlims (pandas-dev#15495) * Avoid setting xlims since recent matplotlib already does it correctly * and we should let it apply its default styles where possible * TST: plotting: update expected results for matplotlib 2 Matplotlib 2.0 uses new defaults that cause some of our tests to fail. This adds appropriate new sets of expected results to the following tests in tests/plotting/test_datetimelike.py: test_finder_daily test_finder_quarterly test_finder_annual test_finder_hourly test_finder_minutely test_finder_monthly test_format_timedelta_ticks_narrow test_format_timedelta_ticks_wide * TST: plotting: Relax some tests to work with matplotlib 2.0 Matplotlib 2.0 by default now adds some padding between the boundaries of the data and the boundaries of the plot. This causes some of our tests to fail if we don't relax them slightly. modified: pandas/tests/plotting/test_datetimelike.py test_irregular_ts_shared_ax_xlim test_mixed_freq_regular_first test_mixed_freq_regular_first_df test_secondary_y_irregular_ts_xlim test_secondary_y_non_ts_xlim test_secondary_y_regular_ts_xlim modified: pandas/tests/plotting/test_frame.py test_area_lim test_line_lim modified: pandas/tests/plotting/test_series.py test_ts_area_lim test_ts_line_lim * TST: Add lineplot tests with unsorted x data Two new tests check interaction of non-monotonic x data and xlims: test_frame / test_unsorted_index_lims test_series / test_unsorted_index_xlim * DOC: lineplot/xlims whatsnew entry for v0.21.0
1 parent 8e50b03 commit 061897d

File tree

6 files changed

+150
-59
lines changed

6 files changed

+150
-59
lines changed

doc/source/whatsnew/v0.21.0.txt

+2
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,8 @@ Plotting
525525
^^^^^^^^
526526
- Bug in plotting methods using ``secondary_y`` and ``fontsize`` not setting secondary axis font size (:issue:`12565`)
527527
- Bug when plotting ``timedelta`` and ``datetime`` dtypes on y-axis (:issue:`16953`)
528+
- Line plots no longer assume monotonic x data when calculating xlims, they show the entire lines now even for unsorted x data. (:issue:`11310`)(:issue:`11471`)
529+
- With matplotlib 2.0.0 and above, calculation of x limits for line plots is left to matplotlib, so that its new default settings are applied. (:issue:`15495`)
528530
- Bug in ``Series.plot.bar`` or ``DataFramee.plot.bar`` with ``y`` not respecting user-passed ``color`` (:issue:`16822`)
529531

530532

pandas/plotting/_core.py

+6-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
from pandas.util._decorators import Appender
3232

3333
from pandas.plotting._compat import (_mpl_ge_1_3_1,
34-
_mpl_ge_1_5_0)
34+
_mpl_ge_1_5_0,
35+
_mpl_ge_2_0_0)
3536
from pandas.plotting._style import (mpl_stylesheet, plot_params,
3637
_get_standard_colors)
3738
from pandas.plotting._tools import (_subplots, _flatten, table,
@@ -969,9 +970,10 @@ def _make_plot(self):
969970
**kwds)
970971
self._add_legend_handle(newlines[0], label, index=i)
971972

972-
lines = _get_all_lines(ax)
973-
left, right = _get_xlim(lines)
974-
ax.set_xlim(left, right)
973+
if not _mpl_ge_2_0_0():
974+
lines = _get_all_lines(ax)
975+
left, right = _get_xlim(lines)
976+
ax.set_xlim(left, right)
975977

976978
@classmethod
977979
def _plot(cls, ax, x, y, style=None, column_num=None,

pandas/plotting/_tools.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -361,8 +361,8 @@ def _get_xlim(lines):
361361
left, right = np.inf, -np.inf
362362
for l in lines:
363363
x = l.get_xdata(orig=False)
364-
left = min(x[0], left)
365-
right = max(x[-1], right)
364+
left = min(np.nanmin(x), left)
365+
right = max(np.nanmax(x), right)
366366
return left, right
367367

368368

pandas/tests/plotting/test_datetimelike.py

+87-33
Original file line numberDiff line numberDiff line change
@@ -386,16 +386,24 @@ def test_get_finder(self):
386386

387387
@pytest.mark.slow
388388
def test_finder_daily(self):
389-
xp = Period('1999-1-1', freq='B').ordinal
390389
day_lst = [10, 40, 252, 400, 950, 2750, 10000]
391-
for n in day_lst:
390+
391+
if self.mpl_ge_2_0_0:
392+
xpl1 = [7565, 7564, 7553, 7546, 7518, 7428, 7066]
393+
xpl2 = [7566, 7564, 7554, 7546, 7519, 7429, 7066]
394+
else:
395+
xpl1 = xpl2 = [Period('1999-1-1', freq='B').ordinal] * len(day_lst)
396+
397+
for i, n in enumerate(day_lst):
398+
xp = xpl1[i]
392399
rng = bdate_range('1999-1-1', periods=n)
393400
ser = Series(np.random.randn(len(rng)), rng)
394401
_, ax = self.plt.subplots()
395402
ser.plot(ax=ax)
396403
xaxis = ax.get_xaxis()
397404
rs = xaxis.get_majorticklocs()[0]
398405
assert xp == rs
406+
xp = xpl2[i]
399407
vmin, vmax = ax.get_xlim()
400408
ax.set_xlim(vmin + 0.9, vmax)
401409
rs = xaxis.get_majorticklocs()[0]
@@ -404,16 +412,24 @@ def test_finder_daily(self):
404412

405413
@pytest.mark.slow
406414
def test_finder_quarterly(self):
407-
xp = Period('1988Q1').ordinal
408415
yrs = [3.5, 11]
409-
for n in yrs:
416+
417+
if self.mpl_ge_2_0_0:
418+
xpl1 = [68, 68]
419+
xpl2 = [72, 68]
420+
else:
421+
xpl1 = xpl2 = [Period('1988Q1').ordinal] * len(yrs)
422+
423+
for i, n in enumerate(yrs):
424+
xp = xpl1[i]
410425
rng = period_range('1987Q2', periods=int(n * 4), freq='Q')
411426
ser = Series(np.random.randn(len(rng)), rng)
412427
_, ax = self.plt.subplots()
413428
ser.plot(ax=ax)
414429
xaxis = ax.get_xaxis()
415430
rs = xaxis.get_majorticklocs()[0]
416431
assert rs == xp
432+
xp = xpl2[i]
417433
(vmin, vmax) = ax.get_xlim()
418434
ax.set_xlim(vmin + 0.9, vmax)
419435
rs = xaxis.get_majorticklocs()[0]
@@ -422,16 +438,24 @@ def test_finder_quarterly(self):
422438

423439
@pytest.mark.slow
424440
def test_finder_monthly(self):
425-
xp = Period('Jan 1988').ordinal
426441
yrs = [1.15, 2.5, 4, 11]
427-
for n in yrs:
442+
443+
if self.mpl_ge_2_0_0:
444+
xpl1 = [216, 216, 204, 204]
445+
xpl2 = [216, 216, 216, 204]
446+
else:
447+
xpl1 = xpl2 = [Period('Jan 1988').ordinal] * len(yrs)
448+
449+
for i, n in enumerate(yrs):
450+
xp = xpl1[i]
428451
rng = period_range('1987Q2', periods=int(n * 12), freq='M')
429452
ser = Series(np.random.randn(len(rng)), rng)
430453
_, ax = self.plt.subplots()
431454
ser.plot(ax=ax)
432455
xaxis = ax.get_xaxis()
433456
rs = xaxis.get_majorticklocs()[0]
434457
assert rs == xp
458+
xp = xpl2[i]
435459
vmin, vmax = ax.get_xlim()
436460
ax.set_xlim(vmin + 0.9, vmax)
437461
rs = xaxis.get_majorticklocs()[0]
@@ -450,7 +474,11 @@ def test_finder_monthly_long(self):
450474

451475
@pytest.mark.slow
452476
def test_finder_annual(self):
453-
xp = [1987, 1988, 1990, 1990, 1995, 2020, 2070, 2170]
477+
if self.mpl_ge_2_0_0:
478+
xp = [1986, 1986, 1990, 1990, 1995, 2020, 1970, 1970]
479+
else:
480+
xp = [1987, 1988, 1990, 1990, 1995, 2020, 2070, 2170]
481+
454482
for i, nyears in enumerate([5, 10, 19, 49, 99, 199, 599, 1001]):
455483
rng = period_range('1987', periods=nyears, freq='A')
456484
ser = Series(np.random.randn(len(rng)), rng)
@@ -470,7 +498,10 @@ def test_finder_minutely(self):
470498
ser.plot(ax=ax)
471499
xaxis = ax.get_xaxis()
472500
rs = xaxis.get_majorticklocs()[0]
473-
xp = Period('1/1/1999', freq='Min').ordinal
501+
if self.mpl_ge_2_0_0:
502+
xp = Period('1998-12-29 12:00', freq='Min').ordinal
503+
else:
504+
xp = Period('1/1/1999', freq='Min').ordinal
474505
assert rs == xp
475506

476507
def test_finder_hourly(self):
@@ -481,7 +512,10 @@ def test_finder_hourly(self):
481512
ser.plot(ax=ax)
482513
xaxis = ax.get_xaxis()
483514
rs = xaxis.get_majorticklocs()[0]
484-
xp = Period('1/1/1999', freq='H').ordinal
515+
if self.mpl_ge_2_0_0:
516+
xp = Period('1998-12-31 22:00', freq='H').ordinal
517+
else:
518+
xp = Period('1/1/1999', freq='H').ordinal
485519
assert rs == xp
486520

487521
@pytest.mark.slow
@@ -665,8 +699,8 @@ def test_mixed_freq_regular_first(self):
665699
assert idx2.equals(s2.index.to_period('B'))
666700
left, right = ax2.get_xlim()
667701
pidx = s1.index.to_period()
668-
assert left == pidx[0].ordinal
669-
assert right == pidx[-1].ordinal
702+
assert left <= pidx[0].ordinal
703+
assert right >= pidx[-1].ordinal
670704

671705
@pytest.mark.slow
672706
def test_mixed_freq_irregular_first(self):
@@ -696,8 +730,8 @@ def test_mixed_freq_regular_first_df(self):
696730
assert idx2.equals(s2.index.to_period('B'))
697731
left, right = ax2.get_xlim()
698732
pidx = s1.index.to_period()
699-
assert left == pidx[0].ordinal
700-
assert right == pidx[-1].ordinal
733+
assert left <= pidx[0].ordinal
734+
assert right >= pidx[-1].ordinal
701735

702736
@pytest.mark.slow
703737
def test_mixed_freq_irregular_first_df(self):
@@ -1211,8 +1245,8 @@ def test_irregular_ts_shared_ax_xlim(self):
12111245

12121246
# check that axis limits are correct
12131247
left, right = ax.get_xlim()
1214-
assert left == ts_irregular.index.min().toordinal()
1215-
assert right == ts_irregular.index.max().toordinal()
1248+
assert left <= ts_irregular.index.min().toordinal()
1249+
assert right >= ts_irregular.index.max().toordinal()
12161250

12171251
@pytest.mark.slow
12181252
def test_secondary_y_non_ts_xlim(self):
@@ -1228,7 +1262,7 @@ def test_secondary_y_non_ts_xlim(self):
12281262
s2.plot(secondary_y=True, ax=ax)
12291263
left_after, right_after = ax.get_xlim()
12301264

1231-
assert left_before == left_after
1265+
assert left_before >= left_after
12321266
assert right_before < right_after
12331267

12341268
@pytest.mark.slow
@@ -1245,7 +1279,7 @@ def test_secondary_y_regular_ts_xlim(self):
12451279
s2.plot(secondary_y=True, ax=ax)
12461280
left_after, right_after = ax.get_xlim()
12471281

1248-
assert left_before == left_after
1282+
assert left_before >= left_after
12491283
assert right_before < right_after
12501284

12511285
@pytest.mark.slow
@@ -1278,8 +1312,8 @@ def test_secondary_y_irregular_ts_xlim(self):
12781312
ts_irregular[:5].plot(ax=ax)
12791313

12801314
left, right = ax.get_xlim()
1281-
assert left == ts_irregular.index.min().toordinal()
1282-
assert right == ts_irregular.index.max().toordinal()
1315+
assert left <= ts_irregular.index.min().toordinal()
1316+
assert right >= ts_irregular.index.max().toordinal()
12831317

12841318
def test_plot_outofbounds_datetime(self):
12851319
# 2579 - checking this does not raise
@@ -1294,9 +1328,14 @@ def test_format_timedelta_ticks_narrow(self):
12941328
if is_platform_mac():
12951329
pytest.skip("skip on mac for precision display issue on older mpl")
12961330

1297-
expected_labels = [
1298-
'00:00:00.00000000{:d}'.format(i)
1299-
for i in range(10)]
1331+
if self.mpl_ge_2_0_0:
1332+
expected_labels = [''] + [
1333+
'00:00:00.00000000{:d}'.format(2 * i)
1334+
for i in range(5)] + ['']
1335+
else:
1336+
expected_labels = [
1337+
'00:00:00.00000000{:d}'.format(i)
1338+
for i in range(10)]
13001339

13011340
rng = timedelta_range('0', periods=10, freq='ns')
13021341
df = DataFrame(np.random.randn(len(rng), 3), rng)
@@ -1312,17 +1351,32 @@ def test_format_timedelta_ticks_wide(self):
13121351
if is_platform_mac():
13131352
pytest.skip("skip on mac for precision display issue on older mpl")
13141353

1315-
expected_labels = [
1316-
'00:00:00',
1317-
'1 days 03:46:40',
1318-
'2 days 07:33:20',
1319-
'3 days 11:20:00',
1320-
'4 days 15:06:40',
1321-
'5 days 18:53:20',
1322-
'6 days 22:40:00',
1323-
'8 days 02:26:40',
1324-
''
1325-
]
1354+
if self.mpl_ge_2_0_0:
1355+
expected_labels = [
1356+
'',
1357+
'00:00:00',
1358+
'1 days 03:46:40',
1359+
'2 days 07:33:20',
1360+
'3 days 11:20:00',
1361+
'4 days 15:06:40',
1362+
'5 days 18:53:20',
1363+
'6 days 22:40:00',
1364+
'8 days 02:26:40',
1365+
'9 days 06:13:20',
1366+
''
1367+
]
1368+
else:
1369+
expected_labels = [
1370+
'00:00:00',
1371+
'1 days 03:46:40',
1372+
'2 days 07:33:20',
1373+
'3 days 11:20:00',
1374+
'4 days 15:06:40',
1375+
'5 days 18:53:20',
1376+
'6 days 22:40:00',
1377+
'8 days 02:26:40',
1378+
''
1379+
]
13261380

13271381
rng = timedelta_range('0', periods=10, freq='1 d')
13281382
df = DataFrame(np.random.randn(len(rng), 3), rng)

pandas/tests/plotting/test_frame.py

+31-8
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,29 @@ def test_unsorted_index(self):
304304
rs = Series(rs[:, 1], rs[:, 0], dtype=np.int64, name='y')
305305
tm.assert_series_equal(rs, df.y)
306306

307+
def test_unsorted_index_lims(self):
308+
df = DataFrame({'y': [0., 1., 2., 3.]}, index=[1., 0., 3., 2.])
309+
ax = df.plot()
310+
xmin, xmax = ax.get_xlim()
311+
lines = ax.get_lines()
312+
assert xmin <= np.nanmin(lines[0].get_data()[0])
313+
assert xmax >= np.nanmax(lines[0].get_data()[0])
314+
315+
df = DataFrame({'y': [0., 1., np.nan, 3., 4., 5., 6.]},
316+
index=[1., 0., 3., 2., np.nan, 3., 2.])
317+
ax = df.plot()
318+
xmin, xmax = ax.get_xlim()
319+
lines = ax.get_lines()
320+
assert xmin <= np.nanmin(lines[0].get_data()[0])
321+
assert xmax >= np.nanmax(lines[0].get_data()[0])
322+
323+
df = DataFrame({'y': [0., 1., 2., 3.], 'z': [91., 90., 93., 92.]})
324+
ax = df.plot(x='z', y='y')
325+
xmin, xmax = ax.get_xlim()
326+
lines = ax.get_lines()
327+
assert xmin <= np.nanmin(lines[0].get_data()[0])
328+
assert xmax >= np.nanmax(lines[0].get_data()[0])
329+
307330
@pytest.mark.slow
308331
def test_subplots(self):
309332
df = DataFrame(np.random.rand(10, 3),
@@ -735,14 +758,14 @@ def test_line_lim(self):
735758
ax = df.plot()
736759
xmin, xmax = ax.get_xlim()
737760
lines = ax.get_lines()
738-
assert xmin == lines[0].get_data()[0][0]
739-
assert xmax == lines[0].get_data()[0][-1]
761+
assert xmin <= lines[0].get_data()[0][0]
762+
assert xmax >= lines[0].get_data()[0][-1]
740763

741764
ax = df.plot(secondary_y=True)
742765
xmin, xmax = ax.get_xlim()
743766
lines = ax.get_lines()
744-
assert xmin == lines[0].get_data()[0][0]
745-
assert xmax == lines[0].get_data()[0][-1]
767+
assert xmin <= lines[0].get_data()[0][0]
768+
assert xmax >= lines[0].get_data()[0][-1]
746769

747770
axes = df.plot(secondary_y=True, subplots=True)
748771
self._check_axes_shape(axes, axes_num=3, layout=(3, 1))
@@ -751,8 +774,8 @@ def test_line_lim(self):
751774
assert not hasattr(ax, 'right_ax')
752775
xmin, xmax = ax.get_xlim()
753776
lines = ax.get_lines()
754-
assert xmin == lines[0].get_data()[0][0]
755-
assert xmax == lines[0].get_data()[0][-1]
777+
assert xmin <= lines[0].get_data()[0][0]
778+
assert xmax >= lines[0].get_data()[0][-1]
756779

757780
def test_area_lim(self):
758781
df = DataFrame(rand(6, 4), columns=['x', 'y', 'z', 'four'])
@@ -763,8 +786,8 @@ def test_area_lim(self):
763786
xmin, xmax = ax.get_xlim()
764787
ymin, ymax = ax.get_ylim()
765788
lines = ax.get_lines()
766-
assert xmin == lines[0].get_data()[0][0]
767-
assert xmax == lines[0].get_data()[0][-1]
789+
assert xmin <= lines[0].get_data()[0][0]
790+
assert xmax >= lines[0].get_data()[0][-1]
768791
assert ymin == 0
769792

770793
ax = _check_plot_works(neg_df.plot.area, stacked=stacked)

0 commit comments

Comments
 (0)