Skip to content

Feat more advanced sharing of axes. #244

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

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ea6d34d
fix twin sharing
cvanelteren Jun 2, 2025
05e9f2c
Merge branch 'main' into fix-twinned
cvanelteren Jun 2, 2025
f7323dd
add typing to overrides
cvanelteren Jun 2, 2025
75f1955
move around share and remove forcing of ticks on draw
cvanelteren Jun 2, 2025
9e95103
add two unittests for visual fidelity
cvanelteren Jun 2, 2025
9a8600d
conditional import on override
cvanelteren Jun 2, 2025
5a8b936
refator test case
cvanelteren Jun 2, 2025
ce599bb
fix minor issue
cvanelteren Jun 2, 2025
49a7fc4
Merge branch 'main' into fix-twinned
beckermr Jun 3, 2025
ed44313
updated tests
cvanelteren Jun 3, 2025
fde835e
minor fixes
cvanelteren Jun 3, 2025
6b82dee
this may work
cvanelteren Jun 3, 2025
378324c
more fixes
cvanelteren Jun 3, 2025
49a073f
also include spanning for determining subplot borders
cvanelteren Jun 4, 2025
13d0f6d
also include spanning for determining subplot borders
cvanelteren Jun 4, 2025
1ab0325
stash
cvanelteren Jun 4, 2025
0a2a228
Merge branch 'main' into fix-twinned
cvanelteren Jun 12, 2025
94598f4
spelling
cvanelteren Jun 12, 2025
4629a3f
clean up logic for apply sharing and add label handler
cvanelteren Jun 12, 2025
8eb8540
set default for border_axes
cvanelteren Jun 12, 2025
8b42a6e
merge continue
cvanelteren Jun 12, 2025
4da6a73
add sharing tests
cvanelteren Jun 12, 2025
f2390b0
fix typo
cvanelteren Jun 12, 2025
069e730
rm debug
cvanelteren Jun 12, 2025
fc9652d
add missing param for unittest
cvanelteren Jun 12, 2025
29cf749
turn on axis sharing from figure control
cvanelteren Jun 12, 2025
53fe647
only share when we are actually sharing
cvanelteren Jun 12, 2025
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
19 changes: 18 additions & 1 deletion ultraplot/axes/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1589,7 +1589,7 @@ def shared(paxs):
return [pax for pax in paxs if not pax._panel_hidden and pax._panel_share]

# Internal axis sharing, share stacks of panels and main axes with each other
# NOTE: This is called on the main axes whenver a panel is created.
# NOTE: This is called on the main axes whenever a panel is created.
# NOTE: This block is why, even though we have figure-wide share[xy], we
# still need the axes-specific _share[xy]_override attribute.
if not self._panel_side: # this is a main axes
Expand Down Expand Up @@ -3197,6 +3197,23 @@ def _is_panel_group_member(self, other: "Axes") -> bool:
return True

# Not in the same panel group

def _is_ticklabel_on(self, side: str) -> bool:
"""
Check if tick labels are on for the specified sides.
"""
# NOTE: This is a helper function to check if tick labels are on
# for the specified sides. It returns True if any of the specified
# sides have tick labels turned on.
axis = self.xaxis
if side in ["labelleft", "labelright"]:
axis = self.yaxis
label = "label1"
if side in ["labelright", "labeltop"]:
label = "label2"
for tick in axis.get_major_ticks():
if getattr(tick, label).get_visible():
return True
return False

def _is_ticklabel_on(self, side: str) -> bool:
Expand Down
145 changes: 125 additions & 20 deletions ultraplot/axes/cartesian.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from ..internals import ic # noqa: F401
from ..internals import _not_none, _pop_rc, _version_mpl, docstring, labels, warnings
from . import plot, shared
import matplotlib.axis as maxis

__all__ = ["CartesianAxes"]

Expand Down Expand Up @@ -373,35 +374,139 @@ def _apply_axis_sharing(self):
Enforce the "shared" axis labels and axis tick labels. If this is not
called at drawtime, "shared" labels can be inadvertantly turned off.
"""
# X axis
# NOTE: Critical to apply labels to *shared* axes attributes rather
# than testing extents or we end up sharing labels with twin axes.
# NOTE: Similar to how _align_super_labels() calls _apply_title_above() this
# is called inside _align_axis_labels() so we align the correct text.
# NOTE: The "panel sharing group" refers to axes and panels *above* the
# bottommost or to the *right* of the leftmost panel. But the sharing level
# used for the leftmost and bottommost is the *figure* sharing level.
axis = self.xaxis
if self._sharex is not None and axis.get_visible():
level = 3 if self._panel_sharex_group else self.figure._sharex
if level > 0:
labels._transfer_label(axis.label, self._sharex.xaxis.label)
axis.label.set_visible(False)
if level > 2:
# WARNING: Cannot set NullFormatter because shared axes share the
# same Ticker(). Instead use approach copied from mpl subplots().
axis.set_tick_params(which="both", labelbottom=False, labeltop=False)
# Y axis
axis = self.yaxis
if self._sharey is not None and axis.get_visible():
level = 3 if self._panel_sharey_group else self.figure._sharey
if level > 0:
labels._transfer_label(axis.label, self._sharey.yaxis.label)
axis.label.set_visible(False)
if level > 2:
axis.set_tick_params(which="both", labelleft=False, labelright=False)

# Get border axes once for efficiency
border_axes = self.figure._get_border_axes()

# Apply X axis sharing
self._apply_axis_sharing_for_axis("x", border_axes)

# Apply Y axis sharing
self._apply_axis_sharing_for_axis("y", border_axes)

def _apply_axis_sharing_for_axis(self, axis_name, border_axes):
"""
Apply axis sharing for a specific axis (x or y).

Parameters
----------
axis_name : str
Either 'x' or 'y'
border_axes : dict
Dictionary from _get_border_axes() containing border information
"""
if axis_name == "x":
axis = self.xaxis
shared_axis = self._sharex
panel_group = self._panel_sharex_group
sharing_level = self.figure._sharex
label_params = ["labeltop", "labelbottom"]
border_sides = ["top", "bottom"]
else: # axis_name == 'y'
axis = self.yaxis
shared_axis = self._sharey
panel_group = self._panel_sharey_group
sharing_level = self.figure._sharey
label_params = ["labelleft", "labelright"]
border_sides = ["left", "right"]

if shared_axis is None or not axis.get_visible():
return

level = 3 if panel_group else sharing_level

# Handle axis label sharing (level > 0)
if level > 0:
shared_axis_obj = getattr(shared_axis, f"{axis_name}axis")
labels._transfer_label(axis.label, shared_axis_obj.label)
axis.label.set_visible(False)

# Handle tick label sharing (level > 2)
if level > 2:
label_visibility = self._determine_tick_label_visibility(
axis,
shared_axis,
axis_name,
label_params,
border_sides,
border_axes,
)
axis.set_tick_params(which="both", **label_visibility)

# Set minor formatter for last processed axis
axis.set_minor_formatter(mticker.NullFormatter())

def _determine_tick_label_visibility(
self,
axis: maxis.Axis,
shared_axis: maxis.Axis,
axis_name: str,
label_params: list,
border_sides: list,
border_axes: dict,
) -> dict:
"""
Determine which tick labels should be visible based on sharing rules and borders.

Parameters
----------
axis : matplotlib axis
The current axis object
shared_axis : Axes
The axes this one shares with
axis_name : str
Either 'x' or 'y'
label_params : list
List of label parameter names (e.g., ['labeltop', 'labelbottom'])
border_sides : list
List of border side names (e.g., ['top', 'bottom'])
border_axes : dict
Dictionary from _get_border_axes()

Returns
-------
dict
Dictionary of label visibility parameters
"""
ticks = axis.get_tick_params()
shared_axis_obj = getattr(shared_axis, f"{axis_name}axis")
sharing_ticks = shared_axis_obj.get_tick_params()

label_visibility = {}

for label_param, border_side in zip(label_params, border_sides):
# Check if user has explicitly set label location via format()
user_override = getattr(self, f"_user_{axis_name}ticklabelloc", None)

if self._panel_dict[border_side]:
label_visibility[label_param] = False
elif user_override is not None:
# Use user's explicit choice - handle different formats
side_name = border_side # 'top', 'bottom', 'left', 'right'
# Handle short forms: 't', 'b', 'l', 'r'
side_short = side_name[0] # 't', 'b', 'l', 'r'

label_visibility[label_param] = (
user_override == side_name
or user_override == side_short
or user_override == "both"
or user_override == "all"
)
else:
# Use automatic border detection logic
label_visibility[label_param] = (
ticks[label_param] or sharing_ticks[label_param]
) and self in border_axes.get(border_side, [])

return label_visibility

def _add_alt(self, sx, **kwargs):
"""
Add an alternate axes.
Expand Down
10 changes: 9 additions & 1 deletion ultraplot/axes/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
from ..utils import _fontsize_to_pt, _not_none, units
from ..axes import Axes

try:
# From python 3.12
from typing import override
except ImportError:
# From Python 3.5
from typing_extensions import override


class _SharedAxes(object):
"""
Expand Down Expand Up @@ -186,10 +193,11 @@ def _update_ticks(
for lab in obj.get_ticklabels():
lab.update(kwtext_extra)

# Override matplotlib defaults to handle multiple axis sharing
@override
def sharex(self, other):
return self._share_axis_with(other, which="x")

@override
def sharey(self, other):
self._share_axis_with(other, which="y")

Expand Down
Loading
Loading