Skip to content

BUG: Handle overlapping line and bar on the same plot #61173

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

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions doc/source/whatsnew/v3.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -762,8 +762,9 @@ Plotting
- Bug in :meth:`.DataFrameGroupBy.boxplot` failed when there were multiple groupings (:issue:`14701`)
- 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`)
- Bug in :meth:`DataFrame.plot.line` raising ``ValueError`` when set both color and a ``dict`` style (:issue:`59461`)
- Bug in :meth:`DataFrame.plot` that causes a shift to the right when the frequency multiplier is greater than one. (:issue:`57587`)
- Bug in :meth:`Series.plot` preventing a line and scatter plot from being aligned (:issue:`61005`)
- Bug in :meth:`DataFrame.plot` that causes a shift to the right when the frequency multiplier is greater than one. (:issue:`57587`
- Bug in :meth:`Series.plot` preventing a line and bar from being aligned on the same plot (:issue:`611
- Bug in :meth:`Series.plot` preventing a line and scatter plot from being aligned (:issue:`6100
- Bug in :meth:`Series.plot` with ``kind="pie"`` with :class:`ArrowDtype` (:issue:`59192`)

Groupby/resample/rolling
Expand Down
24 changes: 14 additions & 10 deletions pandas/plotting/_matplotlib/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,16 +225,20 @@ def __call__(self, x, pos: int | None = 0) -> str:
class PeriodConverter(mdates.DateConverter):
@staticmethod
def convert(values, units, axis):
if not hasattr(axis, "freq"):
raise TypeError("Axis must have `freq` set to convert to Periods")
return PeriodConverter.convert_from_freq(values, axis.freq)

@staticmethod
def convert_from_freq(values, freq):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

convert only uses the freq attribute of axis, so one should allow the user to pass freq without axis object.

if is_nested_list_like(values):
values = [PeriodConverter._convert_1d(v, units, axis) for v in values]
values = [PeriodConverter._convert_1d(v, freq) for v in values]
else:
values = PeriodConverter._convert_1d(values, units, axis)
values = PeriodConverter._convert_1d(values, freq)
return values

@staticmethod
def _convert_1d(values, units, axis):
if not hasattr(axis, "freq"):
raise TypeError("Axis must have `freq` set to convert to Periods")
def _convert_1d(values, freq):
valid_types = (str, datetime, Period, pydt.date, pydt.time, np.datetime64)
with warnings.catch_warnings():
warnings.filterwarnings(
Expand All @@ -248,17 +252,17 @@ def _convert_1d(values, units, axis):
or is_integer(values)
or is_float(values)
):
return get_datevalue(values, axis.freq)
return get_datevalue(values, freq)
elif isinstance(values, PeriodIndex):
return values.asfreq(axis.freq).asi8
return values.asfreq(freq).asi8
elif isinstance(values, Index):
return values.map(lambda x: get_datevalue(x, axis.freq))
return values.map(lambda x: get_datevalue(x, freq))
elif lib.infer_dtype(values, skipna=False) == "period":
# https://github.com/pandas-dev/pandas/issues/24304
# convert ndarray[period] -> PeriodIndex
return PeriodIndex(values, freq=axis.freq).asi8
return PeriodIndex(values, freq=freq).asi8
elif isinstance(values, (list, tuple, np.ndarray, Index)):
return [get_datevalue(x, axis.freq) for x in values]
return [get_datevalue(x, freq) for x in values]
return values


Expand Down
17 changes: 15 additions & 2 deletions pandas/plotting/_matplotlib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@

from pandas.io.formats.printing import pprint_thing
from pandas.plotting._matplotlib import tools
from pandas.plotting._matplotlib.converter import register_pandas_matplotlib_converters
from pandas.plotting._matplotlib.converter import (
PeriodConverter,
register_pandas_matplotlib_converters,
)
from pandas.plotting._matplotlib.groupby import reconstruct_data_with_by
from pandas.plotting._matplotlib.misc import unpack_single_str_list
from pandas.plotting._matplotlib.style import get_standard_colors
Expand Down Expand Up @@ -1858,7 +1861,6 @@ def __init__(
self.bar_width = width
self._align = align
self._position = position
self.tick_pos = np.arange(len(data))

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

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

if self._is_ts_plot():
self.tick_pos = np.array(
PeriodConverter.convert_from_freq(
self._get_xticks(),
data.index.freq,
)
)
else:
self.tick_pos = np.arange(len(data))

@cache_readonly
def ax_pos(self) -> np.ndarray:
return self.tick_pos - self.tickoffset
Expand Down Expand Up @@ -1900,6 +1912,7 @@ def lim_offset(self):

# error: Signature of "_plot" incompatible with supertype "MPLPlot"
@classmethod
@register_pandas_matplotlib_converters
def _plot( # type: ignore[override]
cls,
ax: Axes,
Expand Down
14 changes: 14 additions & 0 deletions pandas/tests/plotting/test_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,3 +971,17 @@ def test_secondary_y_subplot_axis_labels(self):
s1.plot(ax=ax2)
assert len(ax.xaxis.get_minor_ticks()) == 0
assert len(ax.get_xticklabels()) > 0

def test_bar_line_plot(self):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fails on main

"""
Test that bar and line plots with the same x values are superposed
"""
# GH61161
index = period_range("2023", periods=3, freq="Y")
s = Series([1, 2, 3], index=index)
ax = plt.subplot()
s.plot(kind="bar", ax=ax)
bar_xticks = ax.get_xticks().tolist()
s.plot(kind="line", ax=ax, color="r")
line_xticks = ax.get_xticks()[: len(s)].tolist()
Copy link
Member

Choose a reason for hiding this comment

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

I'm seeing ax.get_xticks() here have the same length as s; why have [: len(s)] here?

Copy link
Contributor Author

@MartinBraquet MartinBraquet Apr 14, 2025

Choose a reason for hiding this comment

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

After plotting the line, the ticks change on my machine:

ax.get_xticks()
Out[1]: array([53, 54, 55, 56])
ax.get_xticklabels()
Out[2]: 
[Text(53, 0, '2023'),
 Text(54, 0, '2024'),
 Text(55, 0, '2025'),
 Text(56, 0, '2026')]

A fourth tick is added by matplotlib for '2026'.

Even for integer indices, matplotlib does not do a 1-to-1 mapping between each x-tick and data point, as attested below:

ax = plt.subplot()
plt.plot(s.values, [1, 2,3])
ax.get_xticks()
array([0.75, 1.  , 1.25, 1.5 , 1.75, 2.  , 2.25, 2.5 , 2.75, 3.  , 3.25])

Not sure if you can reproduce those results on your machine, but I think that the exact number of ticks is irrelevant. I would suggest that this test checks that both x-tick labels (after bar plot and after line plot) include at least the 3 three ticks in the index (2023, 2024, 2025) and that the associated x-tick positions are the same for both plots. This would ensure that the two plots are superposed.
Additionally, the test can check that x-lim includes those three x-tick positions, which would ensure that the current plot view render the two plots.

I added a commit to reflect what I said above. Happy to discuss or update the code further if there is a need.

assert line_xticks == bar_xticks
Loading