Skip to content

Add support for global figure titles#414

Merged
OriolAbril merged 7 commits intoarviz-devs:mainfrom
Shlokpalrecha:add-figure-title
Feb 9, 2026
Merged

Add support for global figure titles#414
OriolAbril merged 7 commits intoarviz-devs:mainfrom
Shlokpalrecha:add-figure-title

Conversation

@Shlokpalrecha
Copy link
Contributor

Closes #370

Summary

Adds support for global figure titles across all backends, as discussed in the issue.

Changes

  • Added add_title() method to PlotCollection (following the add_legend() pattern as suggested)
  • Added figure_title parameter to PlotCollection.grid() and PlotCollection.wrap() for convenience
  • Implemented for all backends:
    • matplotlib: fig.suptitle()
    • plotly: fig.update_layout(title=...)
    • bokeh: Div element above the grid

Usage

# Option 1: At creation time
pc = PlotCollection.grid(data, figure_title="My Title")

# Option 2: After creation (like add_legend)
pc = PlotCollection.grid(data)
pc.add_title("My Title")

@aloctavodia
Copy link
Contributor

Overall lgtm. We should update one or two examples in the gallery to show this new feature. Two candidates are:

https://python.arviz.org/projects/plots/en/latest/gallery/plot_dist_kde.html (suggested title Posterior of mu per chain)
https://python.arviz.org/projects/plots/en/latest/gallery/plot_ppc_rootogram.html (suggested title "Posterior Predictive Rootogram for Rugby Model").

I suggested these two examples because it should be easy to spot that the figure has a title from the miniatures, but other could work as well.
My suggested titles are not that clever, so feel free to use something else if you can think of better titles.

@codecov-commenter
Copy link

codecov-commenter commented Jan 22, 2026

Codecov Report

❌ Patch coverage is 54.54545% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.19%. Comparing base (d02d993) to head (53380ef).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
src/arviz_plots/backend/bokeh/core.py 31.25% 11 Missing ⚠️
src/arviz_plots/backend/matplotlib/core.py 40.00% 3 Missing ⚠️
src/arviz_plots/backend/plotly/core.py 40.00% 3 Missing ⚠️
src/arviz_plots/plot_collection.py 83.33% 2 Missing ⚠️
src/arviz_plots/backend/none/core.py 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #414      +/-   ##
==========================================
- Coverage   85.33%   85.19%   -0.15%     
==========================================
  Files          59       59              
  Lines        6862     6925      +63     
==========================================
+ Hits         5856     5900      +44     
- Misses       1006     1025      +19     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@Shlokpalrecha
Copy link
Contributor Author

Shlokpalrecha commented Jan 22, 2026

@aloctavodia Thanks for the suggestions! I've added figure titles to three gallery examples:

  1. plot_dist_kde: "KDE of μ by Chain (Centered Eight)" : Shows the chain-wise density as suggested
  2. plot_ppc_rootogram: "Posterior Predictive Rootogram for Rugby Model" : Used your suggested title as it clearly describes the plot
  3. plot_trace (bonus): "MCMC Sampling Traces: Centered Eight Model" : Added this as a third example to demonstrate the feature on diagnostic plots

The titles should be clearly visible in the gallery thumbnails. Let me know if you'd like any adjustments!

Gallery examples added! The ReadTheDocs error looks unrelated to these changes - happy to wait while other updates are being sorted out. Let me know if there's anything else you need from me!

Copy link
Member

@OriolAbril OriolAbril left a comment

Choose a reason for hiding this comment

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

I haven't looked into the tests yet but my guess is we'll want to divide them between global plotcollection things (which might not even be parametrized with backend) and backend specific tests for the function that interfaces with the plotting backend and sets the title

@OriolAbril
Copy link
Member

@Shlokpalrecha if you rebase on main the readthedocs failure should be fixed

@Shlokpalrecha
Copy link
Contributor Author

Thanks for the detailed review! I see what you mean about the architecture- moving the backend-specific title code into each backend's core.py makes much more sense for maintainability.

I'll work on:

  1. Creating set_figure_title() functions in each backend's core.py
  2. Moving figure_title into figure_kwargs instead of top-level
  3. Updating the PlotCollection method to call the backend functions
  4. Rebasing on main to fix the ReadTheDocs issue

I'll have these changes ready soon. Thanks for the clear guidance

Adds figure_title parameter to PlotCollection.grid() and PlotCollection.wrap()
methods, plus an add_title() method following the add_legend() pattern.

Supported backends:
- matplotlib: uses fig.suptitle()
- plotly: uses fig.update_layout(title=...)
- bokeh: uses Div element above the grid

Closes arviz-devs#370
- Add descriptive title to plot_dist_kde showing KDE by chain
- Add title to plot_ppc_rootogram for rugby model rootogram
- Add title to plot_trace for MCMC diagnostic visualization

Demonstrates the new figure_title feature across different plot types.
Moved title functionality from PlotCollection into backend-specific
set_figure_title() functions in each backend's core.py. This follows
the same pattern as other backend operations like show() and savefig().

Changes:
- Created set_figure_title() in matplotlib, plotly, and bokeh backends
- Updated add_title() to call backend functions instead of inline logic
- Removed figure_title parameter from grid() and wrap() - users should
  call add_title() instead
- Updated gallery examples to use add_title() method
- Reorganized tests to separate PlotCollection tests from backend tests
@Shlokpalrecha
Copy link
Contributor Author

Thanks for the detailed feedback @OriolAbril! I've refactored everything as you suggested:

Created set_figure_title() functions in each backend's core.py
Updated add_title() to call those backend functions
Removed the figure_title parameter from grid() and wrap()
Updated gallery examples and tests
Rebased on main and pushed. Ready for review whenever you get a chance.

Copy link
Member

@OriolAbril OriolAbril left a comment

Choose a reason for hiding this comment

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

This is starting to look good, thanks! I have still left some higher level comments but some are already nits and implementation details.

@OriolAbril OriolAbril changed the title Add support for global figure titles (#370) Add support for global figure titles Jan 23, 2026
@Shlokpalrecha
Copy link
Contributor Author

Hey! Thanks for the detailed review, just pushed all the changes.

Switched to the unset pattern for bokeh, moved those imports to the top, and refactored to use the Styles class instead of inline CSS. Also cleaned up the docstrings with the proper sphinx references and removed the return value from add_title() like you suggested.

Everything's working on my end. Let me know if I missed anything or if there's anything else you'd like me to adjust.

@read-the-docs-community
Copy link

read-the-docs-community bot commented Jan 25, 2026

Documentation build overview

📚 arviz-plots | 🛠️ Build #31339666 | 📁 Comparing 53380ef against latest (1c1641d)


🔍 Preview build

Show files changed (145 files in total): 📝 141 modified | ➕ 0 added | ➖ 4 deleted
File Status
genindex.html 📝 modified
_modules/index.html 📝 modified
api/plots.html 📝 modified
contributing/docs.html 📝 modified
contributing/testing.html 📝 modified
gallery/add_reference_bands.html 📝 modified
gallery/add_reference_lines.html 📝 modified
gallery/combine_plots.html 📝 modified
gallery/plot_autocorr.html 📝 modified
gallery/plot_bf.html 📝 modified
gallery/plot_compare.html 📝 modified
gallery/plot_convergence_dist.html 📝 modified
gallery/plot_dgof.html 📝 modified
gallery/plot_dgof_dist.html 📝 modified
gallery/plot_dist_ecdf.html 📝 modified
gallery/plot_dist_hist.html 📝 modified
gallery/plot_dist_kde.html 📝 modified
gallery/plot_dist_models.html 📝 modified
gallery/plot_dist_qds.html 📝 modified
gallery/plot_ecdf_coverage.html 📝 modified
gallery/plot_ecdf_pit.html 📝 modified
gallery/plot_energy.html 📝 modified
gallery/plot_ess_evolution.html 📝 modified
gallery/plot_ess_local.html 📝 modified
gallery/plot_ess_models.html 📝 modified
gallery/plot_ess_quantile.html 📝 modified
gallery/plot_forest.html 📝 modified
gallery/plot_forest_ess.html 📝 modified
gallery/plot_forest_models.html 📝 modified
gallery/plot_forest_pp_obs.html 📝 modified
gallery/plot_forest_shade.html 📝 modified
gallery/plot_khat.html 📝 modified
gallery/plot_khat_aesthetics.html 📝 modified
gallery/plot_khat_facet_cols.html 📝 modified
gallery/plot_khat_facet_grid.html 📝 modified
gallery/plot_loo_pit.html 📝 modified
gallery/plot_mcse.html 📝 modified
gallery/plot_pair.html 📝 modified
gallery/plot_pair_distribution.html 📝 modified
gallery/plot_pair_focus.html 📝 modified
gallery/plot_pair_focus_distribution.html 📝 modified
gallery/plot_parallel.html 📝 modified
gallery/plot_pava_calibration.html 📝 modified
gallery/plot_ppc_censored.html 📝 modified
gallery/plot_ppc_coverage.html 📝 modified
gallery/plot_ppc_dist.html 📝 modified
gallery/plot_ppc_interval.html 📝 modified
gallery/plot_ppc_pava_residuals.html 📝 modified
gallery/plot_ppc_pit.html 📝 modified
gallery/plot_ppc_rootogram.html 📝 modified
gallery/plot_ppc_tstat.html 📝 modified
gallery/plot_prior_posterior.html 📝 modified
gallery/plot_psense.html 📝 modified
gallery/plot_psense_quantities.html 📝 modified
gallery/plot_rank.html 📝 modified
gallery/plot_rank_dist.html 📝 modified
gallery/plot_ridge.html 📝 modified
gallery/plot_ridge_multiple.html 📝 modified
gallery/plot_trace.html 📝 modified
gallery/plot_trace_dist.html 📝 modified
_modules/arviz_plots/plot_collection.html 📝 modified
_modules/arviz_plots/visuals.html 📝 modified
api/generated/arviz_plots.PlotCollection.generate_aes_dt.html 📝 modified
api/generated/arviz_plots.PlotCollection.grid.html 📝 modified
api/generated/arviz_plots.PlotCollection.html 📝 modified
api/generated/arviz_plots.PlotCollection.wrap.html 📝 modified
api/generated/arviz_plots.PlotMatrix.html 📝 modified
api/generated/arviz_plots.plot_autocorr.html 📝 modified
api/generated/arviz_plots.plot_bf.html 📝 modified
api/generated/arviz_plots.plot_compare.html 📝 modified
api/generated/arviz_plots.plot_convergence_dist.html 📝 modified
api/generated/arviz_plots.plot_dgof.html ➖ deleted
api/generated/arviz_plots.plot_dgof_dist.html ➖ deleted
api/generated/arviz_plots.plot_dist.html 📝 modified
api/generated/arviz_plots.plot_ecdf_pit.html 📝 modified
api/generated/arviz_plots.plot_energy.html 📝 modified
api/generated/arviz_plots.plot_ess.html 📝 modified
api/generated/arviz_plots.plot_ess_evolution.html 📝 modified
api/generated/arviz_plots.plot_forest.html 📝 modified
api/generated/arviz_plots.plot_khat.html 📝 modified
api/generated/arviz_plots.plot_lm.html 📝 modified
api/generated/arviz_plots.plot_loo_pit.html 📝 modified
api/generated/arviz_plots.plot_mcse.html 📝 modified
api/generated/arviz_plots.plot_pair.html 📝 modified
api/generated/arviz_plots.plot_pair_focus.html 📝 modified
api/generated/arviz_plots.plot_parallel.html 📝 modified
api/generated/arviz_plots.plot_ppc_censored.html 📝 modified
api/generated/arviz_plots.plot_ppc_dist.html 📝 modified
api/generated/arviz_plots.plot_ppc_interval.html 📝 modified
api/generated/arviz_plots.plot_ppc_pava.html 📝 modified
api/generated/arviz_plots.plot_ppc_pava_residuals.html 📝 modified
api/generated/arviz_plots.plot_ppc_pit.html 📝 modified
api/generated/arviz_plots.plot_ppc_rootogram.html 📝 modified
api/generated/arviz_plots.plot_ppc_tstat.html 📝 modified
api/generated/arviz_plots.plot_prior_posterior.html 📝 modified
api/generated/arviz_plots.plot_psense_dist.html 📝 modified
api/generated/arviz_plots.plot_psense_quantities.html 📝 modified
api/generated/arviz_plots.plot_rank.html 📝 modified
api/generated/arviz_plots.plot_rank_dist.html 📝 modified
api/generated/arviz_plots.plot_ridge.html 📝 modified
api/generated/arviz_plots.plot_trace.html 📝 modified
api/generated/arviz_plots.plot_trace_dist.html 📝 modified
api/generated/arviz_plots.visuals.fill_between_y.html 📝 modified
_modules/arviz_plots/plots/autocorr_plot.html 📝 modified
_modules/arviz_plots/plots/bf_plot.html 📝 modified
_modules/arviz_plots/plots/compare_plot.html 📝 modified
_modules/arviz_plots/plots/convergence_dist_plot.html 📝 modified
_modules/arviz_plots/plots/dgof_dist_plot.html ➖ deleted
_modules/arviz_plots/plots/dgof_plot.html ➖ deleted
_modules/arviz_plots/plots/dist_plot.html 📝 modified
_modules/arviz_plots/plots/ecdf_plot.html 📝 modified
_modules/arviz_plots/plots/energy_plot.html 📝 modified
_modules/arviz_plots/plots/ess_plot.html 📝 modified
_modules/arviz_plots/plots/evolution_plot.html 📝 modified
_modules/arviz_plots/plots/forest_plot.html 📝 modified
_modules/arviz_plots/plots/khat_plot.html 📝 modified
_modules/arviz_plots/plots/lm_plot.html 📝 modified
_modules/arviz_plots/plots/loo_pit_plot.html 📝 modified
_modules/arviz_plots/plots/mcse_plot.html 📝 modified
_modules/arviz_plots/plots/pair_focus_plot.html 📝 modified
_modules/arviz_plots/plots/pair_plot.html 📝 modified
_modules/arviz_plots/plots/parallel_plot.html 📝 modified
_modules/arviz_plots/plots/pava_calibration_plot.html 📝 modified
_modules/arviz_plots/plots/pava_residual_plot.html 📝 modified
_modules/arviz_plots/plots/ppc_censored_plot.html 📝 modified
_modules/arviz_plots/plots/ppc_dist_plot.html 📝 modified
_modules/arviz_plots/plots/ppc_interval_plot.html 📝 modified
_modules/arviz_plots/plots/ppc_pit_plot.html 📝 modified
_modules/arviz_plots/plots/ppc_rootogram_plot.html 📝 modified
_modules/arviz_plots/plots/ppc_tstat.html 📝 modified
_modules/arviz_plots/plots/prior_posterior_plot.html 📝 modified
_modules/arviz_plots/plots/psense_dist_plot.html 📝 modified
_modules/arviz_plots/plots/psense_quantities_plot.html 📝 modified
_modules/arviz_plots/plots/rank_dist_plot.html 📝 modified
_modules/arviz_plots/plots/rank_plot.html 📝 modified
_modules/arviz_plots/plots/ridge_plot.html 📝 modified
_modules/arviz_plots/plots/trace_dist_plot.html 📝 modified
_modules/arviz_plots/plots/trace_plot.html 📝 modified
api/backend/generated/arviz_plots.backend.bokeh.create_plotting_grid.html 📝 modified
api/backend/generated/arviz_plots.backend.matplotlib.create_plotting_grid.html 📝 modified
api/backend/generated/arviz_plots.backend.plotly.create_plotting_grid.html 📝 modified
_modules/arviz_plots/backend/bokeh/core.html 📝 modified
_modules/arviz_plots/backend/matplotlib/core.html 📝 modified
_modules/arviz_plots/backend/none/core.html 📝 modified
_modules/arviz_plots/backend/plotly/core.html 📝 modified

- Add @expand_aesthetic_aliases decorator to all backends (except none)
- Use unset defaults instead of None for color/size parameters
- Use _filter_kwargs helper for consistent arg handling
- Add width: auto to bokeh Div for proper centering
- Add ALLOW_KWARGS check to none backend
- Use B1 as default color in add_title
- Rename artist_kws to kwargs in add_title for user clarity
- Filter size=None to avoid passing it to backend
@Shlokpalrecha
Copy link
Contributor Author

Addressed all feedback- ready for re-review, thankyou

@Shlokpalrecha
Copy link
Contributor Author

Hi @OriolAbril, just a gentle ping to say I’ve pushed updates addressing all the review comments and requested a re-review.

No rush at all, just whenever you have time. Thanks again for the detailed feedback.

The title text object.
"""
kwargs = {"color": color, "fontsize": size}
title_obj = figure.suptitle(text, **_filter_kwargs(kwargs, None, artist_kws))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
title_obj = figure.suptitle(text, **_filter_kwargs(kwargs, None, artist_kws))
title_obj = figure.suptitle(text, **_filter_kwargs(kwargs, Text, artist_kws))

Comment on lines 328 to 331
title_kwargs = {"text": text, "x": 0.5, "xanchor": "center"}
title_kwargs = _filter_kwargs(
{"font_color": color, "font_size": size}, {**title_kwargs, **artist_kws}
)
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
title_kwargs = {"text": text, "x": 0.5, "xanchor": "center"}
title_kwargs = _filter_kwargs(
{"font_color": color, "font_size": size}, {**title_kwargs, **artist_kws}
)
title_kwargs = _filter_kwargs(
{"font_color": color, "font_size": size, "text": text}, {"x": 0.5, "xanchor": "center"} | artist_kws
)

The number of lines doesn't really matter, the reasoning for the changes is being as clear as possible on how we want every argument to behave like. color, font size or text should only be passed through the dedicated arguments. If I use color="red", font_color="blue" the result should be red. However, if I want a left aligned title or any other funky looking title x and xanchor can be modified through artist_kws.

note: the 2nd positional argument is named text, so in practice artist_kws can't really have a text key to overwrite the one in the original title_kwargs but I think separating the arguments depending on behaviour reduced the cognitive load in understanding what the function is doing.

Copy link
Member

Choose a reason for hiding this comment

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

this should be deleted

class TestPlotCollectionTitleMethod:
"""Test PlotCollection.add_title() method (not backend-specific)."""

def test_add_title_method_exists(self, dataset):
Copy link
Member

Choose a reason for hiding this comment

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

we don't need this test. It is a normal method not something dynamically added or other complicated architectures, and we have tests for it, so if someone deletes it by mistake tests will fail.

pc = PlotCollection.grid(
dataset,
cols=["__variable__"],
backend="matplotlib",
Copy link
Member

Choose a reason for hiding this comment

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

backend agnostic tests should use the none backend. Given we don't care about what gets rendered, only we an call the method and it modifies pc.viz there is no need to use a proper plotting backend, it only slows down the test suite.

@pytest.mark.parametrize("backend", ["matplotlib", "bokeh", "plotly"])
@pytest.mark.usefixtures("clean_plots")
@pytest.mark.usefixtures("check_skips")
class TestBackendSetFigureTitle:
Copy link
Member

Choose a reason for hiding this comment

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

these tests should go in the backend specific files (this depends on #409 which I am hoping to finish in 1-2 days). You'll see there the pattern is generally one test using the backend function with only required arguments, and one test using arguments like color and size. If we identify other cases that need dedicated tests then we add more.

If you want to avoid getting too many git conflicts you can start the tests here even if they don't run and I can move them and fix conflicts when I merge the other PR.

class TestBackendSetFigureTitle:
"""Test backend-specific set_figure_title functions."""

def test_set_figure_title_basic(self, dataset, backend):
Copy link
Member

Choose a reason for hiding this comment

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

this can be the base for the first test I mention in the other comment but using the backend functions directly.

Comment on lines 69 to 88
def test_set_figure_title_with_color(self, dataset, backend):
"""Test set_figure_title with color parameter."""
pc = PlotCollection.grid(
dataset,
cols=["__variable__"],
backend=backend,
)
pc.add_title("Colored Title", color="red")
assert "figure_title" in pc.viz

def test_set_figure_title_with_size(self, dataset, backend):
"""Test set_figure_title with size parameter."""
pc = PlotCollection.grid(
dataset,
cols=["__variable__"],
backend=backend,
)
# Use string size for bokeh compatibility
pc.add_title("Sized Title", size="16px")
assert "figure_title" in pc.viz
Copy link
Member

Choose a reason for hiding this comment

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

These two should be combined into a single one. Also note the main goal of this test using arguments is ensuring they are propagated properly to the backend. So the assert figure_title in pc.viz doesn't add anything (and there won't be a plot_collection in the test either) and what needs to be checked is the backend object has the right properties

pc.add_title("Sized Title", size="16px")
assert "figure_title" in pc.viz

def test_backward_compatibility_no_title(self, dataset, backend):
Copy link
Member

Choose a reason for hiding this comment

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

this test is not needed

"""Verify that a title was added (basic check)."""
if backend == "matplotlib":
assert hasattr(fig, "_suptitle")
assert fig._suptitle is not None
Copy link
Member

Choose a reason for hiding this comment

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

for when adapting the test to backend specific. if there is no alternative we can use private attributes but if we can it is best to avoid them as any minor change in matplotlib could break our tests. For this I think a combination of get_suptitle and fig.texts will allow us to check everything we care about.

- Use Text class for matplotlib _filter_kwargs validation
- Restructure plotly _filter_kwargs with dict union operator for clear argument precedence
- Move title tests to test_plot_collection.py
- Use 'none' backend for agnostic tests
- Remove github_comment.txt
@Shlokpalrecha
Copy link
Contributor Author

Hey! @OriolAbril Just pushed the changes from your latest review. Switched to the Text class for matplotlib's filter validation, restructured the plotly kwargs with the union operator for clearer precedence, and moved the tests over to test_plot_collection.py with the none backend.

Should be good now. Let me know if you need anything else.

@OriolAbril OriolAbril merged commit 23d4323 into arviz-devs:main Feb 9, 2026
4 checks passed
@OriolAbril
Copy link
Member

Thanks @Shlokpalrecha! That was a very big PR to get started

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.

Add Support for Global Figure Titles

4 participants