Skip to content

Fix scatter histogram layouts built with inset axes - #363

Draft
FarhanAliRaza wants to merge 1 commit into
mainfrom
codex/fix-inset_axes-for-scatter-histogram-example
Draft

Fix scatter histogram layouts built with inset axes#363
FarhanAliRaza wants to merge 1 commit into
mainfrom
codex/fix-inset_axes-for-scatter-histogram-example

Conversation

@FarhanAliRaza

@FarhanAliRaza FarhanAliRaza commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Motivation

  • The Matplotlib "scatter + marginal histograms" pattern using Axes.inset_axes stacked inset artists into the parent axes and failed to render parent-relative marginal panels correctly (regression reported in scatter + histogram example using mpl insert_axes does not work #354).
  • Constrained/tight layout did not reserve space for insets that extend outside the parent axes, causing clipping/overlap when adding marginal axes.

Description

  • Change Axes.inset_axes to create a free-form figure panel by converting the parent-relative bounds to a figure rectangle and calling Figure.add_axes, rather than projecting inset artists into the parent data space, and preserve static sharex/sharey links via Figure._share_subplot_axes.
  • Record inset provenance on the new axes (_inset_parent, _inset_bounds, _inset_layout_base) and make Figure._axes_rect return parent-relative positions for inset axes so they follow aspect corrections and layout solves.
  • When Figure uses the tight/constrained engine, reserve space for inset families that extend outside the parent by fitting the inset family into the solved parent allocation so marginal panels are contained instead of clipped or stacked.
  • Add regression tests that exercise scatter + top/right histogram insets, verify sharex/sharey static sharing, assert marginal axes are independent panels and that constrained layout reserves room for out-of-bounds insets.
  • Update compatibility documentation to record the supported Axes.inset_axes semantics.

Testing

  • Ran uv run pytest tests/pyplot/test_frame_geometry.py -q and the new tests passed (24 passed).
  • Ran uv run pytest tests/pyplot/test_layout_text_parity_fixes.py tests/pyplot/test_gallery_layout_api_regressions.py -q (44 passed).
  • Ran uv run pytest tests/pyplot -q: 1,099 passed, 67 skipped, with one pre-existing performance guardrail failing (unrelated timing assertion in test_pyplot_build_tracks_declarative for 10k rows).
  • Ran style checks: uv run ruff check . and uv run ruff format --check . (passed); repository hooks via pre-commit could not be executed because the environment could not fetch pre-commit from PyPI.
  • Reproduced the gallery example by running a script that saves /tmp/scatter_hist_xy.png and visually inspected that the top and right histograms render as independent marginal panels rather than stacked onto the central scatter.

Summary by CodeRabbit

  • New Features

    • Inset axes now render as independent panels positioned relative to their parent axes.
    • Insets can extend beyond the parent panel while preserving draw order and requested axis sharing.
    • Constrained layouts now account for inset axes when fitting plot regions.
  • Bug Fixes

    • Corrected inset placement and prevented marginal artists from being projected onto the parent plot.
  • Documentation

    • Documented inset axes behavior and added a compatibility note for the updated behavior.

@FarhanAliRaza
FarhanAliRaza marked this pull request as draft July 28, 2026 15:30
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Independent inset axes

Layer / File(s) Summary
Inset creation and sharing
python/xy/pyplot/_axes.py
Axes.inset_axes creates independent figure axes from parent-relative bounds, records inset metadata, validates dimensions, and preserves requested axis sharing.
Parent-relative geometry and layout
python/xy/pyplot/_axes.py, python/xy/pyplot/_mplfig.py
Inset rectangles are resolved from the parent position, and tight constrained layout accounts for the inset family.
Compatibility and geometry validation
spec/matplotlib/compat.md, spec/matplotlib/compat-changelog.md, tests/pyplot/test_frame_geometry.py
Documentation and tests cover independent placement, sharing, composed plot rectangles, and constrained-layout positioning.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ParentAxes
  participant Figure
  participant InsetAxes
  Caller->>ParentAxes: call inset_axes(bounds, sharex, sharey)
  ParentAxes->>Figure: convert bounds and call add_axes()
  Figure-->>ParentAxes: return independent InsetAxes
  ParentAxes->>InsetAxes: store parent-relative metadata
  ParentAxes->>Figure: share requested axis properties
  Figure->>InsetAxes: resolve rectangle from parent position
Loading

Suggested reviewers: sselvakumaran, alek99

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main fix: scatter plots with marginal histograms using inset axes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-inset_axes-for-scatter-histogram-example

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 28, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 103 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing codex/fix-inset_axes-for-scatter-histogram-example (dd17330) with main (d35a1b2)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@python/xy/pyplot/_axes.py`:
- Around line 5249-5274: Update the tight-layout handling around the
inset-family logic to recompute fitting on every layout solve, not only when
_inset_layout_base is unset. Preserve the parent’s original solved allocation
during layout resolution before aspect adjustment, then use that allocation as
the base for _figure_rect and recalculate the family bounds and fitted rectangle
each solve. Avoid using get_position() as the stored base, preventing
aspect-adjusted geometry from being applied twice.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 869abe8c-368a-4828-a1f1-44961dbf5626

📥 Commits

Reviewing files that changed from the base of the PR and between d35a1b2 and dd17330.

📒 Files selected for processing (5)
  • python/xy/pyplot/_axes.py
  • python/xy/pyplot/_mplfig.py
  • spec/matplotlib/compat-changelog.md
  • spec/matplotlib/compat.md
  • tests/pyplot/test_frame_geometry.py

Comment thread python/xy/pyplot/_axes.py
Comment on lines +5249 to +5274
if self.figure._layout_options.get("engine") == "tight":
# Constrained layout must leave figure room for marginal axes
# whose parent-relative coordinates extend above/right (or below/
# left) of the main panel. Preserve the first solved parent box
# and fit the whole inset family inside that allocation.
if self._inset_layout_base is None:
self._inset_layout_base = self.get_position().bounds
family = [
child._inset_bounds
for child in self.figure.axes
if child._inset_parent is self and child._inset_bounds is not None
]
min_x = min([0.0, *(item[0] for item in family)])
min_y = min([0.0, *(item[1] for item in family)])
max_x = max([1.0, *(item[0] + item[2] for item in family)])
max_y = max([1.0, *(item[1] + item[3] for item in family)])
base_x, base_y, base_w, base_h = self._inset_layout_base
fitted_w = base_w / (max_x - min_x)
fitted_h = base_h / (max_y - min_y)
self._figure_rect = (
base_x - min_x * fitted_w,
base_y - min_y * fitted_h,
fitted_w,
fitted_h,
)
self._invalidate()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Recompute inset-family fitting as part of every tight-layout solve.

This runs only when an inset is created while tight layout is already active. Calling fig.tight_layout() after creating insets—or invalidating a constrained layout later—does not refit the family. Also, get_position() captures the aspect-adjusted active box, then assigning it to _figure_rect causes adjustable-box aspect handling to be applied again.

Store/recompute the parent’s original solved allocation during layout resolution, then derive the fitted parent rectangle from that allocation on every solve.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@python/xy/pyplot/_axes.py` around lines 5249 - 5274, Update the tight-layout
handling around the inset-family logic to recompute fitting on every layout
solve, not only when _inset_layout_base is unset. Preserve the parent’s original
solved allocation during layout resolution before aspect adjustment, then use
that allocation as the base for _figure_rect and recalculate the family bounds
and fitted rectangle each solve. Avoid using get_position() as the stored base,
preventing aspect-adjusted geometry from being applied twice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant