Skip to content

feat: advanced axis sharing refactor + enhancements #256

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 77 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 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
5ceb08e
Merge branch 'main' into feat-advanced-sharing
cvanelteren Jun 12, 2025
d81a465
update type hinting
cvanelteren Jun 12, 2025
7f383b2
update type hinting
cvanelteren Jun 12, 2025
41ba8f1
update call count due to internal changes
cvanelteren Jun 12, 2025
79fb5a4
make crawler private
cvanelteren Jun 12, 2025
c465cff
update gridspec to retrieve grid position of main plots
cvanelteren Jun 12, 2025
3a6024b
update crawler to use grid coordinate
cvanelteren Jun 12, 2025
c06d603
simplify label logic
cvanelteren Jun 12, 2025
71909f7
minor refactor to improve readability
cvanelteren Jun 12, 2025
c48c966
Merge remote-tracking branch 'uplt' into feat-advanced-sharing
cvanelteren Jun 12, 2025
a94f22f
Merge branch 'main' into feat-advanced-sharing
beckermr Jun 12, 2025
c566e4b
update comment to reflect why it is happening
cvanelteren Jun 14, 2025
a9b9b08
update logic to include axis but ignore colorbars and update the
cvanelteren Jun 14, 2025
d8d45af
Merge branch 'main' into feat-advanced-sharing
cvanelteren Jun 14, 2025
9ac418d
Update return statements of tests to be compliant with pytest 8.4.0 (…
cvanelteren Jun 16, 2025
de262c4
looks good by eye
cvanelteren Jun 16, 2025
a3d76ea
Merge branch 'main' into feat-advanced-sharing
cvanelteren Jun 16, 2025
2571f17
add label parsing for python 3.9 and below
cvanelteren Jun 16, 2025
4e9bb26
correction
cvanelteren Jun 16, 2025
cb8d505
update logic to check for adjacent plots
cvanelteren Jun 16, 2025
be7d464
refactor logic
cvanelteren Jun 16, 2025
edbb7af
refactor test
cvanelteren Jun 16, 2025
d44e30e
rm junk and simplify logic
cvanelteren Jun 16, 2025
786842d
add panel logic
cvanelteren Jun 16, 2025
cc207b5
add comment and logic comment
cvanelteren Jun 16, 2025
6c1c0b6
rm unncessary draw
cvanelteren Jun 16, 2025
d89c744
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 16, 2025
f936a71
Merge branch 'main' into feat-advanced-sharing
beckermr Jun 16, 2025
eace593
update panel logic
cvanelteren Jun 16, 2025
c1c462e
restore behavior and add comment
cvanelteren Jun 16, 2025
2d27931
Update ultraplot/utils.py
cvanelteren Jun 16, 2025
800aead
Update ultraplot/axes/cartesian.py
cvanelteren Jun 16, 2025
ac08772
remove duplicate
cvanelteren Jun 16, 2025
1d04aee
restore removed return
cvanelteren Jun 16, 2025
08f9b98
Merge branch 'main' into feat-advanced-sharing
cvanelteren Jun 18, 2025
75ddfd3
tmp set debug to see if dirs are still in the PR
cvanelteren Jun 19, 2025
d49424e
resetting
cvanelteren Jun 19, 2025
623461d
Merge branch 'main' into feat-advanced-sharing
beckermr Jun 19, 2025
0ad8ff2
Merge branch 'main' into feat-advanced-sharing
cvanelteren Jun 19, 2025
5da2eab
Merge branch 'main' into feat-advanced-sharing
beckermr Jun 20, 2025
879471b
Merge branch 'main' into feat-advanced-sharing
beckermr Jun 20, 2025
8e43be3
Merge branch 'main' into feat-advanced-sharing
beckermr Jun 20, 2025
2a90800
add tick checking
cvanelteren Jun 24, 2025
08ed28a
rn test and add label checking
cvanelteren Jun 24, 2025
728ed9d
rm debug
cvanelteren Jun 24, 2025
c30953f
add asserts to altx and y
cvanelteren Jun 24, 2025
62e88fe
Merge branch 'main' into feat-advanced-sharing
beckermr Jun 25, 2025
4b0fa30
wrap _get_subplots_layout
cvanelteren Jun 25, 2025
a106cdc
wrap crawler docstring
cvanelteren Jun 25, 2025
ab00752
mv comment up
cvanelteren Jun 25, 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
3 changes: 1 addition & 2 deletions 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 @@ -3195,7 +3195,6 @@ def _is_panel_group_member(self, other: "Axes") -> bool:
and self._panel_parent is other._panel_parent
):
return True

# Not in the same panel group
return False

Expand Down
181 changes: 161 additions & 20 deletions ultraplot/axes/cartesian.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@
import matplotlib.ticker as mticker
import numpy as np

from packaging import version

from .. import constructor
from .. import scale as pscale
from .. import ticker as pticker
from ..config import rc
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 +376,173 @@ 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: str,
border_axes: dict[str, plot.PlotAxes],
) -> None:
"""
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)
# Turn minor ticks off
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[str],
border_sides: list[str],
border_axes: dict[str, list[plot.PlotAxes]],
) -> dict[str, bool]:
"""
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 = {}

def _convert_label_param(label_param: str) -> str:
# Deal with logic not being consistent
# in prior mpl versions
if version.parse(str(_version_mpl)) <= version.parse("3.9"):
if label_param == "labeltop" and axis_name == "x":
label_param = "labelright"
elif label_param == "labelbottom" and axis_name == "x":
label_param = "labelleft"
return label_param

for label_param, border_side in zip(label_params, border_sides):
# Check if user has explicitly set label location via format()
label_visibility[label_param] = False
has_panel = False
for panel in self._panel_dict[border_side]:
# Check if the panel is a colorbar
colorbars = [
values
for key, values in self._colorbar_dict.items()
if border_side in key # key is tuple (side, top | center | lower)
]
if not panel in colorbars:
# Skip colorbar as their
# yaxis is not shared
has_panel = True
break
# When we have a panel, let the panel have
# the labels and turn-off for this axis + side.
if has_panel:
continue
is_border = self in border_axes.get(border_side, [])
is_panel = (
self in shared_axis._panel_dict[border_side]
and self == shared_axis._panel_dict[border_side][-1]
)

# Use automatic border detection logic
# if we are a panel we "push" the labels outwards
if is_border or is_panel:
# Deal with mpl version for label_param
label_param = _convert_label_param(label_param)
is_this_tick_on = ticks[label_param]
is_parent_tick_on = sharing_ticks[label_param]
# Only turn on the labels for the current axis
# if the axis it is sharing with is a main
# and we are not panel
# For shared axes we turn them on if either or are on, but turn off the parent
if is_this_tick_on or is_parent_tick_on:
# Note: we set the current axis to visible
# as we are dealing with borders
# or panels
getattr(shared_axis, f"{axis_name}axis").set_tick_params(
**{label_param: False}
)
label_visibility[label_param] = True
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