Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
54 changes: 54 additions & 0 deletions ultraplot/axes/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6191,6 +6191,32 @@ def _parse_box_violin(fillcolor, fillalpha, edgecolor, **kw):
edgecolor = edgecolor[0]
return fillcolor, fillalpha, edgecolor, kw

def _boxplot_has_shared_tick_axis(self, axis_name: str) -> bool:
"""
Return whether the boxplot tick axis is shared with sibling axes.
"""
shared = (
self.get_shared_x_axes() if axis_name == "x" else self.get_shared_y_axes()
)
return len(shared.get_siblings(self)) > 1

def _apply_boxplot_tick_manager(
self,
axis_name: str,
positions: Iterable[Any],
tick_labels: Optional[Iterable[Any]] = None,
) -> None:
"""
Apply fixed tick locations/labels without appending duplicates on shared axes.
"""
axis = self._axis_map[axis_name]
locator_positions = np.asarray(axis.convert_units(positions))
label_values = positions if tick_labels is None else tick_labels
axis.set_major_locator(mticker.FixedLocator(locator_positions))
axis.set_major_formatter(
mticker.FixedFormatter([str(label) for label in label_values])
)

def _apply_boxplot(
self,
x,
Expand Down Expand Up @@ -6255,6 +6281,27 @@ def _apply_boxplot(

# Plot boxes
kw.setdefault("positions", x)
tick_labels = kw.get("tick_labels", kw.get("labels"))
manage_ticks = kw.pop("manage_ticks", True)
axis_name = "x" if vert else "y"
# Matplotlib's boxplot tick manager appends onto an existing
# FixedLocator/FixedFormatter pair. UltraPlot's stronger sharex/sharey
# modes currently share ticker state across sibling axes, so repeated
# boxplot calls in one shared group can duplicate tick labels.
#
# For now, avoid the native tick-manager path only for shared axes and
# install the intended fixed ticks ourselves. This keeps the fix narrow
# to the reported regression instead of changing global axis-sharing
# behavior in a bugfix PR.
#
# TODO: Revisit the shared ticker design more broadly. A deeper fix may
# be to stop aliasing ticker containers across shared axes, or to make
# the statistical-plot tick management path deduplicate shared fixed
# locators/formatters after the native call.
native_manage_ticks = manage_ticks and not self._boxplot_has_shared_tick_axis(
axis_name
)
kw["manage_ticks"] = native_manage_ticks
if means:
kw["showmeans"] = kw["meanline"] = True
y = inputs._dist_clean(y)
Expand All @@ -6279,6 +6326,13 @@ def _apply_boxplot(
# Use vert parameter
artists = self._call_native("boxplot", y, vert=vert, **kw)

if manage_ticks and not native_manage_ticks:
self._apply_boxplot_tick_manager(
axis_name,
kw["positions"],
tick_labels=tick_labels,
)

artists = artists or {} # necessary?
artists = {
key: cbook.silent_list(type(objs[0]).__name__, objs) if objs else objs
Expand Down
21 changes: 21 additions & 0 deletions ultraplot/tests/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,27 @@ def test_boxplot_mpl_versions(
assert "orientation" not in kwargs


def test_boxplot_shared_x_axes_do_not_duplicate_tick_labels():
data = [np.random.random(size=j * 50) for j in range(1, 11)]
fig, axs = uplt.subplots(
nrows=2,
ncols=2,
sharex=2,
sharey=False,
xrotation=45,
xminorlocator="null",
grid=False,
)
for ax in axs:
ax.boxplot(data, showfliers=False, lw=0.5)

expected = [str(i) for i in range(len(data))]
for ax in axs:
labels = [tick.get_text() for tick in ax.xaxis.get_ticklabels()]
assert labels == expected
uplt.close(fig)


def test_quiver_discrete_colors(rng):
"""
Edge case where colors are discrete for quiver plots
Expand Down
Loading