Skip to content

Commit fb379d8

Browse files
Inconsistent indexes for tick label plotting (#28733)
* TST: Test for issues #26186 and #11465 * BUG: Generate the tick position in BarPlot using convert tools from matlab. Generate the tick position in BarPlot using convert tools from matlab. * TST: Modify tests/plotting/test_frame.test_bar_categorical Ticklocs are now float also for categorical bar data (as they are position on the axis). The test is changed to compare to a array of np.float. * TST: Fix test for windows OS * TST: Add test for plotting MultiIndex bar plot A fix to issue #26186 revealed no tests existed about plotting a bar plot for a MultiIndex, but a section of the user guide visualization did. This section of the user guide is now in the test suite. * BUG: Special case for MultiIndex bar plot * DOC: Add whatsnew entry for PR #28733 * CLN: Clean up in code and doc * CLN: Clean up test_bar_numeric * DOC Move to whatsnew v1.1 * FIX: Make tick dtype int for backwards compatibility * DOC: Improve whatsnew message * ENH: Add UserWarning when plotting bar plot with MultiIndex * CLN: Remove duplicate code line * TST: Capture UserWarning for Bar plot with MultiIndex * TST: Improve test explanation * ENH: Raise UserWarning only if redrawing on existing axis with data * DOC: Move to whatsnew v1.2.9 Co-authored-by: Marco Gorelli <[email protected]>
1 parent f823a85 commit fb379d8

File tree

3 files changed

+97
-4
lines changed

3 files changed

+97
-4
lines changed

doc/source/whatsnew/v1.2.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,7 @@ Plotting
668668

669669
- Bug in :meth:`DataFrame.plot` was rotating xticklabels when ``subplots=True``, even if the x-axis wasn't an irregular time series (:issue:`29460`)
670670
- Bug in :meth:`DataFrame.plot` where a marker letter in the ``style`` keyword sometimes causes a ``ValueError`` (:issue:`21003`)
671+
- Bug in :func:`DataFrame.plot.bar` and :func:`Series.plot.bar`. Ticks position were assigned by value order instead of using the actual value for numeric, or a smart ordering for string. (:issue:`26186` and :issue:`11465`)
671672
- Twinned axes were losing their tick labels which should only happen to all but the last row or column of 'externally' shared axes (:issue:`33819`)
672673
- Bug in :meth:`Series.plot` and :meth:`DataFrame.plot` was throwing :exc:`ValueError` with a :class:`Series` or :class:`DataFrame`
673674
indexed by a :class:`TimedeltaIndex` with a fixed frequency when x-axis lower limit was greater than upper limit (:issue:`37454`)

pandas/plotting/_matplotlib/core.py

+21-4
Original file line numberDiff line numberDiff line change
@@ -1359,7 +1359,6 @@ def __init__(self, data, **kwargs):
13591359
self.bar_width = kwargs.pop("width", 0.5)
13601360
pos = kwargs.pop("position", 0.5)
13611361
kwargs.setdefault("align", "center")
1362-
self.tick_pos = np.arange(len(data))
13631362

13641363
self.bottom = kwargs.pop("bottom", 0)
13651364
self.left = kwargs.pop("left", 0)
@@ -1382,7 +1381,16 @@ def __init__(self, data, **kwargs):
13821381
self.tickoffset = self.bar_width * pos
13831382
self.lim_offset = 0
13841383

1385-
self.ax_pos = self.tick_pos - self.tickoffset
1384+
if isinstance(self.data.index, ABCMultiIndex):
1385+
if kwargs["ax"] is not None and kwargs["ax"].has_data():
1386+
warnings.warn(
1387+
"Redrawing a bar plot with a MultiIndex is not supported "
1388+
+ "and may lead to inconsistent label positions.",
1389+
UserWarning,
1390+
)
1391+
self.ax_index = np.arange(len(data))
1392+
else:
1393+
self.ax_index = self.data.index
13861394

13871395
def _args_adjust(self):
13881396
if is_list_like(self.bottom):
@@ -1409,6 +1417,15 @@ def _make_plot(self):
14091417

14101418
for i, (label, y) in enumerate(self._iter_data(fillna=0)):
14111419
ax = self._get_ax(i)
1420+
1421+
if self.orientation == "vertical":
1422+
ax.xaxis.update_units(self.ax_index)
1423+
self.tick_pos = ax.convert_xunits(self.ax_index).astype(np.int)
1424+
elif self.orientation == "horizontal":
1425+
ax.yaxis.update_units(self.ax_index)
1426+
self.tick_pos = ax.convert_yunits(self.ax_index).astype(np.int)
1427+
self.ax_pos = self.tick_pos - self.tickoffset
1428+
14121429
kwds = self.kwds.copy()
14131430
if self._is_series:
14141431
kwds["color"] = colors
@@ -1480,8 +1497,8 @@ def _post_plot_logic(self, ax: "Axes", data):
14801497
str_index = [pprint_thing(key) for key in range(data.shape[0])]
14811498
name = self._get_index_name()
14821499

1483-
s_edge = self.ax_pos[0] - 0.25 + self.lim_offset
1484-
e_edge = self.ax_pos[-1] + 0.25 + self.bar_width + self.lim_offset
1500+
s_edge = self.ax_pos.min() - 0.25 + self.lim_offset
1501+
e_edge = self.ax_pos.max() + 0.25 + self.bar_width + self.lim_offset
14851502

14861503
self._decorate_ticks(ax, name, str_index, s_edge, e_edge)
14871504

pandas/tests/plotting/frame/test_frame.py

+75
Original file line numberDiff line numberDiff line change
@@ -2190,6 +2190,81 @@ def test_xlabel_ylabel_dataframe_plane_plot(self, kind, xlabel, ylabel):
21902190
assert ax.get_xlabel() == (xcol if xlabel is None else xlabel)
21912191
assert ax.get_ylabel() == (ycol if ylabel is None else ylabel)
21922192

2193+
@pytest.mark.slow
2194+
@pytest.mark.parametrize("method", ["bar", "barh"])
2195+
def test_bar_ticklabel_consistence(self, method):
2196+
# Draw two consecutiv bar plot with consistent ticklabels
2197+
# The labels positions should not move between two drawing on the same axis
2198+
# GH: 26186
2199+
def get_main_axis(ax):
2200+
if method == "barh":
2201+
return ax.yaxis
2202+
elif method == "bar":
2203+
return ax.xaxis
2204+
2205+
# Plot the first bar plot
2206+
data = {"A": 0, "B": 3, "C": -4}
2207+
df = DataFrame.from_dict(data, orient="index", columns=["Value"])
2208+
ax = getattr(df.plot, method)()
2209+
ax.get_figure().canvas.draw()
2210+
2211+
# Retrieve the label positions for the first drawing
2212+
xticklabels = [t.get_text() for t in get_main_axis(ax).get_ticklabels()]
2213+
label_positions_1 = dict(zip(xticklabels, get_main_axis(ax).get_ticklocs()))
2214+
2215+
# Modify the dataframe order and values and plot on same axis
2216+
df = df.sort_values("Value") * -2
2217+
ax = getattr(df.plot, method)(ax=ax, color="red")
2218+
ax.get_figure().canvas.draw()
2219+
2220+
# Retrieve the label positions for the second drawing
2221+
xticklabels = [t.get_text() for t in get_main_axis(ax).get_ticklabels()]
2222+
label_positions_2 = dict(zip(xticklabels, get_main_axis(ax).get_ticklocs()))
2223+
2224+
# Assert that the label positions did not change between the plotting
2225+
assert label_positions_1 == label_positions_2
2226+
2227+
def test_bar_numeric(self):
2228+
# Bar plot with numeric index have tick location values equal to index
2229+
# values
2230+
# GH: 11465
2231+
df = DataFrame(np.random.rand(10), index=np.arange(10, 20))
2232+
ax = df.plot.bar()
2233+
ticklocs = ax.xaxis.get_ticklocs()
2234+
expected = np.arange(10, 20, dtype=np.int64)
2235+
tm.assert_numpy_array_equal(ticklocs, expected)
2236+
2237+
def test_bar_multiindex(self):
2238+
# Test from pandas/doc/source/user_guide/visualization.rst
2239+
# at section Plotting With Error Bars
2240+
# Related to issue GH: 26186
2241+
2242+
ix3 = pd.MultiIndex.from_arrays(
2243+
[
2244+
["a", "a", "a", "a", "b", "b", "b", "b"],
2245+
["foo", "foo", "bar", "bar", "foo", "foo", "bar", "bar"],
2246+
],
2247+
names=["letter", "word"],
2248+
)
2249+
2250+
df3 = DataFrame(
2251+
{"data1": [3, 2, 4, 3, 2, 4, 3, 2], "data2": [6, 5, 7, 5, 4, 5, 6, 5]},
2252+
index=ix3,
2253+
)
2254+
2255+
# Group by index labels and take the means and standard deviations
2256+
# for each group
2257+
gp3 = df3.groupby(level=("letter", "word"))
2258+
means = gp3.mean()
2259+
errors = gp3.std()
2260+
2261+
# No assertion we just ensure that we can plot a MultiIndex bar plot
2262+
# and are getting a UserWarning if redrawing
2263+
with tm.assert_produces_warning(None):
2264+
ax = means.plot.bar(yerr=errors, capsize=4)
2265+
with tm.assert_produces_warning(UserWarning):
2266+
means.plot.bar(yerr=errors, capsize=4, ax=ax)
2267+
21932268

21942269
def _generate_4_axes_via_gridspec():
21952270
import matplotlib as mpl

0 commit comments

Comments
 (0)