Python: Respect figure sizing options in notebooks and Quarto inline output - #15234
Conversation
also narrow the figsize passed to Figure for pyright on the 3.9 pins, and refresh the sizing-precedence and show() docs
the two module-level globals become instance state on a PositronBackendRegistry singleton, mirroring matplotlib's own backend_registry. __init__.py is now docs-only; every import site points at the new modules. also splits test_switching.py into test_backend.py + test_enable_matplotlib.py and renames 'flavor' to 'backend' throughout.
the post-execute hook now skips figures a cell already displayed, instead of showing every figure again
|
E2E Tests 🚀 Why these tags?
More on automatic tags from changed files. |
isabelizimm
left a comment
There was a problem hiding this comment.
I was trying out with this code to make some more complex plots, and I can't quite seem to get the fig-height hash pipes to work 🤔 are you seeing the same behavior?
```{python}
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Load the dataset
raw_data = "https://raw.githubusercontent.com/rfordatascience/tidytuesday/refs/heads/main/data/2023/2023-05-23/squirrel_data.csv"
df = pd.read_csv(raw_data)
categorical_cols = ["Age", "Primary Fur Color", "Location"]
for col in categorical_cols:
df[col] = df[col].fillna("Unknown")
# Drop columns with excessive missing values that are not needed for analysis
df = df.drop(columns=["Color notes", "Specific Location", "Other Activities", "Other Interactions"])
```
```{python}
#| fig-height: 2
# What color occurs most often?
plt.figure()
sns.countplot(data=df, x="Primary Fur Color", order=df["Primary Fur Color"].value_counts().index)
plt.xlabel("Primary Fur Color")
plt.ylabel("Count")
plt.xticks(rotation=30)
plt.grid(axis="y", linestyle="--", alpha=0.7)
plt.show()
```
|
The failing tests are a real "3.9 no longer supported" problem 😓 we've been holding on for a while, but it might be time to drop it? |
|
@isabelizimm thanks for the review! The current behavior only applies when both I'll see if there's an easy way to fix 3.9 support so we can kick that can down the road a little longer. |
|
The test that was failing on 3.9 is actually failing because that specific behavior doesn't support IPython < 8.24. The specific behavior is a rare edge case, so it's safe to skip that test on older IPython versions. No need to drop 3.9 just yet |
|
Ah, folks will be expecting to be able to set just one of If we don't do this correctly with ark yet, we should address that, but let's get it right here for Python if possible now. |
|
Cool, makes sense! Fixed in bd23bf0:
I confirmed it is wrong in R:
Also confirmed that |
|
I'll open an issue shortly for R. |
isabelizimm
left a comment
There was a problem hiding this comment.
this looks great! it feels really nice and acts as i expect when altering in code and via hash pipes.
|
Ty! I'm gonna merge despite the failing ext-host test. It appears to be unrelated to this PR. |


Fixes #12660, #15253, #15254.
Relates to #15116: fixes switching to and from Positron's matplotlib backends with the
%matplotlibmagic. This was previously unhandled and buggy.IPython's
matplotlib_inline.backend_inline.set_matplotlib_formatsstill works with our notebook backend and is now a no-op in console sessions.Summary
Python figures now respect the figure sizing options that Quarto and Positron's notebooks send with each execution. In a notebook session (Quarto inline output, Positron notebooks, Jupyter notebooks):
#| fig-width/#| fig-heightbecomes the default figure sizeoutput_pixel_ratiodrives the rendering DPI so plots are sharp on hiDPI displays instead of blurry and squareGetting there required our own notebook matplotlib backend (we were using IPython's
matplotlib_backend.backend_inline, somatplotlib_backend.pyis now a package:notebook.py- new backend for notebook sessions, replacingmatplotlib_inline. Interceptsnew_figure_managerto apply the metadata size at figure creation (which is what makes the issue's same-cellimport matplotlibrepro work) and sets the canvas device pixel ratio.console.py- the existing console backend, moved unchanged in behavior. Seaborn figure detaching moved out of the kernel's variables poll into the backend's ownpost_executehook.registry.py- manages which backend is active, and switching between them.formats.py- image encoding and theoutput_pixel_ratioDPI scaling.backend.py/compat.py- shared backend names and shims for matplotlib/IPython versions without backend-registry support.Screenshots
Before (
#| fig-width: 8/#| fig-height: 4ignored - blurry and square):After (sized 2:1 and sharp):
Release Notes
New Features
fig-widthandfig-heightcell options, and render at the display's pixel ratio (Python: Figure sizing options not respected for Quarto inline output #12660)Bug Fixes
matplotlib_inline.backend_inline.set_matplotlib_formats(...)breaking Python figure rendering%matplotlibto toggle matplotlib's backend between the Plots pane and an interactive backend such asqt, and back againValidation Steps
@:quarto @:plots @:positron-notebooks
Kernel-level tests, from
extensions/positron-python/python_files/posit:python -m pytest positron/tests/test_matplotlib_backend positron/tests/test_execute_request.pyFigure sizing (Quarto inline output). Open a
.qmd, start a Python session, and run:main.#| fig-width: 8(no height) and re-run: still the default size - a lone dimension is a no-op.fig, ax = plt.subplots(figsize=(2, 2))in the cell body: small and square - explicit code wins.plt.rcParams['figure.figsize'] = (12, 3)in one cell, then the sized cell: still 8x4. Then an unsized cell: 12x3, so thercParamsassignment survived.Backend switching. In a console session:
%matplotlib -llistspositron-consoleandpositron-notebook.%matplotlib qt, then plot - an interactive window opens.%matplotlib inline(or bare%matplotlib), then plot - back in the Plots pane, no stray or duplicated output.%matplotlib inlinein the first cell, then re-run the sizing check above: thefig-width/fig-heightoptions are still honored.set_matplotlib_formats. In a notebook cell,from matplotlib_inline.backend_inline import set_matplotlib_formats; set_matplotlib_formats('retina'), then plot: the figure renders at the correct size (not doubled). Repeat with'png','svg', and'pdf'. In a console session, the same call leaves the Plots pane working with no extra inline image output.No-matplotlib environment. Start a Python session in an environment without matplotlib installed and run some non-plotting code: the session starts and executes normally.