Skip to content

Commit 5f354ca

Browse files
BUG: Handle overlapping line and bar on the same plot (#61173)
* Handle line and box on the same plot * Handle line and box on the same plot * Refactor PeriodConverter to separate frequency handling. Introduce `convert_from_freq` method and streamline the conversion process by passing `freq` directly instead of using `axis.freq`. This improves modularity and ensures clearer separation of concerns for frequency handling in Period conversion. * Refactor time series plotting logic in Matplotlib backend. Simplified `tick_pos` calculation by reusing helper methods and added a decorator to register pandas Matplotlib converters in the `_plot` method. This improves clarity and ensures proper integration with the pandas Matplotlib ecosystem. * Add test for bar and line plot superposition with same x values This test ensures that bar and line plots with identical x values are correctly superposed on the same axes. It verifies that the x-tick positions remain consistent across plot types. * Fix alignment issue in Series.plot with line and bar. Resolved a bug that prevented a line and bar from aligning on the same plot in `Series.plot`. This addresses issue #61161 and improves plot consistency when combining these chart types. * Refactor time series handling in matplotlib plotting. Move `x_compat` logic and time series helper methods from `LinePlot` to `MPLPlot` for better reusability and maintainability. This simplifies the `LinePlot` class and centralizes common functionality. * Update doc/source/whatsnew/v3.0.0.rst * Apply suggestions from code review * Fix bar and line plot alignment and x-axis visibility in tests Ensure bar and line plots share consistent x-axis tick labels and verify that x-axis limits are adjusted to make all plotted elements visible in `test_bar_line_plot` test. These changes improve the robustness of visual alignment and boundary checks. * Ensure bar_xticks length matches index in test_series.py Add an assertion to verify the length of `bar_xticks` aligns with the length of the index. This improves the test's robustness by ensuring the data and ticks remain consistent.
1 parent 5fef979 commit 5f354ca

File tree

4 files changed

+54
-12
lines changed

4 files changed

+54
-12
lines changed

Diff for: doc/source/whatsnew/v3.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,7 @@ Plotting
769769
- Bug in :meth:`DataFrame.plot.bar` with ``stacked=True`` where labels on stacked bars with zero-height segments were incorrectly positioned at the base instead of the label position of the previous segment (:issue:`59429`)
770770
- Bug in :meth:`DataFrame.plot.line` raising ``ValueError`` when set both color and a ``dict`` style (:issue:`59461`)
771771
- Bug in :meth:`DataFrame.plot` that causes a shift to the right when the frequency multiplier is greater than one. (:issue:`57587`)
772+
- Bug in :meth:`Series.plot` preventing a line and bar from being aligned on the same plot (:issue:`61161`)
772773
- Bug in :meth:`Series.plot` preventing a line and scatter plot from being aligned (:issue:`61005`)
773774
- Bug in :meth:`Series.plot` with ``kind="pie"`` with :class:`ArrowDtype` (:issue:`59192`)
774775

Diff for: pandas/plotting/_matplotlib/converter.py

+14-10
Original file line numberDiff line numberDiff line change
@@ -225,16 +225,20 @@ def __call__(self, x, pos: int | None = 0) -> str:
225225
class PeriodConverter(mdates.DateConverter):
226226
@staticmethod
227227
def convert(values, units, axis):
228+
if not hasattr(axis, "freq"):
229+
raise TypeError("Axis must have `freq` set to convert to Periods")
230+
return PeriodConverter.convert_from_freq(values, axis.freq)
231+
232+
@staticmethod
233+
def convert_from_freq(values, freq):
228234
if is_nested_list_like(values):
229-
values = [PeriodConverter._convert_1d(v, units, axis) for v in values]
235+
values = [PeriodConverter._convert_1d(v, freq) for v in values]
230236
else:
231-
values = PeriodConverter._convert_1d(values, units, axis)
237+
values = PeriodConverter._convert_1d(values, freq)
232238
return values
233239

234240
@staticmethod
235-
def _convert_1d(values, units, axis):
236-
if not hasattr(axis, "freq"):
237-
raise TypeError("Axis must have `freq` set to convert to Periods")
241+
def _convert_1d(values, freq):
238242
valid_types = (str, datetime, Period, pydt.date, pydt.time, np.datetime64)
239243
with warnings.catch_warnings():
240244
warnings.filterwarnings(
@@ -248,17 +252,17 @@ def _convert_1d(values, units, axis):
248252
or is_integer(values)
249253
or is_float(values)
250254
):
251-
return get_datevalue(values, axis.freq)
255+
return get_datevalue(values, freq)
252256
elif isinstance(values, PeriodIndex):
253-
return values.asfreq(axis.freq).asi8
257+
return values.asfreq(freq).asi8
254258
elif isinstance(values, Index):
255-
return values.map(lambda x: get_datevalue(x, axis.freq))
259+
return values.map(lambda x: get_datevalue(x, freq))
256260
elif lib.infer_dtype(values, skipna=False) == "period":
257261
# https://github.com/pandas-dev/pandas/issues/24304
258262
# convert ndarray[period] -> PeriodIndex
259-
return PeriodIndex(values, freq=axis.freq).asi8
263+
return PeriodIndex(values, freq=freq).asi8
260264
elif isinstance(values, (list, tuple, np.ndarray, Index)):
261-
return [get_datevalue(x, axis.freq) for x in values]
265+
return [get_datevalue(x, freq) for x in values]
262266
return values
263267

264268

Diff for: pandas/plotting/_matplotlib/core.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,10 @@
5959

6060
from pandas.io.formats.printing import pprint_thing
6161
from pandas.plotting._matplotlib import tools
62-
from pandas.plotting._matplotlib.converter import register_pandas_matplotlib_converters
62+
from pandas.plotting._matplotlib.converter import (
63+
PeriodConverter,
64+
register_pandas_matplotlib_converters,
65+
)
6366
from pandas.plotting._matplotlib.groupby import reconstruct_data_with_by
6467
from pandas.plotting._matplotlib.misc import unpack_single_str_list
6568
from pandas.plotting._matplotlib.style import get_standard_colors
@@ -1858,7 +1861,6 @@ def __init__(
18581861
self.bar_width = width
18591862
self._align = align
18601863
self._position = position
1861-
self.tick_pos = np.arange(len(data))
18621864

18631865
if is_list_like(bottom):
18641866
bottom = np.array(bottom)
@@ -1871,6 +1873,16 @@ def __init__(
18711873

18721874
MPLPlot.__init__(self, data, **kwargs)
18731875

1876+
if self._is_ts_plot():
1877+
self.tick_pos = np.array(
1878+
PeriodConverter.convert_from_freq(
1879+
self._get_xticks(),
1880+
data.index.freq,
1881+
)
1882+
)
1883+
else:
1884+
self.tick_pos = np.arange(len(data))
1885+
18741886
@cache_readonly
18751887
def ax_pos(self) -> np.ndarray:
18761888
return self.tick_pos - self.tickoffset
@@ -1900,6 +1912,7 @@ def lim_offset(self):
19001912

19011913
# error: Signature of "_plot" incompatible with supertype "MPLPlot"
19021914
@classmethod
1915+
@register_pandas_matplotlib_converters
19031916
def _plot( # type: ignore[override]
19041917
cls,
19051918
ax: Axes,

Diff for: pandas/tests/plotting/test_series.py

+24
Original file line numberDiff line numberDiff line change
@@ -971,3 +971,27 @@ def test_secondary_y_subplot_axis_labels(self):
971971
s1.plot(ax=ax2)
972972
assert len(ax.xaxis.get_minor_ticks()) == 0
973973
assert len(ax.get_xticklabels()) > 0
974+
975+
def test_bar_line_plot(self):
976+
"""
977+
Test that bar and line plots with the same x values are superposed
978+
and that the x limits are set such that the plots are visible.
979+
"""
980+
# GH61161
981+
index = period_range("2023", periods=3, freq="Y")
982+
years = set(index.year.astype(str))
983+
s = Series([1, 2, 3], index=index)
984+
ax = plt.subplot()
985+
s.plot(kind="bar", ax=ax)
986+
bar_xticks = [
987+
label for label in ax.get_xticklabels() if label.get_text() in years
988+
]
989+
s.plot(kind="line", ax=ax, color="r")
990+
line_xticks = [
991+
label for label in ax.get_xticklabels() if label.get_text() in years
992+
]
993+
assert len(bar_xticks) == len(index)
994+
assert bar_xticks == line_xticks
995+
x_limits = ax.get_xlim()
996+
assert x_limits[0] <= bar_xticks[0].get_position()[0]
997+
assert x_limits[1] >= bar_xticks[-1].get_position()[0]

0 commit comments

Comments
 (0)