Skip to content

Commit 39699eb

Browse files
fangchenliclaude
andcommitted
ENH: Apply TimeSeries_DateFormatter to bar plots with datetime indices (GH#1918)
Bar plots now use TimeSeries_DateFormatter for datetime and period indices, matching the formatting behavior of line plots. This provides consistent date formatting across different plot types. Changes: - Modified BarPlot._post_plot_logic() to detect time series indices - Convert DatetimeIndex to PeriodIndex for consistent formatting - Apply format_dateaxis() for proper date label formatting - Added test to verify bar plots use TimeSeries_DateFormatter - Updated secondary legend tests to use 'D' frequency instead of deprecated 'B' 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent efe1a5c commit 39699eb

File tree

3 files changed

+47
-12
lines changed

3 files changed

+47
-12
lines changed

doc/source/whatsnew/v3.0.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ Other enhancements
207207
- :meth:`.DataFrameGroupBy.transform`, :meth:`.SeriesGroupBy.transform`, :meth:`.DataFrameGroupBy.agg`, :meth:`.SeriesGroupBy.agg`, :meth:`.SeriesGroupBy.apply`, :meth:`.DataFrameGroupBy.apply` now support ``kurt`` (:issue:`40139`)
208208
- :meth:`DataFrame.apply` supports using third-party execution engines like the Bodo.ai JIT compiler (:issue:`60668`)
209209
- :meth:`DataFrame.iloc` and :meth:`Series.iloc` now support boolean masks in ``__getitem__`` for more consistent indexing behavior (:issue:`60994`)
210+
- :meth:`DataFrame.plot.bar` and :meth:`Series.plot.bar` now use ``TimeSeries_DateFormatter`` for datetime and period indices, matching the formatting behavior of line plots (:issue:`1918`)
210211
- :meth:`DataFrame.to_csv` and :meth:`Series.to_csv` now support Python's new-style format strings (e.g., ``"{:.6f}"``) for the ``float_format`` parameter, in addition to old-style ``%`` format strings and callables. This allows for more flexible and modern formatting of floating point numbers when exporting to CSV. (:issue:`49580`)
211212
- :meth:`DataFrameGroupBy.transform`, :meth:`SeriesGroupBy.transform`, :meth:`DataFrameGroupBy.agg`, :meth:`SeriesGroupBy.agg`, :meth:`RollingGroupby.apply`, :meth:`ExpandingGroupby.apply`, :meth:`Rolling.apply`, :meth:`Expanding.apply`, :meth:`DataFrame.apply` with ``engine="numba"`` now supports positional arguments passed as kwargs (:issue:`58995`)
212213
- :meth:`Rolling.agg`, :meth:`Expanding.agg` and :meth:`ExponentialMovingWindow.agg` now accept :class:`NamedAgg` aggregations through ``**kwargs`` (:issue:`28333`)

pandas/plotting/_matplotlib/core.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
from pandas.plotting._matplotlib.misc import unpack_single_str_list
6767
from pandas.plotting._matplotlib.style import get_standard_colors
6868
from pandas.plotting._matplotlib.timeseries import (
69+
decorate_axes,
6970
format_dateaxis,
7071
maybe_convert_index,
7172
prepare_ts_data,
@@ -2041,15 +2042,37 @@ def _make_plot(self, fig: Figure) -> None:
20412042
self._append_legend_handles_labels(rect, label)
20422043

20432044
def _post_plot_logic(self, ax: Axes, data) -> None:
2044-
if self.use_index:
2045-
str_index = [pprint_thing(key) for key in data.index]
2046-
else:
2047-
str_index = [pprint_thing(key) for key in range(data.shape[0])]
2048-
20492045
s_edge = self.ax_pos[0] - 0.25 + self.lim_offset
20502046
e_edge = self.ax_pos[-1] + 0.25 + self.bar_width + self.lim_offset
20512047

2052-
self._decorate_ticks(ax, self._get_index_name(), str_index, s_edge, e_edge)
2048+
# GH#1918: Use date formatter for time series indices
2049+
if self._is_ts_plot():
2050+
ax.set_xlim((s_edge, e_edge))
2051+
2052+
if self.xticks is not None:
2053+
ax.set_xticks(np.array(self.xticks))
2054+
else:
2055+
ax.set_xticks(self.tick_pos)
2056+
2057+
if self._get_index_name() is not None and self.use_index:
2058+
ax.set_xlabel(self._get_index_name())
2059+
2060+
freq = data.index.freq
2061+
decorate_axes(ax, freq)
2062+
2063+
index = data.index
2064+
if isinstance(index, ABCDatetimeIndex):
2065+
index = index.to_period(freq=freq)
2066+
2067+
if isinstance(index, (ABCPeriodIndex,)):
2068+
format_dateaxis(ax, freq, index)
2069+
else:
2070+
if self.use_index:
2071+
str_index = [pprint_thing(key) for key in data.index]
2072+
else:
2073+
str_index = [pprint_thing(key) for key in range(data.shape[0])]
2074+
2075+
self._decorate_ticks(ax, self._get_index_name(), str_index, s_edge, e_edge)
20532076

20542077
def _decorate_ticks(
20552078
self,

pandas/tests/plotting/test_datetimelike.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,7 @@ def test_secondary_legend(self):
12641264
df = DataFrame(
12651265
np.random.default_rng(2).standard_normal((10, 4)),
12661266
columns=Index(list("ABCD"), dtype=object),
1267-
index=date_range("2000-01-01", periods=10, freq="B"),
1267+
index=date_range("2000-01-01", periods=10, freq="D"),
12681268
)
12691269
df.plot(secondary_y=["A", "B"], ax=ax)
12701270
leg = ax.get_legend()
@@ -1285,7 +1285,7 @@ def test_secondary_legend_right(self):
12851285
df = DataFrame(
12861286
np.random.default_rng(2).standard_normal((10, 4)),
12871287
columns=Index(list("ABCD"), dtype=object),
1288-
index=date_range("2000-01-01", periods=10, freq="B"),
1288+
index=date_range("2000-01-01", periods=10, freq="D"),
12891289
)
12901290
fig = mpl.pyplot.figure()
12911291
ax = fig.add_subplot(211)
@@ -1301,7 +1301,7 @@ def test_secondary_legend_bar(self):
13011301
df = DataFrame(
13021302
np.random.default_rng(2).standard_normal((10, 4)),
13031303
columns=Index(list("ABCD"), dtype=object),
1304-
index=date_range("2000-01-01", periods=10, freq="B"),
1304+
index=date_range("2000-01-01", periods=10, freq="D"),
13051305
)
13061306
fig, ax = mpl.pyplot.subplots()
13071307
df.plot(kind="bar", secondary_y=["A"], ax=ax)
@@ -1313,7 +1313,7 @@ def test_secondary_legend_bar_right(self):
13131313
df = DataFrame(
13141314
np.random.default_rng(2).standard_normal((10, 4)),
13151315
columns=Index(list("ABCD"), dtype=object),
1316-
index=date_range("2000-01-01", periods=10, freq="B"),
1316+
index=date_range("2000-01-01", periods=10, freq="D"),
13171317
)
13181318
fig, ax = mpl.pyplot.subplots()
13191319
df.plot(kind="bar", secondary_y=["A"], mark_right=False, ax=ax)
@@ -1325,14 +1325,14 @@ def test_secondary_legend_multi_col(self):
13251325
df = DataFrame(
13261326
np.random.default_rng(2).standard_normal((10, 4)),
13271327
columns=Index(list("ABCD"), dtype=object),
1328-
index=date_range("2000-01-01", periods=10, freq="B"),
1328+
index=date_range("2000-01-01", periods=10, freq="D"),
13291329
)
13301330
fig = mpl.pyplot.figure()
13311331
ax = fig.add_subplot(211)
13321332
df = DataFrame(
13331333
np.random.default_rng(2).standard_normal((10, 4)),
13341334
columns=Index(list("ABCD"), dtype=object),
1335-
index=date_range("2000-01-01", periods=10, freq="B"),
1335+
index=date_range("2000-01-01", periods=10, freq="D"),
13361336
)
13371337
ax = df.plot(secondary_y=["C", "D"], ax=ax)
13381338
leg = ax.get_legend()
@@ -1691,6 +1691,17 @@ def test_pickle_fig(self, temp_file, frame_or_series, idx):
16911691
with temp_file.open(mode="wb") as path:
16921692
pickle.dump(fig, path)
16931693

1694+
def test_bar_plot_with_datetime_index_uses_date_formatter(self):
1695+
# GH#1918 - bar plots should use DateFormatter for datetime indices
1696+
df = DataFrame(
1697+
np.random.default_rng(2).standard_normal((10, 2)),
1698+
index=date_range("2020-01-01", periods=10),
1699+
columns=["A", "B"],
1700+
)
1701+
ax_bar = df.plot(kind="bar")
1702+
bar_formatter = ax_bar.get_xaxis().get_major_formatter()
1703+
assert isinstance(bar_formatter, conv.TimeSeries_DateFormatter)
1704+
16941705

16951706
def _check_plot_works(f, freq=None, series=None, *args, **kwargs):
16961707
fig = plt.gcf()

0 commit comments

Comments
 (0)