From 46ebe0a373c198726407049d2be17be645803983 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Thu, 23 Apr 2026 15:23:43 -0400 Subject: [PATCH 01/99] ft2font null checks added --- src/ft2font.h | 2 +- src/ft2font_wrapper.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ft2font.h b/src/ft2font.h index 0c438d9107de..09e028f3404c 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -41,7 +41,7 @@ inline char const* ft_error_string(FT_Error error) { #undef __FTERRORS_H__ #define FT_ERROR_START_LIST switch (error) { #define FT_ERRORDEF( e, v, s ) case v: return s; -#define FT_ERROR_END_LIST default: return NULL; } +#define FT_ERROR_END_LIST default: return "unknown error"; } #include FT_ERRORS_H } diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index d0df659c5918..771f1db5a191 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -409,9 +409,9 @@ class PyFT2Font final : public FT2Font { std::set::iterator it = family_names.begin(); std::stringstream ss; - ss<<*it; + ss<< (*it ? *it : "unknown family name"); while(++it != family_names.end()){ - ss<<", "<<*it; + ss<<", "<< (*it ? *it : "unknown family name"); } auto text_helpers = py::module_::import("matplotlib._text_helpers"); From f92a7e6eb7a2d685fd1b10d7219924a61f0f425e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 01:06:19 -0400 Subject: [PATCH 02/99] DOC: Prepare GitHub stats for 3.11 --- doc/release/github_stats.rst | 1303 ++++++++++++++++- .../prev_whats_new/github_stats_3.10.9.rst | 103 ++ tools/github_stats.py | 2 + 3 files changed, 1353 insertions(+), 55 deletions(-) create mode 100644 doc/release/prev_whats_new/github_stats_3.10.9.rst diff --git a/doc/release/github_stats.rst b/doc/release/github_stats.rst index e01a1727b162..62c3242b7eb7 100644 --- a/doc/release/github_stats.rst +++ b/doc/release/github_stats.rst @@ -2,107 +2,1300 @@ .. _github-stats: -GitHub statistics for 3.10.9 (Apr 23, 2026) +GitHub statistics for 3.11.0 (Apr 24, 2026) =========================================== -GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/23 +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/24 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 10 issues and merged 34 pull requests. -The full list can be seen `on GitHub `__ +We closed 246 issues and merged 764 pull requests. +The full list can be seen `on GitHub `__ -The following 37 authors contributed 519 commits. +The following 264 authors contributed 4590 commits. +* 34j +* Aaratrika-Shelly +* Aaron Meurer * Aasma Gupta +* Abhiroop Batabyal +* Abitamim Bharmal +* Adam Ormondroyd +* AdamOrmondroyd +* Aditya Singh +* aditya-singh597 +* AdrashDec +* Aishling Cooke +* Alan Burlot +* Albert Y. Shih +* ALBIN BABU VARGHESE +* albus-droid +* Alexandra Khoo +* Allison +* alphanoobie +* Aman Kushwaha +* AMAN KUSHWAHA +* Aman Nijjar +* Aman Parganiha * Aman Srivastava +* Amisha Mehta +* amishamehta99 +* Amitesh Singh +* Anabelle VanDenburgh +* Andrea Alberti +* Andres Gutierrrez +* Andrew Landau +* Andrés Gutierrez +* Anselm Hahn +* anTon +* Anton * Antony Lee +* Archil Jain +* Arnaud Patard +* Barbier--Darnal Joseph * beelauuu +* Ben Greiner * Ben Root +* Bodhi Silberling +* Brian Christian +* BriAnna Foreman +* brk +* Carlos Ramos Carreño +* Cemonix +* Chaoyi Hu +* Charlie Thornton +* Chirag Sharma +* Chirag3841 +* chrisjbillington * Christine P. Chai +* clairefio +* Clemens Brunner +* Clément Robert +* cmp0xff +* Colton Lathrop +* Constantinos Menelaou +* Corenthin ZOZOR +* cvanelteren +* Daniel Weiss +* Danny +* David Lowry-Duda * David Stansby * dependabot[bot] +* DerWeh +* Diksha +* Dominik Stiller +* Doron Behar +* Duncan Macleod +* DWesl +* Edge-Seven +* ee25b003 +* ellie * Elliott Sales de Andrade -* G.D. McBain +* Emmanuel Ferdman +* EncryptedDoom +* Eric Firing +* Eric Larson +* Evgenii Radchenko +* Eytan Adler +* Fazeel Usmani +* founta +* francisayyad03 +* Francisco Cardozo +* G Karthik Koundinya +* G\. D\. McBain +* G26Karthik +* ganglike +* Geoffrey Thomas +* Gguidini * Greg Lucas +* guillermodotn * hannah +* Hannan7812 +* Hasan Rashid +* Hassan Kibirige +* heinrich5991 * hu-xiaonan +* Husain Gadiwala +* Ian Hunt-Isaak * Ian Thomas +* ianlv +* IdiotCoffee +* ilakk manoharan +* Ilakkuvaselvi Manoharan +* intelliking * Inês Cachola +* ishan372or +* James Addison +* Javier Pérez Robles +* jaya prajapati +* jayaprajapatii +* Jaylon +* Jimmy Shah +* jocelynvj +* JOD +* joddeepesh-cloud * Jody Klymak +* Johannes Kopton +* Jonas Drotleff +* Jonathan Reimer * Jouni K. Seppänen +* Julian Chen +* Kaustbh +* Kaustubh +* kdpenner * Khushi_29 +* Khushikela29 +* KIU Shueng Chuan +* konmenel +* Kris Rubiano +* kusch lionel +* Kyle Martin * Kyle Sunden +* Kyra Cho +* landoskape +* LangQi99 +* Larry Bradley +* leakyH +* Leo Singer +* Leon Merten Lohse +* lilfer +* litchi +* Logan Pageler +* Logan-Pageler +* Lucas Gruwez +* Lucx33 +* Luka Aladashvili +* Lukas Hergt +* lukashergt * Lumberbot (aka Jack) +* Lívia Lutz * m-sahare +* Mafalda Botelho +* Manit Roy +* manit2004 +* Manthan Nagvekar +* marbled-toast +* Marco Barbosa +* Marco Gorelli +* Marie +* Marten H. van Kerkwijk +* Marten Henric van Kerkwijk +* martincornejo +* masih.khatibzdeh +* Mateusz Sokół +* Matthew Feickert +* Melissa Weber Mendonça +* Melwyn Francis Carlo +* MengAiDev +* Milan Gittler +* MiniX16 +* Miriam +* Miriam Simone +* miriamsimone +* MKhatibzadeh +* Mohit Pal +* Moniza Kidwai +* MQY +* mromanie +* Muhammad Hannan Akram +* musvaage * N R Navaneet +* NabeelShar +* nakano * Nathan G. Wiseman +* Nathan Goldbaum +* Nathan Hansen +* Nathan McDougall +* Nick Coish +* Nicolai Weitkemper +* Niklas Mertsch +* null-dreams +* Obliman * Oscar Gustafsson +* Owl +* Parsa Homayouni +* Patrick Seitz +* Pedro Marques +* pedrom2002 +* Pieter Eendebak +* Pirzada Ahmad Faraz +* pirzada-ahmadfaraz * Praful Gulani +* Pranav +* Pranav Raghu +* pre-commit-ci[bot] +* proximalf +* q33566 * Qian Zhang +* r3kste +* Rafael Katri +* Rahul +* Rahul Monani * Raphael Erik Hviding * Raphael Quast +* RETHICK CB +* RogueRebel33 * Roman +* Roman A * Ruth Comer +* ruvilonix +* Ryan May +* Saakshi Gupta +* Sai Chaitanya, Sanivada * saikarna913 +* Sanchit Rishi +* Saumya * Scott Shambaugh +* Sebastien Wieckowski +* Siddharth_Savani +* Sonu Singh +* star1327p +* statxc +* Stefan van der Walt +* Stefan Vujadinovic * Steve Berardi +* Steve Nicholson +* tfpf * Thomas A Caswell +* thomashopkins32 +* Tiago Marques +* Tim Heap * Tim Hoffmann +* Timon Erhart +* Tine Zivic +* Tingwei Zhu * Trygve Magnus Ræder +* Ubuntu +* Vagner Messias +* Vedant Madane +* Victor Liu +* Vidya * Vikash Kumar +* Vishal Pankaj Chandratreya +* Vraj Rajpura +* Weh Andreas +* Wiliam +* Yuepeng Gu +* Zhongqi LUO +* ZPyrolink GitHub issues and pull requests: -Pull Requests (34): - -* :ghpull:`31556`: FIX: Inverted PyErr_Occurred check in enum type caster (_enums.h) -* :ghpull:`31078`: Backport PR #31075 on branch v3.10.x (Fix remove method for figure title and xy-labels) -* :ghpull:`31280`: Backport PR #31278 on branch v3.10.x (Fix ``clabel`` manual argument not accepting unit-typed coordinates) -* :ghpull:`31520`: Backport PR #31020 on branch v3.10.x (DOC: Fix doc builds with Sphinx 9) -* :ghpull:`31511`: Backport PR #31504 on branch v3.10.x (Re-order variants to prioritize narrower types) -* :ghpull:`31504`: Re-order variants to prioritize narrower types -* :ghpull:`31445`: Backport PR #31437: mathtext: Fix type inconsistency with fontmaps -* :ghpull:`31437`: mathtext: Fix type inconsistency with fontmaps -* :ghpull:`31411`: Backport PR #31323 on branch v3.10.x (FIX: Prevent crash when removing a subfigure containing subplots) -* :ghpull:`31421`: Backport PR #31420 on branch v3.10.x (Fix outdated Savannah URL for freetype download) -* :ghpull:`31420`: Fix outdated Savannah URL for freetype download -* :ghpull:`31418`: Backport PR #31401: BLD: Temporarily pin setuptools-scm<10 -* :ghpull:`31323`: FIX: Prevent crash when removing a subfigure containing subplots -* :ghpull:`31401`: BLD: Temporarily pin setuptools-scm<10 -* :ghpull:`31278`: Fix ``clabel`` manual argument not accepting unit-typed coordinates -* :ghpull:`31154`: Backport PR #31153 on branch v3.10.x (TST: Use correct method of clearing mock objects) -* :ghpull:`31153`: TST: Use correct method of clearing mock objects -* :ghpull:`31075`: Fix remove method for figure title and xy-labels -* :ghpull:`31036`: Backport PR #31035 on branch v3.10.x (DOCS: Fix typo in time array step size comment) -* :ghpull:`30986`: Backport PR #30985 on branch v3.10.x (MNT: do not assign a numpy array shape) -* :ghpull:`30985`: MNT: do not assign a numpy array shape -* :ghpull:`30971`: Backport PR #30969 on branch v3.10.x (DOC: Simplify barh() example) -* :ghpull:`30965`: Backport PR #30952 on branch v3.10.x (DOC: Tutorial on API shortcuts) -* :ghpull:`30964`: Backport PR #30960 on branch v3.10.x (SVG backend - handle font weight as integer) -* :ghpull:`30960`: SVG backend - handle font weight as integer -* :ghpull:`30924`: Backport PR #30910 on branch v3.10.x (DOC: Improve writer parameter docs of Animation.save()) -* :ghpull:`30870`: Backport PR #30869 on branch v3.10.x (FIX: Accept array for zdir) -* :ghpull:`30869`: FIX: Accept array for zdir -* :ghpull:`30860`: Backport PR #30858 on branch v3.10.x (DOC: reinstate "codex" search term) -* :ghpull:`30818`: Backport PR #30817 on branch v3.10.x (Update sphinx-gallery header patch) -* :ghpull:`30801`: Backport PR #30763 on branch v3.10.x (DOC: Add example how to align tick labels) -* :ghpull:`30791`: Backport PR #30788 on branch v3.10.8-doc (Fix typo in key-mapping for "f11") -* :ghpull:`30790`: Backport PR #30788 on branch v3.10.x (Fix typo in key-mapping for "f11") -* :ghpull:`30788`: Fix typo in key-mapping for "f11" - -Issues (10): - -* :ghissue:`31495`: Unavoidable warnings with pybind11 main branch -* :ghissue:`31433`: [MNT]: Mypy error -* :ghissue:`31340`: [Bug]: outdated savannah URL in subprojects/freetype-2.6.1.wrap -* :ghissue:`31319`: [Bug]: Crash when removing a subfigure with a subplot in a figure -* :ghissue:`27525`: [Bug]: clabel manual argument does not accept units -* :ghissue:`31112`: [TST] Upcoming dependency test failures -* :ghissue:`31073`: [Bug]: Crash when Removing Suptitle in a Figure with Constrained Layout -* :ghissue:`30981`: [TST] Upcoming dependency test failures -* :ghissue:`30868`: [Bug]: Axe3D text() method does not allow zdir=numpy.array(...) -* :ghissue:`21566`: [ENH]: set_horizontalalignment("right") on Y axis labels when yaxis.ticks_right() is used. +Pull Requests (764): + +* :ghpull:`31561`: Fixed bug with an uninitialized colormap in parallel threads +* :ghpull:`31555`: FIX: removing colorbar's axes also removes colorbar +* :ghpull:`31560`: merge up v3.10.9 +* :ghpull:`31416`: MNT: Privatize Formatter attributes +* :ghpull:`23616`: feat(mathtext): support underline +* :ghpull:`31554`: BUG: avoid a deprecation warning from numpy 2.5 (calling ``datetime64('NaT')`` without a unit is deprecated) +* :ghpull:`31535`: DOC: fix broken link to wxPython Widget Inspection Tool +* :ghpull:`31551`: Bump https://github.com/pre-commit/mirrors-mypy from v1.20.1 to 1.20.2 +* :ghpull:`31552`: Bump scientific-python/upload-nightly-action from 0.6.3 to 0.6.4 in the actions group +* :ghpull:`31478`: Fix errorbar autoscaling inconsistency on log axes +* :ghpull:`31522`: MNT: Update all pre-commit hooks +* :ghpull:`31365`: Add thumbnail for embedding in user interfaces examples +* :ghpull:`31530`: BUG: Fix relim() to support Collection artists (scatter, etc.) +* :ghpull:`31514`: Add suggestions to more lookup errors +* :ghpull:`31465`: lib/matplotlib/tests/test_inset.py: Fix tolerance on aarch64 +* :ghpull:`31521`: Drop support for font hinting factor +* :ghpull:`31492`: MNT: Ensure all types from matplotlib.typing are documented +* :ghpull:`31524`: FIX: Disallow twinx/twiny on Axes3D +* :ghpull:`31540`: DOC: replace dolphin license RDF block with prose attribution +* :ghpull:`31426`: Fix: Optimize Cursor clearing on mouse exit to prevent lag +* :ghpull:`31512`: Document that ``TimedAnimation`` should not be used +* :ghpull:`31518`: DOC: add tags to tick locator and formatter examples +* :ghpull:`31519`: Bump the actions group with 3 updates +* :ghpull:`31517`: [DOC] make headers in pie example consistent +* :ghpull:`31515`: Remove unnecessary ruff lint exceptions +* :ghpull:`31516`: TST: account for flakiness with Numpy v1 (part 3) +* :ghpull:`31489`: Fixed: specified exception type in cbook.py +* :ghpull:`31314`: DOC: setting active axes position is ineffective +* :ghpull:`31148`: TST: Use explicit style in all image_comparison calls +* :ghpull:`31486`: ENH: Add an environment variable to ignore system fonts +* :ghpull:`31507`: PR template: always ask for AI declaration +* :ghpull:`31503`: TST: Harden handling of Popen subprocesses +* :ghpull:`31490`: DOC: Minor style improvement of radio buttons examples +* :ghpull:`31181`: ENH: Give control whether twinx() or twiny() overlays the main axis +* :ghpull:`31485`: MNT: Update bundled font libraries +* :ghpull:`31484`: MNT: Use new defaults in set_font_settings_for_testing +* :ghpull:`31483`: Bump the actions group across 1 directory with 2 updates +* :ghpull:`31476`: DOC: Improve Radio Buttons example +* :ghpull:`31275`: DOC: use minigallery for tutorial thumbnails +* :ghpull:`29763`: Shorten Agg template usage with class template argument deduction. +* :ghpull:`31353`: Fix #21409: Make twin axes inherit parent position +* :ghpull:`31431`: FIX: Guard against already-removed labels in ContourSet.remove() +* :ghpull:`31428`: Relax type hints for xy and xytext in annotate +* :ghpull:`31468`: DOC: Replace ``skip_deprecated`` extension by standard Sphinx metadata +* :ghpull:`30161`: Font and text overhaul +* :ghpull:`31461`: Support font features/language in default RendererBase.draw_text +* :ghpull:`31303`: TST: Reset tolerances on tests changed by text overhaul +* :ghpull:`31471`: DOC: Use FuncAnimation in 3D animations +* :ghpull:`31477`: DOC: Improve Radio Buttons Grid example +* :ghpull:`31470`: MNT: Deprecate matplotlib.image.thumbnail +* :ghpull:`31475`: Purge gitter links +* :ghpull:`31466`: DOC: make simple animation example easier to find +* :ghpull:`31469`: Change if condition to allow handles to be passed as a ndarray and not only Python list or tuple, etc. +* :ghpull:`31459`: DOC: Improve AI policy +* :ghpull:`31444`: Bump the actions group with 3 updates +* :ghpull:`31456`: Clarify fonttype switch in backend_pdf. +* :ghpull:`31300`: TST: Set tests touched by text overhaul to mpl20 style +* :ghpull:`31449`: Fix: improve log-scale error message wording +* :ghpull:`30385`: Add type stubs for functions in matplotlib.dates +* :ghpull:`31442`: TST: account for flakiness with Numpy v1 (part 2) +* :ghpull:`31440`: Fix FreeType runtime version check +* :ghpull:`31295`: TST: Cleanup back-compat code in tests touched by text overhaul +* :ghpull:`31408`: Merge branch 'main' into text-overhaul +* :ghpull:`31407`: BLD: Update bundled FreeType to 2.14.3 +* :ghpull:`31439`: Clarify SecondaryAxes limit behavior via documentation +* :ghpull:`31432`: DOC: More concise page title: Development setup +* :ghpull:`31423`: DOC: Remove pyplot vs. OO interface discussion from lifecycle example +* :ghpull:`31413`: ENH: Support partial figsize with None (#31400) +* :ghpull:`31368`: Fix: Prevent Cursor blitting from erasing overlapping axes (#25670) +* :ghpull:`31409`: Bump the actions group with 2 updates +* :ghpull:`31417`: DOC: Explain return value of secondary_x/yaxis +* :ghpull:`31412`: MNT: Minor cleanup of label formatting in PathCollection.legend_elements +* :ghpull:`31422`: Improve legend loc and bbox_to_anchor documentation (#26620) +* :ghpull:`31414`: DOC: Improve Formatter documentation +* :ghpull:`31419`: Add a short example to StrMethodFormatter docstring +* :ghpull:`31405`: Tweak secondary_{x,y}axis docs. +* :ghpull:`31372`: BLD: Update bundled libraqm to 0.10.4 +* :ghpull:`31198`: Allow tuning the shape of {L,R,D}Arrow tips. +* :ghpull:`31183`: ENH: Allow fonts to be addressed by any of their SFNT family names +* :ghpull:`31371`: ps/pdf: Override font height metrics to support AFM files +* :ghpull:`31343`: TST: Restore some tolerances for some arch/platform-specific failures +* :ghpull:`31248`: SEC: Remove eval() from validate_cycler +* :ghpull:`31395`: doc: mention ``bar_label`` in ``bar`` and ``barh`` +* :ghpull:`31385`: Make font search case insensitive in logo example +* :ghpull:`31399`: DOC: Rename gallery README.txt files to GALLERY_HEADER.rst +* :ghpull:`29998`: Implement head resizing (and reversal) for larrow/rarrow/darrow +* :ghpull:`24744`: Addresses issue #24618 "Road sign" boxstyle/annotation, alternative to #24697 +* :ghpull:`31392`: Tweak Formatter method docstrings. +* :ghpull:`31200`: DOC: moderation and enforcement +* :ghpull:`30513`: TST: Remove redundant font tests +* :ghpull:`31363`: Update black requirement from <26 to <27 +* :ghpull:`31355`: Bump the actions group across 1 directory with 8 updates +* :ghpull:`31370`: Update dead link for Ware 1988 in colormap docs +* :ghpull:`31357`: ci: Configure dependabot to skip minver requirements +* :ghpull:`31358`: TST: Replace pywin32 with ctypes wrapper +* :ghpull:`29281`: Port requirements to PEP735 +* :ghpull:`31347`: FIX: Deprecate using clabel() with filled contours +* :ghpull:`31349`: DOC: Correct a few typos in documentation +* :ghpull:`31244`: PERF: Sticky edges speedup +* :ghpull:`31306`: [MNT]: Implement ``Scale.val_in_range`` and refactor ``_point_in_data_domain`` +* :ghpull:`31291`: text: Use font metrics to determine line heights +* :ghpull:`30900`: Added Turbo License doc +* :ghpull:`31307`: FIX: avoid applying dashed patterns to zero-width lines and patches +* :ghpull:`31338`: MAINT: Fix formatting on autoclose bot message +* :ghpull:`31313`: Fixed lingering bugs with image rendering related to exact half display pixels +* :ghpull:`31329`: DOC: Add note about opening multiple PRs +* :ghpull:`29093`: Add wasm CI +* :ghpull:`31283`: MNT: Add autoclose bot inspired by scikit-learn +* :ghpull:`31322`: DOC: fix pcolormesh doc +* :ghpull:`31308`: DOC: Add thumbnail for multipage_pdf gallery example +* :ghpull:`31315`: [BUG] Warn when legend() receives mismatched handles and labels in 2-argument positional form +* :ghpull:`31251`: Emit xlim_changed / ylim_changed when limits expand via set_xticks / set_yticks +* :ghpull:`31316`: DOC: clarify explanation of axline in infinite lines example +* :ghpull:`31309`: DOC: update pandas intersphinx mapping +* :ghpull:`31281`: Drop axis_artist tickdir image compat, due to text-overhaul merge. +* :ghpull:`31294`: MNT: Restrict webagg toolbar actions to valid actions +* :ghpull:`31282`: SEC: Block shell escapes in latex and ps commands +* :ghpull:`31252`: DOC: Fix rendering of quiver documentation +* :ghpull:`31285`: ENH: Ignore empty text for tightbbox +* :ghpull:`31230`: API: Raise ValueError in subplots if num refers to existing figure +* :ghpull:`31133`: fix: resolve FigureCanvasTkAgg clipping on Windows HiDPI +* :ghpull:`30908`: mathtext support for \phantom, \llap, \rlap for faking text metrics. +* :ghpull:`31261`: Bump the actions group with 2 updates +* :ghpull:`30369`: Support standard tickdir control (in/out/inout) in axisartist. +* :ghpull:`27987`: qhull: Fix inconsistent formatting function arguments +* :ghpull:`31061`: BUG: Fix text appearing far outside valid axis scale range +* :ghpull:`31117`: Clarify introductory description in scatter_star_poly example. +* :ghpull:`31203`: Fix Axes.hist crash for numpy timedelta64 inputs +* :ghpull:`31262`: DOC: Correct ``byweekday`` description in ``WeekdayLocator`` +* :ghpull:`31260`: MNT: Raise NotImplementedError for 3D semilog plots +* :ghpull:`31143`: Deprecate public access to XMLWriter; simplify some attribute settings +* :ghpull:`31258`: DOC: Document that set_aspect applies the aspect lazily +* :ghpull:`31005`: PERF: Bezier root finding speedup +* :ghpull:`30980`: Fix 3D axes to properly support non-linear scales (log, symlog, etc.) +* :ghpull:`30844`: allow passing a function to ``CallbackRegistry.disconnect_func`` +* :ghpull:`30995`: PERF: Speed up ticks processing when not visible or using a NullLocator +* :ghpull:`31128`: Fix relim() ignoring scatter PathCollection offsets +* :ghpull:`31166`: Add private Artist-level autoscale participation flag +* :ghpull:`31238`: CI: Explicitly define CI workflow permissions +* :ghpull:`31228`: Bump the actions group with 3 updates +* :ghpull:`29469`: MNT: Separate property cycle handling from _process_plot_var_args +* :ghpull:`31121`: mathtext: add mathnormal and distinguish between normal and italic family +* :ghpull:`31170`: Cleanup QuiverKey init and deprecate some attributes. +* :ghpull:`31004`: PERF: More speedups +* :ghpull:`31226`: ft2font: Read more entries from OS/2 font table +* :ghpull:`31191`: TST: Switch mathtext tests to mpl20 +* :ghpull:`31231`: DOC: make nightly download command one line so it works on Windows +* :ghpull:`30754`: MNT: Improve Grouper +* :ghpull:`31236`: DOC: Remove gitter links and direct folks to Discourse chat +* :ghpull:`31145`: ENH: Snap 3D view angle changes when holding Control key +* :ghpull:`31179`: Remove mpl.text._get_textbox. +* :ghpull:`31202`: ENH: Adds ``errorbar.capthick`` and ``errorbar.elinewidth`` to mplstyle +* :ghpull:`31222`: DOC: Rewrite tickabel rotation example to use rotation_mode +* :ghpull:`31001`: PERF: Text handling speedups +* :ghpull:`30975`: Use LOCALAPPDATA for config/cache directories on Windows +* :ghpull:`30795`: Fix array alpha to multiply (not replace) existing RGBA alpha +* :ghpull:`31021`: Fixed inaccurate image placement and even more resampling bugs +* :ghpull:`31110`: mathtext: Fetch quad width & axis height from font metrics +* :ghpull:`31193`: DOC: Clarify computed_zorder applies to Collections and Patches only +* :ghpull:`31217`: DOC: use pivot='middle' instead of 'mid' in quiver demo +* :ghpull:`31212`: DOC: discourage pivot='mid' for quiver +* :ghpull:`31204`: Reword the "fully-new contributor" section. +* :ghpull:`31201`: DOC: Add sections to rcParams documentation +* :ghpull:`31196`: DOC: Document which files need to be updated for new rcparams +* :ghpull:`31163`: DOC: update new contributor guidance re timelines, AI, reaching out +* :ghpull:`31124`: MAINT: add AI disclosure to pr template +* :ghpull:`31076`: Avoid using pyplot for check_figures_equal +* :ghpull:`31189`: Bump the actions group with 2 updates +* :ghpull:`31188`: Remove use of the discouraged plt.imread() in the docs. +* :ghpull:`31007`: TST: Skip tests that use a large amount of memory by default +* :ghpull:`30967`: ENH: Implement gapcolor for patch edges +* :ghpull:`31142`: doc: explain that gfi is for training and add no AI policy +* :ghpull:`31137`: TST: Simplify image testing decorator calls +* :ghpull:`31119`: MNT: Normalize internal set_foreground calls to RGBA +* :ghpull:`31107`: Fix confusion between text height and ascent in metrics calculations. +* :ghpull:`31168`: Fix docstring ``lib/matplotlib/pyplot.py`` and related ``lib/matplotlib/__init__.py`` +* :ghpull:`31167`: Copy-edit the transform tutorial. +* :ghpull:`31160`: Bump the actions group across 1 directory with 4 updates +* :ghpull:`29374`: DOC: Emphasize artist as annotation in AnnotationBbox demo and add to annotation guide +* :ghpull:`31151`: Add mlx support +* :ghpull:`31141`: Fix mutable default arguments in backend_svg.py +* :ghpull:`31140`: DOC: Document set_figure() is a low-level API +* :ghpull:`31026`: DOC: Explicitly prohibit bots/agents to post contents +* :ghpull:`31131`: MAINT: added don't solve AI note to gfi bot +* :ghpull:`31043`: MAINT: new contributor bot ask for AI usage +* :ghpull:`30803`: {Radio,Check}Buttons: Add 2D grid labels layout support +* :ghpull:`31111`: Remove some code for compatibility with pyparsing<3 +* :ghpull:`31046`: Implement TeX's fraction and script alignment +* :ghpull:`31085`: Refactor RendererAgg.draw_{mathtext,text,tex} to use same base algorithm +* :ghpull:`28814`: patheffects.SimpleLineShadow calling non-existent get_foreground method from GraphicsContextBase +* :ghpull:`31090`: MAINT: Move to first-contribution action +* :ghpull:`31069`: Fix positioning of wide mathtext accents. +* :ghpull:`30938`: Update bundled FreeType and HarfBuzz libraries +* :ghpull:`31091`: BUG: Fix IndexLocator.tick_values returning values greater than vmax +* :ghpull:`31050`: ft2font: Extend OS/2 table with new fields +* :ghpull:`30039`: Rasterize dvi files without dvipng. +* :ghpull:`31081`: Switch from pre-commit to prek +* :ghpull:`30993`: PERF: Speed up log and symlog scale transforms +* :ghpull:`31082`: MNT: Rename check_getitem to getitem_checked +* :ghpull:`31080`: DOC: Fix missing references for updated FT2Font.set_text +* :ghpull:`30746`: Fix PDF bloat for off-axis scatter with per-point colors +* :ghpull:`31062`: Bump the actions group across 1 directory with 4 updates +* :ghpull:`31063`: Merge main back into text-overhaul branch +* :ghpull:`31056`: Keep mathtext boxes in xywh representation throughout. +* :ghpull:`31060`: MNT: Remove unused eventson context from artist property update +* :ghpull:`31059`: PERF: Refactor bezier poly coefficient calcs for speedup +* :ghpull:`31000`: PERF: Skip kwargs normalization in Artist._cm_set +* :ghpull:`31028`: DOC: Generate rcParams docs directly during build +* :ghpull:`31058`: TST: add basic test for set +* :ghpull:`31057`: DOC: Clarify Artist.set() behavior +* :ghpull:`31041`: Add tests for invalid properties and duplicate aliases in Artist.set +* :ghpull:`30978`: MNT: Discourage Artist.update +* :ghpull:`31016`: Doc: Clarify default levels behavior in contour/contourf +* :ghpull:`31031`: RadioButtons: fix self._clicked method (followup to #30997) +* :ghpull:`30059`: Drop the FT2Font intermediate buffer. +* :ghpull:`31013`: docs: improve contour docstring and wrap long lines +* :ghpull:`31044`: fix for sphinx_gallery < 0.16.0 +* :ghpull:`31033`: Add type hint for fig_kw in subplots +* :ghpull:`31030`: DOC: bring the credits page a little more up-to-date +* :ghpull:`31034`: DOC: Make grammatical corrections to documentation +* :ghpull:`30752`: Improving error message for width and position type mismatch in violinplot +* :ghpull:`31023`: Speedup normalize_kwargs by storing aliases in a more practical format. +* :ghpull:`31014`: TST: Fix warnings from Pillow for unavailable features +* :ghpull:`30935`: FIX: Handle AxesWidget cleanup after failed init +* :ghpull:`31020`: DOC: Fix doc builds with Sphinx 9 +* :ghpull:`31025`: DOC: move doc build options into tables and tabs +* :ghpull:`31024`: Fix formatting: add space after # in TODO comment +* :ghpull:`30997`: widgets: use a shared _Buttons class for {Radio,Check}Buttons +* :ghpull:`31010`: DOC: update and slightly reorg docs docs +* :ghpull:`31011`: Fix grammar: 'it would better' -> 'it would be better' in comment +* :ghpull:`31002`: Remove outdated notion of property alias priority from docs. +* :ghpull:`29881`: feat(CI): add Codecov Test Analytics for flaky and failed tests +* :ghpull:`30999`: Bump the actions group across 1 directory with 2 updates +* :ghpull:`30991`: Improve findfont cache invalidation. +* :ghpull:`30992`: Fix typo: remove extra space in MultiCursor deprecation message +* :ghpull:`30984`: DOC: update interactive rebase instructions +* :ghpull:`27946`: Add support for horizontal CheckButtons +* :ghpull:`30778`: MNT: remove decorator frames from traceback +* :ghpull:`30838`: Do not fail when markers are numpy integers +* :ghpull:`30977`: Revert exception handling case after numpy minver bump to 1.25 +* :ghpull:`30849`: Fix Axes.grid() to respect alpha in color tuples +* :ghpull:`30939`: DOC: Improve widgets API documentation +* :ghpull:`30970`: DOC: Move spectral plot examples from lines to statistics +* :ghpull:`30945`: Prevent blitting errors after canvas swap in RadioButtons and CheckButtons +* :ghpull:`30184`: Fixed several accuracy bugs with image resampling +* :ghpull:`30973`: DOC: modernise barh example +* :ghpull:`30956`: DOC: Some small additions to the API docs +* :ghpull:`30959`: DOC: Clarify matplotlib vs. matplotlib-base in conda +* :ghpull:`30950`: TST: account for flakiness with Numpy v1 +* :ghpull:`30954`: Fix trivial typo in example. +* :ghpull:`30947`: TST: always force the SETUPTOOLS_SCM version in test subprocesses +* :ghpull:`30949`: Add uv.lock to .gitignore +* :ghpull:`30948`: DOC: Improve linkage between rcParams-related documentation +* :ghpull:`30871`: Define the supported rcParams as code +* :ghpull:`30886`: BUG: Fix Windows subprocess timeouts with CREATE_NO_WINDOW flag +* :ghpull:`30777`: DOC: Introduce backend versions +* :ghpull:`30824`: Fixed bilinear interpolation for ``SegmentedBivarColormap`` +* :ghpull:`30942`: Bump pypa/cibuildwheel from 3.3.0 to 3.3.1 in the actions group +* :ghpull:`30918`: TST: account for asyncio changes in py314 +* :ghpull:`30937`: Merge branch 'v3.10.x' into main +* :ghpull:`30936`: DOC: Clarify data inputs for boxplot() and violinplot() +* :ghpull:`30855`: DOC: Clarify and unify set_linestyle +* :ghpull:`30921`: Exclude confirmed bugs from stale bot +* :ghpull:`30892`: Bump the actions group across 1 directory with 11 updates +* :ghpull:`30920`: FIX: Increase reruns for flaky test_invisible_Line_rendering (#30809) +* :ghpull:`30889`: MNT: Make transforms helper functions private +* :ghpull:`30922`: Reduce stale bot to run once per week +* :ghpull:`30912`: Pcolormesh Doc Fix +* :ghpull:`30916`: Docs: Remove outdated annotate_transform example, link to annotation tutorial +* :ghpull:`30919`: DOC: Correct typos on a/an usage including print messages +* :ghpull:`30914`: Fix outdated documentation links for violin/boxplot example +* :ghpull:`30907`: Inline intermediate constructs in axisartist demos. +* :ghpull:`30867`: Handle single color for multiple datasets in ``hist`` +* :ghpull:`30591`: FIX: Make widget blitting compatible with swapped canvas +* :ghpull:`30821`: Implements the Okabe-Ito accessible colormap. +* :ghpull:`30737`: Deprecate unused canvas parameter to MultiCursor +* :ghpull:`29966`: Fix AxesWidgets on inset_axes that are outside their parent. +* :ghpull:`30600`: Implement warning for Text3D's rotation/rotation_mode parameters +* :ghpull:`30847`: Fix test_ensure_multivariate_data on 32-bit systems +* :ghpull:`30856`: DOC: Rectangle: Link to FancyBboxPatch for rounded corners +* :ghpull:`30854`: DOC: Improve docs of legend loc=best +* :ghpull:`30863`: Fix macOS toolbar crash +* :ghpull:`30853`: Minor doc fixes re: close()ing figures. +* :ghpull:`30846`: Add pixi and uv install options to bug template +* :ghpull:`30842`: Update release docs for new publish workflow, remove old publish step +* :ghpull:`30841`: Add type annotation for LocationEvent.modifiers +* :ghpull:`30775`: FIX: figureoptions updates title string only +* :ghpull:`30726`: Enh/Add hatch pattern support to Axes.grouped_bar +* :ghpull:`30808`: Consolidate style parameter handling for plotting methods that call other plotting methods +* :ghpull:`30815`: MNT: Fix handling of ints in rgb_to_hsv() +* :ghpull:`30533`: gtk: Add more explicit version requirements +* :ghpull:`30835`: Improve error messages for mismatched s arg to scatter(). +* :ghpull:`30750`: FIX: when creating a canvas from a Figure use original dpi +* :ghpull:`30822`: DOC: Define the effect of rcParams["figure.raise_window"] = False +* :ghpull:`30052`: Setting imshow(animated=True) still show does not show an image +* :ghpull:`30820`: DOC: Add parameters documentation for FFMpegFileWriter +* :ghpull:`30816`: Fix typos in API interfaces documentation +* :ghpull:`30814`: DOC: Discouraged duplicate colormaps +* :ghpull:`30813`: Add legend.linewidth to rcParam type hint +* :ghpull:`30705`: Add testing for rcParams Literal type hints +* :ghpull:`30812`: DOC: remove duplicate whatsnew heading +* :ghpull:`30810`: Fix rstcheck failures +* :ghpull:`30334`: Add support for loading all fonts from collections +* :ghpull:`30760`: Fix axis3d to include offset text in tight bounding box calculation +* :ghpull:`30780`: Add legend.linewidth parameter to control legend box edge linewidth +* :ghpull:`30799`: DOC: don't index or unpack the return value of pie +* :ghpull:`30766`: Fix colorbar alignment with suptitle in compressed layout mode +* :ghpull:`30756`: Add legend support for PatchCollection +* :ghpull:`30782`: DOC: Reintroduce glossary +* :ghpull:`29494`: github: added explicit do not merge label to label check +* :ghpull:`30784`: correct statement about available methods in ``Quiver`` docstring +* :ghpull:`30733`: ENH: introduce PieContainer and pie_label method +* :ghpull:`30783`: DOC: Add example usage to make_keyword_only() +* :ghpull:`30776`: MNT: Declare table() to be not further developed +* :ghpull:`30774`: DOC: Fix documentation error of hexbin +* :ghpull:`30607`: Implement libraqm for vector outputs +* :ghpull:`30753`: Update mpl-sphinx-theme in environment.yml +* :ghpull:`30699`: [DOC] dev landing page admonition about AI usage/link to policy +* :ghpull:`30761`: DOC: Clarify restrictions on GenAI usage +* :ghpull:`30724`: Bump github/codeql-action from 4.31.0 to 4.31.2 in the actions group +* :ghpull:`30665`: Grammar corrections in User guide FAQ +* :ghpull:`30741`: Add :code-caption: option to plot directive +* :ghpull:`30736`: DOC: Correct grammatical issues especially on a/an usage +* :ghpull:`30627`: Remove forced fallback from FT2Font::load_char +* :ghpull:`30715`: Fix spacing in r"$\max f$". +* :ghpull:`30723`: Add file extension to whatsnew entry +* :ghpull:`30690`: Bump the actions group with 3 updates +* :ghpull:`30560`: Consistent zoom boxes +* :ghpull:`30565`: fix: Qt5Agg support darkmode icon by using svg +* :ghpull:`29989`: fix: Fix unstable tkagg small plot size. +* :ghpull:`30708`: doc: make external scipy link explicit +* :ghpull:`30511`: Update Colorizer/ColorizingArtist to work with MultiNorm +* :ghpull:`30696`: FIX: Account for horizontal/vertical lines in tightbox +* :ghpull:`30316`: Create RCKeyType +* :ghpull:`30686`: DOC: Remove notebook instructions from image tutorial +* :ghpull:`30684`: Update README links to static images +* :ghpull:`30640`: Bump the actions group across 1 directory with 6 updates +* :ghpull:`30677`: Merge branch 'main' into text-overhaul +* :ghpull:`30668`: cibw: Switch macos 13 to 15 Intel +* :ghpull:`30667`: DOC: Correct typos: lets -> let's [ci docs] +* :ghpull:`28831`: Improve the cache when getting font metrics +* :ghpull:`30655`: simplify ContourSet.draw +* :ghpull:`30652`: Stale action: sort issues by last updated +* :ghpull:`30636`: FIX: Keep legacy alpha behavior for violinplot without facecolor +* :ghpull:`30646`: merge up v3.10.7 +* :ghpull:`30639`: DOC: Add note about linear colorbar scale option for TwoSlopeNorm +* :ghpull:`30629`: Fix test_mult_norm_call_types on 32-bit systems +* :ghpull:`30634`: Don't force axes limits in hist2d. +* :ghpull:`29221`: Multivariate plotting in imshow, pcolor and pcolormesh +* :ghpull:`30630`: Update first-interaction from v3.0.0 to v3.1.0 +* :ghpull:`29695`: Add font feature API to Text +* :ghpull:`30608`: Prepare ``CharacterTracker`` for advanced font features +* :ghpull:`30531`: MNT: Pending-deprecate setting colormap extremes in-place +* :ghpull:`30543`: ENH: support x/y-axis zoom +* :ghpull:`30590`: MNT: Define Protocol for Animation.event_source +* :ghpull:`30619`: Include step info in str(scroll_event). +* :ghpull:`30620`: Add --debug flag to python -mmatplotlib.dviread CLI. +* :ghpull:`30499`: Improve cursor icons with RectangleSelector +* :ghpull:`30610`: Bump mpl-sphinx-theme version +* :ghpull:`30615`: Use auto to remove long typedefs in dlsym/GetProcAddress calls. +* :ghpull:`30616`: DOC: add what's new info for violin_stats +* :ghpull:`30606`: DOC: Fix raw string in mathtext unicode example +* :ghpull:`30603`: MNT: Fix some broken deprecations +* :ghpull:`30512`: pdf: Improve text with characters outside embedded font limits +* :ghpull:`29936`: Fix auto-sized glyphs with BaKoMa fonts +* :ghpull:`30573`: Add os.PathLike support to FT2Font constructor, and FontManager +* :ghpull:`30595`: ft2font: Split layouting from set_text +* :ghpull:`30596`: Cleanup donuts example. +* :ghpull:`29794`: Add language parameter to Text objects +* :ghpull:`30583`: MNT: Streamline deferred initialization of Colormap +* :ghpull:`30582`: MNT: Do not use colormap setters in tests +* :ghpull:`30567`: pdf: Merge loops for single byte text chunk output +* :ghpull:`30579`: Merge main back into text-overhaul branch to fix CI +* :ghpull:`30586`: ci: Bump Ubuntu ARM builder to 24.04 +* :ghpull:`30581`: TST: Force Agg backend in test_openin_any_paranoid +* :ghpull:`30569`: Copy-edit the "fonts in pdf and postscript" table. +* :ghpull:`30208`: Make path extension a bit safer +* :ghpull:`30577`: MNT: Move all Colormap extremes setter logic into a single _set_extremes() +* :ghpull:`30562`: DOC: improve description of boilerplate.py +* :ghpull:`30566`: pdf/ps: Track full character map in CharacterTracker +* :ghpull:`30335`: Use glyph indices for font tracking in vector formats +* :ghpull:`30561`: Bump github/codeql-action from 3.30.1 to 3.30.3 in the actions group +* :ghpull:`29855`: ENH: Allow to register standalone figures with pyplot +* :ghpull:`29742`: DOC: Explain how to start the mainloop after show(block=False) +* :ghpull:`29502`: CI: remove xfail on OSX + tk due to issues in image +* :ghpull:`30514`: Prepare for MetaFont/PK font support. +* :ghpull:`30536`: DOC: Cleanup/restructure PR guidelines +* :ghpull:`30405`: ENH: Scroll to zoom +* :ghpull:`30530`: Bump the actions group across 1 directory with 10 updates +* :ghpull:`30532`: MNT: Change default name of ListedColormaps +* :ghpull:`30535`: Fix: pytest warning - GioUnix was imported without specifying version +* :ghpull:`30520`: pdf: Simplify Type 3 font character encoding +* :ghpull:`30387`: MNT: Refactor default violin KDE estimator +* :ghpull:`30462`: FIX: Mark shared Axes as stale when propagating adjustable +* :ghpull:`30507`: DOC: Clarify draft PR and move from ways to contribute to PR guidelines +* :ghpull:`30465`: removed test_image_cursor_formatting() +* :ghpull:`29939`: Parse {lua,xe}tex-generated dvi in dviread. +* :ghpull:`30510`: Update syntax for PR welcome workflow +* :ghpull:`30000`: Implement text shaping with libraqm +* :ghpull:`30408`: MNT/DOC: Deprecate anchor in Axes3D.set_aspect +* :ghpull:`30491`: merge up v3.10.6 +* :ghpull:`30475`: Fix spelling error in ``contains_branch_separately`` method name +* :ghpull:`30505`: Add Linux Foundation Health Score badge to README +* :ghpull:`30423`: Fix Line3DCollection with autolim=True for lines of different lengths +* :ghpull:`30479`: Clarify inset_locator.inset_axes demo. +* :ghpull:`30467`: Let ticklabels respect set_in_layout(False). +* :ghpull:`30478`: MNT: correct _replacer docstring +* :ghpull:`30471`: DOC: Fix text formatting of imshow_extent example +* :ghpull:`30469`: Deprecate redundant axes parameter to RadialLocator. +* :ghpull:`30384`: Add datetime test for ax.violin +* :ghpull:`30470`: No need to sanitize extrema in Colorizer.set_clim +* :ghpull:`30468`: Let triage_tests support test modules with only figure_equals tests. +* :ghpull:`30433`: Use standard property alias machinery in contour(). +* :ghpull:`30459`: DOC: simplify hat graph example +* :ghpull:`30456`: DOC: Correct a typo: confuzzlment -> confuzzlement +* :ghpull:`30455`: DOC: Fix typo in axes docstring +* :ghpull:`30454`: Added handling for undetermined home directory +* :ghpull:`30453`: DOC: Fix missing references on text-overhaul branch +* :ghpull:`30401`: merge up v3.10.5 +* :ghpull:`30452`: DOC: Move capture_scroll What's new note to new directory +* :ghpull:`30403`: Add scroll capture functionality to WebAgg backend +* :ghpull:`29876`: MultiNorm class +* :ghpull:`30446`: Added hardcoded colormap attributes for type checker support +* :ghpull:`30441`: Bump github/codeql-action from 3.29.8 to 3.29.10 in the actions group +* :ghpull:`30328`: Fix legend ``labelcolor=‘linecolor’`` to handle various corner cases, e.g. step histograms and transparent markers +* :ghpull:`30440`: Document relative font sizes +* :ghpull:`30402`: Update release guide +* :ghpull:`30031`: merge up 3.10.3 +* :ghpull:`30425`: Remove outdated reference to matplotlibbaselinemarker in tex sources. +* :ghpull:`29358`: MNT: Registered 3rd party scales do not need an axis parameter anymore +* :ghpull:`30422`: DOC: remove some usages of None as explicit defaults +* :ghpull:`30304`: Move release related docs to new sub-folder +* :ghpull:`30416`: Bump the actions group across 1 directory with 7 updates +* :ghpull:`30404`: DOC: Scale axis parameter +* :ghpull:`30324`: Make PyFT2Font a subclass of FT2Font +* :ghpull:`30362`: {,Range}Slider: accept callable valfmt arguments +* :ghpull:`30226`: ENH: Add properties bottoms, tops, and position_centers to BarContainer +* :ghpull:`30398`: TST: Remove qt_core fixture +* :ghpull:`30396`: Fix the link to latest stable documentation +* :ghpull:`30382`: MNT: Remove explicit use of default value add_collection(..., autolim=True) +* :ghpull:`30383`: DOC: Simplify Line, Poly and RegularPoly example +* :ghpull:`29958`: ENH: ax.add_collection(..., autolim=True) updates view limits +* :ghpull:`30374`: TST: Make determinism test plots look less pathological +* :ghpull:`29716`: ENH: Add align parameter to broken_barh() +* :ghpull:`30284`: Fixed the overdeletion of source images for failing tests +* :ghpull:`30348`: Keep default minor log ticks if there's 1 major & 1 minor tick. +* :ghpull:`30273`: Fix mlab fallback for 32-bit systems +* :ghpull:`30143`: TYP: Make glyph indices distinct from character codes +* :ghpull:`29465`: ENH: Type the possible str legend locs as Literals +* :ghpull:`30375`: Fix highlighting of install docs. +* :ghpull:`30376`: Shorten setup of axes in simple_axis_pad demo. +* :ghpull:`30367`: Support passing xticks/yticks when constructing secondary_axis. +* :ghpull:`30368`: Switch get_grid_info to take a single Bbox as parameter. +* :ghpull:`29993`: Trigger events via standard callbacks in widget testing. +* :ghpull:`30363`: Register 'avif' format when available in Pillow +* :ghpull:`29890`: Show subprocess stdout and stderr on pytest failure +* :ghpull:`30373`: Mnt/test qol improvements +* :ghpull:`30359`: ENH: Allow tuple for borderpad in AnchoredOffsetbox +* :ghpull:`30366`: Cross-ref the two-scales and secondary-axes examples. +* :ghpull:`30349`: Axes can't set navigate_mode. +* :ghpull:`30347`: Small cleanups. +* :ghpull:`30322`: Deprecate setting text kerning factor to any non-None value +* :ghpull:`30332`: CI: Harden GHA configuration +* :ghpull:`30346`: MNT: Fix isort line length setting +* :ghpull:`30314`: [MNT] Typing: correct typing overloads for ````Figure.subfigures```` +* :ghpull:`30343`: Fix broken/deprecated documentation links in MEPs and testing guides +* :ghpull:`30330`: [fix] Spine.set_bounds() does not take parameter **None** as expected +* :ghpull:`30339`: MNT: Prefer capitalized logging levels +* :ghpull:`30340`: Bump the actions group with 2 updates +* :ghpull:`30302`: [MNT] Typing: Use Literal for set_loglevel +* :ghpull:`30001`: Include close matches in error message when key not found +* :ghpull:`30333`: FIX: cast Patch linewidth to float for dash scaling +* :ghpull:`30329`: Deprecate font_manager.is_opentype_cff_font +* :ghpull:`25573`: FIX: be very paranoid about checking what the current canvas is +* :ghpull:`30319`: Don't set a default size for FT2Font +* :ghpull:`29816`: Update FreeType to 2.13.3 +* :ghpull:`30317`: fix broken configobj link +* :ghpull:`30261`: [TYP] Add more literals to MarkerType +* :ghpull:`30312`: Replace deprecated imports +* :ghpull:`30315`: Fix link to pango +* :ghpull:`30272`: Log a warning if selected font weight differs from requested +* :ghpull:`30311`: Bump the actions group with 2 updates +* :ghpull:`30309`: Improve custom sphinx link redirect extension +* :ghpull:`30174`: FIX: Ensure Locators on RadialAxis are always correctly wrapped +* :ghpull:`30281`: Fix several minor typos +* :ghpull:`30275`: Create events type and update plt.connect and mpl_connect +* :ghpull:`30279`: fix(config): Correct invalid value for svg.fonttype in matplotlibrc +* :ghpull:`30134`: Add typing to AFM parser +* :ghpull:`30274`: ci: Fix image preload with multiple conflicts +* :ghpull:`30231`: ci: Preload existing test images from text-overhaul-figures branch +* :ghpull:`29115`: Use old stride_windows implementation on 32-bit builds +* :ghpull:`30235`: Don't expose private styles in style.available +* :ghpull:`30266`: DOC: fix artist see also sections +* :ghpull:`30258`: Clean up mypy & ruff config +* :ghpull:`30262`: Tweak docstrings of get_window_extent/get_tightbbox. +* :ghpull:`30239`: Upgrade to Visual Studio 2022 in appveyor.yml +* :ghpull:`30245`: Adjust logic in RcParams to allow for inheritance +* :ghpull:`30232`: Bump github/codeql-action from 3.29.0 to 3.29.2 in the actions group +* :ghpull:`30196`: agg: Replace facepair_t with std::optional +* :ghpull:`30200`: Add explicit signatures for pyplot.{polar,savefig,set_loglevel} +* :ghpull:`30178`: Abstract base class for Normalize +* :ghpull:`30220`: BUG: Include python-including headers first in src/ft2font.{cpp,h} +* :ghpull:`30199`: Add explicit getter / setter overloads for pyplot.{xlim,ylim} +* :ghpull:`30202`: Add explicit overloads for pyplot.{show,subplot} +* :ghpull:`29988`: Refactoring: Removing axis parameter from scales +* :ghpull:`30082`: Simplify dviFontInfo layout in backend pdf. +* :ghpull:`30163`: Prepare to turn matplotlib.style into a plain module. +* :ghpull:`30206`: Use collections.deque to store animation cache data. +* :ghpull:`29481`: Support individual styling of major and minor grid through rcParams +* :ghpull:`28764`: Fix argument types in examples and tests +* :ghpull:`30197`: DOC: Remove last userdemo example +* :ghpull:`30191`: Simplify RendererAgg::draw_markers buffers +* :ghpull:`30188`: Fixed incomplete deletion of all images that have passed tests before upload +* :ghpull:`30168`: Remove fallback code for glyph indices +* :ghpull:`29102`: TST: Calculate RMS and diff image in C++ +* :ghpull:`30145`: Remove ttconv backwards-compatibility code +* :ghpull:`30181`: Bump the actions group with 3 updates +* :ghpull:`28187`: Add a filename-prefix option to the Sphinx plot directive +* :ghpull:`30154`: Bump github/codeql-action from 3.28.18 to 3.28.19 in the actions group +* :ghpull:`30054`: Fixed an off-by-half-pixel bug in image resampling when using a nonaffine transform (e.g., a log axis) +* :ghpull:`30150`: Update font-related documentation +* :ghpull:`29199`: Fix center of rotation with rotation_mode='anchor' +* :ghpull:`30153`: Throw exception when alpha is out of bounds +* :ghpull:`30151`: Fix typo in backend_ps.py comment: change 'and them scale them' to 'and then scale them' +* :ghpull:`30107`: Add example to histogram colorbar on galleries +* :ghpull:`20716`: Type-1 font subsetting +* :ghpull:`30067`: Remove deprecations: is_bbox and more +* :ghpull:`28560`: ENH: Add grouped_bar() method +* :ghpull:`30137`: BLD: Remove FreeType from Agg backend extension +* :ghpull:`29392`: Fill hatch in PDF backend +* :ghpull:`30130`: Make NavigationToolbar.configure_subplots return value consistent +* :ghpull:`30132`: DOC: Clarify that types in docstrings do not use formal type annotation syntax +* :ghpull:`30131`: DOC: Document the properties of Normalize +* :ghpull:`30112`: Update to docs with regards to colorbar and colorizer +* :ghpull:`30004`: Remove apply_theta_transforms argument +* :ghpull:`30070`: Deprecate point_at_t and document that a BezierSegment can be called +* :ghpull:`30121`: Clean up AFM code +* :ghpull:`30123`: Fix FT_CHECK compat with macOS 10.15 +* :ghpull:`30088`: Parse FontBBox in type1font. +* :ghpull:`30099`: Fix tight-bbox computation of HostAxes. +* :ghpull:`30102`: Simplify/improve error reporting from ft2font. +* :ghpull:`30113`: Bump scientific-python/circleci-artifacts-redirector-action from 1.0.0 to 1.1.0 in the actions group +* :ghpull:`30100`: Use fix-cm instead of type1cm. +* :ghpull:`30109`: DOC: expand petroff10 example to include 6- and 8- styles +* :ghpull:`30044`: Replace FT2Image by plain numpy arrays. +* :ghpull:`30097`: remove point troubling regex +* :ghpull:`30090`: Simplify some Sphinx tests +* :ghpull:`30061`: Move test data into a single subdirectory +* :ghpull:`30085`: DOC: add API docs content guidelines to api docs instructions +* :ghpull:`30084`: DOCS: add plot types content guidance to docs +* :ghpull:`30087`: DOC: Add petroff6 and petroff8 to 'Named color sequences' example +* :ghpull:`30080`: Bump the actions group with 3 updates +* :ghpull:`30065`: ENH: Add Petroff 6 and 8 color cycle style sheets +* :ghpull:`30077`: Fix deprecated attribute name in backend_pdf. +* :ghpull:`30069`: Close star polygons +* :ghpull:`30062`: Add 3D scatter test for cmap update +* :ghpull:`30066`: Remove get_bbox_header +* :ghpull:`30045`: CI: try running the precommit hooks on GHA +* :ghpull:`29910`: DOC: add warnings about get_window_extent and BboxImage +* :ghpull:`30032`: Add Matplotlib Journey online course to external resources +* :ghpull:`30055`: Renamed an RST file to remove a leading space in its filename +* :ghpull:`30049`: DOC: consolidate version switcher guidance +* :ghpull:`30050`: DOC: Additional tip to exclude undesired matches in GitHub code search +* :ghpull:`30005`: Remove cm.get_cmap +* :ghpull:`30048`: DOC: version switcher update on release +* :ghpull:`30047`: Update version switcher for 3.10.3 +* :ghpull:`30036`: Remove cutout for missing font file in PdfFile._embedTeXFont. +* :ghpull:`29847`: ci: restrict 'pygobject-ver' for Ubuntu 22.04 jobs +* :ghpull:`30030`: Add "sans" alias to rc() to allow users to set font.sans-serif +* :ghpull:`30040`: Improve usetex and pgf troubleshooting docs. +* :ghpull:`30037`: Update top message matplotlibrc file +* :ghpull:`30035`: Remove meson-python pinning +* :ghpull:`30006`: Enable linting of .pyi files +* :ghpull:`30020`: Micro-optimize _to_rgba_no_colorcycle. +* :ghpull:`30027`: Make PdfFile font-related attributes private. +* :ghpull:`29829`: Rework mapping of dvi glyph indices to freetype indices. +* :ghpull:`30023`: Remove unused ``_api`` import +* :ghpull:`30014`: Remove deprecated get_tick_iterator() +* :ghpull:`30015`: Expire deprecation of nth_coord arguments +* :ghpull:`30019`: FIX #30007: Raise ValueError when all wedge sizes are zero in ax.pie +* :ghpull:`30016`: Bump github/codeql-action from 3.28.16 to 3.28.17 in the actions group +* :ghpull:`30003`: DOC: missing word + add latex dep section +* :ghpull:`29341`: Type annotation add_subplot for projection="3d" +* :ghpull:`29764`: added latex requirements from fedora spec +* :ghpull:`29918`: DOC: Add descriptions to matplotlib.typing +* :ghpull:`27576`: Fix specifying number of levels with log contour +* :ghpull:`29879`: Adding elinestyle property to errorbar +* :ghpull:`29984`: FIX: Typing of FuncAnimation +* :ghpull:`29973`: Use inline lambdas to define most FT2Font properties. +* :ghpull:`29982`: Bump the actions group with 5 updates +* :ghpull:`29972`: Improve repr of mathtext internal structures; minor cleanup. +* :ghpull:`29356`: Add a last resort font for missing glyphs +* :ghpull:`29873`: Handled non finite values in ax.pie - issue #29860 +* :ghpull:`29916`: Bump the actions group with 2 updates +* :ghpull:`27183`: Fix behaviour of Figure.clear() for SubplotParams +* :ghpull:`29954`: Simplify ``colored_line()`` implementation in Multicolored lines example +* :ghpull:`29956`: MNT: make signature of GridSpec.update explicit +* :ghpull:`29203`: Fixed imsave() saving incorrect color map +* :ghpull:`29946`: Changed "Autoscaling axes" to "Autoscaling axes on user guide page for issue & closes #29906 +* :ghpull:`29948`: Check Axes/Figure import paths in boilerplate.py +* :ghpull:`29904`: API: bump minimum supported version of Python and numpy +* :ghpull:`29945`: Doc fixed aspect colorbar +* :ghpull:`29944`: DEV: have ruff check blank-line counts +* :ghpull:`29923`: Fix signature of disabled draw methods +* :ghpull:`29614`: add detail to doc string in Line3DCollection +* :ghpull:`29843`: Fix loading of Type1 "native" charmap. +* :ghpull:`29911`: Bump pre-commit versions +* :ghpull:`29892`: FIX: make_image should not modify original array +* :ghpull:`29905`: Remove hatchcolors parameter from draw_quad_mesh +* :ghpull:`29898`: backend_bases.pyi: ``@overload`` ``FigureCanvasBase.mpl_connect()`` for different event types +* :ghpull:`29745`: Use PEP8 style method and function names from pyparsing +* :ghpull:`29762`: Use ruff instead of flake8 to check PEP8 +* :ghpull:`29885`: Bump github/codeql-action from 3.28.13 to 3.28.14 in the actions group +* :ghpull:`29592`: DOC: Remove simple_legend examples from User Demo +* :ghpull:`29875`: DOC: Improve description of background/bbox handling for Text +* :ghpull:`29612`: ENH: Support units when specifying the figsize +* :ghpull:`29833`: TST: remove (most) text from constrained layout tests +* :ghpull:`29870`: doc: a grammatical error in pyplot comment +* :ghpull:`29831`: Inline _calc_extents_from_path. +* :ghpull:`29851`: Do not extraneously clip 3D plots +* :ghpull:`29846`: ci: cleanup: remove stale/outdated version range restrictions +* :ghpull:`29841`: Bump the actions group with 2 updates +* :ghpull:`29850`: MNT: Use Gcf.destroy(manager) instead of Gcf.destroy(manager.num) +* :ghpull:`29765`: ci: Introduce ubuntu-24.04 to restore GTK test coverage with recent PyGObject versions +* :ghpull:`29838`: Switch Tfm metrics to TrueType-compatible API. +* :ghpull:`29783`: Fix log scaling for pcolor and pcolormesh +* :ghpull:`29832`: MNT: expire legend-related deprecations +* :ghpull:`29044`: Add hatchcolor parameter for Collections +* :ghpull:`29828`: Improve output of dvi debug parsing. +* :ghpull:`29798`: Ensure polar plot radial lower limit remains at 0 after set_rticks + plot +* :ghpull:`29830`: Fix git fetch on development workflow +* :ghpull:`29776`: Filter images in premultiplied alpha mode. +* :ghpull:`29821`: Tweak minimal checks for GUI binding installs. +* :ghpull:`29808`: ENH: set default color cycle to named color sequence +* :ghpull:`29817`: Prepare for {xe,lua}tex support in usetex. +* :ghpull:`27972`: Fix ngrids support in axes_grid.Grid(). +* :ghpull:`29804`: replace quansight-labs/setup-python with actions/setup-python +* :ghpull:`29800`: Bump the actions group with 6 updates +* :ghpull:`29083`: DOC: Update page to note installation for ninja library +* :ghpull:`29698`: Improve tick subsampling in LogLocator. +* :ghpull:`29701`: Bump the actions group across 1 directory with 7 updates +* :ghpull:`28352`: Add compilers to conda environment +* :ghpull:`29696`: ENH: Add support for per-label padding in bar_label +* :ghpull:`29582`: Add ``rasterized`` option to ``contourf`` +* :ghpull:`29759`: DOC: expand use of fun tag +* :ghpull:`29758`: DOC: consolidate tags +* :ghpull:`29756`: Consolidate color tags +* :ghpull:`29747`: Revert "NEP 29 > SPEC 0 in dependency policy" +* :ghpull:`29744`: NEP 29 > SPEC 0 in dependency policy +* :ghpull:`29700`: merge up v3.10.1 +* :ghpull:`26774`: Connect the Animation event source callback in the constructor. +* :ghpull:`29729`: DOC: Improve What's new entry description +* :ghpull:`29718`: Update version switcher for 3.10.1 +* :ghpull:`29602`: MNT: Reduce the use of get_xticklabels() in examples +* :ghpull:`29705`: DOC: improve dev install docs +* :ghpull:`29644`: [Doc] Added images of hatches to hatch API page +* :ghpull:`29697`: MNT: remove ``plot_date`` +* :ghpull:`29690`: Add test cases for patch.force_edgecolor behavior with facecolor="none" +* :ghpull:`29558`: Consolidate align_labels_demo and align_ylabels gallery examples +* :ghpull:`29660`: fix: broken link +* :ghpull:`29639`: Bump the actions group across 1 directory with 7 updates +* :ghpull:`29620`: DOC: Add tip how to use GitHub code search to estimate the impact of a deprecation +* :ghpull:`29613`: doc: add link to analytics page +* :ghpull:`29593`: Fix tick_params() label rotation mode +* :ghpull:`29589`: DOC: Minor example cleanup +* :ghpull:`29580`: DOC: More cleanup of missing-references.json +* :ghpull:`29581`: Use functools.cache instead of lru_cache to establish singletons. +* :ghpull:`29566`: DOC: Remove invalid link in Communication Guide +* :ghpull:`29565`: Remove rcParams deprecation machinery +* :ghpull:`29561`: DOC: Document _CollectionWithSizes +* :ghpull:`29569`: Ignore ImageMagick deprecation of "convert" command. +* :ghpull:`29574`: 3D depthshade what's new plot +* :ghpull:`29052`: FIX: Checks for (value, color) tuples in LinearSegmentedColormap.from_list +* :ghpull:`29556`: Spacing for description of linecolor +* :ghpull:`28784`: Improve fallback font export tests +* :ghpull:`28968`: Implement xtick and ytick rotation modes +* :ghpull:`29450`: Remove some unused resample code +* :ghpull:`29503`: Improve error message for shape mismatches in barh function +* :ghpull:`29553`: DOC: update active social media list +* :ghpull:`27304`: Allow user to specify colors in violin plots with constructor method +* :ghpull:`29287`: Fix depth shading on 3D scatterplots +* :ghpull:`29398`: Speed up Collection.set_paths +* :ghpull:`29525`: Add new method Colormap.with_alpha() +* :ghpull:`29537`: Fix: Ensure ScalarFormatter.set_useOffset properly distinguishes betw… +* :ghpull:`29533`: Minor cleanups. +* :ghpull:`29397`: 3D plotting performance improvements +* :ghpull:`29529`: MNT: Deprecate other capitalization than "None" in matplotlibrc +* :ghpull:`29526`: DOC: better separation of codespace instructions +* :ghpull:`29486`: FIX: Make stem() baseline follow the curvature in polar plots +* :ghpull:`29460`: ENH: Add bad, under, over kwargs to Colormap +* :ghpull:`29435`: Fix ``plot_wireframe`` with nonequal ``rstride``, ``cstride``, plus additional speedups +* :ghpull:`29491`: Bump the actions group across 1 directory with 2 updates +* :ghpull:`29375`: Doc: document pending deprecation procedure +* :ghpull:`29497`: ci: Fix cache key for Matplotlib data +* :ghpull:`29473`: CI: add py312 and py313 on windows on azure to test matrix +* :ghpull:`29477`: ci: Add an ARM Linux test workflow +* :ghpull:`29372`: DOC / BUG: Fix savefig to GIF format with .gif suffix +* :ghpull:`29028`: Update colormap usage documentation to prioritize string colormap names +* :ghpull:`29461`: DOC: Use color specification reference in matplotlib.colors docs +* :ghpull:`29438`: ft2font: Avoid undefined enum values +* :ghpull:`29463`: Fix dead links in dev workflow docs +* :ghpull:`29464`: DOC: Add missing examples for legend outside positions +* :ghpull:`29433`: Remove erroneous statement in multipage PDF example +* :ghpull:`29441`: DOC: Rename Twitter to X +* :ghpull:`29399`: plot_wireframe plotting speedup +* :ghpull:`29325`: Propagate Axes class and kwargs for twinx and twiny +* :ghpull:`29424`: MNT: Turn Axes._axis_map into a static dict instead of a property +* :ghpull:`29427`: BUG: Fix regression with set_hatchcolor +* :ghpull:`29419`: Merge v3.10.x into main +* :ghpull:`29413`: [pre-commit.ci] pre-commit autoupdate +* :ghpull:`29415`: Bump the actions group across 1 directory with 5 updates +* :ghpull:`29338`: Use set_window_title rather than set_label to set title of webagg figure +* :ghpull:`29388`: FIX: get_tick_position() should disregard major/minor ticks that are not drawn +* :ghpull:`27327`: Update for checking whether colors have an alpha channel +* :ghpull:`29405`: DOC: Clearer wording for the installation of external dependencies +* :ghpull:`29402`: Expand set_ticklabels warning +* :ghpull:`29400`: Fix/Suppress more missing references +* :ghpull:`29394`: Tick rendering speedups +* :ghpull:`29386`: MNT: Remove ``*args`` for ``OffsetBox.__init__()`` +* :ghpull:`28104`: Separates edgecolor from hatchcolor +* :ghpull:`29377`: DOC: change wording on new contributor path +* :ghpull:`29376`: API: bump the minimum version of pillow +* :ghpull:`29333`: ENH: Streamplot control for integration max step and error +* :ghpull:`29342`: MNT: Warn on using pixel marker for scatter() +* :ghpull:`29344`: MNT: Coerce LineStyleType strings to Literal +* :ghpull:`29354`: Use _val_or_rc in more places +* :ghpull:`29360`: DOC: update switcher for 3.10 +* :ghpull:`29174`: ``indicate_inset`` transform support +* :ghpull:`27551`: Move axisartist towards untransposed transforms (operating on (N, 2) arrays instead of (2, N) arrays). +* :ghpull:`24714`: Improve handling of degenerate jacobians in non-rectilinear grids. +* :ghpull:`29343`: MNT: Discourage alternate strings for 'none' linestyle +* :ghpull:`29054`: Label log minor ticks if only one log major tick is drawn. +* :ghpull:`29346`: DOC: fix typos +* :ghpull:`29340`: FIX: pass renderer through adjust_bbox +* :ghpull:`29345`: MNT: Remove duplicate assignment +* :ghpull:`29329`: CI: allow pandas install to fail on nightly test run +* :ghpull:`29322`: DOC: Add [*Discouraged*] prefix to summary lines +* :ghpull:`25870`: Adds error handling around install_repl_displayhook +* :ghpull:`29303`: DOC: Enhance documentation on discouraged API +* :ghpull:`29280`: Apply some modernization to C++ extensions +* :ghpull:`23085`: Update art3d.py to address strange behavior of depthshading on 3D scatterplots with close points +* :ghpull:`29215`: added venv to gitignore +* :ghpull:`29257`: fix typo +* :ghpull:`28775`: DOC: manually placing images example +* :ghpull:`29222`: TST: Simplify parts of animation tests +* :ghpull:`29220`: DOC: Set stable version to 3.9.3 +* :ghpull:`29214`: Fix typo in _LazyTickList class comment (lis -> list) +* :ghpull:`29171`: ci: Remove Linux & macOS from Azure +* :ghpull:`29187`: DOC: verify your changes +* :ghpull:`29184`: Minor tweaks to image docs. +* :ghpull:`29172`: Minor cleanups to docstrings, comments, and error messages. +* :ghpull:`29155`: Delay warning for deprecated parameter 'vert' of box and violin +* :ghpull:`27617`: Add new num_arrows option to streamplot +* :ghpull:`29135`: Deprecate ListedColormap(..., N=...) parameter +* :ghpull:`29147`: Simplify synthetic event generation in interactive pan/zoom tests. +* :ghpull:`29150`: TST: Run macosx backends in a subprocess +* :ghpull:`29066`: Check pressed mouse buttons in pan/zoom drag handlers. +* :ghpull:`29087`: DOC: escape broken cross links +* :ghpull:`29127`: MNT: Refactor matplotlib.colors.from_levels_and_colors() +* :ghpull:`29125`: Make ListedColormap.monochrome a property +* :ghpull:`29074`: Add "standard" Axes wrapper getters/setters for Axis invertedness. +* :ghpull:`29078`: Document how to discourage API +* :ghpull:`29079`: DOC: Replaced colormap for colorblindness +* :ghpull:`29077`: DOC: Replaced green with blue for colorblindness + +Issues (246): + +* :ghissue:`14235`: Add \underline to mathtext? +* :ghissue:`31462`: [Bug]: Errorbar plot on log-scaled Axes sets incorrect automatic lower limits +* :ghissue:`30859`: [Bug]: ax.relim() ignores scatter artist +* :ghissue:`31523`: [Bug]: twinx() and twiny() crash with cryptic errors on 3D axes +* :ghissue:`26901`: [ENH]: Remove ``canvas.draw`` from ``widgets.Cursor.onmove`` +* :ghissue:`30831`: [Bug]: AttributeError: 'TimedAnimation' object has no attribute '_framedata' +* :ghissue:`31513`: [Bug]: Flaky test_contour.py::test_labels on minver CI +* :ghissue:`24716`: [TST]: Add classic style to all old image tests. +* :ghissue:`28488`: [ENH]: Provide a way to avoid subcommands on import. +* :ghissue:`30413`: [MNT]: c++11 narrowing error when building for 32 bit targets +* :ghissue:`31122`: [ENH]: Give control whether twinx() or twiny() overlays the main axis +* :ghissue:`4822`: Light font variants cannot be accessed by common name +* :ghissue:`21409`: [Bug]: twinx and twiny ignores previous set_position +* :ghissue:`31404`: [Bug]: Crash when removing contour set after removing contour labels +* :ghissue:`30365`: [Bug]: Type hints for xy and xycoords in annotate are too strict +* :ghissue:`13044`: Center of rotation for text with rotation_mode='anchor' +* :ghissue:`29253`: [Bug]: Numbers in words not italic +* :ghissue:`31220`: Should we use font metrics for line height instead of "lp"? +* :ghissue:`22172`: [Bug]: \genfrac has bad spacing with (high) custom ruler +* :ghissue:`18389`: Vertical positioning in mathtext fraction rendering could be improved +* :ghissue:`18086`: sub/superscripts should be moved further from the baseline following large delimiters +* :ghissue:`3135`: Please add support for ttc font files (PDF/PS output) +* :ghissue:`16566`: OTF feature support (alternate figure styles, etc.) +* :ghissue:`20842`: [MNT]: Please update freetype version +* :ghissue:`8765`: Indic Script labels not rendered correctly +* :ghissue:`2071`: matplotlib can't handle "newer" TrueType fonts +* :ghissue:`23082`: [Bug]: Font rendering bug for Devanagari text +* :ghissue:`29357`: [Bug]: Incorrect rendering of Abugida fonts on Matplotlib visualization +* :ghissue:`29806`: [Feature Request] Proper Arabic Language Support in Matplotlib Plots +* :ghissue:`5210`: Unexpected replacement of \right) with exclamation point in MathTextParser output +* :ghissue:`9681`: Determine if ``hinting_factor`` setting can be dropped +* :ghissue:`21797`: [Bug]: Math fonts (Type 3) incorrectly embedded in PDF? +* :ghissue:`31464`: [Doc]: finding the simple example +* :ghissue:`31454`: [Doc]: Amend AI policy by a concrete list of dos and don’ts +* :ghissue:`31337`: wording questions +* :ghissue:`31406`: [ENH]: [Bug]: secondary_xaxes().set_xlim/xbound should warn or raise that it is ineffective +* :ghissue:`31400`: [ENH]: Support partial figsize setting +* :ghissue:`26620`: [Doc]: Improve legend loc and bbox_to_anchor documentation +* :ghissue:`31369`: Dead link in colormap docs [Ware] +* :ghissue:`31344`: [Bug]: Adding contour labels affects the shape of filled contours +* :ghissue:`31286`: [MNT]: Scale ``val_in_range`` method +* :ghissue:`30651`: [MNT]: Add copyright information for google's "turbo" colormap? +* :ghissue:`28298`: [Bug]: set linestyle='dashed' raise error with quiver and legend +* :ghissue:`31302`: ``stairs`` with dashed linestyle and fill=True raises ValueError +* :ghissue:`27870`: [ENH]: out-of-tree Pyodide builds in CI for Matplotlib +* :ghissue:`31164`: [MNT]: Adopt Scikit Learn's autoclose bot +* :ghissue:`31320`: [DOC]: Using matplotlib.pyplot.pcolormesh with shading='flat' +* :ghissue:`31247`: [Bug]: Changing limits by setting ticks does not emit "x/ylim_changed" +* :ghissue:`18159`: Add zoom_factory to matplotlib - where to put? +* :ghissue:`31235`: [Doc]: bad rendering of matplotlib.pyplot.quiver docs +* :ghissue:`31126`: [Bug]: FigureCanvasTkAgg renders clipped/oversized when embedded in layout-managed container on Windows HiDPI +* :ghissue:`15313`: star (*) symbol in text box cuts off bottom of text when saved +* :ghissue:`31182`: [Bug]: ``ax.hist()`` fails on sequence of timedeltas due to comparison with ``np.inf`` +* :ghissue:`31256`: [ENH]: Extend semilogx, etc to 3D +* :ghissue:`209`: 3D scatter plots don't work in logscale +* :ghissue:`23306`: [ENH]: allow passing a function to ``CallbackRegistry.disconnect`` +* :ghissue:`28766`: [Bug]: Alignment of minus sign when using LaTeX +* :ghissue:`31093`: [ENH]: Modifier key to discretize rotations for 3D plots +* :ghissue:`31194`: [ENH]: add ``errorbar.capthick`` and ``errorbar.elinewidth`` to mplstyle +* :ghissue:`31221`: [Doc]: ticks/ticklabels_rotation example should mention rotation_mode="xtick"/"ytick" +* :ghissue:`20779`: [ENH]: move .matplotlib folder from %USERPROFILE% on Windows +* :ghissue:`31225`: [Bug]: set_edgecolor(None) cannot recover the default style after changing the edge color of wedges with hatches +* :ghissue:`26092`: [Bug]: alpha array-type not working with RGB image in imshow() +* :ghissue:`31009`: [Bug]: Large pixels may overlap when using imshow() +* :ghissue:`31127`: [Doc]: quiver 3d does not support "mid" as an alias for "middle", but quiver 2d does +* :ghissue:`30848`: [MNT]: Should we request contributors to declare usage of AI? +* :ghissue:`25914`: [Doc]: replace usages of ``.imread`` with PIL.Image.open +* :ghissue:`30934`: [ENH]: Implement gapcolor for patch edges +* :ghissue:`24499`: [Doc]: Transformation tutorial uses outdated description for polar transform +* :ghissue:`31149`: [ENH]: Improve compatibility with array-like objects implementing __array__ (e.g. MLX arrays) +* :ghissue:`31135`: [Bug]: Setting figure for polar axes breaks the polar coordinates +* :ghissue:`28793`: ``patheffects.SimpleLineShadow`` calling non-existent ``get_foreground`` method from GraphicsContextBase +* :ghissue:`30658`: [MNT]: First contributor workflow fails for first contributors +* :ghissue:`19299`: wide mathtext accents are mis-centered +* :ghissue:`31086`: [Bug]: Colorbar get_ticks() return the incorrect array +* :ghissue:`2488`: Off-axes scatter() points unnecessarily saved to PDF when coloured +* :ghissue:`29551`: [Bug]: 3D tick label position jitter when rotating the plot view +* :ghissue:`30957`: [MNT]: Clarify the difference between Artist.set and Artist.update +* :ghissue:`30996`: [Doc]: ``contour`` and ``contourf`` levels default not specified +* :ghissue:`31003`: [ENH]: Add types for ``fig_kw`` argument in ``subplots`` +* :ghissue:`30417`: [ENH]: Support using datetimes as ``positions`` argument to violin(...) +* :ghissue:`30575`: [Bug]: Regression in widget behavior +* :ghissue:`23763`: [Bug]: Inconsistent rendering between backends when rendering Mathtext horizontal rule +* :ghissue:`23860`: [Bug]: Font weight of label cannot be overwritten from rcParams when using mathtext +* :ghissue:`29475`: [Doc]: interactive rebase instructions outdated? +* :ghissue:`29863`: [ENH]: Should we hide _preprocess_data from the stack trace? +* :ghissue:`30836`: [Bug]: Markers can be integers, but numpy integers fail +* :ghissue:`22231`: [Bug]: Axes.grid(color) ignores alpha +* :ghissue:`14143`: imshow pixel boundaries wrong when zoomed in +* :ghissue:`1441`: Misalignment imshow vs. grid lines +* :ghissue:`30882`: [Bug]: Flaky tests with "Python 3.11 on ubuntu-22.04 (Minimum Versions)" +* :ghissue:`27590`: [Bug]: Qt5 backend icons should be white when macOS in dark mode +* :ghissue:`23531`: [Doc]: Documentation of rc parameters could be improved +* :ghissue:`30559`: [ENH]: Backend versioning +* :ghissue:`30917`: [Bug]: TimerAsyncio does not work with Python 3.14 +* :ghissue:`30709`: [Bug]: Mismatch in documented default behaviour of pcolormesh 'snap' +* :ghissue:`30463`: [Doc]: Two sources of a gallery figure for normal and high-DPI screen are different +* :ghissue:`28983`: [Doc]: outdated links for violin/boxplot +* :ghissue:`30857`: [Bug]: ValueError: The 'color' keyword argument must have one color per dataset +* :ghissue:`29332`: [ENH]: Typing: broaden acceptable floats +* :ghissue:`23633`: [MNT]: Deprecated / discourage less used Axes methods forwarding to Axis methods +* :ghissue:`21496`: [MNT]: MultiCursor should not take canvas as first parameter +* :ghissue:`30563`: [Bug]: 3D text does not respect rotation to make it parallel with a given zdir axis +* :ghissue:`27969`: [ENH]: Please add ``matplotlib.patches.RoundedRectangle`` +* :ghissue:`29319`: [Bug]: Legend with location set to ‘best’ overlaps with the title when the titles is moved down +* :ghissue:`28513`: [Bug]: Segfault when using ``close_event`` with macosx backend and tk +* :ghissue:`30840`: [MNT]: ``LocationEvent.modifiers`` missing in type stub +* :ghissue:`30770`: [Bug]: Bug / Inconsistency: Title Format Lost After Interactive Editing +* :ghissue:`30673`: [ENH]: Add custom hatch styling to grouped_bar +* :ghissue:`30804`: [Bug]: Stackplot does not pass ``facecolor(s)`` correctly to fill_between +* :ghissue:`30537`: Permanent solution for GioUnix warning +* :ghissue:`27224`: [Bug]: pickling and unpickling hidpi a qt figure that has been already shown doubles its physical size +* :ghissue:`26380`: [Bug]: DPI keeps doubling when creating a new MatPlotLib QtWidget in qt6 +* :ghissue:`20415`: figure.raise_window keyword produces inconsistent results +* :ghissue:`18985`: Why does setting imshow(animated=True) still show an image? +* :ghissue:`22831`: [Doc]: Arguments of FFMpegFileWriter not clear. +* :ghissue:`30796`: [Doc]: Information about deprecated colormaps missing from recent versions of the documentation +* :ghissue:`7059`: Decoupling hatch from edges +* :ghissue:`30744`: [Bug]: axis3d.Axis.get_tightbbox() is not including the offset_text +* :ghissue:`30767`: [ENH]: Add rcParams for the width of the legend's box edge +* :ghissue:`30472`: [Bug]: layout=compressed conflict with suptitle +* :ghissue:`23998`: Labels for PatchCollection do not show +* :ghissue:`28889`: [Doc]: Reintroduce glossary for matplotlib terms and concepts +* :ghissue:`22402`: [Doc]: Quiver docstring incorrectly claims that only ``UVC`` can be set +* :ghissue:`19338`: Allow option to display absolute values for pie chart +* :ghissue:`30664`: [MNT]: Declare table() to be not further developed +* :ghissue:`30764`: [Bug]: Hexbin with bins='log' doesn't handle zeros as described +* :ghissue:`30439`: [Doc]: Link AI policy on contributing page +* :ghissue:`30740`: [ENH]: Support caption for code block in sphinx plot directive +* :ghissue:`30695`: [Bug]: bbox_inches='tight' works differently when ax.plot() have markers +* :ghissue:`30257`: [MNT] [TYPING]: Use of Literal +* :ghissue:`20724`: ToolHandles/ToolLineHandles could set the mouse cursor when hovered over or active +* :ghissue:`20554`: Remove discussion of jupyter backends from image tutorial +* :ghissue:`28827`: [Bug]: FontProperties objects are not deleted when using fig.savefig +* :ghissue:`30644`: [Doc]: Stable docs reporting as unstable +* :ghissue:`30613`: [Bug]: violin's default alpha no longer persists +* :ghissue:`22197`: [Bug]: TwoSlopeNorm behaves like CenteredNorm +* :ghissue:`30522`: [MNT]: PR Greeting GHA not working +* :ghissue:`30574`: [Bug]: Unicode symbols encoded with ``\u....`` with mathtext raise ParseFatalException +* :ghissue:`27190`: [Doc]: clarify when and how to use boilerplate.py +* :ghissue:`26739`: Write a separate doc-string for Line3DCollection +* :ghissue:`19956`: Native support for showing OOP-created figures +* :ghissue:`28412`: [ENH]: Zoom in/out on rolling the mouse wheel +* :ghissue:`30525`: [Bug]: Pipeline fails with "GioUnix was imported without specifying a version first" +* :ghissue:`30436`: [Doc]: new contributor guidance on draft PRs +* :ghissue:`30364`: [MNT]/[DOC]: Look into Axes3D.set_aspect ``anchor`` and ``adjustable`` arguments +* :ghissue:`30474`: [Bug]: Typo in method name: contains_branch_separately +* :ghissue:`30418`: [Bug]: error using ``add_collection3d`` of ``Line3DCollection`` with ``autolims=True`` and lines containing different numbers of points +* :ghissue:`30263`: [ENH]: Allow ignoring x-extent (but not y-extent) of xticklabels when computing axes extents (e.g. for geometry manager) +* :ghissue:`30296`: [MNT]: Deprecate the axes parameter to RadialLocator +* :ghissue:`29774`: [Bug]: triage_tests.py is brittle against failures in test modules that have only check_figures_equal test +* :ghissue:`29349`: [MNT]: Remove axis parameter from scales +* :ghissue:`1963`: Singular keyword arguments in contour don't raise exceptions +* :ghissue:`30449`: [Bug]: Config directory location finder doesn't account for the home directory being undetermined. +* :ghissue:`30438`: [Bug]: missing stubs for ``plt.cm`` (a.k.a. ``matplotlib.pyplot.cm``) +* :ghissue:`30298`: [Bug]: Legend kwarg ``labelcolor='linecolor'`` not working properly when ``facecolor`` is ``'None'`` +* :ghissue:`30437`: [Doc]: Clarification of relative font sizes +* :ghissue:`30400`: [Bug]: Megabyte-level memory leak when using imshow() in a loop +* :ghissue:`29957`: [ENH]: add_collection(..., autolim=True) should update view limits as well +* :ghissue:`22720`: [MNT]: Generalize widget mouse testing +* :ghissue:`28809`: [ENH]: Support avif as output format +* :ghissue:`30331`: [ENH]: inset_axes has borderpadding, but not x/y individually. +* :ghissue:`29300`: [Bug]: Background of rotated png is rendered black +* :ghissue:`30323`: [MNT]: validate linewidth +* :ghissue:`25572`: [Bug]: Artist.remove() isn't fully removing it from figure +* :ghissue:`30325`: [Bug]: fig.savefig throws error after radiobutton axes is removed +* :ghissue:`15529`: Chinese font can``t change the weight +* :ghissue:`30164`: [Bug]: Removing spines in polar plot causes distortion of the plot +* :ghissue:`27232`: BUG: .notdef glyph has to be present in fonts in fontlist +* :ghissue:`14239`: rotated text does not align +* :ghissue:`23021`: [Bug]: Text rotation leads to characters being misplaced within their bounding boxes. Attempted solution provided. +* :ghissue:`30160`: [MNT]: pyplot type hints +* :ghissue:`13919`: Impossible to configure minor/major grid line style independently in rcParams +* :ghissue:`25800`: [MNT]: Remove the userdemo section in examples +* :ghissue:`24313`: [ENH]: API discussion for grouped bar charts +* :ghissue:`29722`: [MNT]: Upcoming version of ``pyparsing`` will start emitting ``DeprecationWarnings`` for legacy pre-PEP8 method and argument names +* :ghissue:`30026`: [Doc]: add histogram as colorbar example +* :ghissue:`127`: When text.usetex=True with pdf backend, full subset of latex fonts is embedded into pdf file +* :ghissue:`10034`: Hatching is rendered differently by agg, pdf and svg backends. +* :ghissue:`19832`: Positioning floating_axes.FloatingSubplot +* :ghissue:`29791`: [Bug]: Saving as an SVG and PDF produce different outputs with Latex characters, with wrong character sizing +* :ghissue:`28675`: [Bug]: ``multialignment='right'`` in ``ax.text()`` with ``path_effects`` breaks when using LaTeX package ``\usepackage[T1]{fontenc}`` +* :ghissue:`27654`: [MNT]: Use fix-cm rather than type1cm for LaTeX +* :ghissue:`30086`: Add petroff6 and petroff8 color cycles to named color sequences example +* :ghissue:`30060`: Add the 6 color and 8 color sequence for the Petroff color cycles +* :ghissue:`28750`: Followup documentation for petroff color sequence +* :ghissue:`18931`: 3D collections do not proper handle ``edgecolor='face'`` +* :ghissue:`2831`: Bug when saving to vector format (pdf, svg, eps) +* :ghissue:`30046`: [Doc]: Documentation of the stable version still prompts that it is an unstable development version +* :ghissue:`29844`: [MNT]: CI: pygobject fails to install during ubuntu-22.04 GitHub Actions jobs +* :ghissue:`30021`: [Bug]: Setting font.sans-serif is impossible by the intended way using matplotlib.rc because it contains a hyphen. +* :ghissue:`30007`: Axes.pie([0, 0]) crashes with “cannot convert float NaN to integer” when all slice sizes are zero +* :ghissue:`29334`: [Bug]: Type annotation for ``add_subplots`` has incorrect return type for ``projection="3d"`` +* :ghissue:`29681`: [ENH]: Add parameter 'error_linestyle' to plt.errorbar() +* :ghissue:`29960`: [Bug]: FuncAnimation function not typed properly +* :ghissue:`29860`: ``ax.pie()`` raises ``ValueError`` when input contains ``NaN`` +* :ghissue:`11059`: figure.clf() and subplots_adjust +* :ghissue:`29906`: [Doc]: Autoscaling Axes or Autoscaling Axis? +* :ghissue:`29921`: boilerplate.py seems to remove parameters +* :ghissue:`29938`: [ENH]: plt.colorbar add a colorbar which has the same height/width of original image +* :ghissue:`29891`: [Bug]: image alpha re-applied each draw? +* :ghissue:`29883`: [Bug]: Missing backcompat for backends not supporting hatchcolors in draw_quad_mesh +* :ghissue:`27588`: [ENH]: Add way to automatically fix flake8 errors +* :ghissue:`1369`: add rc param for centimeter support +* :ghissue:`29845`: [MNT]: CI: cleanup: remove stale/outdated version range restrictions +* :ghissue:`29749`: [Bug]: Unit tests: Ubuntu 22.04 lacks dependencies required for recent PyGObject versions +* :ghissue:`29615`: [Bug]: pcolormesh's default x/y range might break ``set_scale('log')`` +* :ghissue:`29528`: [Bug]: set_rticks makes polar autoscale move the origin away from zero +* :ghissue:`29799`: [ENH]: set default color cycle to named color sequence +* :ghissue:`29694`: [Bug]: LogLocator sometimes draws fewer ticks than it can +* :ghissue:`29746`: [Doc]: Add uv and pixi install instructions +* :ghissue:`29647`: [ENH]: Allow list of padding values for bar_label +* :ghissue:`27669`: [Doc]: documentation of how to properly rasterize output of contourf +* :ghissue:`29757`: [Doc]: duplicate tags +* :ghissue:`29753`: [Doc]: color and colormap tags +* :ghissue:`29720`: [Bug]: Inset Axes Failing for Geographic Plot +* :ghissue:`29712`: [Doc]: Stable version of documentation has unstable banner +* :ghissue:`27196`: [Doc]: List supported hatches and link to/embed hatch reference on hatches API page +* :ghissue:`29562`: [MNT]: Remove rcParams deprecation machinery +* :ghissue:`29042`: [Bug]: colors.LinearSegmentedColormap.from_list fails when using a ("", alpha) tuple +* :ghissue:`28951`: [ENH]: Better positioning of rotated tick labels +* :ghissue:`29474`: [ENH]: Show parameter names in error message for mismatched array sizes in bar() +* :ghissue:`27298`: [ENH]: Add color argument to violinplot constructor +* :ghissue:`22861`: [Bug]: 3D scatter plot flips alpha order depending on depth relative to camera +* :ghissue:`29532`: [Bug]: ScalarFormatter can't be forced to use an offset of 1 +* :ghissue:`16659`: Speeding up Axes3D.plot_surface 4-8x +* :ghissue:`29524`: [Doc]: Unclear how to compile ``c_internals`` in code space +* :ghissue:`29489`: [Bug]: Systematic test failures with ubuntu-22.04-arm pipeline +* :ghissue:`28915`: [Doc]: Preferred way of specifying colormaps via ``cmap`` +* :ghissue:`29305`: [Doc]: Dead link in dev workflow docs +* :ghissue:`28763`: [MNT]: ListedColormap inconsistencies +* :ghissue:`29428`: [Doc]: Multipage PDF: unclear which backend supports and which does not support attach_note() +* :ghissue:`29387`: [MNT]: Fix 3.10 release notes and merge up +* :ghissue:`27321`: [Bug]: The method for checking whether a color has an alpha value is outdated +* :ghissue:`29284`: [Bug]: ``get_ticklabels``/``set_ticklabels`` gives incorrect values in log plot +* :ghissue:`26074`: [ENH]: Different edgecolor and hatch color in bar plot +* :ghissue:`29313`: [DOC]: possible typos +* :ghissue:`27763`: [Bug]: colorbar doesn't register inset_axis as cax +* :ghissue:`23770`: [Bug]: crash due to backend issue in ipython session started explicitly with InteractiveShell +* :ghissue:`19017`: Formalize discouraged API (= softer deprecations) +* :ghissue:`22521`: [Bug]: X-Axis date label not rotated +* :ghissue:`29181`: [Doc]: locally testing changes +* :ghissue:`17740`: Multiple Arrows on Streamplots +* :ghissue:`19101`: support for ticks crossing axes in axisartist +* :ghissue:`24050`: No error message in matplotlib.axes.Axes.legend() if there are more labels than handles +* :ghissue:`7305`: RuntimeError In FT2Font with NISC18030.ttf Previous GitHub statistics diff --git a/doc/release/prev_whats_new/github_stats_3.10.9.rst b/doc/release/prev_whats_new/github_stats_3.10.9.rst new file mode 100644 index 000000000000..73d2785531b4 --- /dev/null +++ b/doc/release/prev_whats_new/github_stats_3.10.9.rst @@ -0,0 +1,103 @@ +.. _github-stats_3-10-9: + +GitHub statistics for 3.10.9 (Apr 23, 2026) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/23 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 10 issues and merged 34 pull requests. +The full list can be seen `on GitHub `__ + +The following 37 authors contributed 519 commits. + +* Aasma Gupta +* Aman Srivastava +* Antony Lee +* beelauuu +* Ben Root +* Christine P. Chai +* David Stansby +* dependabot[bot] +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* hu-xiaonan +* Ian Thomas +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lumberbot (aka Jack) +* m-sahare +* N R Navaneet +* Nathan G. Wiseman +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Raphael Erik Hviding +* Raphael Quast +* Roman +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Steve Berardi +* Thomas A Caswell +* Tim Hoffmann +* Trygve Magnus Ræder +* Vikash Kumar + +GitHub issues and pull requests: + +Pull Requests (34): + +* :ghpull:`31556`: FIX: Inverted PyErr_Occurred check in enum type caster (_enums.h) +* :ghpull:`31078`: Backport PR #31075 on branch v3.10.x (Fix remove method for figure title and xy-labels) +* :ghpull:`31280`: Backport PR #31278 on branch v3.10.x (Fix ``clabel`` manual argument not accepting unit-typed coordinates) +* :ghpull:`31520`: Backport PR #31020 on branch v3.10.x (DOC: Fix doc builds with Sphinx 9) +* :ghpull:`31511`: Backport PR #31504 on branch v3.10.x (Re-order variants to prioritize narrower types) +* :ghpull:`31504`: Re-order variants to prioritize narrower types +* :ghpull:`31445`: Backport PR #31437: mathtext: Fix type inconsistency with fontmaps +* :ghpull:`31437`: mathtext: Fix type inconsistency with fontmaps +* :ghpull:`31411`: Backport PR #31323 on branch v3.10.x (FIX: Prevent crash when removing a subfigure containing subplots) +* :ghpull:`31421`: Backport PR #31420 on branch v3.10.x (Fix outdated Savannah URL for freetype download) +* :ghpull:`31420`: Fix outdated Savannah URL for freetype download +* :ghpull:`31418`: Backport PR #31401: BLD: Temporarily pin setuptools-scm<10 +* :ghpull:`31323`: FIX: Prevent crash when removing a subfigure containing subplots +* :ghpull:`31401`: BLD: Temporarily pin setuptools-scm<10 +* :ghpull:`31278`: Fix ``clabel`` manual argument not accepting unit-typed coordinates +* :ghpull:`31154`: Backport PR #31153 on branch v3.10.x (TST: Use correct method of clearing mock objects) +* :ghpull:`31153`: TST: Use correct method of clearing mock objects +* :ghpull:`31075`: Fix remove method for figure title and xy-labels +* :ghpull:`31036`: Backport PR #31035 on branch v3.10.x (DOCS: Fix typo in time array step size comment) +* :ghpull:`30986`: Backport PR #30985 on branch v3.10.x (MNT: do not assign a numpy array shape) +* :ghpull:`30985`: MNT: do not assign a numpy array shape +* :ghpull:`30971`: Backport PR #30969 on branch v3.10.x (DOC: Simplify barh() example) +* :ghpull:`30965`: Backport PR #30952 on branch v3.10.x (DOC: Tutorial on API shortcuts) +* :ghpull:`30964`: Backport PR #30960 on branch v3.10.x (SVG backend - handle font weight as integer) +* :ghpull:`30960`: SVG backend - handle font weight as integer +* :ghpull:`30924`: Backport PR #30910 on branch v3.10.x (DOC: Improve writer parameter docs of Animation.save()) +* :ghpull:`30870`: Backport PR #30869 on branch v3.10.x (FIX: Accept array for zdir) +* :ghpull:`30869`: FIX: Accept array for zdir +* :ghpull:`30860`: Backport PR #30858 on branch v3.10.x (DOC: reinstate "codex" search term) +* :ghpull:`30818`: Backport PR #30817 on branch v3.10.x (Update sphinx-gallery header patch) +* :ghpull:`30801`: Backport PR #30763 on branch v3.10.x (DOC: Add example how to align tick labels) +* :ghpull:`30791`: Backport PR #30788 on branch v3.10.8-doc (Fix typo in key-mapping for "f11") +* :ghpull:`30790`: Backport PR #30788 on branch v3.10.x (Fix typo in key-mapping for "f11") +* :ghpull:`30788`: Fix typo in key-mapping for "f11" + +Issues (10): + +* :ghissue:`31495`: Unavoidable warnings with pybind11 main branch +* :ghissue:`31433`: [MNT]: Mypy error +* :ghissue:`31340`: [Bug]: outdated savannah URL in subprojects/freetype-2.6.1.wrap +* :ghissue:`31319`: [Bug]: Crash when removing a subfigure with a subplot in a figure +* :ghissue:`27525`: [Bug]: clabel manual argument does not accept units +* :ghissue:`31112`: [TST] Upcoming dependency test failures +* :ghissue:`31073`: [Bug]: Crash when Removing Suptitle in a Figure with Constrained Layout +* :ghissue:`30981`: [TST] Upcoming dependency test failures +* :ghissue:`30868`: [Bug]: Axe3D text() method does not allow zdir=numpy.array(...) +* :ghissue:`21566`: [ENH]: set_horizontalalignment("right") on Y axis labels when yaxis.ticks_right() is used. diff --git a/tools/github_stats.py b/tools/github_stats.py index af0255fcefba..6e442d220180 100755 --- a/tools/github_stats.py +++ b/tools/github_stats.py @@ -28,6 +28,8 @@ PER_PAGE = 100 REPORT_TEMPLATE = """\ +.. redirect-from:: /users/github_stats + .. _github-stats: {title} From 7de4f498f45fe1efd1b6c37f6bb435fd201afafc Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 14:18:03 -0400 Subject: [PATCH 03/99] REL: v3.11.0rc1 This is the first release candidate for the meso release 3.11.0. From 3217fe2227aae0db6c8da042fcecc70a55d92e2a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 14:20:30 -0400 Subject: [PATCH 04/99] BLD: bump branch away from tag So the tarballs from GitHub are stable. From 9e11feb1580d72c0594d98bf6d5b7500e4299f9e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 17:59:53 -0400 Subject: [PATCH 05/99] Backport PR #31563: LIC: remove carlogo license --- LICENSE/LICENSE_CARLOGO | 45 ----------------------------------------- doc/project/license.rst | 6 ------ meson.build | 3 +-- 3 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 LICENSE/LICENSE_CARLOGO diff --git a/LICENSE/LICENSE_CARLOGO b/LICENSE/LICENSE_CARLOGO deleted file mode 100644 index 8c99c656a0f5..000000000000 --- a/LICENSE/LICENSE_CARLOGO +++ /dev/null @@ -1,45 +0,0 @@ -----> we renamed carlito -> carlogo to comply with the terms <---- - -Copyright (c) 2010-2013 by tyPoland Lukasz Dziedzic with Reserved Font Name "Carlito". - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. - -The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the copyright statement(s). - -"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. - -"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. - -5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/doc/project/license.rst b/doc/project/license.rst index 6a34eff4637d..251c29204eb7 100644 --- a/doc/project/license.rst +++ b/doc/project/license.rst @@ -151,12 +151,6 @@ Fonts .. literalinclude:: ../../LICENSE/LICENSE_BAKOMA :language: none -.. dropdown:: Carlogo - :class-container: sdd - - .. literalinclude:: ../../LICENSE/LICENSE_CARLOGO - :language: none - .. dropdown:: Courier 10 :class-container: sdd diff --git a/meson.build b/meson.build index 820335e2c9d8..7d1f3a433fbb 100644 --- a/meson.build +++ b/meson.build @@ -7,7 +7,7 @@ project( '-m', 'setuptools_scm', check: true).stdout().strip(), # qt_editor backend is MIT # ResizeObserver at end of lib/matplotlib/backends/web_backend/js/mpl.js is CC0 - # Carlogo, STIX, Computer Modern, and Last Resort are OFL + # STIX, Computer Modern, and Last Resort are OFL # DejaVu is Bitstream Vera and Public Domain license: 'PSF-2.0 AND MIT AND CC0-1.0 AND OFL-1.1 AND Bitstream-Vera AND Public-Domain', license_files: [ @@ -15,7 +15,6 @@ project( 'extern/agg24-svn/src/copying', 'LICENSE/LICENSE_AMSFONTS', 'LICENSE/LICENSE_BAKOMA', - 'LICENSE/LICENSE_CARLOGO', 'LICENSE/LICENSE_COLORBREWER', 'LICENSE/LICENSE_COURIERTEN', 'LICENSE/LICENSE_FREETYPE', From 0dc69de6e4a4379a676602cd964dd749e62e10d2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 28 Apr 2026 17:16:47 -0400 Subject: [PATCH 06/99] Backport PR #31580: DOC: added unregister to colormap guide --- galleries/users_explain/colors/colormap-manipulation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/galleries/users_explain/colors/colormap-manipulation.py b/galleries/users_explain/colors/colormap-manipulation.py index 0cd488857257..8b773b4fac42 100644 --- a/galleries/users_explain/colors/colormap-manipulation.py +++ b/galleries/users_explain/colors/colormap-manipulation.py @@ -299,6 +299,11 @@ def plot_linearmap(cdict): plt.show() +# %% +# Colormaps added to the registry can also be deregistered: + +mpl.colormaps.unregister(my_cmap.name) + # %% # # .. admonition:: References From 9127a7083a005ba7c0277c2498ef136c1bf1d634 Mon Sep 17 00:00:00 2001 From: Brian Lau <103338659+beelauuu@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:32:26 -0400 Subject: [PATCH 07/99] FIX: Polar Radial Tick Warnings Labels Bug (#31577) * Fix polar RadialLocator triggering spurious UserWarning in set_ticklabels Extend the FixedLocator isinstance check in Axis.set_ticklabels and _set_formatter to also recognise locators that wrap a FixedLocator via a .base attribute (e.g. RadialLocator used by polar axes). Previously calling set_ticks(ticks, labels) or set_ticklabels(labels) on a polar axis would always hit the else branch and emit a spurious warning because RadialAxis.set_major_locator automatically wraps any locator in a RadialLocator, hiding the inner FixedLocator from the isinstance check. Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/axis.py | 17 ++++++++++++----- lib/matplotlib/tests/test_polar.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 0ddfee2d537c..48bbac9264ae 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1999,7 +1999,9 @@ def _set_formatter(self, formatter, level): if (isinstance(formatter, mticker.FixedFormatter) and len(formatter.seq) > 0 - and not isinstance(level.locator, mticker.FixedLocator)): + and not isinstance(level.locator, mticker.FixedLocator) + and not (hasattr(level.locator, 'base') and + isinstance(level.locator.base, mticker.FixedLocator))): _api.warn_external('FixedFormatter should only be used together ' 'with FixedLocator') @@ -2142,16 +2144,21 @@ def set_ticklabels(self, labels, *, minor=False, fontdict=None, **kwargs): if not labels: # eg labels=[]: formatter = mticker.NullFormatter() - elif isinstance(locator, mticker.FixedLocator): + elif (isinstance(locator, mticker.FixedLocator) or + (hasattr(locator, 'base') and + isinstance(locator.base, mticker.FixedLocator))): + # Also handles locators that wrap a FixedLocator (e.g. RadialLocator). + fixed_locator = (locator if isinstance(locator, mticker.FixedLocator) + else locator.base) # Passing [] as a list of labels is often used as a way to # remove all tick labels, so only error for > 0 labels - if len(locator.locs) != len(labels) and len(labels) != 0: + if len(fixed_locator.locs) != len(labels) and len(labels) != 0: raise ValueError( "The number of FixedLocator locations" - f" ({len(locator.locs)}), usually from a call to" + f" ({len(fixed_locator.locs)}), usually from a call to" " set_ticks, does not match" f" the number of labels ({len(labels)}).") - tickd = {loc: lab for loc, lab in zip(locator.locs, labels)} + tickd = {loc: lab for loc, lab in zip(fixed_locator.locs, labels)} func = functools.partial(self._format_with_dict, tickd) formatter = mticker.FuncFormatter(func) else: diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index a805fb61d238..6bb534b96f25 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -591,3 +591,13 @@ def test_radial_locator_wrapping(): assert ax.yaxis.isDefault_majloc assert isinstance(ax.yaxis.get_major_locator(), RadialLocator) assert isinstance(ax.yaxis.get_major_locator().base, mticker.LogLocator) + + +def test_set_rticks_ticklabels_no_warning(): + # Regression test: RadialLocator wrapping a FixedLocator must not trigger + # the "set_ticklabels() should only be used with a fixed number of ticks" + # UserWarning when set_ticks()/set_rticks() was called first. + + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.set_rticks([0, 1, 2, 3]) + ax.yaxis.set_ticklabels(['zero', 'one', 'two', 'three']) From 1c625168466e9384b73b34454dc58f2c14879ef3 Mon Sep 17 00:00:00 2001 From: Brian Lau <103338659+beelauuu@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:32:26 -0400 Subject: [PATCH 08/99] Backport PR #31577: FIX: Polar Radial Tick Warnings Labels Bug --- lib/matplotlib/axis.py | 17 ++++++++++++----- lib/matplotlib/tests/test_polar.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 0ddfee2d537c..48bbac9264ae 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1999,7 +1999,9 @@ def _set_formatter(self, formatter, level): if (isinstance(formatter, mticker.FixedFormatter) and len(formatter.seq) > 0 - and not isinstance(level.locator, mticker.FixedLocator)): + and not isinstance(level.locator, mticker.FixedLocator) + and not (hasattr(level.locator, 'base') and + isinstance(level.locator.base, mticker.FixedLocator))): _api.warn_external('FixedFormatter should only be used together ' 'with FixedLocator') @@ -2142,16 +2144,21 @@ def set_ticklabels(self, labels, *, minor=False, fontdict=None, **kwargs): if not labels: # eg labels=[]: formatter = mticker.NullFormatter() - elif isinstance(locator, mticker.FixedLocator): + elif (isinstance(locator, mticker.FixedLocator) or + (hasattr(locator, 'base') and + isinstance(locator.base, mticker.FixedLocator))): + # Also handles locators that wrap a FixedLocator (e.g. RadialLocator). + fixed_locator = (locator if isinstance(locator, mticker.FixedLocator) + else locator.base) # Passing [] as a list of labels is often used as a way to # remove all tick labels, so only error for > 0 labels - if len(locator.locs) != len(labels) and len(labels) != 0: + if len(fixed_locator.locs) != len(labels) and len(labels) != 0: raise ValueError( "The number of FixedLocator locations" - f" ({len(locator.locs)}), usually from a call to" + f" ({len(fixed_locator.locs)}), usually from a call to" " set_ticks, does not match" f" the number of labels ({len(labels)}).") - tickd = {loc: lab for loc, lab in zip(locator.locs, labels)} + tickd = {loc: lab for loc, lab in zip(fixed_locator.locs, labels)} func = functools.partial(self._format_with_dict, tickd) formatter = mticker.FuncFormatter(func) else: diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index a805fb61d238..6bb534b96f25 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -591,3 +591,13 @@ def test_radial_locator_wrapping(): assert ax.yaxis.isDefault_majloc assert isinstance(ax.yaxis.get_major_locator(), RadialLocator) assert isinstance(ax.yaxis.get_major_locator().base, mticker.LogLocator) + + +def test_set_rticks_ticklabels_no_warning(): + # Regression test: RadialLocator wrapping a FixedLocator must not trigger + # the "set_ticklabels() should only be used with a fixed number of ticks" + # UserWarning when set_ticks()/set_rticks() was called first. + + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.set_rticks([0, 1, 2, 3]) + ax.yaxis.set_ticklabels(['zero', 'one', 'two', 'three']) From edc2cd0a0addbb02d20d5a4a5e30bbe5d0fcb149 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Wed, 29 Apr 2026 10:36:35 -0400 Subject: [PATCH 09/99] fix --- lib/matplotlib/lines.py | 2 +- lib/matplotlib/tests/test_collections.py | 12 ++++++++++++ pyproject.toml | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 69ad36fb768b..9f179e7bfe42 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -36,7 +36,7 @@ def _get_dash_pattern(style): if isinstance(style, str): style = ls_mapper.get(style, style) # un-dashed styles - if style in ['solid', 'None']: + if style in ['solid', 'None', 'none', '', ' ']: offset = 0 dashes = None # dashed styles diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 9c9b4e643014..b88cd3b3b8a3 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -704,6 +704,18 @@ def test_set_wrong_linestyle(): c.set_linestyle('fuzzy') +@pytest.mark.parametrize('backend', ['agg', 'pdf', 'svg', 'ps']) +@pytest.mark.parametrize('ls', ['', ' ', 'none']) +def test_scatter_empty_linestyle(backend, ls): + # Regression Test: Saving to PDF (and other backends) with ls="" + # on a scatter collection used to crash with "zero-size array + # to reduction operation maximum". + plt.switch_backend(backend) + fig, ax = plt.subplots() + ax.scatter([0, 1], [0, 1], ls=ls) + fig.savefig(io.BytesIO()) + + @mpl.style.context('default') def test_capstyle(): col = mcollections.PathCollection([]) diff --git a/pyproject.toml b/pyproject.toml index f3c38512a2c9..d75db711faf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ extend-exclude = [ "doc/tutorials", "tools/gh_api.py", ] +target-version = "py311" line-length = 88 [tool.ruff.lint] @@ -295,6 +296,7 @@ convention = "numpy" "galleries/users_explain/text/text_props.py" = ["E501"] [tool.mypy] +python_version = "3.11" ignore_missing_imports = true enable_error_code = [ "ignore-without-code", From b8991ce78a9de20670291a4d62d1ee4824c48f93 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Wed, 29 Apr 2026 10:42:42 -0400 Subject: [PATCH 10/99] other backends don't crash --- lib/matplotlib/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index b88cd3b3b8a3..ecababbb8304 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -704,7 +704,7 @@ def test_set_wrong_linestyle(): c.set_linestyle('fuzzy') -@pytest.mark.parametrize('backend', ['agg', 'pdf', 'svg', 'ps']) +@pytest.mark.parametrize('backend', ['agg', 'pdf']) @pytest.mark.parametrize('ls', ['', ' ', 'none']) def test_scatter_empty_linestyle(backend, ls): # Regression Test: Saving to PDF (and other backends) with ls="" From da77088ecae81233ccc68ff0d45ce3a5e1e18850 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Wed, 29 Apr 2026 10:43:20 -0400 Subject: [PATCH 11/99] stupid build --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d75db711faf0..f3c38512a2c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,7 +184,6 @@ extend-exclude = [ "doc/tutorials", "tools/gh_api.py", ] -target-version = "py311" line-length = 88 [tool.ruff.lint] @@ -296,7 +295,6 @@ convention = "numpy" "galleries/users_explain/text/text_props.py" = ["E501"] [tool.mypy] -python_version = "3.11" ignore_missing_imports = true enable_error_code = [ "ignore-without-code", From 99808374bbd16809a13db4ddae0042198f444691 Mon Sep 17 00:00:00 2001 From: Brian Lau <103338659+beelauuu@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:16:56 -0400 Subject: [PATCH 12/99] Update test_scatter_empty_linestyle for PDF backend Refactor test_scatter_empty_linestyle to use 'pdf' backend only. --- lib/matplotlib/tests/test_collections.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index ecababbb8304..45c860fa0075 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -704,13 +704,9 @@ def test_set_wrong_linestyle(): c.set_linestyle('fuzzy') -@pytest.mark.parametrize('backend', ['agg', 'pdf']) @pytest.mark.parametrize('ls', ['', ' ', 'none']) -def test_scatter_empty_linestyle(backend, ls): - # Regression Test: Saving to PDF (and other backends) with ls="" - # on a scatter collection used to crash with "zero-size array - # to reduction operation maximum". - plt.switch_backend(backend) +def test_scatter_empty_linestyle_pdf(ls): + plt.switch_backend('pdf') fig, ax = plt.subplots() ax.scatter([0, 1], [0, 1], ls=ls) fig.savefig(io.BytesIO()) From c23808dc98ea296a90cde57a821164f23891d5aa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 29 Apr 2026 15:25:17 -0400 Subject: [PATCH 13/99] Expire some missed deprecations from 3.9 --- doc/api/next_api_changes/removals/31588-ES.rst | 18 ++++++++++++++++++ lib/matplotlib/axes/_axes.py | 4 +--- lib/matplotlib/backend_bases.py | 14 -------------- 3 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 doc/api/next_api_changes/removals/31588-ES.rst diff --git a/doc/api/next_api_changes/removals/31588-ES.rst b/doc/api/next_api_changes/removals/31588-ES.rst new file mode 100644 index 000000000000..8709c5a77f5f --- /dev/null +++ b/doc/api/next_api_changes/removals/31588-ES.rst @@ -0,0 +1,18 @@ +``boxplot`` tick labels +^^^^^^^^^^^^^^^^^^^^^^^ + +The parameter *labels* has been removed in favour of *tick_labels* for clarity and +consistency with `~.Axes.bar`. + +Image path semantics of toolmanager-based tools +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, MEP22 ("toolmanager-based") Tools would try to load their icon +(``tool.image``) relative to the current working directory, or, as a fallback, from +Matplotlib's own image directory. Because both approaches are problematic for +third-party tools (the end-user may change the current working directory at any time, +and third-parties cannot add new icons in Matplotlib's image directory), this behavior +has been removed; instead, ``tool.image`` is now interpreted relative to the directory +containing the source file where the ``Tool.image`` class attribute is defined. +(Defining ``tool.image`` as an absolute path also works and is compatible with both the +old and the new semantics.) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 66770e426386..76659ae2c83d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4302,7 +4302,6 @@ def apply_mask(arrays, mask): @_api.make_keyword_only("3.10", "notch") @_preprocess_data() - @_api.rename_parameter("3.9", "labels", "tick_labels") def boxplot(self, x, notch=None, sym=None, vert=None, orientation='vertical', whis=None, positions=None, widths=None, patch_artist=None, bootstrap=None, @@ -4444,8 +4443,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, values. .. versionchanged:: 3.9 - Renamed from *labels*, which is deprecated since 3.9 - and will be removed in 3.11. + Renamed from *labels*, which is also removed in 3.11. manage_ticks : bool, default: True If True, the tick locations and labels will be adjusted to match diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 508b744ca04d..27c3752858a7 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3516,20 +3516,6 @@ def _get_image_filename(self, tool): for filename in [filename, filename + self._icon_extension]: if os.path.isfile(filename): return os.path.abspath(filename) - for fname in [ # Fallback; once deprecation elapses. - tool.image, - tool.image + self._icon_extension, - cbook._get_data_path("images", tool.image), - cbook._get_data_path("images", tool.image + self._icon_extension), - ]: - if os.path.isfile(fname): - _api.warn_deprecated( - "3.9", message=f"Loading icon {tool.image!r} from the current " - "directory or from Matplotlib's image directory. This behavior " - "is deprecated since %(since)s and will be removed in %(removal)s; " - "Tool.image should be set to a path relative to the Tool's source " - "file, or to an absolute path.") - return os.path.abspath(fname) def trigger_tool(self, name): """ From f6e850d2a26f9ac63e320f2e7ad7f8462ad8584b Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:32:09 +0100 Subject: [PATCH 14/99] ENH: add wedge_labels parameter for pie (#29152) --- .../deprecations/29152_REC.rst | 13 ++ .../next_whats_new/pie_wedge_labels.rst | 26 ++++ galleries/examples/misc/svg_filter_pie.py | 6 +- .../pie_and_polar_charts/bar_of_pie.py | 7 +- .../pie_and_polar_charts/pie_features.py | 70 ++++++----- lib/matplotlib/_api/__init__.pyi | 1 + lib/matplotlib/axes/_axes.py | 85 ++++++++++++-- lib/matplotlib/axes/_axes.pyi | 5 +- lib/matplotlib/pyplot.py | 10 +- lib/matplotlib/tests/test_axes.py | 111 +++++++++++++----- lib/matplotlib/tests/test_container.py | 7 +- tools/boilerplate.py | 2 + 12 files changed, 262 insertions(+), 81 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/29152_REC.rst create mode 100644 doc/release/next_whats_new/pie_wedge_labels.rst diff --git a/doc/api/next_api_changes/deprecations/29152_REC.rst b/doc/api/next_api_changes/deprecations/29152_REC.rst new file mode 100644 index 000000000000..cedc91e81410 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/29152_REC.rst @@ -0,0 +1,13 @@ +``pie`` *labels* and *labeldistance* parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Currently the *labels* parameter of `~.Axes.pie` is used both for annotating the +pie wedges directly, and for automatic legend entries. For consistency +with other plotting methods, in future *labels* will only be used for the legend. + +The *labeldistance* parameter will therefore default to ``None`` from Matplotlib +3.14, when it will also be deprecated and then removed in Matplotlib 3.16. To +preserve the existing behavior for now, set ``labeldistance=1.1``. For the longer +term, to place labels on the wedges use the new *wedge_labels* and +*wedge_label_distance* parameters of `~.Axes.pie` or the `~.Axes.pie_label` method. +Note that `~.Axes.pie_label` allows for more customization of the label positions via +the *rotate* and *alignment* parameters as well as *distance*. diff --git a/doc/release/next_whats_new/pie_wedge_labels.rst b/doc/release/next_whats_new/pie_wedge_labels.rst new file mode 100644 index 000000000000..9c72742e005e --- /dev/null +++ b/doc/release/next_whats_new/pie_wedge_labels.rst @@ -0,0 +1,26 @@ +New *wedge_labels* parameter for pie +------------------------------------ + +`~.Axes.pie` now accepts a *wedge_labels* parameter as a shortcut to the +`~.Axes.pie_label` method. This may be used for simple annotation of the wedges +of the pie chart. It can take + +* a list of strings, similar to the existing *labels* parameter +* a format string similar to the existing *autopct* parameter, except that it + uses the `str.format` method and it can handle absolute values as well as + fractions/percentages + +*wedge_labels* has an accompanying *wedge_label_distance* parameter, to control +the distance of the labels from the center of the pie. + + +.. plot:: + :include-source: true + :alt: Two pie charts. The chart on the left has labels 'foo' and 'bar' outside the wedges. The chart on the right has labels '1' and '2' inside the wedges. + + import matplotlib.pyplot as plt + + fig, (ax1, ax2) = plt.subplots(ncols=2, layout='constrained') + + ax1.pie([1, 2], wedge_labels=['foo', 'bar'], wedge_label_distance=1.1) + ax2.pie([1, 2], wedge_labels='{absval:d}', wedge_label_distance=0.6) diff --git a/galleries/examples/misc/svg_filter_pie.py b/galleries/examples/misc/svg_filter_pie.py index f8ccc5bcb22b..d438fe77b8a6 100644 --- a/galleries/examples/misc/svg_filter_pie.py +++ b/galleries/examples/misc/svg_filter_pie.py @@ -28,11 +28,11 @@ # We want to draw the shadow for each pie, but we will not use "shadow" # option as it doesn't save the references to the shadow patches. -pie = ax.pie(fracs, explode=explode, labels=labels, autopct='%1.1f%%') +pie = ax.pie(fracs, explode=explode, wedge_labels=labels, wedge_label_distance=1.1) -for w in pie.wedges: +for w, label in zip(pie.wedges, labels): # set the id with the label. - w.set_gid(w.get_label()) + w.set_gid(label) # we don't want to draw the edge of the pie w.set_edgecolor("none") diff --git a/galleries/examples/pie_and_polar_charts/bar_of_pie.py b/galleries/examples/pie_and_polar_charts/bar_of_pie.py index 7c703976db2e..6e58bba5209d 100644 --- a/galleries/examples/pie_and_polar_charts/bar_of_pie.py +++ b/galleries/examples/pie_and_polar_charts/bar_of_pie.py @@ -25,8 +25,11 @@ explode = [0.1, 0, 0] # rotate so that first wedge is split by the x-axis angle = -180 * overall_ratios[0] -pie = ax1.pie(overall_ratios, autopct='%1.1f%%', startangle=angle, - labels=labels, explode=explode) +pie = ax1.pie(overall_ratios, startangle=angle, explode=explode) + +# label the wedges with our label strings and the ratios as percentages +ax1.pie_label(pie, labels, distance=1.1) +ax1.pie_label(pie, '{frac:.1%}', distance=0.6) # bar chart parameters age_ratios = [.33, .54, .07, .06] diff --git a/galleries/examples/pie_and_polar_charts/pie_features.py b/galleries/examples/pie_and_polar_charts/pie_features.py index 8510c09f23a5..80b8ade230b2 100644 --- a/galleries/examples/pie_and_polar_charts/pie_features.py +++ b/galleries/examples/pie_and_polar_charts/pie_features.py @@ -15,15 +15,15 @@ # ------------ # # Plot a pie chart of animals and label the slices. To add -# labels, pass a list of labels to the *labels* parameter +# labels, pass a list of labels to the *wedge_labels* parameter. import matplotlib.pyplot as plt labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' -sizes = [15, 30, 45, 10] +sizes = [12, 24, 36, 8] fig, ax = plt.subplots() -ax.pie(sizes, labels=labels) +ax.pie(sizes, wedge_labels=labels) # %% # Each slice of the pie chart is a `.patches.Wedge` object; therefore in @@ -31,16 +31,44 @@ # the *wedgeprops* argument, as demonstrated in # :doc:`/gallery/pie_and_polar_charts/nested_pie`. # +# Set label positions +# ------------------- +# If you want the labels outside the pie, set a *wedge_label_distance* greater than 1. +# This is the distance from the center of the pie as a fraction of its radius. + +fig, ax = plt.subplots() +ax.pie(sizes, wedge_labels=labels, wedge_label_distance=1.1) + +# %% +# # Auto-label slices # ----------------- # -# Pass a function or format string to *autopct* to label slices. +# Pass a format string to *wedge_labels* to label slices with their values... + +fig, ax = plt.subplots() +ax.pie(sizes, wedge_labels='{absval:.1f}') + +# %% +# +# ...or with their percentages... + +fig, ax = plt.subplots() +ax.pie(sizes, wedge_labels='{frac:.1%}') + +# %% +# +# ...or both. fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, autopct='%1.1f%%') +ax.pie(sizes, wedge_labels='{absval:d}\n{frac:.1%}') + +# %% +# +# For more control over labels, or to add multiple sets, see +# :doc:`/gallery/pie_and_polar_charts/pie_label`. # %% -# By default, the label values are obtained from the percent size of the slice. # # Color slices # ------------ @@ -48,8 +76,7 @@ # Pass a list of colors to *colors* to set the color of each slice. fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, - colors=['olivedrab', 'rosybrown', 'gray', 'saddlebrown']) +ax.pie(sizes, colors=['olivedrab', 'rosybrown', 'gray', 'saddlebrown']) # %% # Hatch slices @@ -58,22 +85,9 @@ # Pass a list of hatch patterns to *hatch* to set the pattern of each slice. fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, hatch=['**O', 'oO', 'O.O', '.||.']) - -# %% -# Swap label and autopct text positions -# ------------------------------------- -# Use the *labeldistance* and *pctdistance* parameters to position the *labels* -# and *autopct* text respectively. - -fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, autopct='%1.1f%%', - pctdistance=1.25, labeldistance=.6) +ax.pie(sizes, hatch=['**O', 'oO', 'O.O', '.||.']) # %% -# *labeldistance* and *pctdistance* are ratios of the radius; therefore they -# vary between ``0`` for the center of the pie and ``1`` for the edge of the -# pie, and can be set to greater than ``1`` to place text outside the pie. # # Explode, shade, and rotate slices # --------------------------------- @@ -86,11 +100,10 @@ # # This example orders the slices, separates (explodes) them, and rotates them. -explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') +explode = (0, 0.2, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') fig, ax = plt.subplots() -ax.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%', - shadow=True, startangle=90) +ax.pie(sizes, explode=explode, wedge_labels='{frac:.1%}', shadow=True, startangle=90) plt.show() # %% @@ -107,8 +120,7 @@ fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, autopct='%.0f%%', - textprops={'size': 'small'}, radius=0.5) +ax.pie(sizes, wedge_labels='{frac:.1%}', textprops={'size': 'small'}, radius=0.5) plt.show() # %% @@ -119,8 +131,8 @@ # the `.Shadow` patch. This can be used to modify the default shadow. fig, ax = plt.subplots() -ax.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%', - shadow={'ox': -0.04, 'edgecolor': 'none', 'shade': 0.9}, startangle=90) +ax.pie(sizes, explode=explode, shadow={'ox': -0.04, 'edgecolor': 'none', 'shade': 0.9}, + startangle=90) plt.show() # %% diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 0bcce210634f..cf24edf65db5 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -19,6 +19,7 @@ from .deprecation import ( # noqa: F401, re-exported API _T = TypeVar("_T") class _Unset: ... +UNSET = _Unset() class classproperty(Any): def __init__( diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 66770e426386..d5f2ae1168c7 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -41,6 +41,7 @@ BarContainer, ErrorbarContainer, PieContainer, StemContainer) from matplotlib.text import Text from matplotlib.transforms import _ScaledRotation +from matplotlib._api import UNSET as _UNSET _log = logging.getLogger(__name__) @@ -3534,13 +3535,13 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, self.add_container(stem_container) return stem_container - @_api.make_keyword_only("3.10", "explode") - @_preprocess_data(replace_names=["x", "explode", "labels", "colors"]) - def pie(self, x, explode=None, labels=None, colors=None, - autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, - startangle=0, radius=1, counterclock=True, - wedgeprops=None, textprops=None, center=(0, 0), - frame=False, rotatelabels=False, *, normalize=True, hatch=None): + @_preprocess_data(replace_names=["x", "explode", "labels", "colors", + "wedge_labels"]) + def pie(self, x, *, explode=None, labels=None, colors=None, wedge_labels=None, + wedge_label_distance=0.6, autopct=None, pctdistance=0.6, shadow=False, + labeldistance=_UNSET, startangle=0, radius=1, counterclock=True, + wedgeprops=None, textprops=None, center=(0, 0), frame=False, + rotatelabels=False, normalize=True, hatch=None): """ Plot a pie chart. @@ -3560,7 +3561,13 @@ def pie(self, x, explode=None, labels=None, colors=None, of the radius with which to offset each wedge. labels : list, default: None - A sequence of strings providing the labels for each wedge + A sequence of strings providing the legend labels for each wedge. + + .. deprecated:: 3.12 + In future these labels will not appear on the wedges but only + be made available for the legend (see *labeldistance* below). + To place labels on the wedges, use *wedge_labels* or the + `pie_label` method. colors : :mpltype:`color` or list of :mpltype:`color`, default: None A sequence of colors through which the pie chart will cycle. If @@ -3573,12 +3580,35 @@ def pie(self, x, explode=None, labels=None, colors=None, .. versionadded:: 3.7 + wedge_labels : str or list of str, optional + A sequence of strings providing the labels for each wedge, or a format + string with ``absval`` and/or ``frac`` placeholders. For example, to label + each wedge with its value and the percentage in brackets:: + + wedge_labels="{absval:d} ({frac:.0%})" + + For more control or to add multiple sets of labels, use `pie_label` + instead. + + .. versionadded:: 3.12 + + wedge_label_distance : float, default: 0.6 + The radial position of the wedge labels, relative to the pie radius. + Values > 1 are outside the wedge and values < 1 are inside the wedge. + + .. versionadded:: 3.12 + autopct : None or str or callable, default: None If not *None*, *autopct* is a string or function used to label the wedges with their numeric value. The label will be placed inside the wedge. If *autopct* is a format string, the label will be ``fmt % pct``. If *autopct* is a function, then it will be called. + .. admonition:: Discouraged + + Consider using the *wedge_labels* parameter or `pie_label` + method instead. + pctdistance : float, default: 0.6 The relative distance along the radius at which the text generated by *autopct* is drawn. To draw the text outside the pie, @@ -3591,6 +3621,11 @@ def pie(self, x, explode=None, labels=None, colors=None, If set to ``None``, labels are not drawn but are still stored for use in `.legend`. + .. deprecated:: 3.12 + From v3.14 *labeldistance* will default to ``None`` and will + later be removed altogether. Use *wedge_labels* and + *wedge_label_distance* or the `pie_label` method instead. + shadow : bool or dict, default: False If bool, whether to draw a shadow beneath the pie. If dict, draw a shadow passing the properties in the dict to `.Shadow`. @@ -3672,8 +3707,33 @@ def pie(self, x, explode=None, labels=None, colors=None, raise ValueError('Cannot plot an unnormalized pie with sum(x) > 1') else: fracs = x + + if labeldistance is _UNSET: + # NB: when the labeldistance default changes, both labeldistance and + # rotatelabels should be deprecated for removal. + if labels is not None: + msg = ( + "From %(removal)s labeldistance will default to None, so that the " + "strings provided in the labels parameter are only available for " + "the legend. Later labeldistance will be removed completely. To " + "preserve existing behavior for now, pass labeldistance=1.1. " + "Consider using the wedge_labels parameter or the pie_label method " + "instead of the labels parameter." + ) + _api.warn_deprecated("3.12", message=msg) + labeldistance = 1.1 + if labels is None: labels = [''] * len(x) + else: + if wedge_labels is not None and labeldistance is not None: + raise ValueError( + 'wedge_labels is a replacement for labels when annotating the ' + 'wedges, so the two should not be used together unless ' + 'labeldistance is None. To add multiple sets of labels, use the ' + 'pie_label method.' + ) + if explode is None: explode = [0] * len(x) if len(x) != len(labels): @@ -3731,11 +3791,16 @@ def get_next_color(): pc = PieContainer(slices, x, normalize) - if labeldistance is None: + if wedge_labels is not None: + self.pie_label(pc, wedge_labels, distance=wedge_label_distance, + textprops=textprops) + + elif labeldistance is None: # Insert an empty list of texts for backwards compatibility of the # return value. pc.add_texts([]) - else: + + if labeldistance is not None: # Add labels to the wedges. labels_textprops = { 'fontsize': mpl.rcParams['xtick.labelsize'], diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 1c3e1e560d07..b0871167dd75 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -31,6 +31,7 @@ import matplotlib.tri as mtri import matplotlib.table as mtable import matplotlib.stackplot as mstack import matplotlib.streamplot as mstream +from matplotlib._api import _Unset import PIL.Image from collections.abc import Callable, Iterable, Sequence @@ -310,10 +311,12 @@ class Axes(_AxesBase): explode: ArrayLike | None = ..., labels: Sequence[str] | None = ..., colors: ColorType | Sequence[ColorType] | None = ..., + wedge_labels: str | Sequence | None = ..., + wedge_label_distance: float | Sequence = ..., autopct: str | Callable[[float], str] | None = ..., pctdistance: float = ..., shadow: bool = ..., - labeldistance: float | None = ..., + labeldistance: float | None | _Unset = ..., startangle: float = ..., radius: float = ..., counterclock: bool = ..., diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index dd80da45e332..57f5ac08e398 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -60,6 +60,7 @@ import matplotlib import matplotlib.image from matplotlib import _api +from matplotlib._api import UNSET as _UNSET # Re-exported (import x as x) for typing. from matplotlib import get_backend as get_backend, rcParams as rcParams from matplotlib import cm as cm # noqa: F401 @@ -153,6 +154,7 @@ LogLevel ) from matplotlib.widgets import SubplotTool + from matplotlib._api import _Unset _P = ParamSpec('_P') _R = TypeVar('_R') @@ -3963,13 +3965,16 @@ def phase_spectrum( @_copy_docstring_and_deprecators(Axes.pie) def pie( x: ArrayLike, + *, explode: ArrayLike | None = None, labels: Sequence[str] | None = None, colors: ColorType | Sequence[ColorType] | None = None, + wedge_labels: str | Sequence | None = None, + wedge_label_distance: float | Sequence = 0.6, autopct: str | Callable[[float], str] | None = None, pctdistance: float = 0.6, shadow: bool = False, - labeldistance: float | None = 1.1, + labeldistance: float | None | _Unset = _UNSET, startangle: float = 0, radius: float = 1, counterclock: bool = True, @@ -3978,7 +3983,6 @@ def pie( center: tuple[float, float] = (0, 0), frame: bool = False, rotatelabels: bool = False, - *, normalize: bool = True, hatch: str | Sequence[str] | None = None, data=None, @@ -3988,6 +3992,8 @@ def pie( explode=explode, labels=labels, colors=colors, + wedge_labels=wedge_labels, + wedge_label_distance=wedge_label_distance, autopct=autopct, pctdistance=pctdistance, shadow=shadow, diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6751666360b1..c812a93cbb6f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6628,8 +6628,23 @@ def test_pie_default(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') fig1, ax1 = plt.subplots(figsize=(8, 6)) - ax1.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90) + ax1.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90) + + +@image_comparison(['pie_default.png'], style='mpl20') +def test_pie_default_legacy(): + # Same as above, but uses labels parameter. Remove after labeldistance + # parameter deprecation expires. + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + fig1, ax1 = plt.subplots(figsize=(8, 6)) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + ax1.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90) @image_comparison(['pie_linewidth_0.png', 'pie_linewidth_0.png', 'pie_linewidth_0.png'], @@ -6641,27 +6656,30 @@ def test_pie_linewidth_0(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') - # Reuse testcase from above for a labeled data test + # Reuse testcase from above for a labeled data test. Include legend labels + # to smoke test that they are correctly unpacked. data = {"l": labels, "s": sizes, "c": colors, "ex": explode} fig = plt.figure() ax = fig.gca() - ax.pie("s", explode="ex", labels="l", colors="c", + ax.pie("s", explode="ex", wedge_labels="l", colors="c", wedge_label_distance=1.1, autopct='%1.1f%%', shadow=True, startangle=90, - wedgeprops={'linewidth': 0}, data=data) + labels="l", labeldistance=None, wedgeprops={'linewidth': 0}, + data=data) ax.axis('equal') # And again to test the pyplot functions which should also be able to be # called with a data kwarg plt.figure() - plt.pie("s", explode="ex", labels="l", colors="c", + plt.pie("s", explode="ex", wedge_labels="l", colors="c", wedge_label_distance=1.1, autopct='%1.1f%%', shadow=True, startangle=90, - wedgeprops={'linewidth': 0}, data=data) + labels="l", labeldistance=None, wedgeprops={'linewidth': 0}, + data=data) plt.axis('equal') @@ -6674,8 +6692,8 @@ def test_pie_center_radius(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}, center=(1, 2), radius=1.5) plt.annotate("Center point", xy=(1, 2), xytext=(1, 1.3), @@ -6694,8 +6712,8 @@ def test_pie_linewidth_2(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 2}) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6709,8 +6727,8 @@ def test_pie_ccw_true(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, counterclock=True) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6725,35 +6743,53 @@ def test_pie_frame_grid(): # only "explode" the 2nd slice (i.e. 'Hogs') explode = (0, 0.1, 0, 0) - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(2, 2)) - plt.pie(sizes[::-1], explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes[::-1], explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(5, 2)) - plt.pie(sizes, explode=explode[::-1], labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode[::-1], wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(3, 5)) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') +@image_comparison(['pie_rotatelabels_true.png'], style='mpl20') +def test_pie_label_rotate(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Frogs') + + pie = plt.pie(sizes, explode=explode, wedge_labels='{frac:.1%}', colors=colors, + shadow=True, startangle=90) + plt.pie_label(pie, labels, distance=1.1, rotate=True) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + @image_comparison(['pie_rotatelabels_true.png'], style='mpl20') def test_pie_rotatelabels_true(): + # As above but using legacy labels and rotatelabels parameters. Remove + # when the labeldistance parameter deprecation expires. # The slices will be ordered and plotted counter-clockwise. labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' sizes = [15, 30, 45, 10] colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] - explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Frogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, - rotatelabels=True) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + rotatelabels=True) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6765,7 +6801,7 @@ def test_pie_nolabel_but_legend(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, labeldistance=None, + wedge_labels='{frac:.1%}', shadow=True, startangle=90, labeldistance=None, rotatelabels=True) plt.axis('equal') plt.ylim(-1.2, 1.2) @@ -6806,9 +6842,13 @@ def test_pie_textprops(): rotation_mode="anchor", size=12, color="red") - _, texts, autopct = plt.gca().pie(data, labels=labels, autopct='%.2f', - textprops=textprops) - for labels in [texts, autopct]: + fig, ax = plt.subplots() + + pie1 = ax.pie(data, wedge_labels=labels, autopct='%.2f', textprops=textprops) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + pie2 = ax.pie(data, labels=labels, textprops=textprops) + + for labels in pie1.texts + pie2.texts: for tx in labels: assert tx.get_ha() == textprops["horizontalalignment"] assert tx.get_va() == textprops["verticalalignment"] @@ -6836,7 +6876,7 @@ def test_pie_invalid_labels(): # Test ValueError raised when feeding short labels list to axes.pie fig, ax = plt.subplots() with pytest.raises(ValueError): - ax.pie([1, 2, 3], labels=["One", "Two"]) + ax.pie([1, 2, 3], labels=["One", "Two"], labeldistance=None) def test_pie_invalid_radius(): @@ -6846,6 +6886,13 @@ def test_pie_invalid_radius(): ax.pie([1, 2, 3], radius=-5) +def test_pie_wedge_labels_and_labels(): + fig, ax = plt.subplots() + with pytest.raises(ValueError, match='wedge_labels is a replacement for labels'): + ax.pie([1, 2], wedge_labels=['spam', 'eggs'], labels=['bacon', 'beans'], + labeldistance=1.2) + + def test_normalize_kwarg_pie(): fig, ax = plt.subplots() x = [0.3, 0.3, 0.1] @@ -10323,13 +10370,13 @@ def test_pie_non_finite_values(): df = [5, float('nan'), float('inf')] with pytest.raises(ValueError, match='Wedge sizes must be finite numbers'): - ax.pie(df, labels=['A', 'B', 'C']) + ax.pie(df) def test_pie_all_zeros(): fig, ax = plt.subplots() with pytest.raises(ValueError, match="All wedge sizes are zero"): - ax.pie([0, 0], labels=["A", "B"]) + ax.pie([0, 0]) def test_animated_artists_not_drawn_by_default(): diff --git a/lib/matplotlib/tests/test_container.py b/lib/matplotlib/tests/test_container.py index b7dfe1196685..d27ee1115171 100644 --- a/lib/matplotlib/tests/test_container.py +++ b/lib/matplotlib/tests/test_container.py @@ -57,10 +57,13 @@ def test_barcontainer_position_centers__bottoms__tops(): def test_piecontainer_remove(): fig, ax = plt.subplots() - pie = ax.pie([2, 3], labels=['foo', 'bar'], autopct="%1.0f%%") + pie = ax.pie([2, 3], wedge_labels=['foo', 'bar'], autopct="%1.0f%%") ax.pie_label(pie, ['baz', 'qux']) + assert len(ax.patches) == 2 - assert len(ax.texts) == 6 + # We have added 6 labels but pie also adds an empty Text artist to each + # wedge if labeldistance is not None and labels is not passed + assert len(ax.texts) == 8 pie.remove() assert not ax.patches diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 0a1a26c7cb76..c312929b67a2 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -95,6 +95,8 @@ def __init__(self, value): self._repr = "np.mean" elif value is _api.deprecation._deprecated_parameter: self._repr = "_api.deprecation._deprecated_parameter" + elif value is _api.UNSET: + self._repr = "_UNSET" elif isinstance(value, Enum): # Enum str is Class.Name whereas their repr is . self._repr = f'{type(value).__name__}.{value.name}' From de9e18441334c35637f406aeedbb2d56a877dd3a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 29 Apr 2026 15:28:03 -0400 Subject: [PATCH 15/99] Extend invalid hatch pattern deprecation again The comment says it should be 1 release after custom hatches are implemented, and they aren't implemented in 3.11, so this needs to go up. --- lib/matplotlib/hatch.py | 2 +- lib/matplotlib/tests/test_axes.py | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/hatch.py b/lib/matplotlib/hatch.py index 5e0b6d761a98..4feb90d34715 100644 --- a/lib/matplotlib/hatch.py +++ b/lib/matplotlib/hatch.py @@ -206,7 +206,7 @@ def _validate_hatch_pattern(hatch): invalids = ''.join(sorted(invalids)) _api.warn_deprecated( '3.4', - removal='3.11', # one release after custom hatches (#20690) + removal='3.13', # one release after custom hatches (#20690) message=f'hatch must consist of a string of "{valid}" or ' 'None, but found the following invalid values ' f'"{invalids}". Passing invalid values is deprecated ' diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6751666360b1..91836f9455b3 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -10047,21 +10047,13 @@ def assert_not_in_reference_cycle(start): def test_boxplot_tick_labels(): - # Test the renamed `tick_labels` parameter. - # Test for deprecation of old name `labels`. + # Test the `tick_labels` parameter. np.random.seed(19680801) data = np.random.random((10, 3)) - fig, axs = plt.subplots(nrows=1, ncols=2, sharey=True) - # Should get deprecation warning for `labels` - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match='has been renamed \'tick_labels\''): - axs[0].boxplot(data, labels=['A', 'B', 'C']) - assert [l.get_text() for l in axs[0].get_xticklabels()] == ['A', 'B', 'C'] - - # Test the new tick_labels parameter - axs[1].boxplot(data, tick_labels=['A', 'B', 'C']) - assert [l.get_text() for l in axs[1].get_xticklabels()] == ['A', 'B', 'C'] + fig, ax = plt.subplots() + ax.boxplot(data, tick_labels=['A', 'B', 'C']) + assert [l.get_text() for l in ax.get_xticklabels()] == ['A', 'B', 'C'] @needs_usetex From 7814ada10b25ba4a8947957e717a9c0406a98ae8 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:50:53 +0200 Subject: [PATCH 16/99] DOC: Improve testing documentation (#31584) Rordered topics and made descriptions more concise. --------- Co-authored-by: hannah --- doc/devel/testing.rst | 117 ++++++++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 34 deletions(-) diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index cbde2bed7979..0b9390477b10 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -100,34 +100,45 @@ to the folder where the baseline test images are stored. The triage tool require :ref:`QT ` is installed. -Writing a simple test ---------------------- +Writing tests +------------- +Tests are located in :file:`lib/matplotlib/tests`. They are organized to mirror +the structure of the code in :file:`lib/matplotlib`. For example, tests for +the ``mathtext.py`` module are in :file:`lib/matplotlib/tests/test_mathtext.py`. -Many elements of Matplotlib can be tested using standard tests. For -example, here is a test from :file:`matplotlib/tests/test_basic.py`:: +Naming follows standard pytest conventions: - def test_simple(): - """ - very simple example test - """ - assert 1 + 1 == 2 +- files begin with ``"test_"`` +- test functions begin with ``"test_"`` +- test classes begin with ``"Test"``. -Pytest determines which functions are tests by searching for files whose names -begin with ``"test_"`` and then within those files for functions beginning with -``"test"`` or classes beginning with ``"Test"``. +We prefer simple test functions, but test classes are also acceptable. +Test function names should be descriptive of what they are testing, and long names +like ``test_to_rgba_array_accepts_color_alpha_tuple_with_multiple_colors()`` are +perfectly fine. -Some tests have internal side effects that need to be cleaned up after their -execution (such as created figures or modified `.rcParams`). The pytest fixture -``matplotlib.testing.conftest.mpl_test_settings`` will automatically clean -these up; there is no need to do anything further. +Unit tests +^^^^^^^^^^ -Random data in tests --------------------- +Many elements of Matplotlib can be tested using simple unit tests, e.g. :: -Random data is a very convenient way to generate data for examples, -however the randomness is problematic for testing (as the tests -must be deterministic!). To work around this set the seed in each test. -For numpy's default random number generator use:: + def test_to_rgba_explicit_alpha_overrides_tuple_alpha(): + assert mcolors.to_rgba(('red', 0.1), alpha=0.9) == (1, 0, 0, 0.9) + +Data in tests +^^^^^^^^^^^^^ +Try to use minimal explicit data, such as +``[1, 2, 3]``, ``range(5)`` or ``np.arange(5)``, because it +makes the test more readable. + +When you need more and non-trivial data, generate it programmatically, e.g. :: + + x = np.linspace(0, 2*np.pi, 101) + y = 2 * np.sin(x) + 1 + +Use random numbers only when an algorithmic way to generate the data is too +cumbersome or impossible. In this case, set the seed to a fixed value to make +the test deterministic. For numpy's default random number generator use :: import numpy as np rng = np.random.default_rng(19680801) @@ -136,10 +147,56 @@ and then use ``rng`` when generating the random numbers. The seed is :ref:`John Hunter's ` birthday. +Test cleanup +^^^^^^^^^^^^ +We often need to create figures or to modify `.rcParams` to test some functionality. +Cleanup of such side effects is handled automatically through a pytest fixture +(``matplotlib.testing.conftest.mpl_test_settings``) so that no manual cleanup is +necessary. + +In particular, you don't need to call ``plt.close()``. + +Testing with figures and Axes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +When you need figures and/or Axes, create them through the standard methods +(``plt.figure()``, ``plt.subplots()``, etc.). + +Creating figures and Axes is rather expensive (>100ms). Only create as many as you need for +the test, and reuse them if possible. It is perfectly fine to test multiple parametrizations +or related functionality in one test; i.e. extend the classical test structure +*Arrange–Act–Assert* with multiple *Act-Assert* blocks, e.g. :: + + def test_stackplot_facecolor(): + # Test that facecolors are properly passed and take precedence over colors parameter + x = np.linspace(0, 10, 10) + y1 = 1.0 * x + y2 = 2.0 * x + 1 + + fig, ax = plt.subplots() + + facecolors = ['r', 'b'] + + colls = ax.stackplot(x, y1, y2, facecolor=facecolors, colors=['c', 'm']) + for coll, fcolor in zip(colls, facecolors): + assert mcolors.same_color(coll.get_facecolor(), fcolor) + + # Plural alias should also work + colls = ax.stackplot(x, y1, y2, facecolors=facecolors, colors=['c', 'm']) + for coll, fcolor in zip(colls, facecolors): + assert mcolors.same_color(coll.get_facecolor(), fcolor) + +Assert values rather than visual results when feasible. This is clearer, +less computationally expensive and less fragile than comparing images, e.g. :: + + def test_savefig_preserve_layout_engine(): + fig = plt.figure(layout='compressed') + fig.savefig(io.BytesIO(), bbox_inches='tight') + assert fig.get_layout_engine()._compress + .. _image-comparison: -Writing an image comparison test --------------------------------- +Testing with reference images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Writing an image-based test is only slightly more difficult than a simple test. The main consideration is that you must specify the "baseline", or @@ -180,9 +237,8 @@ texts (labels, tick labels, etc) are not really part of what is tested, use the will lead to smaller figures and reduce possible issues with font mismatch on different platforms. - -Compare two methods of creating an image -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Testing by comparing two methods to create an image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Baseline images take a lot of space in the Matplotlib repository. An alternative approach for image comparison tests is to use the @@ -228,13 +284,6 @@ See the documentation of `~matplotlib.testing.decorators.image_comparison` and `~matplotlib.testing.decorators.check_figures_equal` for additional information about their use. -Creating a new module in matplotlib.tests ------------------------------------------ - -We try to keep the tests categorized by the primary module they are -testing. For example, the tests related to the ``mathtext.py`` module -are in ``test_mathtext.py``. - Using GitHub Actions for CI --------------------------- From cf25ca50e487012bee9742b2f8b80d357b785073 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:46:25 +0200 Subject: [PATCH 17/99] FIX: URL links in SVG should have target='_blank' When embedding SVG in HTML, link `href`s can target different browsing contexts. This e.g. leads to links inside SVGs in #31497 replacing the SVG image instead of opening a new browser window. The solution is to set `target="_blank"` for all links that implement `Artist.get_url()`. This is always intended as an external link, so universally adding `target="_blank"` is justified. Background info: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/target https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/target#_blank --- doc/api/next_api_changes/behavior/31578-TH.rst | 10 ++++++++++ lib/matplotlib/backends/backend_svg.py | 10 +++++----- lib/matplotlib/tests/test_backend_svg.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/31578-TH.rst diff --git a/doc/api/next_api_changes/behavior/31578-TH.rst b/doc/api/next_api_changes/behavior/31578-TH.rst new file mode 100644 index 000000000000..0607652c7c8f --- /dev/null +++ b/doc/api/next_api_changes/behavior/31578-TH.rst @@ -0,0 +1,10 @@ +SVG links open in new tab or window +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +`.Artist.set_url` allows to turn the Artist into a link. In SVG output, +this has been implemented without specifying a `target`_ attribute. The default +target value "_self" resulted in replacing the SVG document with the linked page +when the link was clicked. + +The target is now set to "_blank" so that the link opens in a new tab or window. + +.. _target: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/target diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 6445915de38b..24790356b9d7 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -697,7 +697,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): sketch=gc.get_sketch_params()) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) self.writer.element('path', d=path_data, **self._get_clip_attrs(gc), style=self._get_style(gc, rgbFace)) if gc.get_url() is not None: @@ -730,7 +730,7 @@ def draw_markers( writer.start('g', **self._get_clip_attrs(gc)) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': f'#{oid}'} clip = (0, 0, self.width*72, self.height*72) @@ -788,7 +788,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, antialiaseds, urls, offset_position, hatchcolors=hatchcolors): url = gc0.get_url() if url is not None: - writer.start('a', attrib={'xlink:href': url}) + writer.start('a', attrib={'xlink:href': url, 'target': '_blank'}) clip_attrs = self._get_clip_attrs(gc0) if clip_attrs: writer.start('g', **clip_attrs) @@ -966,7 +966,7 @@ def draw_image(self, gc, x, y, im, transform=None): url = gc.get_url() if url is not None: - self.writer.start('a', attrib={'xlink:href': url}) + self.writer.start('a', attrib={'xlink:href': url, 'target': '_blank'}) attrib = {} oid = gc.get_gid() @@ -1288,7 +1288,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): self.writer.start('g', **clip_attrs) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) if mpl.rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index ba565eadb01b..6b63990f7620 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -65,7 +65,7 @@ def test_text_urls(): fig.savefig(fd, format='svg') buf = fd.getvalue().decode() - expected = f'' + expected = f'' assert expected in buf From 014076e2126a05954904c46589d87b4957ace9de Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:58:33 +0200 Subject: [PATCH 18/99] Backport PR #31588: Expire some missed deprecations from 3.9 (#31592) Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- doc/api/next_api_changes/removals/31588-ES.rst | 18 ++++++++++++++++++ lib/matplotlib/axes/_axes.py | 4 +--- lib/matplotlib/backend_bases.py | 14 -------------- lib/matplotlib/hatch.py | 2 +- lib/matplotlib/tests/test_axes.py | 16 ++++------------ 5 files changed, 24 insertions(+), 30 deletions(-) create mode 100644 doc/api/next_api_changes/removals/31588-ES.rst diff --git a/doc/api/next_api_changes/removals/31588-ES.rst b/doc/api/next_api_changes/removals/31588-ES.rst new file mode 100644 index 000000000000..8709c5a77f5f --- /dev/null +++ b/doc/api/next_api_changes/removals/31588-ES.rst @@ -0,0 +1,18 @@ +``boxplot`` tick labels +^^^^^^^^^^^^^^^^^^^^^^^ + +The parameter *labels* has been removed in favour of *tick_labels* for clarity and +consistency with `~.Axes.bar`. + +Image path semantics of toolmanager-based tools +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, MEP22 ("toolmanager-based") Tools would try to load their icon +(``tool.image``) relative to the current working directory, or, as a fallback, from +Matplotlib's own image directory. Because both approaches are problematic for +third-party tools (the end-user may change the current working directory at any time, +and third-parties cannot add new icons in Matplotlib's image directory), this behavior +has been removed; instead, ``tool.image`` is now interpreted relative to the directory +containing the source file where the ``Tool.image`` class attribute is defined. +(Defining ``tool.image`` as an absolute path also works and is compatible with both the +old and the new semantics.) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 66770e426386..76659ae2c83d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4302,7 +4302,6 @@ def apply_mask(arrays, mask): @_api.make_keyword_only("3.10", "notch") @_preprocess_data() - @_api.rename_parameter("3.9", "labels", "tick_labels") def boxplot(self, x, notch=None, sym=None, vert=None, orientation='vertical', whis=None, positions=None, widths=None, patch_artist=None, bootstrap=None, @@ -4444,8 +4443,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, values. .. versionchanged:: 3.9 - Renamed from *labels*, which is deprecated since 3.9 - and will be removed in 3.11. + Renamed from *labels*, which is also removed in 3.11. manage_ticks : bool, default: True If True, the tick locations and labels will be adjusted to match diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 508b744ca04d..27c3752858a7 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3516,20 +3516,6 @@ def _get_image_filename(self, tool): for filename in [filename, filename + self._icon_extension]: if os.path.isfile(filename): return os.path.abspath(filename) - for fname in [ # Fallback; once deprecation elapses. - tool.image, - tool.image + self._icon_extension, - cbook._get_data_path("images", tool.image), - cbook._get_data_path("images", tool.image + self._icon_extension), - ]: - if os.path.isfile(fname): - _api.warn_deprecated( - "3.9", message=f"Loading icon {tool.image!r} from the current " - "directory or from Matplotlib's image directory. This behavior " - "is deprecated since %(since)s and will be removed in %(removal)s; " - "Tool.image should be set to a path relative to the Tool's source " - "file, or to an absolute path.") - return os.path.abspath(fname) def trigger_tool(self, name): """ diff --git a/lib/matplotlib/hatch.py b/lib/matplotlib/hatch.py index 5e0b6d761a98..4feb90d34715 100644 --- a/lib/matplotlib/hatch.py +++ b/lib/matplotlib/hatch.py @@ -206,7 +206,7 @@ def _validate_hatch_pattern(hatch): invalids = ''.join(sorted(invalids)) _api.warn_deprecated( '3.4', - removal='3.11', # one release after custom hatches (#20690) + removal='3.13', # one release after custom hatches (#20690) message=f'hatch must consist of a string of "{valid}" or ' 'None, but found the following invalid values ' f'"{invalids}". Passing invalid values is deprecated ' diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6751666360b1..91836f9455b3 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -10047,21 +10047,13 @@ def assert_not_in_reference_cycle(start): def test_boxplot_tick_labels(): - # Test the renamed `tick_labels` parameter. - # Test for deprecation of old name `labels`. + # Test the `tick_labels` parameter. np.random.seed(19680801) data = np.random.random((10, 3)) - fig, axs = plt.subplots(nrows=1, ncols=2, sharey=True) - # Should get deprecation warning for `labels` - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match='has been renamed \'tick_labels\''): - axs[0].boxplot(data, labels=['A', 'B', 'C']) - assert [l.get_text() for l in axs[0].get_xticklabels()] == ['A', 'B', 'C'] - - # Test the new tick_labels parameter - axs[1].boxplot(data, tick_labels=['A', 'B', 'C']) - assert [l.get_text() for l in axs[1].get_xticklabels()] == ['A', 'B', 'C'] + fig, ax = plt.subplots() + ax.boxplot(data, tick_labels=['A', 'B', 'C']) + assert [l.get_text() for l in ax.get_xticklabels()] == ['A', 'B', 'C'] @needs_usetex From f94bee58fd9c4128607df4a9f0baf5513d388f85 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 1 May 2026 11:44:52 +0200 Subject: [PATCH 19/99] DOC: Explain how to selectively restore ticks that are removed by sharex Closes #24958. --- .../subplots_axes_and_figures/subplots_demo.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 0e3cb1102230..0477a15d49d9 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -141,8 +141,17 @@ # %% # For subplots that are sharing axes one set of tick labels is enough. Tick # labels of inner Axes are automatically removed by *sharex* and *sharey*. -# Still there remains an unused empty space between the subplots. -# +# You can selectively restore them using `~.axes.Axes.tick_params`. + +fig, axs = plt.subplots(3, sharex=True, sharey=True) +fig.suptitle('Restored xtick labels on to Axes') +axs[0].plot(x, y ** 2) +axs[1].plot(x, 0.3 * y, 'o') +axs[2].plot(x, y, '+') + +axs[0].tick_params(labelbottom=True) + +# It is also possible to remove the empty space between the subplots. # To precisely control the positioning of the subplots, one can explicitly # create a `.GridSpec` with `.Figure.add_gridspec`, and then call its # `~.GridSpecBase.subplots` method. For example, we can reduce the height From 2c41a2dee2945dea7826c20287a0ecfe9202dc8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Weber=20Mendon=C3=A7a?= Date: Fri, 1 May 2026 15:06:00 -0300 Subject: [PATCH 20/99] DOC, DX: Add note about stepping back from PR reviews --- doc/devel/pr_guide.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/devel/pr_guide.rst b/doc/devel/pr_guide.rst index f29475cbf8d5..8cf31db0254b 100644 --- a/doc/devel/pr_guide.rst +++ b/doc/devel/pr_guide.rst @@ -208,12 +208,21 @@ Review push changes to the contributor branch, or merge the PR and then open a new PR against upstream. -* If you push to a contributor branch leave a comment explaining what +* If you push to a contributor branch, leave a comment explaining what you did, ex "I took the liberty of pushing a small clean-up PR to your branch, thanks for your work.". If you are going to make substantial changes to the code or intent of the PR please check with the contributor first. +* If you find yourself spending too much time on a PR, or feeling frustrated, + it's ok to step back. You can ask for help from other reviewers, or if you are + the only reviewer, you can ask the contributor to find another reviewer or to + wait until you have more time. Make sure to communicate with the contributor + to set the right expectations, e.g. "I currently don't have the bandwidth to + review this PR, but will try to loop someone else in." If you feel like this + PR is not a good fit for the project, you can close it with an explanation or + add the "status: autoclose candidate" label to trigger the autoclose workflow. + .. _pr-approval: Approval From 4ff533c88350b7bcc11f57e9a91aca32957a7256 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 1 May 2026 21:16:32 +0200 Subject: [PATCH 21/99] Update galleries/examples/subplots_axes_and_figures/subplots_demo.py Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- galleries/examples/subplots_axes_and_figures/subplots_demo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 0477a15d49d9..87f5ed6a3d34 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -151,6 +151,7 @@ axs[0].tick_params(labelbottom=True) +# %% # It is also possible to remove the empty space between the subplots. # To precisely control the positioning of the subplots, one can explicitly # create a `.GridSpec` with `.Figure.add_gridspec`, and then call its From 409ef90ab423956d1cb42e83dbf41b77d3f80583 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 22:44:58 +0000 Subject: [PATCH 22/99] Bump the actions group with 2 updates Bumps the actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [j178/prek-action](https://github.com/j178/prek-action). Updates `github/codeql-action` from 4.35.2 to 4.35.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/95e58e9a2cdfd71adc6e0353d5c52f41a045d225...e46ed2cbd01164d986452f91f178727624ae40d7) Updates `j178/prek-action` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/j178/prek-action/releases) - [Commits](https://github.com/j178/prek-action/compare/cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3...6ad80277337ad479fe43bd70701c3f7f8aa74db3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: j178/prek-action dependency-version: 2.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/linting.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9a7b40ccac10..9ee6ac545967 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: languages: ${{ matrix.language }} @@ -45,4 +45,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f6af70b8b233..0d6e71198817 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - - uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2 + - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 with: extra-args: --hook-stage manual --all-files From dcd4f31ad41a9235cbc3a341e28dbc7aed601a9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 22:52:07 +0000 Subject: [PATCH 23/99] Bump https://github.com/astral-sh/ruff-pre-commit Bumps [https://github.com/astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit) from v0.15.11 to 0.15.12. This release includes the previously tagged commit. - [Release notes](https://github.com/astral-sh/ruff-pre-commit/releases) - [Commits](https://github.com/astral-sh/ruff-pre-commit/compare/d1b833175a5d08a925900115526febd8fe71c98e...6fec9b7edb08fd9989088709d864a7826dc74e80) --- updated-dependencies: - dependency-name: https://github.com/astral-sh/ruff-pre-commit dependency-version: 0.15.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bfce3c54a030..7de40cf539ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: pass_filenames: false - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: d1b833175a5d08a925900115526febd8fe71c98e # frozen: v0.15.11 + rev: 6fec9b7edb08fd9989088709d864a7826dc74e80 # frozen: v0.15.12 hooks: # Run the linter. - id: ruff-check From 75754b55e8763c13805a1d07aba3064640b8907b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 1 May 2026 23:59:56 -0400 Subject: [PATCH 24/99] Backport PR #31600: Bump https://github.com/astral-sh/ruff-pre-commit from v0.15.11 to 0.15.12 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bfce3c54a030..7de40cf539ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: pass_filenames: false - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: d1b833175a5d08a925900115526febd8fe71c98e # frozen: v0.15.11 + rev: 6fec9b7edb08fd9989088709d864a7826dc74e80 # frozen: v0.15.12 hooks: # Run the linter. - id: ruff-check From d066418ff55ecc15cf86f14cf8dcd4b05960c6cb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 2 May 2026 00:03:33 -0400 Subject: [PATCH 25/99] Backport PR #31599: Bump the actions group with 2 updates --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/linting.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9a7b40ccac10..9ee6ac545967 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: languages: ${{ matrix.language }} @@ -45,4 +45,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f6af70b8b233..0d6e71198817 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - - uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2 + - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 with: extra-args: --hook-stage manual --all-files From 6441aff0cba368fc425c8583c5a90595b39ea59b Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 1 May 2026 08:58:20 +0200 Subject: [PATCH 26/99] DOC: Improve docs on writing documentation --- doc/devel/document.rst | 104 +++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 55 deletions(-) diff --git a/doc/devel/document.rst b/doc/devel/document.rst index bea4771af383..a4a4926fdd95 100644 --- a/doc/devel/document.rst +++ b/doc/devel/document.rst @@ -154,8 +154,8 @@ for opening them in your default browser is: .. _writing-rest-pages: -Write ReST pages -================ +reStructuredText pages +====================== Most documentation is either in the docstrings of individual classes and methods, in explicit ``.rst`` files, or in examples and tutorials. @@ -243,11 +243,15 @@ nor the ````literal```` role: Do not describe ``argument`` like this. -Write mathematical expressions ------------------------------- +Mathematical expressions +------------------------ +Use sphinx's built in math support: + +- **Inline math:** Use the ``:math:`` + `role `__ +- **Math blocks:** Use the ``.. math::`` + `directive `__ -In most cases, you will likely want to use one of `Sphinx's builtin Math -extensions `__. In rare cases we want the rendering of the mathematical text in the documentation html to exactly match with the rendering of the mathematical expression in the Matplotlib figure. In these cases, you can use the @@ -257,17 +261,17 @@ expression in the Matplotlib figure. In these cases, you can use the .. _internal-section-refs: -Refer to other documents and sections -------------------------------------- +Cross-references +---------------- Sphinx_ supports internal references_: -========== =============== =========================================== -Role Links target Representation in rendered HTML -========== =============== =========================================== -|doc-dir|_ document link to a page -|ref-dir|_ reference label link to an anchor associated with a heading -========== =============== =========================================== +========== ============================== =========================================== +Role Link target Representation in rendered HTML +========== ============================== =========================================== +|doc-dir|_ :ref:`page ` link to a page +|ref-dir|_ :ref:`section ` link to an anchor associated with a heading +========== ============================== =========================================== .. The following is a hack to have a link with literal formatting See https://stackoverflow.com/a/4836544 @@ -277,63 +281,53 @@ Role Links target Representation in rendered HTML .. |ref-dir| replace:: ``:ref:`` .. _ref-dir: https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref -Examples: +.. _link-pages: -.. code-block:: rst +Link to pages +^^^^^^^^^^^^^ - See the :doc:`/install/index` +To cross-link to another page, use the ``:doc:`` role. We generally prefer +absolute paths, starting with ``/`` as the :file:`doc` root directory. + +Example: - See the tutorial :ref:`quick_start` +.. code-block:: rst - See the example :doc:`/gallery/lines_bars_and_markers/simple_plot` + See the :doc:`/install/index` will render as: See the :doc:`/install/index` - See the tutorial :ref:`quick_start` - - See the example :doc:`/gallery/lines_bars_and_markers/simple_plot` +.. _link-sections: -Sections can also be given reference labels. For instance from the -:doc:`/install/index` link: - -.. code-block:: rst - - .. _clean-install: - - How to completely remove Matplotlib - =================================== +Link to sections +^^^^^^^^^^^^^^^^ - Occasionally, problems with Matplotlib can be solved with a clean... +Use hyphen-separated, descriptive names for reference labels. +Do not encode the documentation hierarchy in the label as that may change; +e.g. do not prefix all *User guide* labels with ``user-``. -and refer to it using the standard reference syntax: +To cross-link a specific section, add a reference label ``.. _label-name:`` +before the section .. code-block:: rst - See :ref:`clean-install` + .. _pr-author-guidelines: -will give the following link: :ref:`clean-install` + Summary for pull request authors + ================================ -To maximize internal consistency in section labeling and references, -use hyphen separated, descriptive labels for section references. -Keep in mind that contents may be reorganized later, so -avoid top level names in references like ``user`` or ``devel`` -or ``faq`` unless necessary, because for example the FAQ "what is a -backend?" could later become part of the users guide, so the label: +and then link to with ``:ref:`label-name``` .. code-block:: rst - .. _what-is-a-backend: - -is better than: + See the :ref:`pr-author-guidelines` -.. code-block:: rst +This will render as: - .. _faq-backend: + See the :ref:`pr-author-guidelines` -In addition, since underscores are widely used by Sphinx itself, use -hyphens to separate words. .. _referring-to-other-code: @@ -461,8 +455,8 @@ For clarity, do not use relative links. .. _writing-docstrings: -Write API documentation -======================= +API documentation +================= The API reference documentation describes the library interfaces, e.g. inputs, outputs, and expected behavior. Most of the API documentation is written in docstrings. These are @@ -957,8 +951,8 @@ Example: .. _writing-examples-and-tutorials: -Write examples and tutorials -============================ +Examples and tutorials +====================== Examples and tutorials are Python scripts that are run by `Sphinx Gallery`_. Sphinx Gallery finds ``*.py`` files in source directories and runs the files to @@ -1226,10 +1220,10 @@ Format :code: The code should be about 5-10 lines with minimal customization. Plots in this gallery use the ``_mpl-gallery`` stylesheet for a uniform aesthetic. -Analytics -========== +Website analytics +================= -Documentation page analytics are available at +Analytics of our hosted documentation https://matplotlib.org is available at https://views.scientific-python.org/matplotlib.org. From f32cfc57a2c5bf0fbce2f75cd082ce0bfd2d5860 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 2 May 2026 01:51:57 -0400 Subject: [PATCH 27/99] Backport PR #31594: DOC: Explain how to selectively restore ticks that are removed by sharex --- .../subplots_axes_and_figures/subplots_demo.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 0e3cb1102230..87f5ed6a3d34 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -141,8 +141,18 @@ # %% # For subplots that are sharing axes one set of tick labels is enough. Tick # labels of inner Axes are automatically removed by *sharex* and *sharey*. -# Still there remains an unused empty space between the subplots. -# +# You can selectively restore them using `~.axes.Axes.tick_params`. + +fig, axs = plt.subplots(3, sharex=True, sharey=True) +fig.suptitle('Restored xtick labels on to Axes') +axs[0].plot(x, y ** 2) +axs[1].plot(x, 0.3 * y, 'o') +axs[2].plot(x, y, '+') + +axs[0].tick_params(labelbottom=True) + +# %% +# It is also possible to remove the empty space between the subplots. # To precisely control the positioning of the subplots, one can explicitly # create a `.GridSpec` with `.Figure.add_gridspec`, and then call its # `~.GridSpecBase.subplots` method. For example, we can reduce the height From c19bfe85534b7524b682316e1ea5d3b03b5d2d67 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Sat, 2 May 2026 12:28:58 -0400 Subject: [PATCH 28/99] comment --- lib/matplotlib/tests/test_collections.py | 3 +++ pyproject.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 45c860fa0075..46122b8b1e6a 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -706,6 +706,9 @@ def test_set_wrong_linestyle(): @pytest.mark.parametrize('ls', ['', ' ', 'none']) def test_scatter_empty_linestyle_pdf(ls): + # Regression test: '', ' ', and 'none' are documented "draw nothing" + # linestyle aliases but were not recognized by _get_dash_pattern, causing + # savefig to PDF to crash with "zero-size array to reduction operation maximum". plt.switch_backend('pdf') fig, ax = plt.subplots() ax.scatter([0, 1], [0, 1], ls=ls) diff --git a/pyproject.toml b/pyproject.toml index f3c38512a2c9..d75db711faf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ extend-exclude = [ "doc/tutorials", "tools/gh_api.py", ] +target-version = "py311" line-length = 88 [tool.ruff.lint] @@ -295,6 +296,7 @@ convention = "numpy" "galleries/users_explain/text/text_props.py" = ["E501"] [tool.mypy] +python_version = "3.11" ignore_missing_imports = true enable_error_code = [ "ignore-without-code", From f45ed51e696fd4ffa99730e15517b14f93674483 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Sat, 2 May 2026 12:30:50 -0400 Subject: [PATCH 29/99] build --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d75db711faf0..f3c38512a2c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,7 +184,6 @@ extend-exclude = [ "doc/tutorials", "tools/gh_api.py", ] -target-version = "py311" line-length = 88 [tool.ruff.lint] @@ -296,7 +295,6 @@ convention = "numpy" "galleries/users_explain/text/text_props.py" = ["E501"] [tool.mypy] -python_version = "3.11" ignore_missing_imports = true enable_error_code = [ "ignore-without-code", From 83088b8a64c40110bedbc5881461f022efa5f7cc Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 4 May 2026 23:12:13 +0200 Subject: [PATCH 30/99] Remove outdated comment re: implementation of hinting_factor. (#31608) --- src/ft2font.cpp | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index e99f9a7e1095..e853346bf1f4 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -16,31 +16,6 @@ #define M_PI 3.14159265358979323846264338328 #endif -/** - To improve the hinting of the fonts, this code uses a hack - presented here: - - http://agg.sourceforge.net/antigrain.com/research/font_rasterization/index.html - - The idea is to limit the effect of hinting in the x-direction, while - preserving hinting in the y-direction. Since freetype does not - support this directly, the dpi in the x-direction is set higher than - in the y-direction, which affects the hinting grid. Then, a global - transform is placed on the font to shrink it back to the desired - size. While it is a bit surprising that the dpi setting affects - hinting, whereas the global transform does not, this is documented - behavior of FreeType, and therefore hopefully unlikely to change. - The FreeType 2 tutorial says: - - NOTE: The transformation is applied to every glyph that is - loaded through FT_Load_Glyph and is completely independent of - any hinting process. This means that you won't get the same - results if you load a glyph at the size of 24 pixels, or a glyph - at the size at 12 pixels scaled by 2 through a transform, - because the hints will have been computed differently (except - you have disabled hints). - */ - FT_Library _ft2Library; FT2Image::FT2Image(unsigned long width, unsigned long height) From e0b3138096d267b6b7ffdbb6844aa2e68fac3b4e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 4 May 2026 23:12:13 +0200 Subject: [PATCH 31/99] Backport PR #31608: Remove outdated comment re: implementation of hinting_factor. --- src/ft2font.cpp | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index e99f9a7e1095..e853346bf1f4 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -16,31 +16,6 @@ #define M_PI 3.14159265358979323846264338328 #endif -/** - To improve the hinting of the fonts, this code uses a hack - presented here: - - http://agg.sourceforge.net/antigrain.com/research/font_rasterization/index.html - - The idea is to limit the effect of hinting in the x-direction, while - preserving hinting in the y-direction. Since freetype does not - support this directly, the dpi in the x-direction is set higher than - in the y-direction, which affects the hinting grid. Then, a global - transform is placed on the font to shrink it back to the desired - size. While it is a bit surprising that the dpi setting affects - hinting, whereas the global transform does not, this is documented - behavior of FreeType, and therefore hopefully unlikely to change. - The FreeType 2 tutorial says: - - NOTE: The transformation is applied to every glyph that is - loaded through FT_Load_Glyph and is completely independent of - any hinting process. This means that you won't get the same - results if you load a glyph at the size of 24 pixels, or a glyph - at the size at 12 pixels scaled by 2 through a transform, - because the hints will have been computed differently (except - you have disabled hints). - */ - FT_Library _ft2Library; FT2Image::FT2Image(unsigned long width, unsigned long height) From f4cc437d1bd72ec72cfc2a0e318c867c6969392f Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 5 May 2026 05:26:21 +0200 Subject: [PATCH 32/99] DOC: Consolidate shared axis examples (#31605) * DOC: Consolidate shared axis examples This replaces the two existing examples into one single example, that shows the basic concept of sharing and links out for further details. For now, the link goes to the subplots_demo. The sharing part should be refactored out into a tutorial. But that's a separate topic, and the link anchor will move along in such a refactoring. --------- Co-authored-by: hannah --- .../share_axis_lims_views.py | 32 ---------- .../shared_axis_demo.py | 64 ++++++++----------- .../subplots_demo.py | 2 + 3 files changed, 30 insertions(+), 68 deletions(-) delete mode 100644 galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py diff --git a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py b/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py deleted file mode 100644 index e0aa04d13def..000000000000 --- a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -=========================== -Share axis limits and views -=========================== - -It's common to make two or more plots which share an axis, e.g., two subplots -with time as a common axis. When you pan and zoom around on one, you want the -other to move around with you. To facilitate this, matplotlib Axes support a -``sharex`` and ``sharey`` attribute. When you create a `~.pyplot.subplot` or -`~.pyplot.axes`, you can pass in a keyword indicating what Axes you want to -share with. -""" - -import matplotlib.pyplot as plt -import numpy as np - -t = np.arange(0, 10, 0.01) - -ax1 = plt.subplot(211) -ax1.plot(t, np.sin(2*np.pi*t)) - -ax2 = plt.subplot(212, sharex=ax1) -ax2.plot(t, np.sin(4*np.pi*t)) - -plt.show() - -# %% -# .. tags:: -# -# component: axis -# plot-type: line -# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py b/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py index 848db115456a..e5926e8f7ff4 100644 --- a/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py +++ b/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py @@ -3,46 +3,38 @@ Shared axis =========== -You can share the x- or y-axis limits for one axis with another by -passing an `~.axes.Axes` instance as a *sharex* or *sharey* keyword argument. - -Changing the axis limits on one Axes will be reflected automatically -in the other, and vice-versa, so when you navigate with the toolbar -the Axes will follow each other on their shared axis. Ditto for -changes in the axis scaling (e.g., log vs. linear). However, it is -possible to have differences in tick labeling, e.g., you can selectively -turn off the tick labels on one Axes. - -The example below shows how to customize the tick labels on the -various axes. Shared axes share the tick locator, tick formatter, -view limits, and transformation (e.g., log, linear). But the tick labels -themselves do not share properties. This is a feature and not a bug, -because you may want to make the tick labels smaller on the upper -axes, e.g., in the example below. +Use axis sharing when you want to compare data across multiple subplots, and want to +ensure they are on the same scale. To do so, pass ``sharex=True`` and/or ``sharey=True`` +to `~.pyplot.subplots`. + +This ensures the x- or y-axis limits are synchronized across the subplots. Autoscaling +considers the data on all Axes; therefore, any limit changes, including interactive zoom +and pan, will affect all shared axes. + +The plot below illustrates this by showing two different time-series and using *sharex* +to ensure the times are aligned. + +For more info see :ref:`sharing-axes`. + +.. redirect-from:: /gallery/subplots_axes_and_figures/share_axis_lims_views """ import matplotlib.pyplot as plt import numpy as np -t = np.arange(0.01, 5.0, 0.01) -s1 = np.sin(2 * np.pi * t) -s2 = np.exp(-t) -s3 = np.sin(4 * np.pi * t) - -ax1 = plt.subplot(311) -plt.plot(t, s1) -# reduce the fontsize of the tick labels -plt.tick_params('x', labelsize=6) - -# share x only -ax2 = plt.subplot(312, sharex=ax1) -plt.plot(t, s2) -# make these tick labels invisible -plt.tick_params('x', labelbottom=False) - -# share x and y -ax3 = plt.subplot(313, sharex=ax1, sharey=ax1) -plt.plot(t, s3) -plt.xlim(0.01, 5.0) +t1 = np.linspace(0, 8, 201) +y1 = np.sin(2 * np.pi * t1) +t2 = np.linspace(2, 10, 201) +y2 = 20 * np.cos(2 * np.pi * t2)**2 * np.exp(-0.3*t2) + +fig, (ax1, ax2) = plt.subplots(2, sharex=True) + +ax1.plot(t1, y1) +ax1.set_ylabel("Signal 1") + +ax2.plot(t2, y2) +ax2.set_ylabel("Signal 2") +ax2.set_xlabel("Time (s)") + plt.show() # %% diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 87f5ed6a3d34..ea38a2483fdd 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -108,6 +108,8 @@ ax.label_outer() # %% +# .. _sharing-axes: +# # Sharing axes # """""""""""" # From 488d435233a24cb6c78777355303971e7af5a6b7 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 5 May 2026 21:53:44 -0400 Subject: [PATCH 33/99] Make Scale axis parameter handling more flexible Instead of checking the types ourselves, try to bind the old signature, and if possible, use it. Otherwise, try to use the new signature. This is similar to `_api.select_matching_signature` without needing to write a callable for each signature. --- lib/matplotlib/scale.py | 21 ++++++++++---- lib/matplotlib/tests/test_scale.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 0793bb31e566..a4cce23562d3 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -138,7 +138,7 @@ def _make_axis_parameter_optional(init_func): This decorator ensures backward compatibility for scale classes that previously required an *axis* parameter. It allows constructors to be - callerd with or without the *axis* parameter. + called with or without the *axis* parameter. For simplicity, this does not handle the case when *axis* is passed as a keyword. However, @@ -170,12 +170,16 @@ def _make_axis_parameter_optional(init_func): """ @wraps(init_func) def wrapper(self, *args, **kwargs): - if args and isinstance(args[0], mpl.axis.Axis): - return init_func(self, *args, **kwargs) + sig = inspect.signature(init_func) + try: + # Try old signature. + sig.bind(self, *args, **kwargs) + except TypeError: + # Use the new signature and pass in an unused axis=None. + init_func(self, None, *args, **kwargs) else: - # Remove 'axis' from kwargs to avoid double assignment - axis = kwargs.pop('axis', None) - return init_func(self, axis, *args, **kwargs) + # Use the old signature. + init_func(self, *args, **kwargs) return wrapper @@ -449,6 +453,11 @@ def __init__(self, axis, functions, base=10): ---------- axis : `~matplotlib.axis.Axis` The axis for the scale. + + .. note:: + This parameter is unused and about to be removed in the future. + It can already now be left out because of special preprocessing, + so that ``FuncScaleLog(functions=(forward, inverse))`` is valid. functions : (callable, callable) two-tuple of the forward and inverse functions for the scale. The forward function must be monotonic. diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 95601ce97b65..104c87adab7b 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -3,6 +3,7 @@ import matplotlib.pyplot as plt from matplotlib.scale import ( AsinhScale, AsinhTransform, + FuncScale, LogitScale, LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale @@ -19,6 +20,49 @@ import pytest +def test_optional_axis_signature(): + # There are three types of original signatures possible, and this only tests one + # example class of each: + # 1. `axis` without default: LinearScale, FuncScale, FuncScaleLog + # 2. `axis` with default and more positional parameters: LogitScale + # 3. `axis` with default and only keyword-only parameters: LogScale, AsinhScale, + # SymmetricalLogScale + # Testing with None is sufficient as detection is purely based on the + # signature structure; no type information is involved. + axis = None + + # Old signature with axis positionally. + FuncScale(axis, (lambda x: x, lambda x: x)) + FuncScale(axis, functions=(lambda x: x, lambda x: x)) + LogitScale(axis) + LogitScale(axis, 'clip') + LogitScale(axis, nonpositive='clip') + LogitScale(axis, use_overline=True) + AsinhScale(axis) + AsinhScale(axis, linear_width=2) + AsinhScale(axis, base=3) + AsinhScale(axis, subs=[2, 6]) + # Old signature with axis as keyword. + FuncScale(axis=axis, functions=(lambda x: x, lambda x: x)) + LogitScale(axis=axis) + LogitScale(axis=axis, nonpositive='clip') + LogitScale(axis=axis, use_overline=True) + AsinhScale(axis=axis) + AsinhScale(axis=axis, linear_width=2) + AsinhScale(axis=axis, base=3) + AsinhScale(axis=axis, subs=[2, 6]) + # New signature without axis. + FuncScale((lambda x: x, lambda x: x)) + FuncScale(functions=(lambda x: x, lambda x: x)) + LogitScale() + LogitScale(nonpositive='clip') + LogitScale(use_overline=True) + AsinhScale() + AsinhScale(linear_width=2) + AsinhScale(base=3) + AsinhScale(subs=[2, 6]) + + @check_figures_equal() def test_log_scales(fig_test, fig_ref): ax_test = fig_test.add_subplot(122, yscale='log', xscale='symlog') From b1dfa09c42c0d84fab5b1f9cb2c1051a7a70c5ee Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 6 May 2026 17:11:14 -0400 Subject: [PATCH 34/99] DOC: Inline ScalarMappable reStructuredText entries There is nothing that is dynamic here, so there's no need to generate this with code. --- doc/api/.gitignore | 1 - doc/api/cm_api.rst | 18 +++++++++++++++- doc/conf.py | 53 ---------------------------------------------- 3 files changed, 17 insertions(+), 55 deletions(-) delete mode 100644 doc/api/.gitignore diff --git a/doc/api/.gitignore b/doc/api/.gitignore deleted file mode 100644 index dbed88d89836..000000000000 --- a/doc/api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -scalarmappable.gen_rst diff --git a/doc/api/cm_api.rst b/doc/api/cm_api.rst index c9509389a2bb..8476ab14cb86 100644 --- a/doc/api/cm_api.rst +++ b/doc/api/cm_api.rst @@ -7,4 +7,20 @@ :undoc-members: :show-inheritance: -.. include:: scalarmappable.gen_rst +.. class:: ScalarMappable(colorizer, **kwargs) + :canonical: matplotlib.colorizer._ScalarMappable + + .. automethod:: autoscale + .. automethod:: autoscale_None + .. automethod:: changed + .. autoproperty:: colorbar + .. automethod:: get_alpha + .. automethod:: get_array + .. automethod:: get_clim + .. automethod:: get_cmap + .. autoproperty:: norm + .. automethod:: set_array + .. automethod:: set_clim + .. automethod:: set_cmap + .. automethod:: set_norm + .. automethod:: to_rgba diff --git a/doc/conf.py b/doc/conf.py index 4be3fcf3afee..6651383fcacb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -837,58 +837,6 @@ def linkcode_resolve(domain, info): extensions.append('sphinx.ext.viewcode') -def generate_ScalarMappable_docs(): - - import matplotlib.colorizer - from numpydoc.docscrape_sphinx import get_doc_object - from pathlib import Path - import textwrap - from sphinx.util.inspect import stringify_signature - target_file = Path(__file__).parent / 'api' / 'scalarmappable.gen_rst' - with open(target_file, 'w') as fout: - fout.write(""" -.. class:: ScalarMappable(colorizer, **kwargs) - :canonical: matplotlib.colorizer._ScalarMappable - -""") - for meth in [ - matplotlib.colorizer._ScalarMappable.autoscale, - matplotlib.colorizer._ScalarMappable.autoscale_None, - matplotlib.colorizer._ScalarMappable.changed, - """ - .. attribute:: colorbar - - The last colorbar associated with this ScalarMappable. May be None. -""", - matplotlib.colorizer._ScalarMappable.get_alpha, - matplotlib.colorizer._ScalarMappable.get_array, - matplotlib.colorizer._ScalarMappable.get_clim, - matplotlib.colorizer._ScalarMappable.get_cmap, - """ - .. property:: norm -""", - matplotlib.colorizer._ScalarMappable.set_array, - matplotlib.colorizer._ScalarMappable.set_clim, - matplotlib.colorizer._ScalarMappable.set_cmap, - matplotlib.colorizer._ScalarMappable.set_norm, - matplotlib.colorizer._ScalarMappable.to_rgba, - ]: - if isinstance(meth, str): - fout.write(meth) - else: - name = meth.__name__ - sig = stringify_signature(inspect.signature(meth)) - docstring = textwrap.indent( - str(get_doc_object(meth)), - ' ' - ).rstrip() - fout.write(f""" - .. method:: {name}{sig} -{docstring} - -""") - - # ----------------------------------------------------------------------------- # Sphinx setup # ----------------------------------------------------------------------------- @@ -902,5 +850,4 @@ def setup(app): app.connect('autodoc-process-bases', autodoc_process_bases) if sphinx.version_info[:2] < (7, 1): app.connect('html-page-context', add_html_cache_busting, priority=1000) - generate_ScalarMappable_docs() app.config.autodoc_use_legacy_class_based = True From 85641970d44b4e54a9154d1931bf39b2c5a2c910 Mon Sep 17 00:00:00 2001 From: Ricardo Peres <20727577+ricmperes@users.noreply.github.com> Date: Thu, 7 May 2026 10:12:53 +0100 Subject: [PATCH 35/99] [BUG] Fix alpha bug on 3D PathCollection plots. (#25478) * Fix alpha bug on 3D PathCollection plots. * Fix flake8 linting errors * Handle RGB instead of RGBA case. * Clean main if clause. * linting * Add tests for scatter alpha conversion * Simplify logic for depth shading alpha correction * linting * linting * linting * Comments * restore missing line * linting * Fix logic * Apply suggestion from @timhoffm * Fix alpha not having z_marker_idx sorting when applicable --------- Co-authored-by: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Scott Shambaugh --- lib/mpl_toolkits/mplot3d/art3d.py | 27 ++++++++++++++----- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 13 +++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 6898a8aaf4cf..f664127dcb59 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -819,18 +819,26 @@ def do_3d_projection(self): return np.nan def _maybe_depth_shade_and_sort_colors(self, color_array): - color_array = ( - _zalpha( + # Adjust the color_array alpha values if point depths are defined + # and depth shading is active + alpha = self._alpha + if self._vzs is not None and self._depthshade: + color_array = _zalpha( color_array, self._vzs, min_alpha=self._depthshade_minalpha, ) - if self._vzs is not None and self._depthshade - else color_array - ) + if alpha is not None and color_array.shape[1] == 4: # RGBA, not RGB + alpha = alpha * color_array[:, 3] + + # Adjust the order of the color_array using the _z_markers_idx, + # which has been sorted by z-depth if len(color_array) > 1: color_array = color_array[self._z_markers_idx] - return mcolors.to_rgba_array(color_array, self._alpha) + if np.ndim(alpha) > 0: + alpha = np.asarray(alpha)[self._z_markers_idx] + + return mcolors.to_rgba_array(color_array, alpha) def get_facecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_facecolor()) @@ -1070,6 +1078,7 @@ def _use_zordered_offset(self): def _maybe_depth_shade_and_sort_colors(self, color_array): # Adjust the color_array alpha values if point depths are defined # and depth shading is active + alpha = self._alpha if self._vzs is not None and self._depthshade: color_array = _zalpha( color_array, @@ -1077,13 +1086,17 @@ def _maybe_depth_shade_and_sort_colors(self, color_array): min_alpha=self._depthshade_minalpha, _data_scale=self._data_scale, ) + if alpha is not None and color_array.shape[1] == 4: # RGBA, not RGB + alpha = alpha * color_array[:, 3] # Adjust the order of the color_array using the _z_markers_idx, # which has been sorted by z-depth if len(color_array) > 1: color_array = color_array[self._z_markers_idx] + if np.ndim(alpha) > 0: + alpha = np.asarray(alpha)[self._z_markers_idx] - return mcolors.to_rgba_array(color_array) + return mcolors.to_rgba_array(color_array, alpha) def get_facecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_facecolor()) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 078da596a9a4..2a5593a641c9 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -440,6 +440,19 @@ def test_scatter3d_linewidth(): marker='o', linewidth=np.arange(10)) +@check_figures_equal() +def test_scatter3d_cmap_alpha(fig_ref, fig_test): + # Check that alpha is applied correctly with colormapped scatter. + # Regression test for https://github.com/matplotlib/matplotlib/issues/25468 + x, y, z = np.arange(5), np.zeros(5), np.arange(5) + c = np.array([0, 1, np.nan, 3, 4]) + + ax_test = fig_test.add_subplot(projection='3d') + ax_test.scatter(x, y, z, c=c) + ax_ref = fig_ref.add_subplot(projection='3d') + ax_ref.scatter(x, y, z, c=c, alpha=1) + + @check_figures_equal() def test_scatter3d_linewidth_modification(fig_ref, fig_test): # Changing Path3DCollection linewidths with array-like post-creation From 56e15b53e4442c400f229d9d03660e2973f8813a Mon Sep 17 00:00:00 2001 From: Ricardo Peres <20727577+ricmperes@users.noreply.github.com> Date: Thu, 7 May 2026 10:12:53 +0100 Subject: [PATCH 36/99] Backport PR #25478: [BUG] Fix alpha bug on 3D PathCollection plots. --- lib/mpl_toolkits/mplot3d/art3d.py | 27 ++++++++++++++----- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 13 +++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 6898a8aaf4cf..f664127dcb59 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -819,18 +819,26 @@ def do_3d_projection(self): return np.nan def _maybe_depth_shade_and_sort_colors(self, color_array): - color_array = ( - _zalpha( + # Adjust the color_array alpha values if point depths are defined + # and depth shading is active + alpha = self._alpha + if self._vzs is not None and self._depthshade: + color_array = _zalpha( color_array, self._vzs, min_alpha=self._depthshade_minalpha, ) - if self._vzs is not None and self._depthshade - else color_array - ) + if alpha is not None and color_array.shape[1] == 4: # RGBA, not RGB + alpha = alpha * color_array[:, 3] + + # Adjust the order of the color_array using the _z_markers_idx, + # which has been sorted by z-depth if len(color_array) > 1: color_array = color_array[self._z_markers_idx] - return mcolors.to_rgba_array(color_array, self._alpha) + if np.ndim(alpha) > 0: + alpha = np.asarray(alpha)[self._z_markers_idx] + + return mcolors.to_rgba_array(color_array, alpha) def get_facecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_facecolor()) @@ -1070,6 +1078,7 @@ def _use_zordered_offset(self): def _maybe_depth_shade_and_sort_colors(self, color_array): # Adjust the color_array alpha values if point depths are defined # and depth shading is active + alpha = self._alpha if self._vzs is not None and self._depthshade: color_array = _zalpha( color_array, @@ -1077,13 +1086,17 @@ def _maybe_depth_shade_and_sort_colors(self, color_array): min_alpha=self._depthshade_minalpha, _data_scale=self._data_scale, ) + if alpha is not None and color_array.shape[1] == 4: # RGBA, not RGB + alpha = alpha * color_array[:, 3] # Adjust the order of the color_array using the _z_markers_idx, # which has been sorted by z-depth if len(color_array) > 1: color_array = color_array[self._z_markers_idx] + if np.ndim(alpha) > 0: + alpha = np.asarray(alpha)[self._z_markers_idx] - return mcolors.to_rgba_array(color_array) + return mcolors.to_rgba_array(color_array, alpha) def get_facecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_facecolor()) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 078da596a9a4..2a5593a641c9 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -440,6 +440,19 @@ def test_scatter3d_linewidth(): marker='o', linewidth=np.arange(10)) +@check_figures_equal() +def test_scatter3d_cmap_alpha(fig_ref, fig_test): + # Check that alpha is applied correctly with colormapped scatter. + # Regression test for https://github.com/matplotlib/matplotlib/issues/25468 + x, y, z = np.arange(5), np.zeros(5), np.arange(5) + c = np.array([0, 1, np.nan, 3, 4]) + + ax_test = fig_test.add_subplot(projection='3d') + ax_test.scatter(x, y, z, c=c) + ax_ref = fig_ref.add_subplot(projection='3d') + ax_ref.scatter(x, y, z, c=c, alpha=1) + + @check_figures_equal() def test_scatter3d_linewidth_modification(fig_ref, fig_test): # Changing Path3DCollection linewidths with array-like post-creation From 2836d9b78787c4500beb931dad857888011b9a56 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 7 May 2026 11:38:18 +0200 Subject: [PATCH 37/99] Backport PR #31625: DOC: Inline ScalarMappable reStructuredText entries --- doc/api/.gitignore | 1 - doc/api/cm_api.rst | 18 +++++++++++++++- doc/conf.py | 53 ---------------------------------------------- 3 files changed, 17 insertions(+), 55 deletions(-) delete mode 100644 doc/api/.gitignore diff --git a/doc/api/.gitignore b/doc/api/.gitignore deleted file mode 100644 index dbed88d89836..000000000000 --- a/doc/api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -scalarmappable.gen_rst diff --git a/doc/api/cm_api.rst b/doc/api/cm_api.rst index c9509389a2bb..8476ab14cb86 100644 --- a/doc/api/cm_api.rst +++ b/doc/api/cm_api.rst @@ -7,4 +7,20 @@ :undoc-members: :show-inheritance: -.. include:: scalarmappable.gen_rst +.. class:: ScalarMappable(colorizer, **kwargs) + :canonical: matplotlib.colorizer._ScalarMappable + + .. automethod:: autoscale + .. automethod:: autoscale_None + .. automethod:: changed + .. autoproperty:: colorbar + .. automethod:: get_alpha + .. automethod:: get_array + .. automethod:: get_clim + .. automethod:: get_cmap + .. autoproperty:: norm + .. automethod:: set_array + .. automethod:: set_clim + .. automethod:: set_cmap + .. automethod:: set_norm + .. automethod:: to_rgba diff --git a/doc/conf.py b/doc/conf.py index 4be3fcf3afee..6651383fcacb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -837,58 +837,6 @@ def linkcode_resolve(domain, info): extensions.append('sphinx.ext.viewcode') -def generate_ScalarMappable_docs(): - - import matplotlib.colorizer - from numpydoc.docscrape_sphinx import get_doc_object - from pathlib import Path - import textwrap - from sphinx.util.inspect import stringify_signature - target_file = Path(__file__).parent / 'api' / 'scalarmappable.gen_rst' - with open(target_file, 'w') as fout: - fout.write(""" -.. class:: ScalarMappable(colorizer, **kwargs) - :canonical: matplotlib.colorizer._ScalarMappable - -""") - for meth in [ - matplotlib.colorizer._ScalarMappable.autoscale, - matplotlib.colorizer._ScalarMappable.autoscale_None, - matplotlib.colorizer._ScalarMappable.changed, - """ - .. attribute:: colorbar - - The last colorbar associated with this ScalarMappable. May be None. -""", - matplotlib.colorizer._ScalarMappable.get_alpha, - matplotlib.colorizer._ScalarMappable.get_array, - matplotlib.colorizer._ScalarMappable.get_clim, - matplotlib.colorizer._ScalarMappable.get_cmap, - """ - .. property:: norm -""", - matplotlib.colorizer._ScalarMappable.set_array, - matplotlib.colorizer._ScalarMappable.set_clim, - matplotlib.colorizer._ScalarMappable.set_cmap, - matplotlib.colorizer._ScalarMappable.set_norm, - matplotlib.colorizer._ScalarMappable.to_rgba, - ]: - if isinstance(meth, str): - fout.write(meth) - else: - name = meth.__name__ - sig = stringify_signature(inspect.signature(meth)) - docstring = textwrap.indent( - str(get_doc_object(meth)), - ' ' - ).rstrip() - fout.write(f""" - .. method:: {name}{sig} -{docstring} - -""") - - # ----------------------------------------------------------------------------- # Sphinx setup # ----------------------------------------------------------------------------- @@ -902,5 +850,4 @@ def setup(app): app.connect('autodoc-process-bases', autodoc_process_bases) if sphinx.version_info[:2] < (7, 1): app.connect('html-page-context', add_html_cache_busting, priority=1000) - generate_ScalarMappable_docs() app.config.autodoc_use_legacy_class_based = True From ccabde03a6762de3f19ae643f992c156e52fb3b0 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Thu, 7 May 2026 08:04:29 +0100 Subject: [PATCH 38/99] FIX: use axis lines tight bbox within axis artist tight bbox --- lib/mpl_toolkits/axisartist/axis_artist.py | 2 +- .../axisartist/tests/test_axis_artist.py | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index 3ba70c3d7d3b..75dd978eb6f9 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -1086,7 +1086,7 @@ def get_tightbbox(self, renderer=None): *self.minor_ticklabels.get_window_extents(renderer), self.label.get_window_extent(renderer), self.offsetText.get_window_extent(renderer), - self.line.get_window_extent(renderer), + self.line.get_tightbbox(renderer), ] bb = [b for b in bb if b and (b.width != 0 or b.height != 0)] if bb: diff --git a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index 8c67b18c0349..9145ce666f32 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -1,7 +1,13 @@ +import numpy as np + +import matplotlib as mpl import matplotlib.pyplot as plt +from matplotlib.projections import PolarAxes from matplotlib.testing.decorators import image_comparison +from matplotlib.transforms import Affine2D -from mpl_toolkits.axisartist import AxisArtistHelperRectlinear +from mpl_toolkits.axisartist import (AxisArtistHelperRectlinear, GridHelperCurveLinear, + HostAxes) from mpl_toolkits.axisartist.axis_artist import (AxisArtist, AxisLabel, LabelBase, Ticks, TickLabels) @@ -90,3 +96,24 @@ def test_axis_artist(): axisline.label.set_pad(5) ax.set_ylabel("Test") + + +@mpl.style.context('default') +def test_axisartist_tightbbox(): + fig = plt.figure() + tr = Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform() + grid_helper = GridHelperCurveLinear(tr) + ax = fig.add_subplot(axes_class=HostAxes, grid_helper=grid_helper) + ax.axis["lon"] = ax.new_floating_axis(1, 9) + + ax.set_xlim(-5, 12) + ax.set_ylim(-5, 10) + + ax.axis['lon'].major_ticklabels.set_visible(False) + + # Since the labels are invisible and the lines are clipped to the axes, + # the axis's tight bbox should be contained in the axes box. + renderer = fig._get_renderer() + tight_points = ax.axis['lon'].get_tightbbox(renderer).get_points() + for point in tight_points: + assert ax.bbox.contains(*point) From 4786b8353739f66d4465bf770135e1c922baf2b6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 7 May 2026 15:27:01 -0400 Subject: [PATCH 39/99] Backport PR #31621: Make Scale axis parameter handling more flexible --- lib/matplotlib/scale.py | 21 ++++++++++---- lib/matplotlib/tests/test_scale.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 0793bb31e566..a4cce23562d3 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -138,7 +138,7 @@ def _make_axis_parameter_optional(init_func): This decorator ensures backward compatibility for scale classes that previously required an *axis* parameter. It allows constructors to be - callerd with or without the *axis* parameter. + called with or without the *axis* parameter. For simplicity, this does not handle the case when *axis* is passed as a keyword. However, @@ -170,12 +170,16 @@ def _make_axis_parameter_optional(init_func): """ @wraps(init_func) def wrapper(self, *args, **kwargs): - if args and isinstance(args[0], mpl.axis.Axis): - return init_func(self, *args, **kwargs) + sig = inspect.signature(init_func) + try: + # Try old signature. + sig.bind(self, *args, **kwargs) + except TypeError: + # Use the new signature and pass in an unused axis=None. + init_func(self, None, *args, **kwargs) else: - # Remove 'axis' from kwargs to avoid double assignment - axis = kwargs.pop('axis', None) - return init_func(self, axis, *args, **kwargs) + # Use the old signature. + init_func(self, *args, **kwargs) return wrapper @@ -449,6 +453,11 @@ def __init__(self, axis, functions, base=10): ---------- axis : `~matplotlib.axis.Axis` The axis for the scale. + + .. note:: + This parameter is unused and about to be removed in the future. + It can already now be left out because of special preprocessing, + so that ``FuncScaleLog(functions=(forward, inverse))`` is valid. functions : (callable, callable) two-tuple of the forward and inverse functions for the scale. The forward function must be monotonic. diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 95601ce97b65..104c87adab7b 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -3,6 +3,7 @@ import matplotlib.pyplot as plt from matplotlib.scale import ( AsinhScale, AsinhTransform, + FuncScale, LogitScale, LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale @@ -19,6 +20,49 @@ import pytest +def test_optional_axis_signature(): + # There are three types of original signatures possible, and this only tests one + # example class of each: + # 1. `axis` without default: LinearScale, FuncScale, FuncScaleLog + # 2. `axis` with default and more positional parameters: LogitScale + # 3. `axis` with default and only keyword-only parameters: LogScale, AsinhScale, + # SymmetricalLogScale + # Testing with None is sufficient as detection is purely based on the + # signature structure; no type information is involved. + axis = None + + # Old signature with axis positionally. + FuncScale(axis, (lambda x: x, lambda x: x)) + FuncScale(axis, functions=(lambda x: x, lambda x: x)) + LogitScale(axis) + LogitScale(axis, 'clip') + LogitScale(axis, nonpositive='clip') + LogitScale(axis, use_overline=True) + AsinhScale(axis) + AsinhScale(axis, linear_width=2) + AsinhScale(axis, base=3) + AsinhScale(axis, subs=[2, 6]) + # Old signature with axis as keyword. + FuncScale(axis=axis, functions=(lambda x: x, lambda x: x)) + LogitScale(axis=axis) + LogitScale(axis=axis, nonpositive='clip') + LogitScale(axis=axis, use_overline=True) + AsinhScale(axis=axis) + AsinhScale(axis=axis, linear_width=2) + AsinhScale(axis=axis, base=3) + AsinhScale(axis=axis, subs=[2, 6]) + # New signature without axis. + FuncScale((lambda x: x, lambda x: x)) + FuncScale(functions=(lambda x: x, lambda x: x)) + LogitScale() + LogitScale(nonpositive='clip') + LogitScale(use_overline=True) + AsinhScale() + AsinhScale(linear_width=2) + AsinhScale(base=3) + AsinhScale(subs=[2, 6]) + + @check_figures_equal() def test_log_scales(fig_test, fig_ref): ax_test = fig_test.add_subplot(122, yscale='log', xscale='symlog') From 983dc8eeac9d925bbd005b3c6c9755fc0dd0a2bb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 7 May 2026 15:56:37 -0400 Subject: [PATCH 40/99] Restore PolarTransform(apply_theta_transforms) parameter This partially reverts commit 6083ecd880c950a705ba74107ff18b447e2c53dc, but with a full deprecation of the parameter as a whole. Fixes #31624 --- doc/api/next_api_changes/deprecations/31630-ES.rst | 10 ++++++++++ doc/api/next_api_changes/removals/30004-DS.rst | 10 ---------- lib/matplotlib/projections/polar.py | 8 ++++++-- lib/matplotlib/projections/polar.pyi | 3 +++ 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/31630-ES.rst delete mode 100644 doc/api/next_api_changes/removals/30004-DS.rst diff --git a/doc/api/next_api_changes/deprecations/31630-ES.rst b/doc/api/next_api_changes/deprecations/31630-ES.rst new file mode 100644 index 000000000000..2509b4323022 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/31630-ES.rst @@ -0,0 +1,10 @@ +``apply_theta_transforms`` option in ``PolarTransform`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Applying theta transforms in `~matplotlib.projections.polar.PolarTransform` and +`~matplotlib.projections.polar.InvertedPolarTransform` has been removed, and the +*apply_theta_transforms* keyword argument is deprecated for both classes. + +If you need to retain the behaviour where theta values are transformed, chain the +``PolarTransform`` with a `~matplotlib.transforms.Affine2D` transform that performs the +theta shift and/or sign shift. diff --git a/doc/api/next_api_changes/removals/30004-DS.rst b/doc/api/next_api_changes/removals/30004-DS.rst deleted file mode 100644 index f5fdf214366c..000000000000 --- a/doc/api/next_api_changes/removals/30004-DS.rst +++ /dev/null @@ -1,10 +0,0 @@ -``apply_theta_transforms`` option in ``PolarTransform`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Applying theta transforms in `~matplotlib.projections.polar.PolarTransform` and -`~matplotlib.projections.polar.InvertedPolarTransform` has been removed, and -the ``apply_theta_transforms`` keyword argument removed from both classes. - -If you need to retain the behaviour where theta values -are transformed, chain the ``PolarTransform`` with a `~matplotlib.transforms.Affine2D` -transform that performs the theta shift and/or sign shift. diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 75e1295f77f1..9d999dde2f6f 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -34,7 +34,9 @@ class PolarTransform(mtransforms.Transform): input_dims = output_dims = 2 - def __init__(self, axis=None, use_rmin=True, *, scale_transform=None): + @_api.delete_parameter('3.11', 'apply_theta_transforms') + def __init__(self, axis=None, use_rmin=True, *, + apply_theta_transforms=False, scale_transform=None): """ Parameters ---------- @@ -183,7 +185,9 @@ class InvertedPolarTransform(mtransforms.Transform): """ input_dims = output_dims = 2 - def __init__(self, axis=None, use_rmin=True): + @_api.delete_parameter('3.11', 'apply_theta_transforms') + def __init__(self, axis=None, use_rmin=True, + *, apply_theta_transforms=False): """ Parameters ---------- diff --git a/lib/matplotlib/projections/polar.pyi b/lib/matplotlib/projections/polar.pyi index fc1d508579b5..de1cbc293900 100644 --- a/lib/matplotlib/projections/polar.pyi +++ b/lib/matplotlib/projections/polar.pyi @@ -18,6 +18,7 @@ class PolarTransform(mtransforms.Transform): axis: PolarAxes | None = ..., use_rmin: bool = ..., *, + apply_theta_transforms: bool = ..., scale_transform: mtransforms.Transform | None = ..., ) -> None: ... def inverted(self) -> InvertedPolarTransform: ... @@ -34,6 +35,8 @@ class InvertedPolarTransform(mtransforms.Transform): self, axis: PolarAxes | None = ..., use_rmin: bool = ..., + *, + apply_theta_transforms: bool = ..., ) -> None: ... def inverted(self) -> PolarTransform: ... From b2df08a78127287179f519eb0d05ddc5fcc3770c Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 7 May 2026 22:00:05 +0200 Subject: [PATCH 41/99] DOC: heading style (#31591) --- doc/devel/coding_guide.rst | 4 ++-- doc/devel/min_dep_policy.rst | 4 ++-- doc/devel/release_guide.rst | 10 ++++---- doc/devel/style_guide.rst | 45 ++++++++++++++++++++++++++++++++++++ doc/devel/testing.rst | 34 +++++++++++++-------------- doc/devel/triage.rst | 10 ++++---- 6 files changed, 75 insertions(+), 32 deletions(-) diff --git a/doc/devel/coding_guide.rst b/doc/devel/coding_guide.rst index fe7769909368..7a4b296b52ce 100644 --- a/doc/devel/coding_guide.rst +++ b/doc/devel/coding_guide.rst @@ -190,8 +190,8 @@ local arguments and the rest are passed on as .. _using_logging: -Using logging for debug messages -================================ +Use logging for debug messages +============================== Matplotlib uses the standard Python `logging` library to write verbose warnings, information, and debug messages. Please use it! In all those places diff --git a/doc/devel/min_dep_policy.rst b/doc/devel/min_dep_policy.rst index 81a84491bc4a..517cc872139e 100644 --- a/doc/devel/min_dep_policy.rst +++ b/doc/devel/min_dep_policy.rst @@ -157,8 +157,8 @@ Matplotlib Python NumPy .. _`1.3`: https://matplotlib.org/1.3.0/users/installing.html#build-requirements -Updating Python and NumPy versions -================================== +Update Python and NumPy versions +================================ To update the minimum versions of Python we need to update: diff --git a/doc/devel/release_guide.rst b/doc/devel/release_guide.rst index ccac5b4f8872..eefc31aec07c 100644 --- a/doc/devel/release_guide.rst +++ b/doc/devel/release_guide.rst @@ -45,7 +45,7 @@ versioning scheme: *macro.meso.micro*. .. _release_feature_freeze: -Making the release branch +Create the release branch ========================= .. note:: @@ -379,8 +379,8 @@ to the VER-doc branch and push to GitHub. :: .. _release_bld_bin: -Building binaries -================= +Build binaries +============== We distribute macOS, Windows, and many Linux wheels as well as a source tarball via PyPI. @@ -412,8 +412,8 @@ PyPI. .. _release_upload_bin: -Manually uploading to PyPI -========================== +Manual upload to PyPI +===================== .. note:: diff --git a/doc/devel/style_guide.rst b/doc/devel/style_guide.rst index e35112a65e42..b260872557c5 100644 --- a/doc/devel/style_guide.rst +++ b/doc/devel/style_guide.rst @@ -176,6 +176,51 @@ reliability and consistency in documentation. They are not interchangeable. .. |Axis| replace:: :class:`~matplotlib.axis.Axis` +Headings +-------- +Use sentence case for headings. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | Quick start guide | Quick Start Guide | + +------------------------------------+------------------------------------+ + +Noun phrases and verb phrases are both acceptable for headings. Noun phrases +are preferred for higher-level headings and descriptive sections as they +simply state the content. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | Bug triage and issue curation | Triage bugs and curate issues | + +------------------------------------+------------------------------------+ + +Verb phrases are preferred for instructive and action-oriented sections; in +particular when they cover steps in a process, such as the subsections in +:ref:`installing_for_devs`. + +Use the second-person imperative form of the verb rather than the gerund form. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | Fork the Matplotlib repository | Forking the Matplotlib repository | + +------------------------------------+------------------------------------+ + + Grammar ------- diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index 0b9390477b10..990b9d0b6493 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -13,10 +13,8 @@ testing infrastructure are in :mod:`matplotlib.testing`. .. _pytest-xdist: https://pypi.org/project/pytest-xdist/ -.. _testing_requirements: - -Requirements ------------- +Prerequisites +------------- To run the tests you will need to :ref:`set up Matplotlib for development `. Note in @@ -34,8 +32,8 @@ particular the :ref:`additional dependencies ` for testing. .. _run_tests: -Running the tests ------------------ +Run the tests +------------- In the root directory of your development repository run:: @@ -82,8 +80,8 @@ to avoid clashes between ``pytest``'s import mode and Python's search path: python -m pytest --import-mode prepend -Viewing image test output -^^^^^^^^^^^^^^^^^^^^^^^^^ +View image test output +^^^^^^^^^^^^^^^^^^^^^^ The output of :ref:`image-based ` tests is stored in a ``result_images`` directory. These images can be compiled into one HTML page, containing @@ -100,8 +98,8 @@ to the folder where the baseline test images are stored. The triage tool require :ref:`QT ` is installed. -Writing tests -------------- +Write tests +----------- Tests are located in :file:`lib/matplotlib/tests`. They are organized to mirror the structure of the code in :file:`lib/matplotlib`. For example, tests for the ``mathtext.py`` module are in :file:`lib/matplotlib/tests/test_mathtext.py`. @@ -284,8 +282,8 @@ See the documentation of `~matplotlib.testing.decorators.image_comparison` and `~matplotlib.testing.decorators.check_figures_equal` for additional information about their use. -Using GitHub Actions for CI ---------------------------- +CI with GitHub Actions +---------------------- `GitHub Actions `_ is a hosted CI system "in the cloud". @@ -311,8 +309,8 @@ https://github.com/your_GitHub_user_name/matplotlib/actions -- here's `an example `_. -Using tox ---------- +tox: Test multiple python versions +---------------------------------- `Tox `_ is a tool for running tests against multiple Python environments, including multiple versions of Python @@ -352,8 +350,8 @@ tests are run. For more info on the ``tox.ini`` file, see the `Tox Configuration Specification `_. -Building old versions of Matplotlib ------------------------------------ +Build old versions of Matplotlib +-------------------------------- When running a ``git bisect`` to see which commit introduced a certain bug, you may (rarely) need to build very old versions of Matplotlib. The following @@ -361,8 +359,8 @@ constraints need to be taken into account: - Matplotlib 1.3 (or earlier) requires numpy 1.8 (or earlier). -Testing released versions of Matplotlib ---------------------------------------- +Test released versions of Matplotlib +------------------------------------ Running the tests on an installation of a released version (e.g. PyPI package or conda package) also requires additional setup. diff --git a/doc/devel/triage.rst b/doc/devel/triage.rst index ca06fd515c79..de27afcab111 100644 --- a/doc/devel/triage.rst +++ b/doc/devel/triage.rst @@ -1,9 +1,9 @@ .. _bug_triaging: -******************************* -Bug triaging and issue curation -******************************* +***************************** +Bug triage and issue curation +***************************** The `issue tracker `_ is important to communication in the project because it serves as the @@ -107,8 +107,8 @@ important tasks: question or has been considered as unclear for many years, then it should be closed. -Preparing PRs for review -======================== +Prepare PRs for review +====================== Reviewing code is also encouraged. Contributors and users are welcome to participate to the review process following our :ref:`review guidelines From f05f7a28c73d15a83566d633feba88f3fc1c6202 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 7 May 2026 16:19:22 -0400 Subject: [PATCH 42/99] Backport PR #31557: FIX: Added ft2font null checks added --- src/ft2font.h | 2 +- src/ft2font_wrapper.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ft2font.h b/src/ft2font.h index 0c438d9107de..09e028f3404c 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -41,7 +41,7 @@ inline char const* ft_error_string(FT_Error error) { #undef __FTERRORS_H__ #define FT_ERROR_START_LIST switch (error) { #define FT_ERRORDEF( e, v, s ) case v: return s; -#define FT_ERROR_END_LIST default: return NULL; } +#define FT_ERROR_END_LIST default: return "unknown error"; } #include FT_ERRORS_H } diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index d0df659c5918..771f1db5a191 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -409,9 +409,9 @@ class PyFT2Font final : public FT2Font { std::set::iterator it = family_names.begin(); std::stringstream ss; - ss<<*it; + ss<< (*it ? *it : "unknown family name"); while(++it != family_names.end()){ - ss<<", "<<*it; + ss<<", "<< (*it ? *it : "unknown family name"); } auto text_helpers = py::module_::import("matplotlib._text_helpers"); From e354866d03ada904ebec9acefad23a1c2067fa54 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 8 May 2026 00:52:25 +0200 Subject: [PATCH 43/99] FIX: Prohibit special TeX chars in pgf metadata They would have raised or been swallowed anyway. Note: One could try to escape some chars, but it's quite unclear how hyperref handles the parameters, It seems that escaping does not systematically work. --- lib/matplotlib/backends/backend_pgf.py | 7 +++++++ lib/matplotlib/tests/test_backend_pgf.py | 22 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 3205f294ab2d..d8b29f750dfa 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -154,6 +154,13 @@ def _metadata_to_str(key, value): value = value.name.decode('ascii') else: value = str(value) + + invalid_chars = r"\{}[]()" + if any(c in value for c in invalid_chars): + raise ValueError( + f"Invalid metadata value for {key!r}: {value!r}. " + f"The value must not contain the chars {invalid_chars}.") + return f'{key}={{{value}}}' diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index e5b73c9450f3..4af329fa28d4 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -15,7 +15,7 @@ from matplotlib.testing import _has_tex_package, _check_for_pgf from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib.testing.compare import compare_images -from matplotlib.backends.backend_pgf import PdfPages +from matplotlib.backends.backend_pgf import _metadata_to_str, PdfPages from matplotlib.testing.decorators import ( _image_directories, check_figures_equal, image_comparison) from matplotlib.testing._markers import ( @@ -37,6 +37,26 @@ def compare_figure(fname, savefig_kwargs={}, tol=0): raise ImageComparisonFailure(err) +@pytest.mark.parametrize("key, value, expected_str", [ + ("Author", "me", "Author={me}"), + ("ModDate", + datetime.datetime(1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))), + "ModDate={D:19680801000000Z}"), +]) +def test__metadata_to_str(key, value, expected_str): + assert _metadata_to_str(key, value) == expected_str + + +@pytest.mark.parametrize("value", [ + r"Backslashes, e.g. in \commands", + r"funny braces {}", + r"and square brackets]", +]) +def test__metadata_to_str_error(value): + with pytest.raises(ValueError, match="value must not contain the chars"): + _metadata_to_str("Title", value) + + @needs_pgf_xelatex @needs_ghostscript @pytest.mark.backend('pgf') From c2730be1dcc8d4e21013ed37751863b260668e61 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 8 May 2026 06:17:30 +0200 Subject: [PATCH 44/99] Update lib/matplotlib/backends/backend_pgf.py Co-authored-by: Thomas A Caswell --- lib/matplotlib/backends/backend_pgf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index d8b29f750dfa..c00a1041eacc 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -156,7 +156,7 @@ def _metadata_to_str(key, value): value = str(value) invalid_chars = r"\{}[]()" - if any(c in value for c in invalid_chars): + if any(c in value + key for c in invalid_chars): raise ValueError( f"Invalid metadata value for {key!r}: {value!r}. " f"The value must not contain the chars {invalid_chars}.") From 79bee9bf418c35ed0d6bd9e760c2facf91905ed1 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 02:12:10 -0400 Subject: [PATCH 45/99] TYP: Add missing default value to _api.getitem_checked --- lib/matplotlib/_api/__init__.pyi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index cf24edf65db5..58dee136e233 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -42,9 +42,7 @@ def check_isinstance( def list_suggestion_error_msg(name: str, potential: Any, values: Sequence[Any]) -> str: ... def check_in_list(values: Sequence[Any], /, **kwargs: Any) -> None: ... def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... -def getitem_checked( - mapping: Mapping[Any, _T], /, _error_cls: type[Exception], **kwargs: Any -) -> _T: ... +def getitem_checked(mapping: Mapping[Any, _T], /, _error_cls: type[Exception] = ..., **kwargs: Any) -> _T: ... def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... @overload def define_aliases( From e4906261750e55751491f8ace5017d62231a71d6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 04:29:00 -0400 Subject: [PATCH 46/99] Fix some font-related typing issues - Some places take/emit a `FontPath` and not a `str` any more. - The module-level `findfont` is a copy of an instance method, so it should match that as well. - Also add some private things, because they help find some additional bugs, and at least `_find_fonts_by_props` is possibly going to be public later. --- lib/matplotlib/_mathtext.py | 2 +- lib/matplotlib/backend_bases.pyi | 3 ++- lib/matplotlib/backends/_backend_pdf_ps.py | 5 +++-- lib/matplotlib/font_manager.pyi | 14 +++++++++++--- lib/matplotlib/ft2font.pyi | 3 ++- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index b04386e7666a..17dc1b8fb462 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -1721,8 +1721,8 @@ def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output: off_h = ox off_v = oy + box.height output = Output(box) - phantom: list[bool] = [] + def render(node, *args): if not any(phantom): node.render(*args) diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index a69b36093839..94a8522717cd 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -15,7 +15,7 @@ from matplotlib.figure import Figure from matplotlib.font_manager import FontProperties from matplotlib.path import Path from matplotlib.texmanager import TexManager -from matplotlib.text import Text +from matplotlib.text import Text, TextToPath from matplotlib.transforms import Bbox, BboxBase, Transform, TransformedPath from collections.abc import Callable, Iterable, Sequence @@ -40,6 +40,7 @@ def register_backend( def get_registered_canvas_class(format: str) -> type[FigureCanvasBase]: ... class RendererBase: + _text2path: TextToPath def __init__(self) -> None: ... def open_group(self, s: str, gid: str | None = ...) -> None: ... def close_group(self, s: str) -> None: ... diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index a06779b8efee..87fbae4d749b 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -18,6 +18,7 @@ if typing.TYPE_CHECKING: + from .font_manager import FontPath from .ft2font import CharacterCodeType, FT2Font, GlyphIndexType from fontTools.ttLib import TTFont @@ -34,7 +35,7 @@ def _cached_get_afm_from_fname(fname): return AFM(fh) -def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: +def get_glyphs_subset(fontfile: FontPath, glyphs: set[GlyphIndexType]) -> TTFont: """ Subset a TTF font. @@ -199,7 +200,7 @@ def __init__(self, subset_size: int = 0): self.subset_size = subset_size def track(self, font: FT2Font, s: str, - features: tuple[str, ...] | None = ..., + features: tuple[str, ...] | None = None, language: str | tuple[tuple[str, int, int], ...] | None = None ) -> list[tuple[int, CharacterCodeType]]: """ diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index b5c131d33702..4bb1a8bae2a9 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -133,11 +133,19 @@ class FontManager: self, prop: str | FontProperties, fontext: Literal["ttf", "afm"] = ..., - directory: str | None = ..., + directory: str | os.PathLike | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., ) -> FontPath: ... def get_font_names(self) -> list[str]: ... + def _find_fonts_by_props( + self, + prop: str | FontProperties, + fontext: Literal["ttf", "afm"] = ..., + directory: str | os.PathLike | None = ..., + fallback_to_default: bool = ..., + rebuild_if_missing: bool = ..., + ) -> list[FontPath]: ... def is_opentype_cff_font(filename: str) -> bool: ... def get_font( @@ -149,8 +157,8 @@ fontManager: FontManager def findfont( prop: str | FontProperties, fontext: Literal["ttf", "afm"] = ..., - directory: str | None = ..., + directory: str | os.PathLike | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., -) -> str: ... +) -> FontPath: ... def get_font_names() -> list[str]: ... diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 05f987292ffc..f8057742b376 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -239,7 +239,8 @@ class FT2Font(Buffer): *, face_index: int = ..., _fallback_list: list[FT2Font] | None = ..., - _kerning_factor: int | None = ... + _kerning_factor: int | None = ..., + _warn_if_used: bool = ..., ) -> None: ... if sys.version_info[:2] >= (3, 12): def __buffer__(self, /, flags: int) -> memoryview: ... From faf35f31e748991dd64956486e58ff495af639c8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 04:54:16 -0400 Subject: [PATCH 47/99] Fix RendererCairo._draw_mathtext implementation It wasn't updated to handle the change to `VectorParse` in #30335. --- lib/matplotlib/backends/backend_cairo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index a62890d7c3b1..a16c7a25aec2 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -254,7 +254,10 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.new_path() ctx.select_font_face(*_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(self.points_to_pixels(fontsize)) - ctx.show_glyphs([(idx, ox, -oy) for _, _, idx, ox, oy in font_glyphs]) + ctx.show_glyphs([ + (glyph_index, ox, -oy) + for _font, _size, _ccode, glyph_index, ox, oy in font_glyphs + ]) for ox, oy, w, h in rects: ctx.new_path() From 0260760d18d5584e46d1bde52d9faa8e16026128 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 04:56:24 -0400 Subject: [PATCH 48/99] Fix undefined method in `dviread.Text.glyph_name_or_index` The method was removed in #29939, but this call site was forgotten. --- lib/matplotlib/dviread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index e41618d67579..979744d1ef5c 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -121,7 +121,7 @@ def glyph_name_or_index(self): # control all involved versions and are deeply familiar with the # implementation", but a further mapping bug was fixed in luaotfload # commit 8f2dca4, first included in v3.23). - entry = self._get_pdftexmap_entry() + entry = PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname] return (_parse_enc(entry.encoding)[self.glyph] if entry.encoding is not None else self.glyph) From 34b1c9eebf50a59b3883dca1018ec4383b207eba Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Fri, 8 May 2026 13:24:08 +0100 Subject: [PATCH 49/99] Backport PR #31630: Restore PolarTransform(apply_theta_transforms) parameter --- doc/api/next_api_changes/deprecations/31630-ES.rst | 10 ++++++++++ doc/api/next_api_changes/removals/30004-DS.rst | 10 ---------- lib/matplotlib/projections/polar.py | 8 ++++++-- lib/matplotlib/projections/polar.pyi | 3 +++ 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/31630-ES.rst delete mode 100644 doc/api/next_api_changes/removals/30004-DS.rst diff --git a/doc/api/next_api_changes/deprecations/31630-ES.rst b/doc/api/next_api_changes/deprecations/31630-ES.rst new file mode 100644 index 000000000000..2509b4323022 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/31630-ES.rst @@ -0,0 +1,10 @@ +``apply_theta_transforms`` option in ``PolarTransform`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Applying theta transforms in `~matplotlib.projections.polar.PolarTransform` and +`~matplotlib.projections.polar.InvertedPolarTransform` has been removed, and the +*apply_theta_transforms* keyword argument is deprecated for both classes. + +If you need to retain the behaviour where theta values are transformed, chain the +``PolarTransform`` with a `~matplotlib.transforms.Affine2D` transform that performs the +theta shift and/or sign shift. diff --git a/doc/api/next_api_changes/removals/30004-DS.rst b/doc/api/next_api_changes/removals/30004-DS.rst deleted file mode 100644 index f5fdf214366c..000000000000 --- a/doc/api/next_api_changes/removals/30004-DS.rst +++ /dev/null @@ -1,10 +0,0 @@ -``apply_theta_transforms`` option in ``PolarTransform`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Applying theta transforms in `~matplotlib.projections.polar.PolarTransform` and -`~matplotlib.projections.polar.InvertedPolarTransform` has been removed, and -the ``apply_theta_transforms`` keyword argument removed from both classes. - -If you need to retain the behaviour where theta values -are transformed, chain the ``PolarTransform`` with a `~matplotlib.transforms.Affine2D` -transform that performs the theta shift and/or sign shift. diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 75e1295f77f1..9d999dde2f6f 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -34,7 +34,9 @@ class PolarTransform(mtransforms.Transform): input_dims = output_dims = 2 - def __init__(self, axis=None, use_rmin=True, *, scale_transform=None): + @_api.delete_parameter('3.11', 'apply_theta_transforms') + def __init__(self, axis=None, use_rmin=True, *, + apply_theta_transforms=False, scale_transform=None): """ Parameters ---------- @@ -183,7 +185,9 @@ class InvertedPolarTransform(mtransforms.Transform): """ input_dims = output_dims = 2 - def __init__(self, axis=None, use_rmin=True): + @_api.delete_parameter('3.11', 'apply_theta_transforms') + def __init__(self, axis=None, use_rmin=True, + *, apply_theta_transforms=False): """ Parameters ---------- diff --git a/lib/matplotlib/projections/polar.pyi b/lib/matplotlib/projections/polar.pyi index fc1d508579b5..de1cbc293900 100644 --- a/lib/matplotlib/projections/polar.pyi +++ b/lib/matplotlib/projections/polar.pyi @@ -18,6 +18,7 @@ class PolarTransform(mtransforms.Transform): axis: PolarAxes | None = ..., use_rmin: bool = ..., *, + apply_theta_transforms: bool = ..., scale_transform: mtransforms.Transform | None = ..., ) -> None: ... def inverted(self) -> InvertedPolarTransform: ... @@ -34,6 +35,8 @@ class InvertedPolarTransform(mtransforms.Transform): self, axis: PolarAxes | None = ..., use_rmin: bool = ..., + *, + apply_theta_transforms: bool = ..., ) -> None: ... def inverted(self) -> PolarTransform: ... From e00071c049fc508875e8b8201aae506c97f9e06e Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 8 May 2026 14:03:29 -0500 Subject: [PATCH 50/99] Backport PR #31634: Fix some font-related issues --- lib/matplotlib/_api/__init__.pyi | 4 +--- lib/matplotlib/_mathtext.py | 2 +- lib/matplotlib/backend_bases.pyi | 3 ++- lib/matplotlib/backends/_backend_pdf_ps.py | 5 +++-- lib/matplotlib/backends/backend_cairo.py | 5 ++++- lib/matplotlib/dviread.py | 2 +- lib/matplotlib/font_manager.pyi | 14 +++++++++++--- lib/matplotlib/ft2font.pyi | 3 ++- 8 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 0bcce210634f..aeefaa35ffaf 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -41,9 +41,7 @@ def check_isinstance( def list_suggestion_error_msg(name: str, potential: Any, values: Sequence[Any]) -> str: ... def check_in_list(values: Sequence[Any], /, **kwargs: Any) -> None: ... def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... -def getitem_checked( - mapping: Mapping[Any, _T], /, _error_cls: type[Exception], **kwargs: Any -) -> _T: ... +def getitem_checked(mapping: Mapping[Any, _T], /, _error_cls: type[Exception] = ..., **kwargs: Any) -> _T: ... def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... @overload def define_aliases( diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index b04386e7666a..17dc1b8fb462 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -1721,8 +1721,8 @@ def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output: off_h = ox off_v = oy + box.height output = Output(box) - phantom: list[bool] = [] + def render(node, *args): if not any(phantom): node.render(*args) diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index a69b36093839..94a8522717cd 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -15,7 +15,7 @@ from matplotlib.figure import Figure from matplotlib.font_manager import FontProperties from matplotlib.path import Path from matplotlib.texmanager import TexManager -from matplotlib.text import Text +from matplotlib.text import Text, TextToPath from matplotlib.transforms import Bbox, BboxBase, Transform, TransformedPath from collections.abc import Callable, Iterable, Sequence @@ -40,6 +40,7 @@ def register_backend( def get_registered_canvas_class(format: str) -> type[FigureCanvasBase]: ... class RendererBase: + _text2path: TextToPath def __init__(self) -> None: ... def open_group(self, s: str, gid: str | None = ...) -> None: ... def close_group(self, s: str) -> None: ... diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index a06779b8efee..87fbae4d749b 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -18,6 +18,7 @@ if typing.TYPE_CHECKING: + from .font_manager import FontPath from .ft2font import CharacterCodeType, FT2Font, GlyphIndexType from fontTools.ttLib import TTFont @@ -34,7 +35,7 @@ def _cached_get_afm_from_fname(fname): return AFM(fh) -def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: +def get_glyphs_subset(fontfile: FontPath, glyphs: set[GlyphIndexType]) -> TTFont: """ Subset a TTF font. @@ -199,7 +200,7 @@ def __init__(self, subset_size: int = 0): self.subset_size = subset_size def track(self, font: FT2Font, s: str, - features: tuple[str, ...] | None = ..., + features: tuple[str, ...] | None = None, language: str | tuple[tuple[str, int, int], ...] | None = None ) -> list[tuple[int, CharacterCodeType]]: """ diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index a62890d7c3b1..a16c7a25aec2 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -254,7 +254,10 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.new_path() ctx.select_font_face(*_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(self.points_to_pixels(fontsize)) - ctx.show_glyphs([(idx, ox, -oy) for _, _, idx, ox, oy in font_glyphs]) + ctx.show_glyphs([ + (glyph_index, ox, -oy) + for _font, _size, _ccode, glyph_index, ox, oy in font_glyphs + ]) for ox, oy, w, h in rects: ctx.new_path() diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index e41618d67579..979744d1ef5c 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -121,7 +121,7 @@ def glyph_name_or_index(self): # control all involved versions and are deeply familiar with the # implementation", but a further mapping bug was fixed in luaotfload # commit 8f2dca4, first included in v3.23). - entry = self._get_pdftexmap_entry() + entry = PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname] return (_parse_enc(entry.encoding)[self.glyph] if entry.encoding is not None else self.glyph) diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index b5c131d33702..4bb1a8bae2a9 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -133,11 +133,19 @@ class FontManager: self, prop: str | FontProperties, fontext: Literal["ttf", "afm"] = ..., - directory: str | None = ..., + directory: str | os.PathLike | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., ) -> FontPath: ... def get_font_names(self) -> list[str]: ... + def _find_fonts_by_props( + self, + prop: str | FontProperties, + fontext: Literal["ttf", "afm"] = ..., + directory: str | os.PathLike | None = ..., + fallback_to_default: bool = ..., + rebuild_if_missing: bool = ..., + ) -> list[FontPath]: ... def is_opentype_cff_font(filename: str) -> bool: ... def get_font( @@ -149,8 +157,8 @@ fontManager: FontManager def findfont( prop: str | FontProperties, fontext: Literal["ttf", "afm"] = ..., - directory: str | None = ..., + directory: str | os.PathLike | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., -) -> str: ... +) -> FontPath: ... def get_font_names() -> list[str]: ... diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 05f987292ffc..f8057742b376 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -239,7 +239,8 @@ class FT2Font(Buffer): *, face_index: int = ..., _fallback_list: list[FT2Font] | None = ..., - _kerning_factor: int | None = ... + _kerning_factor: int | None = ..., + _warn_if_used: bool = ..., ) -> None: ... if sys.version_info[:2] >= (3, 12): def __buffer__(self, /, flags: int) -> memoryview: ... From 53d20a9cd26e8f92ce63e36ac4d3b690146925fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 19:13:16 +0000 Subject: [PATCH 51/99] Bump the actions group with 2 updates Bumps the actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [actions/labeler](https://github.com/actions/labeler). Updates `github/codeql-action` from 4.35.3 to 4.35.4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e46ed2cbd01164d986452f91f178727624ae40d7...68bde559dea0fdcac2102bfdf6230c5f70eb485e) Updates `actions/labeler` from 6.0.1 to 6.1.0 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/634933edcd8ababfe52f92936142cc22ac488b1b...f27b608878404679385c85cfa523b85ccb86e213) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: actions/labeler dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/labeler.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9ee6ac545967..71425e9cc3e9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} @@ -45,4 +45,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2914c64a8461..600e7fc34a95 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,6 +12,6 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 with: sync-labels: true From da1f383890abc41317ebae1da19a77a7e24b7f14 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 15:42:29 -0400 Subject: [PATCH 52/99] Backport PR #31628: FIX: use axis lines tight bbox within axis artist tight bbox --- lib/mpl_toolkits/axisartist/axis_artist.py | 2 +- .../axisartist/tests/test_axis_artist.py | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index 3ba70c3d7d3b..75dd978eb6f9 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -1086,7 +1086,7 @@ def get_tightbbox(self, renderer=None): *self.minor_ticklabels.get_window_extents(renderer), self.label.get_window_extent(renderer), self.offsetText.get_window_extent(renderer), - self.line.get_window_extent(renderer), + self.line.get_tightbbox(renderer), ] bb = [b for b in bb if b and (b.width != 0 or b.height != 0)] if bb: diff --git a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index 8c67b18c0349..9145ce666f32 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -1,7 +1,13 @@ +import numpy as np + +import matplotlib as mpl import matplotlib.pyplot as plt +from matplotlib.projections import PolarAxes from matplotlib.testing.decorators import image_comparison +from matplotlib.transforms import Affine2D -from mpl_toolkits.axisartist import AxisArtistHelperRectlinear +from mpl_toolkits.axisartist import (AxisArtistHelperRectlinear, GridHelperCurveLinear, + HostAxes) from mpl_toolkits.axisartist.axis_artist import (AxisArtist, AxisLabel, LabelBase, Ticks, TickLabels) @@ -90,3 +96,24 @@ def test_axis_artist(): axisline.label.set_pad(5) ax.set_ylabel("Test") + + +@mpl.style.context('default') +def test_axisartist_tightbbox(): + fig = plt.figure() + tr = Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform() + grid_helper = GridHelperCurveLinear(tr) + ax = fig.add_subplot(axes_class=HostAxes, grid_helper=grid_helper) + ax.axis["lon"] = ax.new_floating_axis(1, 9) + + ax.set_xlim(-5, 12) + ax.set_ylim(-5, 10) + + ax.axis['lon'].major_ticklabels.set_visible(False) + + # Since the labels are invisible and the lines are clipped to the axes, + # the axis's tight bbox should be contained in the axes box. + renderer = fig._get_renderer() + tight_points = ax.axis['lon'].get_tightbbox(renderer).get_points() + for point in tight_points: + assert ax.bbox.contains(*point) From 84d7bbeefee30eb62ed327b3135e22b899415415 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 8 May 2026 15:10:06 -0500 Subject: [PATCH 53/99] Backport PR #31638: Bump the actions group with 2 updates --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/labeler.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9ee6ac545967..71425e9cc3e9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} @@ -45,4 +45,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2914c64a8461..600e7fc34a95 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,6 +12,6 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 with: sync-labels: true From 13f2d218e7f14e92a22209bde2f8a82653fd8436 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 9 May 2026 03:48:42 +0200 Subject: [PATCH 54/99] Apply suggestion from @timhoffm --- lib/matplotlib/backends/backend_pgf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index c00a1041eacc..36048fe016df 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -155,6 +155,8 @@ def _metadata_to_str(key, value): else: value = str(value) + # ensure that metadata does not contain special TeX chars because we + # insert the metadata as raw text into the TeX source invalid_chars = r"\{}[]()" if any(c in value + key for c in invalid_chars): raise ValueError( From 0302a278459d9c6b0429c5836b0137c5505db78b Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 9 May 2026 12:41:20 +0200 Subject: [PATCH 55/99] DOC: Improve autoscaling and margin docs (#31609) * DOC: Improve autoscaling and margin docs * Apply suggestions from code review Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --------- Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- galleries/users_explain/axes/autoscale.py | 44 +++++++++++++---------- lib/matplotlib/axes/_base.py | 40 ++++++++++++++++++--- lib/matplotlib/axis.py | 9 +++++ 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/galleries/users_explain/axes/autoscale.py b/galleries/users_explain/axes/autoscale.py index 337960302c38..ea0c2d24c55a 100644 --- a/galleries/users_explain/axes/autoscale.py +++ b/galleries/users_explain/axes/autoscale.py @@ -6,33 +6,41 @@ Axis autoscaling ================ -The limits on an axis can be set manually (e.g. ``ax.set_xlim(xmin, xmax)``) -or Matplotlib can set them automatically based on the data already on the Axes. -There are a number of options to this autoscaling behaviour, discussed below. -""" +Basic concept +------------- -# %% -# We will start with a simple line plot showing that autoscaling -# extends the axis limits 5% beyond the data limits (-2π, 2π). +Autoscaling ensures that data is visible within the Axes by automatically adjusting +the axis limits. When you plot data, Matplotlib's autoscaling mechanism updates the +axis limits accordingly. +""" import matplotlib.pyplot as plt import numpy as np -x = np.linspace(-2 * np.pi, 2 * np.pi, 100) +x = np.linspace(-6, 6, 201) y = np.sinc(x) fig, ax = plt.subplots() ax.plot(x, y) # %% +# +# .. _autoscale_margins: +# # Margins # ------- -# The default margin around the data limits is 5%, which is based on the -# default configuration setting of :rc:`axes.xmargin`, :rc:`axes.ymargin`, -# and :rc:`axes.zmargin`: +# To ensure that the data is not at the very edge of the plot, Matplotlib adds a +# margin around the data limits. Note that the *x* data range in the above plot is +# [-6, 6], but the x-axis limits are slightly wider due to the margin. +# +# The default margin is 5%, defined via +# +# - :rc:`axes.xmargin` +# - :rc:`axes.ymargin` +# - :rc:`axes.zmargin` -print(ax.margins()) +print(ax.get_xmargin(), ax.get_ymargin()) # %% # The margin size can be overridden to make them smaller or larger using @@ -116,14 +124,14 @@ ax[1].set_title("Two curves") # %% -# However, there are cases when you don't want to automatically adjust the -# viewport to new data. +# If you don't want automatic updates of the axis limits, either deactivate +# autoscaling with `~.axes.Axes.autoscale` or set the limits +# manually with `~.axes.Axes.set_xlim` / `~.axes.Axes.set_ylim`. # -# One way to disable autoscaling is to manually set the -# axis limit. Let's say that we want to see only a part of the data in +# Let's say that we want to see only a part of the data in # greater detail. Setting the ``xlim`` persists even if we add more curves to -# the data. To recalculate the new limits calling `.Axes.autoscale` will -# toggle the functionality manually. +# the data. Calling `.Axes.autoscale` will re-enable the autoscaling and +# recalculate the limits to fit all the data. fig, ax = plt.subplots(ncols=2, figsize=(12, 8)) ax[0].plot(x, y) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ee933ea138ad..0ddf18b12ec2 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2787,6 +2787,13 @@ def set_autoscale_on(self, b): Parameters ---------- b : bool + + See Also + -------- + :ref:`autoscale` + matplotlib.axes.Axes.autoscale + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.set_autoscaley_on """ for axis in self._axis_map.values(): axis._set_autoscale_on(b) @@ -2825,6 +2832,7 @@ def get_xmargin(self): See Also -------- + :ref:`autoscale_margins` matplotlib.axes.Axes.set_xmargin """ return self._xmargin @@ -2841,6 +2849,7 @@ def get_ymargin(self): See Also -------- + :ref:`autoscale_margins` matplotlib.axes.Axes.set_ymargin """ return self._ymargin @@ -2860,6 +2869,13 @@ def set_xmargin(self, m): Parameters ---------- m : float greater than -0.5 + + See Also + -------- + :ref:`autoscale_margins` + matplotlib.axes.Axes.margins + matplotlib.axes.Axes.get_xmargin + """ if m <= -0.5: raise ValueError("margin must be greater than -0.5") @@ -2882,6 +2898,12 @@ def set_ymargin(self, m): Parameters ---------- m : float greater than -0.5 + + See Also + -------- + :ref:`autoscale_margins` + matplotlib.axes.Axes.margins + matplotlib.axes.Axes.get_ymargin """ if m <= -0.5: raise ValueError("margin must be greater than -0.5") @@ -2945,6 +2967,7 @@ def margins(self, *margins, x=None, y=None, tight=True): See Also -------- + :ref:`autoscale_margins` .Axes.set_xmargin, .Axes.set_ymargin """ @@ -2999,10 +3022,12 @@ def autoscale(self, enable=True, axis='both', tight=None): """ Autoscale the axis view to the data (toggle). - Convenience method for simple axis view autoscaling. - It turns autoscaling on or off, and then, - if autoscaling for either axis is on, it performs - the autoscaling on the specified axis or Axes. + Convenience method for simple axis view autoscaling. This: + + - Turns autoscaling on or off (`~.axes.Axes.set_autoscalex_on` / + `~.axes.Axes.set_autoscaley_on`). + - Ensures that view limits will get updated when needed. - As view limits + are lazy-updated, this technically marks the view limits as stale. Parameters ---------- @@ -3016,6 +3041,13 @@ def autoscale(self, enable=True, axis='both', tight=None): If True, first set the margins to zero. Then, this argument is forwarded to `~.axes.Axes.autoscale_view` (regardless of its value); see the description of its behavior there. + + See Also + -------- + :ref:`autoscale` + matplotlib.axes.Axes.set_autoscale_on + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.set_autoscaley_on """ if enable is None: scalex = True diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 48bbac9264ae..c526b8a2aa6a 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -838,6 +838,15 @@ def _set_autoscale_on(self, b): Parameters ---------- b : bool + + See Also + -------- + matplotlib.axes.Axes.autoscale + matplotlib.axes.Axes.set_autoscale_on + matplotlib.axes.Axes.get_autoscalex_on + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.get_autoscaley_on + matplotlib.axes.Axes.set_autoscaley_on """ if b is not None: self._autoscale_on = b From b64ab85a316e5dfa67f3b05a7aa5d1e9d85dfb22 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 9 May 2026 12:41:20 +0200 Subject: [PATCH 56/99] Backport PR #31609: DOC: Improve autoscaling and margin docs --- galleries/users_explain/axes/autoscale.py | 44 +++++++++++++---------- lib/matplotlib/axes/_base.py | 40 ++++++++++++++++++--- lib/matplotlib/axis.py | 9 +++++ 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/galleries/users_explain/axes/autoscale.py b/galleries/users_explain/axes/autoscale.py index 337960302c38..ea0c2d24c55a 100644 --- a/galleries/users_explain/axes/autoscale.py +++ b/galleries/users_explain/axes/autoscale.py @@ -6,33 +6,41 @@ Axis autoscaling ================ -The limits on an axis can be set manually (e.g. ``ax.set_xlim(xmin, xmax)``) -or Matplotlib can set them automatically based on the data already on the Axes. -There are a number of options to this autoscaling behaviour, discussed below. -""" +Basic concept +------------- -# %% -# We will start with a simple line plot showing that autoscaling -# extends the axis limits 5% beyond the data limits (-2π, 2π). +Autoscaling ensures that data is visible within the Axes by automatically adjusting +the axis limits. When you plot data, Matplotlib's autoscaling mechanism updates the +axis limits accordingly. +""" import matplotlib.pyplot as plt import numpy as np -x = np.linspace(-2 * np.pi, 2 * np.pi, 100) +x = np.linspace(-6, 6, 201) y = np.sinc(x) fig, ax = plt.subplots() ax.plot(x, y) # %% +# +# .. _autoscale_margins: +# # Margins # ------- -# The default margin around the data limits is 5%, which is based on the -# default configuration setting of :rc:`axes.xmargin`, :rc:`axes.ymargin`, -# and :rc:`axes.zmargin`: +# To ensure that the data is not at the very edge of the plot, Matplotlib adds a +# margin around the data limits. Note that the *x* data range in the above plot is +# [-6, 6], but the x-axis limits are slightly wider due to the margin. +# +# The default margin is 5%, defined via +# +# - :rc:`axes.xmargin` +# - :rc:`axes.ymargin` +# - :rc:`axes.zmargin` -print(ax.margins()) +print(ax.get_xmargin(), ax.get_ymargin()) # %% # The margin size can be overridden to make them smaller or larger using @@ -116,14 +124,14 @@ ax[1].set_title("Two curves") # %% -# However, there are cases when you don't want to automatically adjust the -# viewport to new data. +# If you don't want automatic updates of the axis limits, either deactivate +# autoscaling with `~.axes.Axes.autoscale` or set the limits +# manually with `~.axes.Axes.set_xlim` / `~.axes.Axes.set_ylim`. # -# One way to disable autoscaling is to manually set the -# axis limit. Let's say that we want to see only a part of the data in +# Let's say that we want to see only a part of the data in # greater detail. Setting the ``xlim`` persists even if we add more curves to -# the data. To recalculate the new limits calling `.Axes.autoscale` will -# toggle the functionality manually. +# the data. Calling `.Axes.autoscale` will re-enable the autoscaling and +# recalculate the limits to fit all the data. fig, ax = plt.subplots(ncols=2, figsize=(12, 8)) ax[0].plot(x, y) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ee933ea138ad..0ddf18b12ec2 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2787,6 +2787,13 @@ def set_autoscale_on(self, b): Parameters ---------- b : bool + + See Also + -------- + :ref:`autoscale` + matplotlib.axes.Axes.autoscale + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.set_autoscaley_on """ for axis in self._axis_map.values(): axis._set_autoscale_on(b) @@ -2825,6 +2832,7 @@ def get_xmargin(self): See Also -------- + :ref:`autoscale_margins` matplotlib.axes.Axes.set_xmargin """ return self._xmargin @@ -2841,6 +2849,7 @@ def get_ymargin(self): See Also -------- + :ref:`autoscale_margins` matplotlib.axes.Axes.set_ymargin """ return self._ymargin @@ -2860,6 +2869,13 @@ def set_xmargin(self, m): Parameters ---------- m : float greater than -0.5 + + See Also + -------- + :ref:`autoscale_margins` + matplotlib.axes.Axes.margins + matplotlib.axes.Axes.get_xmargin + """ if m <= -0.5: raise ValueError("margin must be greater than -0.5") @@ -2882,6 +2898,12 @@ def set_ymargin(self, m): Parameters ---------- m : float greater than -0.5 + + See Also + -------- + :ref:`autoscale_margins` + matplotlib.axes.Axes.margins + matplotlib.axes.Axes.get_ymargin """ if m <= -0.5: raise ValueError("margin must be greater than -0.5") @@ -2945,6 +2967,7 @@ def margins(self, *margins, x=None, y=None, tight=True): See Also -------- + :ref:`autoscale_margins` .Axes.set_xmargin, .Axes.set_ymargin """ @@ -2999,10 +3022,12 @@ def autoscale(self, enable=True, axis='both', tight=None): """ Autoscale the axis view to the data (toggle). - Convenience method for simple axis view autoscaling. - It turns autoscaling on or off, and then, - if autoscaling for either axis is on, it performs - the autoscaling on the specified axis or Axes. + Convenience method for simple axis view autoscaling. This: + + - Turns autoscaling on or off (`~.axes.Axes.set_autoscalex_on` / + `~.axes.Axes.set_autoscaley_on`). + - Ensures that view limits will get updated when needed. - As view limits + are lazy-updated, this technically marks the view limits as stale. Parameters ---------- @@ -3016,6 +3041,13 @@ def autoscale(self, enable=True, axis='both', tight=None): If True, first set the margins to zero. Then, this argument is forwarded to `~.axes.Axes.autoscale_view` (regardless of its value); see the description of its behavior there. + + See Also + -------- + :ref:`autoscale` + matplotlib.axes.Axes.set_autoscale_on + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.set_autoscaley_on """ if enable is None: scalex = True diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 48bbac9264ae..c526b8a2aa6a 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -838,6 +838,15 @@ def _set_autoscale_on(self, b): Parameters ---------- b : bool + + See Also + -------- + matplotlib.axes.Axes.autoscale + matplotlib.axes.Axes.set_autoscale_on + matplotlib.axes.Axes.get_autoscalex_on + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.get_autoscaley_on + matplotlib.axes.Axes.set_autoscaley_on """ if b is not None: self._autoscale_on = b From 56d23737ce4983aa80b0b7176fa58b59bd90c984 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 9 May 2026 13:11:54 +0200 Subject: [PATCH 57/99] DOC: Document that bar() errorbars do not support individual coloring (#31579) * DOC: Document that bar() errorbars do not support individual coloring Closes #14480. * Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Apply suggestion from @timhoffm --- lib/matplotlib/axes/_axes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d3b93ed2c40a..75dcb4653c52 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2410,11 +2410,16 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", errors. - *None*: No errorbar. (Default) - See :doc:`/gallery/statistics/errorbar_features` for an example on + This is a convenience shortcut for an extra `~.axes.Axes.errorbar` + call. See its documentation and + :doc:`/gallery/statistics/errorbar_features` for an example on the usage of *xerr* and *yerr*. ecolor : :mpltype:`color` or list of :mpltype:`color`, default: 'black' The line color of the errorbars. + Multiple colors are only supported if the errorbars do not have + caps. If you need individually colored errorbars with caps, instead + use explicit `~.axes.Axes.errorbar` calls for each data point. capsize : float, default: :rc:`errorbar.capsize` The length of the error bar caps in points. From f821c45ba29e7735100621510205411521aa903e Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 9 May 2026 13:11:54 +0200 Subject: [PATCH 58/99] Backport PR #31579: DOC: Document that bar() errorbars do not support individual coloring --- lib/matplotlib/axes/_axes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 76659ae2c83d..687bb4b48d3b 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2409,11 +2409,16 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", errors. - *None*: No errorbar. (Default) - See :doc:`/gallery/statistics/errorbar_features` for an example on + This is a convenience shortcut for an extra `~.axes.Axes.errorbar` + call. See its documentation and + :doc:`/gallery/statistics/errorbar_features` for an example on the usage of *xerr* and *yerr*. ecolor : :mpltype:`color` or list of :mpltype:`color`, default: 'black' The line color of the errorbars. + Multiple colors are only supported if the errorbars do not have + caps. If you need individually colored errorbars with caps, instead + use explicit `~.axes.Axes.errorbar` calls for each data point. capsize : float, default: :rc:`errorbar.capsize` The length of the error bar caps in points. From 9d31ef590d944e2cc2416200557fed87d41e4f60 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 9 May 2026 21:13:59 -0400 Subject: [PATCH 59/99] Backport PR #31632: FIX: Prohibit special TeX chars in pgf metadata --- lib/matplotlib/backends/backend_pgf.py | 9 +++++++++ lib/matplotlib/tests/test_backend_pgf.py | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 3205f294ab2d..36048fe016df 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -154,6 +154,15 @@ def _metadata_to_str(key, value): value = value.name.decode('ascii') else: value = str(value) + + # ensure that metadata does not contain special TeX chars because we + # insert the metadata as raw text into the TeX source + invalid_chars = r"\{}[]()" + if any(c in value + key for c in invalid_chars): + raise ValueError( + f"Invalid metadata value for {key!r}: {value!r}. " + f"The value must not contain the chars {invalid_chars}.") + return f'{key}={{{value}}}' diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index e5b73c9450f3..4af329fa28d4 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -15,7 +15,7 @@ from matplotlib.testing import _has_tex_package, _check_for_pgf from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib.testing.compare import compare_images -from matplotlib.backends.backend_pgf import PdfPages +from matplotlib.backends.backend_pgf import _metadata_to_str, PdfPages from matplotlib.testing.decorators import ( _image_directories, check_figures_equal, image_comparison) from matplotlib.testing._markers import ( @@ -37,6 +37,26 @@ def compare_figure(fname, savefig_kwargs={}, tol=0): raise ImageComparisonFailure(err) +@pytest.mark.parametrize("key, value, expected_str", [ + ("Author", "me", "Author={me}"), + ("ModDate", + datetime.datetime(1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))), + "ModDate={D:19680801000000Z}"), +]) +def test__metadata_to_str(key, value, expected_str): + assert _metadata_to_str(key, value) == expected_str + + +@pytest.mark.parametrize("value", [ + r"Backslashes, e.g. in \commands", + r"funny braces {}", + r"and square brackets]", +]) +def test__metadata_to_str_error(value): + with pytest.raises(ValueError, match="value must not contain the chars"): + _metadata_to_str("Title", value) + + @needs_pgf_xelatex @needs_ghostscript @pytest.mark.backend('pgf') From c4d52bf13c922f36e556ac83d65fcb3c909d5536 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 10 May 2026 10:53:57 +0200 Subject: [PATCH 60/99] FIX: Pin rstcheck to prevent CI failure While rstcheck itself is frozen and also was not updated, it depends on rstcheck-core, which was recently updated to 1.3 and contains a bug that results in a false error. https://github.com/rstcheck/rstcheck-core/pull/114#pullrequestreview-4239740896 This PR excludes that version to fix pre-commit rstcheck in our CI. --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7de40cf539ea..4602570328a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,6 +70,7 @@ repos: hooks: - id: rstcheck additional_dependencies: + - rstcheck-core!=1.3 # https://github.com/rstcheck/rstcheck-core/pull/114#pullrequestreview-4239740896 - sphinx>=1.8.1 - tomli - repo: https://github.com/adrienverge/yamllint From 34df90830df39ae772e5eeada2355b1cd3a96d44 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 10 May 2026 11:01:17 +0200 Subject: [PATCH 61/99] MNT: Remove pre-commit/rstcheck dependency on tomli This was only needed to support python<=3.11 and is thus not needed anymore on the main branch. I'm unclear whether we should also remove `sphinx>=1.8.1`. It's far off and I'm quite sure we do not systematically specify lower compatibility boundaries. --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7de40cf539ea..b018c470244b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,6 @@ repos: - id: rstcheck additional_dependencies: - sphinx>=1.8.1 - - tomli - repo: https://github.com/adrienverge/yamllint rev: cba56bcde1fdd01c1deb3f945e69764c291a6530 # frozen: v1.38.0 hooks: From 6dd1f7e114ecf30baa8ef2ede69a8c233e7fb158 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 10 May 2026 12:44:15 +0100 Subject: [PATCH 62/99] Backport PR #31647: FIX: Pin rstcheck to prevent CI failure --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7de40cf539ea..4602570328a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,6 +70,7 @@ repos: hooks: - id: rstcheck additional_dependencies: + - rstcheck-core!=1.3 # https://github.com/rstcheck/rstcheck-core/pull/114#pullrequestreview-4239740896 - sphinx>=1.8.1 - tomli - repo: https://github.com/adrienverge/yamllint From dd89dfddc1312053400c65196d8f8ab8f58b5253 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 10 May 2026 15:43:13 +0200 Subject: [PATCH 63/99] DOC: Prevent ticks from being cut off in tick rotation example (#31649) * DOC: Prevent ticks from being cut off in tick rotation example * Update galleries/examples/ticks/ticklabels_rotation.py Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --------- Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- galleries/examples/ticks/ticklabels_rotation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/galleries/examples/ticks/ticklabels_rotation.py b/galleries/examples/ticks/ticklabels_rotation.py index c17312a61d48..f5ab80745630 100644 --- a/galleries/examples/ticks/ticklabels_rotation.py +++ b/galleries/examples/ticks/ticklabels_rotation.py @@ -7,6 +7,9 @@ Adjust the tick properties using `~.Axes.tick_params`: Set the angle in degrees via the *rotation* parameter. Set the *rotation_mode* parameter to "xtick" / "ytick" to make the text point towards the tick, see also `~.Text.set_rotation_mode`. + +Note: We use ``layout="constrained"`` to make sure there is enough space for the tick +labels so that they are not cut off. """ import matplotlib.pyplot as plt @@ -30,7 +33,7 @@ 'Vietnam': 102.3, } -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ax.bar(population.keys(), population.values()) ax.tick_params("x", rotation=45, rotation_mode="xtick") ax.set_ylabel("population (millions)") From 203af572e8161399b1cc66cbccae7eb9b07d3182 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 10 May 2026 15:43:13 +0200 Subject: [PATCH 64/99] Backport PR #31649: DOC: Prevent ticks from being cut off in tick rotation example --- galleries/examples/ticks/ticklabels_rotation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/galleries/examples/ticks/ticklabels_rotation.py b/galleries/examples/ticks/ticklabels_rotation.py index c17312a61d48..f5ab80745630 100644 --- a/galleries/examples/ticks/ticklabels_rotation.py +++ b/galleries/examples/ticks/ticklabels_rotation.py @@ -7,6 +7,9 @@ Adjust the tick properties using `~.Axes.tick_params`: Set the angle in degrees via the *rotation* parameter. Set the *rotation_mode* parameter to "xtick" / "ytick" to make the text point towards the tick, see also `~.Text.set_rotation_mode`. + +Note: We use ``layout="constrained"`` to make sure there is enough space for the tick +labels so that they are not cut off. """ import matplotlib.pyplot as plt @@ -30,7 +33,7 @@ 'Vietnam': 102.3, } -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ax.bar(population.keys(), population.values()) ax.tick_params("x", rotation=45, rotation_mode="xtick") ax.set_ylabel("population (millions)") From 8ac4071601ef19fae50d4f3825b646595e24d7f7 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 25 May 2025 16:29:38 +0100 Subject: [PATCH 65/99] Fix constrained layout applying pad multiple times --- .../next_api_changes/behavior/30108-REC.rst | 6 +++ lib/matplotlib/_constrained_layout.py | 41 +++++++--------- .../constrained_layout12.png | Bin 12369 -> 31832 bytes .../constrained_layout17.png | Bin 8786 -> 21245 bytes .../constrained_layout8.png | Bin 6995 -> 9221 bytes .../test_submerged_with_colorbar.png | Bin 0 -> 6486 bytes .../tests/test_constrainedlayout.py | 44 ++++++++++++++++++ 7 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/30108-REC.rst create mode 100644 lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png diff --git a/doc/api/next_api_changes/behavior/30108-REC.rst b/doc/api/next_api_changes/behavior/30108-REC.rst new file mode 100644 index 000000000000..ce4fb0833207 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30108-REC.rst @@ -0,0 +1,6 @@ +Complex layouts and constrained layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Constrained layout now produces smaller spacing between subplots in some +circumstances. This should only affect complex layouts where rows or columns +contain different numbers of subplots, for example a layout created with +``plt.subplot_mosaic('AC;BC', layout='constrained')``. diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 33ec8ef985e7..ce488d555898 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -539,14 +539,10 @@ def match_submerged_margins(layoutgrids, fig): # interior columns: if len(ss1.colspan) > 1: - maxsubl = np.max( - lg1.margin_vals['left'][ss1.colspan[1:]] + - lg1.margin_vals['leftcb'][ss1.colspan[1:]] - ) - maxsubr = np.max( - lg1.margin_vals['right'][ss1.colspan[:-1]] + - lg1.margin_vals['rightcb'][ss1.colspan[:-1]] - ) + leftcb = lg1.margin_vals['leftcb'][ss1.colspan[1:]] + rightcb = lg1.margin_vals['rightcb'][ss1.colspan[:-1]] + maxsubl = np.max(lg1.margin_vals['left'][ss1.colspan[1:]] + leftcb) + maxsubr = np.max(lg1.margin_vals['right'][ss1.colspan[:-1]] + rightcb) for ax2 in axs: ss2 = ax2.get_subplotspec() lg2 = layoutgrids[ss2.get_gridspec()] @@ -561,22 +557,17 @@ def match_submerged_margins(layoutgrids, fig): lg2.margin_vals['rightcb'][ss2.colspan[:-1]]) if maxsubr2 > maxsubr: maxsubr = maxsubr2 - for i in ss1.colspan[1:]: - lg1.edit_margin_min('left', maxsubl, cell=i) - for i in ss1.colspan[:-1]: - lg1.edit_margin_min('right', maxsubr, cell=i) + for i, cb in zip(ss1.colspan[1:], leftcb): + lg1.edit_margin_min('left', maxsubl - cb, cell=i) + for i, cb in zip(ss1.colspan[:-1], rightcb): + lg1.edit_margin_min('right', maxsubr - cb, cell=i) # interior rows: if len(ss1.rowspan) > 1: - maxsubt = np.max( - lg1.margin_vals['top'][ss1.rowspan[1:]] + - lg1.margin_vals['topcb'][ss1.rowspan[1:]] - ) - maxsubb = np.max( - lg1.margin_vals['bottom'][ss1.rowspan[:-1]] + - lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]] - ) - + topcb = lg1.margin_vals['topcb'][ss1.rowspan[1:]] + bottomcb = lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]] + maxsubt = np.max(lg1.margin_vals['top'][ss1.rowspan[1:]] + topcb) + maxsubb = np.max(lg1.margin_vals['bottom'][ss1.rowspan[:-1]] + bottomcb) for ax2 in axs: ss2 = ax2.get_subplotspec() lg2 = layoutgrids[ss2.get_gridspec()] @@ -590,10 +581,10 @@ def match_submerged_margins(layoutgrids, fig): lg2.margin_vals['bottom'][ss2.rowspan[:-1]] + lg2.margin_vals['bottomcb'][ss2.rowspan[:-1]] ), maxsubb]) - for i in ss1.rowspan[1:]: - lg1.edit_margin_min('top', maxsubt, cell=i) - for i in ss1.rowspan[:-1]: - lg1.edit_margin_min('bottom', maxsubb, cell=i) + for i, cb in zip(ss1.rowspan[1:], topcb): + lg1.edit_margin_min('top', maxsubt - cb, cell=i) + for i, cb in zip(ss1.rowspan[:-1], bottomcb): + lg1.edit_margin_min('bottom', maxsubb - cb, cell=i) return axs diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png index 6f5625ae26056dd64836efa4690dfc3e64480faa..b4565e4c5c18f99cd524d90f2cff147ebd89059a 100644 GIT binary patch literal 31832 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 literal 8786 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 P0B*WkMw%t+xM%+bNnNe# diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png index 99ba9f294a7a5447095ffd17a43f930a3842b5cc..5bcd4248fe2eb3cc74d60338816a724a7b9ccaf6 100644 GIT binary patch literal 9221 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 mKz_I2sruC>^}?R+JchD!{Xa5V*dKA9^ literal 6995 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 diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png new file mode 100644 index 0000000000000000000000000000000000000000..f53c2b8035a94975fa1ce1efb66a2a9b96101587 GIT binary patch literal 6486 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 M-$d{3KEkj63shQ!D*ylh literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index ff757c1ce9fc..7d1314ed5042 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -809,3 +809,47 @@ def test_submerged_subfig(): for ax in axs[1:]: assert np.allclose(ax.get_position().bounds[-1], axs[0].get_position().bounds[-1], atol=1e-6) + + +def test_submerged_height_gap(): + """Test that the gap between rows does not depend on the number of columns.""" + + mosaic1 = "AC;BC" + mosaic2 = "ACDE;BCDE" + + fig1, ax_dict1 = plt.subplot_mosaic(mosaic1, layout='constrained') + fig2, ax_dict2 = plt.subplot_mosaic(mosaic2, layout='constrained') + for fig in fig1, fig2: + fig.get_layout_engine().set(h_pad=0.2) + fig.draw_without_rendering() + + for label in 'A', 'B': + np.testing.assert_allclose(ax_dict1[label].get_position().bounds[-1], + ax_dict2[label].get_position().bounds[-1]) + + +def test_submerged_width_gap(): + """Test that the gap between columns does not depend on the number of rows.""" + + mosaic1 = "AB;CC" + mosaic2 = "AB;CC;DD" + + fig1, ax_dict1 = plt.subplot_mosaic(mosaic1, layout='constrained') + fig2, ax_dict2 = plt.subplot_mosaic(mosaic2, layout='constrained') + for fig in fig1, fig2: + fig.get_layout_engine().set(w_pad=0.2) + fig.draw_without_rendering() + + for label in 'A', 'B': + np.testing.assert_allclose(ax_dict1[label].get_position().bounds[-2], + ax_dict2[label].get_position().bounds[-2]) + + +@image_comparison(['test_submerged_with_colorbar.png'], style='mpl20') +def test_submerged_with_colorbar(): + mosaic = "AABBCC;DDDEEE" + + fig, ax_dict = plt.subplot_mosaic(mosaic, layout='constrained') + + cf = ax_dict['A'].contourf([[0, 1], [2, 3]]) + fig.colorbar(cf) From 782a1f4d7b5ec33643aa7e9c44baf70a71106ea3 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 10 May 2026 23:23:21 -0400 Subject: [PATCH 66/99] Backport PR #30108: Fix constrained layout applying pad multiple times --- .../next_api_changes/behavior/30108-REC.rst | 6 +++ lib/matplotlib/_constrained_layout.py | 41 +++++++--------- .../constrained_layout12.png | Bin 12369 -> 31832 bytes .../constrained_layout17.png | Bin 8786 -> 21245 bytes .../constrained_layout8.png | Bin 6995 -> 9221 bytes .../test_submerged_with_colorbar.png | Bin 0 -> 6486 bytes .../tests/test_constrainedlayout.py | 44 ++++++++++++++++++ 7 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/30108-REC.rst create mode 100644 lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png diff --git a/doc/api/next_api_changes/behavior/30108-REC.rst b/doc/api/next_api_changes/behavior/30108-REC.rst new file mode 100644 index 000000000000..ce4fb0833207 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30108-REC.rst @@ -0,0 +1,6 @@ +Complex layouts and constrained layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Constrained layout now produces smaller spacing between subplots in some +circumstances. This should only affect complex layouts where rows or columns +contain different numbers of subplots, for example a layout created with +``plt.subplot_mosaic('AC;BC', layout='constrained')``. diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 33ec8ef985e7..ce488d555898 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -539,14 +539,10 @@ def match_submerged_margins(layoutgrids, fig): # interior columns: if len(ss1.colspan) > 1: - maxsubl = np.max( - lg1.margin_vals['left'][ss1.colspan[1:]] + - lg1.margin_vals['leftcb'][ss1.colspan[1:]] - ) - maxsubr = np.max( - lg1.margin_vals['right'][ss1.colspan[:-1]] + - lg1.margin_vals['rightcb'][ss1.colspan[:-1]] - ) + leftcb = lg1.margin_vals['leftcb'][ss1.colspan[1:]] + rightcb = lg1.margin_vals['rightcb'][ss1.colspan[:-1]] + maxsubl = np.max(lg1.margin_vals['left'][ss1.colspan[1:]] + leftcb) + maxsubr = np.max(lg1.margin_vals['right'][ss1.colspan[:-1]] + rightcb) for ax2 in axs: ss2 = ax2.get_subplotspec() lg2 = layoutgrids[ss2.get_gridspec()] @@ -561,22 +557,17 @@ def match_submerged_margins(layoutgrids, fig): lg2.margin_vals['rightcb'][ss2.colspan[:-1]]) if maxsubr2 > maxsubr: maxsubr = maxsubr2 - for i in ss1.colspan[1:]: - lg1.edit_margin_min('left', maxsubl, cell=i) - for i in ss1.colspan[:-1]: - lg1.edit_margin_min('right', maxsubr, cell=i) + for i, cb in zip(ss1.colspan[1:], leftcb): + lg1.edit_margin_min('left', maxsubl - cb, cell=i) + for i, cb in zip(ss1.colspan[:-1], rightcb): + lg1.edit_margin_min('right', maxsubr - cb, cell=i) # interior rows: if len(ss1.rowspan) > 1: - maxsubt = np.max( - lg1.margin_vals['top'][ss1.rowspan[1:]] + - lg1.margin_vals['topcb'][ss1.rowspan[1:]] - ) - maxsubb = np.max( - lg1.margin_vals['bottom'][ss1.rowspan[:-1]] + - lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]] - ) - + topcb = lg1.margin_vals['topcb'][ss1.rowspan[1:]] + bottomcb = lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]] + maxsubt = np.max(lg1.margin_vals['top'][ss1.rowspan[1:]] + topcb) + maxsubb = np.max(lg1.margin_vals['bottom'][ss1.rowspan[:-1]] + bottomcb) for ax2 in axs: ss2 = ax2.get_subplotspec() lg2 = layoutgrids[ss2.get_gridspec()] @@ -590,10 +581,10 @@ def match_submerged_margins(layoutgrids, fig): lg2.margin_vals['bottom'][ss2.rowspan[:-1]] + lg2.margin_vals['bottomcb'][ss2.rowspan[:-1]] ), maxsubb]) - for i in ss1.rowspan[1:]: - lg1.edit_margin_min('top', maxsubt, cell=i) - for i in ss1.rowspan[:-1]: - lg1.edit_margin_min('bottom', maxsubb, cell=i) + for i, cb in zip(ss1.rowspan[1:], topcb): + lg1.edit_margin_min('top', maxsubt - cb, cell=i) + for i, cb in zip(ss1.rowspan[:-1], bottomcb): + lg1.edit_margin_min('bottom', maxsubb - cb, cell=i) return axs diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png index 6f5625ae26056dd64836efa4690dfc3e64480faa..b4565e4c5c18f99cd524d90f2cff147ebd89059a 100644 GIT binary patch literal 31832 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 literal 8786 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 P0B*WkMw%t+xM%+bNnNe# diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png index 99ba9f294a7a5447095ffd17a43f930a3842b5cc..5bcd4248fe2eb3cc74d60338816a724a7b9ccaf6 100644 GIT binary patch literal 9221 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 mKz_I2sruC>^}?R+JchD!{Xa5V*dKA9^ literal 6995 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 diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png new file mode 100644 index 0000000000000000000000000000000000000000..f53c2b8035a94975fa1ce1efb66a2a9b96101587 GIT binary patch literal 6486 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 M-$d{3KEkj63shQ!D*ylh literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index ff757c1ce9fc..7d1314ed5042 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -809,3 +809,47 @@ def test_submerged_subfig(): for ax in axs[1:]: assert np.allclose(ax.get_position().bounds[-1], axs[0].get_position().bounds[-1], atol=1e-6) + + +def test_submerged_height_gap(): + """Test that the gap between rows does not depend on the number of columns.""" + + mosaic1 = "AC;BC" + mosaic2 = "ACDE;BCDE" + + fig1, ax_dict1 = plt.subplot_mosaic(mosaic1, layout='constrained') + fig2, ax_dict2 = plt.subplot_mosaic(mosaic2, layout='constrained') + for fig in fig1, fig2: + fig.get_layout_engine().set(h_pad=0.2) + fig.draw_without_rendering() + + for label in 'A', 'B': + np.testing.assert_allclose(ax_dict1[label].get_position().bounds[-1], + ax_dict2[label].get_position().bounds[-1]) + + +def test_submerged_width_gap(): + """Test that the gap between columns does not depend on the number of rows.""" + + mosaic1 = "AB;CC" + mosaic2 = "AB;CC;DD" + + fig1, ax_dict1 = plt.subplot_mosaic(mosaic1, layout='constrained') + fig2, ax_dict2 = plt.subplot_mosaic(mosaic2, layout='constrained') + for fig in fig1, fig2: + fig.get_layout_engine().set(w_pad=0.2) + fig.draw_without_rendering() + + for label in 'A', 'B': + np.testing.assert_allclose(ax_dict1[label].get_position().bounds[-2], + ax_dict2[label].get_position().bounds[-2]) + + +@image_comparison(['test_submerged_with_colorbar.png'], style='mpl20') +def test_submerged_with_colorbar(): + mosaic = "AABBCC;DDDEEE" + + fig, ax_dict = plt.subplot_mosaic(mosaic, layout='constrained') + + cf = ax_dict['A'].contourf([[0, 1], [2, 3]]) + fig.colorbar(cf) From 11af330ba17c359ad998a5421d8e9ba27404d52e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 11 May 2026 14:46:53 -0400 Subject: [PATCH 67/99] Backport PR #31578: FIX: URL links in SVG should have target='_blank' --- doc/api/next_api_changes/behavior/31578-TH.rst | 10 ++++++++++ lib/matplotlib/backends/backend_svg.py | 10 +++++----- lib/matplotlib/tests/test_backend_svg.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/31578-TH.rst diff --git a/doc/api/next_api_changes/behavior/31578-TH.rst b/doc/api/next_api_changes/behavior/31578-TH.rst new file mode 100644 index 000000000000..0607652c7c8f --- /dev/null +++ b/doc/api/next_api_changes/behavior/31578-TH.rst @@ -0,0 +1,10 @@ +SVG links open in new tab or window +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +`.Artist.set_url` allows to turn the Artist into a link. In SVG output, +this has been implemented without specifying a `target`_ attribute. The default +target value "_self" resulted in replacing the SVG document with the linked page +when the link was clicked. + +The target is now set to "_blank" so that the link opens in a new tab or window. + +.. _target: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/target diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 6445915de38b..24790356b9d7 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -697,7 +697,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): sketch=gc.get_sketch_params()) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) self.writer.element('path', d=path_data, **self._get_clip_attrs(gc), style=self._get_style(gc, rgbFace)) if gc.get_url() is not None: @@ -730,7 +730,7 @@ def draw_markers( writer.start('g', **self._get_clip_attrs(gc)) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': f'#{oid}'} clip = (0, 0, self.width*72, self.height*72) @@ -788,7 +788,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, antialiaseds, urls, offset_position, hatchcolors=hatchcolors): url = gc0.get_url() if url is not None: - writer.start('a', attrib={'xlink:href': url}) + writer.start('a', attrib={'xlink:href': url, 'target': '_blank'}) clip_attrs = self._get_clip_attrs(gc0) if clip_attrs: writer.start('g', **clip_attrs) @@ -966,7 +966,7 @@ def draw_image(self, gc, x, y, im, transform=None): url = gc.get_url() if url is not None: - self.writer.start('a', attrib={'xlink:href': url}) + self.writer.start('a', attrib={'xlink:href': url, 'target': '_blank'}) attrib = {} oid = gc.get_gid() @@ -1288,7 +1288,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): self.writer.start('g', **clip_attrs) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) if mpl.rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index ba565eadb01b..6b63990f7620 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -65,7 +65,7 @@ def test_text_urls(): fig.savefig(fd, format='svg') buf = fd.getvalue().decode() - expected = f'' + expected = f'' assert expected in buf From 5310f9754b5323421da262e882b312e11699f00a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 11 May 2026 19:12:35 -0400 Subject: [PATCH 68/99] ci: Re-arrange AppVeyor pipeline Move the extra packages into the initial install step, and drop the conditional, since `TEST_ALL` is always on. Also, put the build step into the actual build phase of the pipeline. --- .appveyor.yml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 13705adc99f9..4521bc876a8f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -29,16 +29,12 @@ environment: matrix: - PYTHON_VERSION: "3.11" - TEST_ALL: "yes" # We always use a 64-bit machine, but can build x86 distributions # with the PYTHON_ARCH variable platform: - x64 -# all our python builds have to happen in tests_script... -build: false - cache: - '%LOCALAPPDATA%\pip\Cache' - '%USERPROFILE%\.cache\matplotlib' @@ -57,10 +53,18 @@ init: - micromamba info install: - - micromamba env create -f environment.yml python=%PYTHON_VERSION% pywin32 + - set EXTRA_PACKAGES=pywin32 codecov + # These are optional dependencies so that we don't skip so many tests... + - set EXTRA_PACKAGES=%EXTRA_PACKAGES% ffmpeg inkscape + # miktex is available on conda, but seems to fail with permission errors. + # missing packages on conda-forge for imagemagick + # This install sometimes failed randomly :-( + # - choco install imagemagick + + - micromamba env create -f environment.yml python=%PYTHON_VERSION% %EXTRA_PACKAGES% - micromamba activate mpl-dev -test_script: +build_script: # Now build the thing.. - set LINK=/LIBPATH:%cd%\lib - pip install -v --no-build-isolation --editable .[dev] @@ -68,13 +72,7 @@ test_script: - set "DUMPBIN=%VS140COMNTOOLS%\..\..\VC\bin\dumpbin.exe" - '"%DUMPBIN%" /DEPENDENTS lib\matplotlib\ft2font*.pyd | findstr freetype.*.dll && exit /b 1 || exit /b 0' - # this are optional dependencies so that we don't skip so many tests... - - if x%TEST_ALL% == xyes micromamba install -q ffmpeg inkscape - # miktex is available on conda, but seems to fail with permission errors. - # missing packages on conda-forge for imagemagick - # This install sometimes failed randomly :-( - # - choco install imagemagick - +test_script: # Test import of tkagg backend - python -c "import matplotlib as m; m.use('tkagg'); @@ -90,7 +88,6 @@ artifacts: type: Zip on_finish: - - micromamba install codecov - codecov -e PYTHON_VERSION PLATFORM -n "%PYTHON_VERSION% Windows" on_failure: From 38a587c152965c1ce33272af32244ed5a938418b Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Tue, 12 May 2026 11:17:54 +0100 Subject: [PATCH 69/99] Backport PR #31659: ci: Re-arrange AppVeyor pipeline --- .appveyor.yml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 13705adc99f9..4521bc876a8f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -29,16 +29,12 @@ environment: matrix: - PYTHON_VERSION: "3.11" - TEST_ALL: "yes" # We always use a 64-bit machine, but can build x86 distributions # with the PYTHON_ARCH variable platform: - x64 -# all our python builds have to happen in tests_script... -build: false - cache: - '%LOCALAPPDATA%\pip\Cache' - '%USERPROFILE%\.cache\matplotlib' @@ -57,10 +53,18 @@ init: - micromamba info install: - - micromamba env create -f environment.yml python=%PYTHON_VERSION% pywin32 + - set EXTRA_PACKAGES=pywin32 codecov + # These are optional dependencies so that we don't skip so many tests... + - set EXTRA_PACKAGES=%EXTRA_PACKAGES% ffmpeg inkscape + # miktex is available on conda, but seems to fail with permission errors. + # missing packages on conda-forge for imagemagick + # This install sometimes failed randomly :-( + # - choco install imagemagick + + - micromamba env create -f environment.yml python=%PYTHON_VERSION% %EXTRA_PACKAGES% - micromamba activate mpl-dev -test_script: +build_script: # Now build the thing.. - set LINK=/LIBPATH:%cd%\lib - pip install -v --no-build-isolation --editable .[dev] @@ -68,13 +72,7 @@ test_script: - set "DUMPBIN=%VS140COMNTOOLS%\..\..\VC\bin\dumpbin.exe" - '"%DUMPBIN%" /DEPENDENTS lib\matplotlib\ft2font*.pyd | findstr freetype.*.dll && exit /b 1 || exit /b 0' - # this are optional dependencies so that we don't skip so many tests... - - if x%TEST_ALL% == xyes micromamba install -q ffmpeg inkscape - # miktex is available on conda, but seems to fail with permission errors. - # missing packages on conda-forge for imagemagick - # This install sometimes failed randomly :-( - # - choco install imagemagick - +test_script: # Test import of tkagg backend - python -c "import matplotlib as m; m.use('tkagg'); @@ -90,7 +88,6 @@ artifacts: type: Zip on_finish: - - micromamba install codecov - codecov -e PYTHON_VERSION PLATFORM -n "%PYTHON_VERSION% Windows" on_failure: From 75ea50cc0893a1669fe257e01842de1e1c8fbb1b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 12 May 2026 16:10:52 -0400 Subject: [PATCH 70/99] DOC: Prepare GitHub stats for 3.11 rc2 --- doc/release/github_stats.rst | 75 +++++++++++++++++++++++++++++++---- doc/release/release_notes.rst | 9 ++++- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/doc/release/github_stats.rst b/doc/release/github_stats.rst index 62c3242b7eb7..d6beb7b6b059 100644 --- a/doc/release/github_stats.rst +++ b/doc/release/github_stats.rst @@ -2,17 +2,17 @@ .. _github-stats: -GitHub statistics for 3.11.0 (Apr 24, 2026) +GitHub statistics for 3.11.0 (May 12, 2026) =========================================== -GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/24 +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/05/12 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 246 issues and merged 764 pull requests. +We closed 257 issues and merged 812 pull requests. The full list can be seen `on GitHub `__ -The following 264 authors contributed 4590 commits. +The following 266 authors contributed 4674 commits. * 34j * Aaratrika-Shelly @@ -33,8 +33,8 @@ The following 264 authors contributed 4590 commits. * Alexandra Khoo * Allison * alphanoobie -* Aman Kushwaha * AMAN KUSHWAHA +* Aman Kushwaha * Aman Nijjar * Aman Parganiha * Aman Srivastava @@ -58,6 +58,7 @@ The following 264 authors contributed 4590 commits. * Ben Root * Bodhi Silberling * Brian Christian +* Brian Lau * BriAnna Foreman * brk * Carlos Ramos Carreño @@ -234,6 +235,7 @@ The following 264 authors contributed 4590 commits. * Raphael Erik Hviding * Raphael Quast * RETHICK CB +* Ricardo Peres * RogueRebel33 * Roman * Roman A @@ -281,8 +283,56 @@ The following 264 authors contributed 4590 commits. GitHub issues and pull requests: -Pull Requests (764): +Pull Requests (812): +* :ghpull:`31662`: Backport PR #31659 on branch v3.11.x (ci: Re-arrange AppVeyor pipeline) +* :ghpull:`31659`: ci: Re-arrange AppVeyor pipeline +* :ghpull:`31658`: Backport PR #31578 on branch v3.11.x (FIX: URL links in SVG should have target='_blank') +* :ghpull:`31578`: FIX: URL links in SVG should have target='_blank' +* :ghpull:`31654`: Backport PR #30108 on branch v3.11.x (Fix constrained layout applying pad multiple times) +* :ghpull:`30108`: Fix constrained layout applying pad multiple times +* :ghpull:`31651`: Backport PR #31649 on branch v3.11.x (DOC: Prevent ticks from being cut off in tick rotation example) +* :ghpull:`31650`: Backport PR #31647 on branch v3.11.x (FIX: Pin rstcheck to prevent CI failure) +* :ghpull:`31649`: DOC: Prevent ticks from being cut off in tick rotation example +* :ghpull:`31647`: FIX: Pin rstcheck to prevent CI failure +* :ghpull:`31646`: Backport PR #31632 on branch v3.11.x (FIX: Prohibit special TeX chars in pgf metadata) +* :ghpull:`31632`: FIX: Prohibit special TeX chars in pgf metadata +* :ghpull:`31643`: Backport PR #31609 on branch v3.11.x (DOC: Improve autoscaling and margin docs) +* :ghpull:`31644`: Backport PR #31579 on branch v3.11.x (DOC: Document that bar() errorbars do not support individual coloring) +* :ghpull:`31579`: DOC: Document that bar() errorbars do not support individual coloring +* :ghpull:`31609`: DOC: Improve autoscaling and margin docs +* :ghpull:`31640`: Backport PR #31638 on branch v3.11.x (Bump the actions group with 2 updates) +* :ghpull:`31639`: Backport PR #31628 on branch v3.11.x (FIX: use axis lines tight bbox within axis artist tight bbox) +* :ghpull:`31638`: Bump the actions group with 2 updates +* :ghpull:`31637`: Backport PR #31634 on branch v3.11.x (Fix some font-related issues) +* :ghpull:`31628`: FIX: use axis lines tight bbox within axis artist tight bbox +* :ghpull:`31634`: Fix some font-related issues +* :ghpull:`31636`: Backport PR #31630 on branch v3.11.x (Restore PolarTransform(apply_theta_transforms) parameter) +* :ghpull:`31630`: Restore PolarTransform(apply_theta_transforms) parameter +* :ghpull:`31631`: Backport PR #31557 on branch v3.11.x (FIX: Added ft2font null checks added) +* :ghpull:`31629`: Backport PR #31621 on branch v3.11.x (Make Scale axis parameter handling more flexible) +* :ghpull:`31557`: FIX: Added ft2font null checks added +* :ghpull:`31621`: Make Scale axis parameter handling more flexible +* :ghpull:`31627`: Backport PR #31625 on branch v3.11.x (DOC: Inline ScalarMappable reStructuredText entries) +* :ghpull:`31626`: Backport PR #25478 on branch v3.11.x ([BUG] Fix alpha bug on 3D PathCollection plots.) +* :ghpull:`31625`: DOC: Inline ScalarMappable reStructuredText entries +* :ghpull:`25478`: [BUG] Fix alpha bug on 3D PathCollection plots. +* :ghpull:`31611`: Backport PR #31608 on branch v3.11.x (Remove outdated comment re: implementation of hinting_factor.) +* :ghpull:`31608`: Remove outdated comment re: implementation of hinting_factor. +* :ghpull:`31602`: Backport PR #31599 on branch v3.11.x (Bump the actions group with 2 updates) +* :ghpull:`31603`: Backport PR #31594 on branch v3.11.x (DOC: Explain how to selectively restore ticks that are removed by sharex) +* :ghpull:`31594`: DOC: Explain how to selectively restore ticks that are removed by sharex +* :ghpull:`31601`: Backport PR #31600 on branch v3.11.x (Bump https://github.com/astral-sh/ruff-pre-commit from v0.15.11 to 0.15.12) +* :ghpull:`31599`: Bump the actions group with 2 updates +* :ghpull:`31600`: Bump https://github.com/astral-sh/ruff-pre-commit from v0.15.11 to 0.15.12 +* :ghpull:`31592`: Backport PR #31588 on branch v3.11.x (Expire some missed deprecations from 3.9) +* :ghpull:`31588`: Expire some missed deprecations from 3.9 +* :ghpull:`31583`: Backport PR #31577 on branch v3.11.x (FIX: Polar Radial Tick Warnings Labels Bug) +* :ghpull:`31577`: FIX: Polar Radial Tick Warnings Labels Bug +* :ghpull:`31582`: Backport PR #31580 on branch v3.11.x (DOC: added unregister to colormap guide) +* :ghpull:`31580`: DOC: added unregister to colormap guide +* :ghpull:`31564`: Backport PR #31563 on branch v3.11.x (LIC: remove carlogo license) +* :ghpull:`31563`: LIC: remove carlogo license * :ghpull:`31561`: Fixed bug with an uninitialized colormap in parallel threads * :ghpull:`31555`: FIX: removing colorbar's axes also removes colorbar * :ghpull:`31560`: merge up v3.10.9 @@ -1048,8 +1098,19 @@ Pull Requests (764): * :ghpull:`29079`: DOC: Replaced colormap for colorblindness * :ghpull:`29077`: DOC: Replaced green with blue for colorblindness -Issues (246): +Issues (257): +* :ghissue:`23290`: [Bug]: Constrained Layout scaling of layouts with submerged spines +* :ghissue:`31622`: [Bug]: ``tight`` and ``constrained`` layouts honouring invisible parts of ``floating_axis`` +* :ghissue:`31624`: [MNT]: PolarTransform deprecation didn't warn +* :ghissue:`31590`: Should ``_make_axis_parameter_optional`` handle ``None``? +* :ghissue:`25446`: [Bug]: Nan values in scatter 3d plot show in black colour when alpha parameter is passed. +* :ghissue:`22546`: [Doc]: svg.fonttype: None in custom style sheet gives an error +* :ghissue:`24958`: [Doc]: Provide a working example for turning on specific axes labels when sharex or sharey are used with subplots +* :ghissue:`25818`: [Doc]: Heatmap border pixels leak outside grid +* :ghissue:`31574`: [Bug]: polar projection with ``labels`` on ``set_ticks`` gives UserWarning +* :ghissue:`14480`: Multicolor errorbars cannot have caps +* :ghissue:`31330`: [Bug]: Crash when removing colorbar axes in a constrained layout * :ghissue:`14235`: Add \underline to mathtext? * :ghissue:`31462`: [Bug]: Errorbar plot on log-scaled Axes sets incorrect automatic lower limits * :ghissue:`30859`: [Bug]: ax.relim() ignores scatter artist diff --git a/doc/release/release_notes.rst b/doc/release/release_notes.rst index e2cd258ee3a7..d652f5dbcf0f 100644 --- a/doc/release/release_notes.rst +++ b/doc/release/release_notes.rst @@ -13,6 +13,13 @@ Release notes .. include:: release_notes_next.rst +Version 3.11 +^^^^^^^^^^^^ +.. toctree:: + :maxdepth: 1 + + github_stats.rst + Version 3.10 ^^^^^^^^^^^^ .. toctree:: @@ -23,7 +30,7 @@ Version 3.10 ../api/prev_api_changes/api_changes_3.10.7.rst ../api/prev_api_changes/api_changes_3.10.1.rst ../api/prev_api_changes/api_changes_3.10.0.rst - github_stats.rst + prev_whats_new/github_stats_3.10.9.rst prev_whats_new/github_stats_3.10.8.rst prev_whats_new/github_stats_3.10.7.rst prev_whats_new/github_stats_3.10.6.rst From bcdae6fc54232ef2b99a7bb74e7c478dbf84c364 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 12 May 2026 16:14:27 -0400 Subject: [PATCH 71/99] REL: v3.11.0rc2 This is the second release candidate for the meso release 3.11.0. From 7c4bea54917c1bf3e37316c30cedf48a522c14ef Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 12 May 2026 16:15:51 -0400 Subject: [PATCH 72/99] BLD: bump branch away from tag So the tarballs from GitHub are stable. From a61f8667644202682555f901cf5276ad8f4586ef Mon Sep 17 00:00:00 2001 From: hansu650 <2788086371@qq.com> Date: Wed, 13 May 2026 17:12:51 +0800 Subject: [PATCH 73/99] docs: clarify markevery float spacing --- lib/matplotlib/lines.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 69ad36fb768b..2c92b099099e 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -592,9 +592,10 @@ def set_markevery(self, every): ----- Setting *markevery* will still only draw markers at actual data points. While the float argument form aims for uniform visual spacing, it has - to coerce from the ideal spacing to the nearest available data point. - Depending on the number and distribution of data points, the result - may still not look evenly spaced. + to coerce from the ideal spacing along the drawn line to the nearest + available data point. Depending on the number and distribution of data + points, and on how jagged the line is, the result may still not look + evenly spaced along the x- or y-axis. When using a start offset to specify the first marker, the offset will be from the first data point which may be different from the first From 66ebf465a2e796d9defa6ecc8d86671dfe9bef88 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 17:46:15 -0400 Subject: [PATCH 74/99] Remove ResizeObserver ponyfill We added this about 6 years ago for Firefox 68 ESR support. According to [CanIUse](https://caniuse.com/?search=ResizeObserver), `ResizeObserver` is well supported now on 95.84% of browsers by usage, and all browsers by _released version_ since early 2020. For Firefox specifically, support was added in 69, and 68 ESR stopped being supported after 78 ESR was released in mid 2020. --- LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER | 108 ------------------ lib/matplotlib/backends/web_backend/js/mpl.js | 16 +-- .../backends/web_backend/package.json | 3 - meson.build | 2 - tools/embed_js.py | 102 ----------------- 5 files changed, 1 insertion(+), 230 deletions(-) delete mode 100644 LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER delete mode 100644 tools/embed_js.py diff --git a/LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER b/LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER deleted file mode 100644 index 0bc1fa7060b7..000000000000 --- a/LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER +++ /dev/null @@ -1,108 +0,0 @@ -# CC0 1.0 Universal - -## Statement of Purpose - -The laws of most jurisdictions throughout the world automatically confer -exclusive Copyright and Related Rights (defined below) upon the creator and -subsequent owner(s) (each and all, an “owner”) of an original work of -authorship and/or a database (each, a “Work”). - -Certain owners wish to permanently relinquish those rights to a Work for the -purpose of contributing to a commons of creative, cultural and scientific works -(“Commons”) that the public can reliably and without fear of later claims of -infringement build upon, modify, incorporate in other works, reuse and -redistribute as freely as possible in any form whatsoever and for any purposes, -including without limitation commercial purposes. These owners may contribute -to the Commons to promote the ideal of a free culture and the further -production of creative, cultural and scientific works, or to gain reputation or -greater distribution for their Work in part through the use and efforts of -others. - -For these and/or other purposes and motivations, and without any expectation of -additional consideration or compensation, the person associating CC0 with a -Work (the “Affirmer”), to the extent that he or she is an owner of Copyright -and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and -publicly distribute the Work under its terms, with knowledge of his or her -Copyright and Related Rights in the Work and the meaning and intended legal -effect of CC0 on those rights. - -1. Copyright and Related Rights. A Work made available under CC0 may be - protected by copyright and related or neighboring rights (“Copyright and - Related Rights”). Copyright and Related Rights include, but are not limited - to, the following: - 1. the right to reproduce, adapt, distribute, perform, display, communicate, - and translate a Work; - 2. moral rights retained by the original author(s) and/or performer(s); - 3. publicity and privacy rights pertaining to a person’s image or likeness - depicted in a Work; - 4. rights protecting against unfair competition in regards to a Work, - subject to the limitations in paragraph 4(i), below; - 5. rights protecting the extraction, dissemination, use and reuse of data in - a Work; - 6. database rights (such as those arising under Directive 96/9/EC of the - European Parliament and of the Council of 11 March 1996 on the legal - protection of databases, and under any national implementation thereof, - including any amended or successor version of such directive); and - 7. other similar, equivalent or corresponding rights throughout the world - based on applicable law or treaty, and any national implementations - thereof. - -2. Waiver. To the greatest extent permitted by, but not in contravention of, - applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and - unconditionally waives, abandons, and surrenders all of Affirmer’s Copyright - and Related Rights and associated claims and causes of action, whether now - known or unknown (including existing as well as future claims and causes of - action), in the Work (i) in all territories worldwide, (ii) for the maximum - duration provided by applicable law or treaty (including future time - extensions), (iii) in any current or future medium and for any number of - copies, and (iv) for any purpose whatsoever, including without limitation - commercial, advertising or promotional purposes (the “Waiver”). Affirmer - makes the Waiver for the benefit of each member of the public at large and - to the detriment of Affirmer’s heirs and successors, fully intending that - such Waiver shall not be subject to revocation, rescission, cancellation, - termination, or any other legal or equitable action to disrupt the quiet - enjoyment of the Work by the public as contemplated by Affirmer’s express - Statement of Purpose. - -3. Public License Fallback. Should any part of the Waiver for any reason be - judged legally invalid or ineffective under applicable law, then the Waiver - shall be preserved to the maximum extent permitted taking into account - Affirmer’s express Statement of Purpose. In addition, to the extent the - Waiver is so judged Affirmer hereby grants to each affected person a - royalty-free, non transferable, non sublicensable, non exclusive, - irrevocable and unconditional license to exercise Affirmer’s Copyright and - Related Rights in the Work (i) in all territories worldwide, (ii) for the - maximum duration provided by applicable law or treaty (including future time - extensions), (iii) in any current or future medium and for any number of - copies, and (iv) for any purpose whatsoever, including without limitation - commercial, advertising or promotional purposes (the “License”). The License - shall be deemed effective as of the date CC0 was applied by Affirmer to the - Work. Should any part of the License for any reason be judged legally - invalid or ineffective under applicable law, such partial invalidity or - ineffectiveness shall not invalidate the remainder of the License, and in - such case Affirmer hereby affirms that he or she will not (i) exercise any - of his or her remaining Copyright and Related Rights in the Work or (ii) - assert any associated claims and causes of action with respect to the Work, - in either case contrary to Affirmer’s express Statement of Purpose. - -4. Limitations and Disclaimers. - 1. No trademark or patent rights held by Affirmer are waived, abandoned, - surrendered, licensed or otherwise affected by this document. - 2. Affirmer offers the Work as-is and makes no representations or warranties - of any kind concerning the Work, express, implied, statutory or - otherwise, including without limitation warranties of title, - merchantability, fitness for a particular purpose, non infringement, or - the absence of latent or other defects, accuracy, or the present or - absence of errors, whether or not discoverable, all to the greatest - extent permissible under applicable law. - 3. Affirmer disclaims responsibility for clearing rights of other persons - that may apply to the Work or any use thereof, including without - limitation any person’s Copyright and Related Rights in the Work. - Further, Affirmer disclaims responsibility for obtaining any necessary - consents, permissions or other rights required for any use of the Work. - 4. Affirmer understands and acknowledges that Creative Commons is not a - party to this document and has no duty or obligation with respect to this - CC0 or use of the Work. - -For more information, please see -http://creativecommons.org/publicdomain/zero/1.0/. diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index 7745cbcf1e98..d42fe5e17972 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -183,17 +183,7 @@ mpl.figure.prototype._init_canvas = function () { 'z-index: 1;' ); - // Apply a ponyfill if ResizeObserver is not implemented by browser. - if (this.ResizeObserver === undefined) { - if (window.ResizeObserver !== undefined) { - this.ResizeObserver = window.ResizeObserver; - } else { - var obs = _JSXTOOLS_RESIZE_OBSERVER({}); - this.ResizeObserver = obs.ResizeObserver; - } - } - - this.resizeObserverInstance = new this.ResizeObserver(function (entries) { + this.resizeObserverInstance = new ResizeObserver(function (entries) { // There's no need to resize if the WebSocket is not connected: // - If it is still connecting, then we will get an initial resize from // Python once it connects. @@ -728,7 +718,3 @@ mpl.figure.prototype.toolbar_button_onclick = function (name) { mpl.figure.prototype.toolbar_button_onmouseover = function (tooltip) { this.message.textContent = tooltip; }; - -///////////////// REMAINING CONTENT GENERATED BY embed_js.py ///////////////// -// prettier-ignore -var _JSXTOOLS_RESIZE_OBSERVER=function(A){var t,i=new WeakMap,n=new WeakMap,a=new WeakMap,r=new WeakMap,o=new Set;function s(e){if(!(this instanceof s))throw new TypeError("Constructor requires 'new' operator");i.set(this,e)}function h(){throw new TypeError("Function is not a constructor")}function c(e,t,i,n){e=0 in arguments?Number(arguments[0]):0,t=1 in arguments?Number(arguments[1]):0,i=2 in arguments?Number(arguments[2]):0,n=3 in arguments?Number(arguments[3]):0,this.right=(this.x=this.left=e)+(this.width=i),this.bottom=(this.y=this.top=t)+(this.height=n),Object.freeze(this)}function d(){t=requestAnimationFrame(d);var s=new WeakMap,p=new Set;o.forEach((function(t){r.get(t).forEach((function(i){var r=t instanceof window.SVGElement,o=a.get(t),d=r?0:parseFloat(o.paddingTop),f=r?0:parseFloat(o.paddingRight),l=r?0:parseFloat(o.paddingBottom),u=r?0:parseFloat(o.paddingLeft),g=r?0:parseFloat(o.borderTopWidth),m=r?0:parseFloat(o.borderRightWidth),w=r?0:parseFloat(o.borderBottomWidth),b=u+f,F=d+l,v=(r?0:parseFloat(o.borderLeftWidth))+m,W=g+w,y=r?0:t.offsetHeight-W-t.clientHeight,E=r?0:t.offsetWidth-v-t.clientWidth,R=b+v,z=F+W,M=r?t.width:parseFloat(o.width)-R-E,O=r?t.height:parseFloat(o.height)-z-y;if(n.has(t)){var k=n.get(t);if(k[0]===M&&k[1]===O)return}n.set(t,[M,O]);var S=Object.create(h.prototype);S.target=t,S.contentRect=new c(u,d,M,O),s.has(i)||(s.set(i,[]),p.add(i)),s.get(i).push(S)}))})),p.forEach((function(e){i.get(e).call(e,s.get(e),e)}))}return s.prototype.observe=function(i){if(i instanceof window.Element){r.has(i)||(r.set(i,new Set),o.add(i),a.set(i,window.getComputedStyle(i)));var n=r.get(i);n.has(this)||n.add(this),cancelAnimationFrame(t),t=requestAnimationFrame(d)}},s.prototype.unobserve=function(i){if(i instanceof window.Element&&r.has(i)){var n=r.get(i);n.has(this)&&(n.delete(this),n.size||(r.delete(i),o.delete(i))),n.size||r.delete(i),o.size||cancelAnimationFrame(t)}},A.DOMRectReadOnly=c,A.ResizeObserver=s,A.ResizeObserverEntry=h,A}; // eslint-disable-line diff --git a/lib/matplotlib/backends/web_backend/package.json b/lib/matplotlib/backends/web_backend/package.json index 95bd8fdf54e6..e2a4009a971b 100644 --- a/lib/matplotlib/backends/web_backend/package.json +++ b/lib/matplotlib/backends/web_backend/package.json @@ -11,8 +11,5 @@ "lint:check": "npm run prettier:check && npm run eslint:check", "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json}\"", "prettier:check": "prettier --check \"**/*{.ts,.tsx,.js,.jsx,.css,.json}\"" - }, - "dependencies": { - "@jsxtools/resize-observer": "^1.0.4" } } diff --git a/meson.build b/meson.build index 7d1f3a433fbb..24a09821a047 100644 --- a/meson.build +++ b/meson.build @@ -6,7 +6,6 @@ project( find_program('python3', 'python', version: '>= 3.11'), '-m', 'setuptools_scm', check: true).stdout().strip(), # qt_editor backend is MIT - # ResizeObserver at end of lib/matplotlib/backends/web_backend/js/mpl.js is CC0 # STIX, Computer Modern, and Last Resort are OFL # DejaVu is Bitstream Vera and Public Domain license: 'PSF-2.0 AND MIT AND CC0-1.0 AND OFL-1.1 AND Bitstream-Vera AND Public-Domain', @@ -19,7 +18,6 @@ project( 'LICENSE/LICENSE_COURIERTEN', 'LICENSE/LICENSE_FREETYPE', 'LICENSE/LICENSE_HARFBUZZ', - 'LICENSE/LICENSE_JSXTOOLS_RESIZE_OBSERVER', 'LICENSE/LICENSE_LAST_RESORT_FONT', 'LICENSE/LICENSE_LIBRAQM', 'LICENSE/LICENSE_QT4_EDITOR', diff --git a/tools/embed_js.py b/tools/embed_js.py deleted file mode 100644 index 571bf80238e9..000000000000 --- a/tools/embed_js.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Script to embed JavaScript dependencies in mpl.js. -""" - -from collections import namedtuple -from pathlib import Path -import re -import shutil -import subprocess -import sys - - -Package = namedtuple('Package', [ - # The package to embed, in some form that `npm install` can use. - 'name', - # The path to the source file within the package to embed. - 'source', - # The path to the license file within the package to embed. - 'license']) -# The list of packages to embed, in some form that `npm install` can use. -JAVASCRIPT_PACKAGES = [ - # Polyfill/ponyfill for ResizeObserver. - Package('@jsxtools/resize-observer', 'index.js', 'LICENSE.md'), -] -# This is the magic line that must exist in mpl.js, after which the embedded -# JavaScript will be appended. -MPLJS_MAGIC_HEADER = ( - "///////////////// REMAINING CONTENT GENERATED BY embed_js.py " - "/////////////////\n") - - -def safe_name(name): - """ - Make *name* safe to use as a JavaScript variable name. - """ - return '_'.join(re.split(r'[@/-]', name)).upper() - - -def prep_package(web_backend_path, pkg): - source = web_backend_path / 'node_modules' / pkg.name / pkg.source - license = web_backend_path / 'node_modules' / pkg.name / pkg.license - if not source.exists(): - # Exact version should already be saved in package.json, so we use - # --no-save here. - try: - subprocess.run(['npm', 'install', '--no-save', pkg.name], - cwd=web_backend_path) - except FileNotFoundError as err: - raise ValueError( - f'npm must be installed to fetch {pkg.name}') from err - if not source.exists(): - raise ValueError( - f'{pkg.name} package is missing source in {pkg.source}') - elif not license.exists(): - raise ValueError( - f'{pkg.name} package is missing license in {pkg.license}') - - return source, license - - -def gen_embedded_lines(pkg, source): - name = safe_name(pkg.name) - print('Embedding', source, 'as', name) - yield '// prettier-ignore\n' - for line in source.read_text().splitlines(): - yield (line.replace('module.exports=function', f'var {name}=function') - + ' // eslint-disable-line\n') - - -def build_mpljs(web_backend_path, license_path): - mpljs_path = web_backend_path / "js/mpl.js" - mpljs_orig = mpljs_path.read_text().splitlines(keepends=True) - try: - mpljs_orig = mpljs_orig[:mpljs_orig.index(MPLJS_MAGIC_HEADER) + 1] - except IndexError as err: - raise ValueError( - f'The mpl.js file *must* have the exact line: {MPLJS_MAGIC_HEADER}' - ) from err - - with mpljs_path.open('w') as mpljs: - mpljs.writelines(mpljs_orig) - - for pkg in JAVASCRIPT_PACKAGES: - source, license = prep_package(web_backend_path, pkg) - mpljs.writelines(gen_embedded_lines(pkg, source)) - - shutil.copy(license, - license_path / f'LICENSE{safe_name(pkg.name)}') - - -if __name__ == '__main__': - # Write the mpl.js file. - if len(sys.argv) > 1: - web_backend_path = Path(sys.argv[1]) - else: - web_backend_path = (Path(__file__).parent.parent / - "lib/matplotlib/backends/web_backend") - if len(sys.argv) > 2: - license_path = Path(sys.argv[2]) - else: - license_path = Path(__file__).parent.parent / "LICENSE" - build_mpljs(web_backend_path, license_path) From 2f87b7c7d01adf2c149d585e68fcb2bf2f909b62 Mon Sep 17 00:00:00 2001 From: Mira Sato <275437409+oab24413gmai@users.noreply.github.com> Date: Thu, 14 May 2026 05:47:58 +0000 Subject: [PATCH 75/99] docs: fix duplicated "the" in install/dependencies Signed-off-by: Mira Sato <275437409+oab24413gmai@users.noreply.github.com> --- doc/install/dependencies.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/dependencies.rst b/doc/install/dependencies.rst index 8f638ea5ed1d..578fab93ed5a 100644 --- a/doc/install/dependencies.rst +++ b/doc/install/dependencies.rst @@ -232,7 +232,7 @@ Python ``pip`` normally builds packages using :external+pip:doc:`build isolation `, which means that ``pip`` installs the dependencies listed here for the -duration of the build process. However, build isolation is disabled via the the +duration of the build process. However, build isolation is disabled via the :external+pip:ref:`--no-build-isolation ` flag when :ref:`installing Matplotlib for development `, which means that the dependencies must be explicitly installed, either by :ref:`creating a virtual environment ` From ed29fcececc9f4e2d986c89324f5e24ba985c861 Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Tue, 12 May 2026 15:15:08 -0400 Subject: [PATCH 76/99] Use more class template argument deduction (CTAD) --- src/_image_resample.h | 84 +++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/src/_image_resample.h b/src/_image_resample.h index eaaf2306ae9f..f6b88f5f6461 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -704,19 +704,6 @@ void resample( using input_pixfmt_t = typename type_mapping_t::pixfmt_type; using output_pixfmt_t = typename type_mapping_t::pixfmt_type; - using renderer_t = agg::renderer_base; - using rasterizer_t = agg::rasterizer_scanline_aa; - using scanline_t = agg::scanline32_u8; - - using reflect_t = agg::wrap_mode_reflect; - using image_accessor_wrap_t = agg::image_accessor_wrap; - using image_accessor_clip_t = agg::image_accessor_clip; - - using span_alloc_t = agg::span_allocator; - using span_conv_alpha_t = span_conv_alpha; - - using nn_affine_interpolator_t = accurate_interpolator_affine_nn<>; - using affine_interpolator_t = agg::span_interpolator_linear<>; using arbitrary_interpolator_t = agg::span_interpolator_adaptor, lookup_distortion>; @@ -734,24 +721,25 @@ void resample( params.interpolation = NEAREST; } - span_alloc_t span_alloc; - rasterizer_t rasterizer; - scanline_t scanline; + auto span_alloc = agg::span_allocator{}; + auto rasterizer = agg::rasterizer_scanline_aa{}; + auto scanline = agg::scanline32_u8{}; - span_conv_alpha_t conv_alpha(params.alpha); + auto conv_alpha = span_conv_alpha{params.alpha}; agg::rendering_buffer input_buffer; input_buffer.attach( (unsigned char *)input, in_width, in_height, in_width * itemsize); input_pixfmt_t input_pixfmt(input_buffer); - image_accessor_wrap_t input_accessor_wrap(input_pixfmt); - image_accessor_clip_t input_accessor_clip(input_pixfmt, color_type::no_color()); + auto image_accessor_wrap = + agg::image_accessor_wrap{input_pixfmt}; + auto image_accessor_clip = agg::image_accessor_clip{input_pixfmt, color_type::no_color()}; agg::rendering_buffer output_buffer; output_buffer.attach( (unsigned char *)output, out_width, out_height, out_width * itemsize); output_pixfmt_t output_pixfmt(output_buffer); - renderer_t renderer(output_pixfmt); + auto renderer = agg::renderer_base{output_pixfmt}; agg::trans_affine inverted = params.affine; inverted.invert(); @@ -808,24 +796,24 @@ void resample( if (params.interpolation == NEAREST) { if (params.is_affine) { - using span_gen_t = typename type_mapping_t::template span_gen_nn_type; - using span_conv_t = agg::span_converter; - using nn_renderer_t = agg::renderer_scanline_aa; - nn_affine_interpolator_t interpolator(inverted); - span_gen_t span_gen(input_accessor_clip, interpolator); - span_conv_t span_conv(span_gen, conv_alpha); - nn_renderer_t nn_renderer(renderer, span_alloc, span_conv); + auto interpolator = accurate_interpolator_affine_nn{inverted}; + // C++17 cannot deduce arguments for an alias class template, so define the class explicitly + using span_gen_t = typename type_mapping_t:: + template span_gen_nn_type; + auto span_gen = span_gen_t{image_accessor_clip, interpolator}; + auto span_conv = agg::span_converter{span_gen, conv_alpha}; + auto nn_renderer = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; agg::render_scanlines(rasterizer, scanline, nn_renderer); } else { - using span_gen_t = typename type_mapping_t::template span_gen_nn_type; - using span_conv_t = agg::span_converter; - using nn_renderer_t = agg::renderer_scanline_aa; lookup_distortion dist( params.transform_mesh, in_width, in_height, out_width, out_height, true); - arbitrary_interpolator_t interpolator(inverted, dist); - span_gen_t span_gen(input_accessor_clip, interpolator); - span_conv_t span_conv(span_gen, conv_alpha); - nn_renderer_t nn_renderer(renderer, span_alloc, span_conv); + auto interpolator = arbitrary_interpolator_t{inverted, dist}; + // C++17 cannot deduce arguments for an alias class template, so define the class explicitly + using span_gen_t = typename type_mapping_t:: + template span_gen_nn_type; + auto span_gen = span_gen_t{image_accessor_clip, interpolator}; + auto span_conv = agg::span_converter{span_gen, conv_alpha}; + auto nn_renderer = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; agg::render_scanlines(rasterizer, scanline, nn_renderer); } } else { @@ -833,24 +821,24 @@ void resample( get_filter(params, filter); if (params.is_affine && params.resample) { - using span_gen_t = typename type_mapping_t::template span_gen_affine_type; - using span_conv_t = agg::span_converter; - using int_renderer_t = agg::renderer_scanline_aa; - affine_interpolator_t interpolator(inverted); - span_gen_t span_gen(input_accessor_wrap, interpolator, filter); - span_conv_t span_conv(span_gen, conv_alpha); - int_renderer_t int_renderer(renderer, span_alloc, span_conv); + auto interpolator = agg::span_interpolator_linear{inverted}; + // C++17 cannot deduce arguments for an alias class template, so define the class explicitly + using span_gen_t = typename type_mapping_t:: + template span_gen_affine_type; + auto span_gen = span_gen_t{image_accessor_wrap, interpolator, filter}; + auto span_conv = agg::span_converter{span_gen, conv_alpha}; + auto int_renderer = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; agg::render_scanlines(rasterizer, scanline, int_renderer); } else { - using span_gen_t = typename type_mapping_t::template span_gen_filter_type; - using span_conv_t = agg::span_converter; - using int_renderer_t = agg::renderer_scanline_aa; lookup_distortion dist( params.transform_mesh, in_width, in_height, out_width, out_height, false); - arbitrary_interpolator_t interpolator(inverted, dist); - span_gen_t span_gen(input_accessor_wrap, interpolator, filter); - span_conv_t span_conv(span_gen, conv_alpha); - int_renderer_t int_renderer(renderer, span_alloc, span_conv); + auto interpolator = arbitrary_interpolator_t{inverted, dist}; + // C++17 cannot deduce arguments for an alias class template, so define the class explicitly + using span_gen_t = typename type_mapping_t:: + template span_gen_filter_type; + auto span_gen = span_gen_t{image_accessor_wrap, interpolator, filter}; + auto span_conv = agg::span_converter{span_gen, conv_alpha}; + auto int_renderer = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; agg::render_scanlines(rasterizer, scanline, int_renderer); } } From ccb9d5eace20bddff7261b65347f6aa42b73852a Mon Sep 17 00:00:00 2001 From: "Albert Y. Shih" Date: Thu, 14 May 2026 15:10:23 -0400 Subject: [PATCH 77/99] Reduce code repetition --- src/_image_resample.h | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/_image_resample.h b/src/_image_resample.h index f6b88f5f6461..2c48a080a66d 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -693,6 +693,20 @@ static void get_filter(const resample_params_t ¶ms, } +template +void render_image(renderer_t &renderer, rasterizer_t &rasterizer, span_gen_t &span_gen, double alpha) +{ + auto span_alloc = agg::span_allocator{}; + auto conv_alpha = span_conv_alpha{alpha}; + auto span_conv = agg::span_converter{span_gen, conv_alpha}; + + auto renderer_scanline = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; + + auto scanline = agg::scanline32_u8{}; + agg::render_scanlines(rasterizer, scanline, renderer_scanline); +} + + template void resample( const void *input, int in_width, int in_height, @@ -704,6 +718,7 @@ void resample( using input_pixfmt_t = typename type_mapping_t::pixfmt_type; using output_pixfmt_t = typename type_mapping_t::pixfmt_type; + // Need to define this class explicitly because the first argument cannot be deduced using arbitrary_interpolator_t = agg::span_interpolator_adaptor, lookup_distortion>; @@ -721,12 +736,6 @@ void resample( params.interpolation = NEAREST; } - auto span_alloc = agg::span_allocator{}; - auto rasterizer = agg::rasterizer_scanline_aa{}; - auto scanline = agg::scanline32_u8{}; - - auto conv_alpha = span_conv_alpha{params.alpha}; - agg::rendering_buffer input_buffer; input_buffer.attach( (unsigned char *)input, in_width, in_height, in_width * itemsize); @@ -744,6 +753,7 @@ void resample( agg::trans_affine inverted = params.affine; inverted.invert(); + auto rasterizer = agg::rasterizer_scanline_aa{}; rasterizer.clip_box(0, 0, out_width, out_height); agg::path_storage path; @@ -801,9 +811,7 @@ void resample( using span_gen_t = typename type_mapping_t:: template span_gen_nn_type; auto span_gen = span_gen_t{image_accessor_clip, interpolator}; - auto span_conv = agg::span_converter{span_gen, conv_alpha}; - auto nn_renderer = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; - agg::render_scanlines(rasterizer, scanline, nn_renderer); + render_image(renderer, rasterizer, span_gen, params.alpha); } else { lookup_distortion dist( params.transform_mesh, in_width, in_height, out_width, out_height, true); @@ -812,9 +820,7 @@ void resample( using span_gen_t = typename type_mapping_t:: template span_gen_nn_type; auto span_gen = span_gen_t{image_accessor_clip, interpolator}; - auto span_conv = agg::span_converter{span_gen, conv_alpha}; - auto nn_renderer = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; - agg::render_scanlines(rasterizer, scanline, nn_renderer); + render_image(renderer, rasterizer, span_gen, params.alpha); } } else { agg::image_filter_lut filter; @@ -826,9 +832,7 @@ void resample( using span_gen_t = typename type_mapping_t:: template span_gen_affine_type; auto span_gen = span_gen_t{image_accessor_wrap, interpolator, filter}; - auto span_conv = agg::span_converter{span_gen, conv_alpha}; - auto int_renderer = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; - agg::render_scanlines(rasterizer, scanline, int_renderer); + render_image(renderer, rasterizer, span_gen, params.alpha); } else { lookup_distortion dist( params.transform_mesh, in_width, in_height, out_width, out_height, false); @@ -837,9 +841,7 @@ void resample( using span_gen_t = typename type_mapping_t:: template span_gen_filter_type; auto span_gen = span_gen_t{image_accessor_wrap, interpolator, filter}; - auto span_conv = agg::span_converter{span_gen, conv_alpha}; - auto int_renderer = agg::renderer_scanline_aa{renderer, span_alloc, span_conv}; - agg::render_scanlines(rasterizer, scanline, int_renderer); + render_image(renderer, rasterizer, span_gen, params.alpha); } } } From 7856c816e6ee24bdf15ef08abaa3a98f31bf55af Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 14 May 2026 15:34:45 -0400 Subject: [PATCH 78/99] Backport an additional fix to qhull printf strings Some time after my PR was merged, an additional commit was made to the upstream repository fixing a missed NULL check on an internal error message: https://github.com/qhull/qhull/commit/d1c2fc0caa5f644f3a0f220290d4a868c68ed4f6 I have backported the patch, dropping the changelog, and reverting all the whitespace changes so that the change is minimal. --- subprojects/packagefiles/qhull-143.patch | 95 +++++++++++++++++++----- 1 file changed, 75 insertions(+), 20 deletions(-) diff --git a/subprojects/packagefiles/qhull-143.patch b/subprojects/packagefiles/qhull-143.patch index e37a0d28da91..9819f6dd7cae 100644 --- a/subprojects/packagefiles/qhull-143.patch +++ b/subprojects/packagefiles/qhull-143.patch @@ -1,11 +1,13 @@ -From cd8c281da87d38820ecc4c452bbf6fd921155915 Mon Sep 17 00:00:00 2001 +From 61c21986f4ebd1fb68b615ac89231ad1173f6b84 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 28 Mar 2024 00:54:59 -0400 -Subject: [PATCH 1/3] Annotate printf-like functions with GCC's format +Subject: [PATCH 1/4] Annotate printf-like functions with GCC's format attribute This allows checking format strings when building with `-Wformat` (or with `-Wall`). + +Signed-off-by: Elliott Sales de Andrade --- src/libqhull/libqhull.h | 13 ++++++++++--- src/libqhull_r/libqhull_r.h | 13 ++++++++++--- @@ -13,7 +15,7 @@ with `-Wall`). 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/libqhull/libqhull.h b/src/libqhull/libqhull.h -index 90c0519b..1080ec11 100644 +index 90c0519..1080ec1 100644 --- a/src/libqhull/libqhull.h +++ b/src/libqhull/libqhull.h @@ -60,6 +60,13 @@ @@ -48,7 +50,7 @@ index 90c0519b..1080ec11 100644 /***** -geom.c/geom2.c/random.c prototypes (duplicated from geom.h, random.h) ****************/ diff --git a/src/libqhull_r/libqhull_r.h b/src/libqhull_r/libqhull_r.h -index 023e0181..917f96af 100644 +index 376c1e2..b5185bc 100644 --- a/src/libqhull_r/libqhull_r.h +++ b/src/libqhull_r/libqhull_r.h @@ -48,6 +48,13 @@ @@ -83,7 +85,7 @@ index 023e0181..917f96af 100644 /***** -geom_r.c/geom2_r.c/random_r.c prototypes (duplicated from geom_r.h, random_r.h) ****************/ diff --git a/src/testqset_r/testqset_r.c b/src/testqset_r/testqset_r.c -index 671494f3..b0253e0e 100644 +index 671494f..b0253e0 100644 --- a/src/testqset_r/testqset_r.c +++ b/src/testqset_r/testqset_r.c @@ -117,7 +117,7 @@ int error_count= 0; /* Global error_count. checkSetContents(qh) keeps its own @@ -104,12 +106,16 @@ index 671494f3..b0253e0e 100644 void qh_fprintf(qhT *qh, FILE *fp, int msgcode, const char *fmt, ... ) { static int needs_cr= 0; /* True if qh_fprintf needs a CR. testqset_r is not itself reentrant */ +-- +2.54.0 + -From cc7e366259866d4cd24a312a4aaf891ff0abe85a Mon Sep 17 00:00:00 2001 +From 14f0beeffbfb2505c5b0bf4b8d1b0981025461f5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 28 Mar 2024 01:05:06 -0400 -Subject: [PATCH 2/3] Fix arguments inconsistent with their format strings +Subject: [PATCH 2/4] Fix arguments inconsistent with their format strings +Signed-off-by: Elliott Sales de Andrade --- src/libqhull/global.c | 2 +- src/libqhull/merge.c | 11 +++++------ @@ -123,7 +129,7 @@ Subject: [PATCH 2/3] Fix arguments inconsistent with their format strings 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/libqhull/global.c b/src/libqhull/global.c -index 27babbb4..faf67f37 100644 +index 9a81001..47dc46d 100644 --- a/src/libqhull/global.c +++ b/src/libqhull/global.c @@ -2248,7 +2248,7 @@ void qh_lib_check(int qhullLibraryType, int qhTsize, int vertexTsize, int ridgeT @@ -136,7 +142,7 @@ index 27babbb4..faf67f37 100644 } if (last_errcode) { diff --git a/src/libqhull/merge.c b/src/libqhull/merge.c -index de3a0b00..89392dc6 100644 +index de3a0b0..89392dc 100644 --- a/src/libqhull/merge.c +++ b/src/libqhull/merge.c @@ -427,7 +427,7 @@ void qh_appendmergeset(facetT *facet, facetT *neighbor, mergeType mergetype, coo @@ -186,7 +192,7 @@ index de3a0b00..89392dc6 100644 ridge->mergevertex= True; /* disables check for duplicate vertices in qh_checkfacet */ ridgeA->mergevertex= True; diff --git a/src/libqhull/poly2.c b/src/libqhull/poly2.c -index 0bdfa6d6..9077b9c3 100644 +index 4a207b8..b30a2b6 100644 --- a/src/libqhull/poly2.c +++ b/src/libqhull/poly2.c @@ -1144,7 +1144,7 @@ boolT qh_checklists(facetT *facetlist) { @@ -209,7 +215,7 @@ index 0bdfa6d6..9077b9c3 100644 facet->flipped= False; facet->toporient ^= (unsigned char)True; diff --git a/src/libqhull_r/global_r.c b/src/libqhull_r/global_r.c -index 3a0b9c62..c681a715 100644 +index 04b9b4d..01dfe8e 100644 --- a/src/libqhull_r/global_r.c +++ b/src/libqhull_r/global_r.c @@ -2201,7 +2201,7 @@ void qh_lib_check(int qhullLibraryType, int qhTsize, int vertexTsize, int ridgeT @@ -222,7 +228,7 @@ index 3a0b9c62..c681a715 100644 } if (last_errcode) { diff --git a/src/libqhull_r/mem_r.c b/src/libqhull_r/mem_r.c -index 7d5509eb..d811f733 100644 +index 7d5509e..d811f73 100644 --- a/src/libqhull_r/mem_r.c +++ b/src/libqhull_r/mem_r.c @@ -186,7 +186,7 @@ void qh_memcheck(qhT *qh) { @@ -244,7 +250,7 @@ index 7d5509eb..d811f733 100644 /*-mergevertex= True; /* disables check for duplicate vertices in qh_checkfacet */ ridgeA->mergevertex= True; diff --git a/src/libqhull_r/poly2_r.c b/src/libqhull_r/poly2_r.c -index 01758340..4d9a0c51 100644 +index 1ab5244..a97254e 100644 --- a/src/libqhull_r/poly2_r.c +++ b/src/libqhull_r/poly2_r.c @@ -1145,7 +1145,7 @@ boolT qh_checklists(qhT *qh, facetT *facetlist) { @@ -317,7 +323,7 @@ index 01758340..4d9a0c51 100644 facet->flipped= False; facet->toporient ^= (unsigned char)True; diff --git a/src/libqhullcpp/Qhull.cpp b/src/libqhullcpp/Qhull.cpp -index d5c75e92..3123e8ae 100644 +index d5c75e9..3123e8a 100644 --- a/src/libqhullcpp/Qhull.cpp +++ b/src/libqhullcpp/Qhull.cpp @@ -357,7 +357,7 @@ initializeFeasiblePoint(int hulldim) @@ -330,7 +336,7 @@ index d5c75e92..3123e8ae 100644 } qh_qh->feasible_point= static_cast(qh_malloc(static_cast(hulldim) * sizeof(coordT))); diff --git a/src/testqset_r/testqset_r.c b/src/testqset_r/testqset_r.c -index b0253e0e..5ea87394 100644 +index b0253e0..5ea8739 100644 --- a/src/testqset_r/testqset_r.c +++ b/src/testqset_r/testqset_r.c @@ -532,7 +532,7 @@ void testSetequalInEtc(qhT *qh, int numInts, int *intarray, int checkEvery) @@ -380,19 +386,23 @@ index b0253e0e..5ea87394 100644 error_count++; } } +-- +2.54.0 -From f7c3bbdfd23c034f5af66fa1f067691aac3378d8 Mon Sep 17 00:00:00 2001 + +From 2dda51b2f2ec394462cb95ce37b5afc9701c8446 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 28 Mar 2024 05:11:33 -0400 -Subject: [PATCH 3/3] Don't pass user-defined input as format string +Subject: [PATCH 3/4] Don't pass user-defined input as format string +Signed-off-by: Elliott Sales de Andrade --- src/libqhull/io.c | 2 +- src/libqhull_r/io_r.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libqhull/io.c b/src/libqhull/io.c -index beed156a..7b7f4546 100644 +index beed156..7b7f454 100644 --- a/src/libqhull/io.c +++ b/src/libqhull/io.c @@ -1618,7 +1618,7 @@ void qh_printcenter(FILE *fp, qh_PRINT format, const char *string, facetT *facet @@ -405,7 +415,7 @@ index beed156a..7b7f4546 100644 num= qh hull_dim-1; if (!facet->normal || !facet->upperdelaunay || !qh ATinfinity) { diff --git a/src/libqhull_r/io_r.c b/src/libqhull_r/io_r.c -index a80a5b14..389b1aa6 100644 +index a80a5b1..389b1aa 100644 --- a/src/libqhull_r/io_r.c +++ b/src/libqhull_r/io_r.c @@ -1618,7 +1618,7 @@ void qh_printcenter(qhT *qh, FILE *fp, qh_PRINT format, const char *string, face @@ -417,3 +427,48 @@ index a80a5b14..389b1aa6 100644 if (qh->CENTERtype == qh_ASvoronoi) { num= qh->hull_dim-1; if (!facet->normal || !facet->upperdelaunay || !qh->ATinfinity) { +-- +2.54.0 + + +From b6d5a184cd64d160a50d799ad8b179efeffed121 Mon Sep 17 00:00:00 2001 +From: Brad Barber +Date: Sun, 7 Sep 2025 14:52:49 -0400 +Subject: [PATCH 4/4] poly2.c, poly2_r.c: fixed missing + 'getid_(previousvertex)' for #143 + +Signed-off-by: Elliott Sales de Andrade +--- + src/libqhull/poly2.c | 2 +- + src/libqhull_r/poly2_r.c | 2 +- + 2 files changed, 2 insertions(+), 2 deletions(-) + +diff --git a/src/libqhull/poly2.c b/src/libqhull/poly2.c +index b30a2b6..f70180e 100644 +--- a/src/libqhull/poly2.c ++++ b/src/libqhull/poly2.c +@@ -1144,7 +1144,7 @@ boolT qh_checklists(facetT *facetlist) { + vertex->visitid= qh vertex_visit; + if (vertex->previous != previousvertex) { + qh_fprintf(qh ferr, 6427, "qhull internal error (qh_checklists): expecting v%d.previous == v%d. Got v%d\n", +- vertex->id, previousvertex->id, getid_(vertex->previous)); ++ vertex->id, getid_(previousvertex), getid_(vertex->previous)); + waserror= True; + errorvertex= vertex; + } +diff --git a/src/libqhull_r/poly2_r.c b/src/libqhull_r/poly2_r.c +index a97254e..44110db 100644 +--- a/src/libqhull_r/poly2_r.c ++++ b/src/libqhull_r/poly2_r.c +@@ -1145,7 +1145,7 @@ boolT qh_checklists(qhT *qh, facetT *facetlist) { + vertex->visitid= qh->vertex_visit; + if (vertex->previous != previousvertex) { + qh_fprintf(qh, qh->ferr, 6427, "qhull internal error (qh_checklists): expecting v%d.previous == v%d. Got v%d\n", +- vertex->id, previousvertex->id, getid_(vertex->previous)); ++ vertex->id, getid_(previousvertex), getid_(vertex->previous)); + waserror= True; + errorvertex= vertex; + } +-- +2.54.0 + From dd967666ba85280bc570d6feae1ad82bb6e27df2 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 15 May 2026 00:08:25 +0200 Subject: [PATCH 79/99] DOC: Explain the technical background of autoscaling --- galleries/users_explain/axes/autoscale.py | 154 +++++++++++++++++++++- 1 file changed, 153 insertions(+), 1 deletion(-) diff --git a/galleries/users_explain/axes/autoscale.py b/galleries/users_explain/axes/autoscale.py index ea0c2d24c55a..22cdd1f8dc96 100644 --- a/galleries/users_explain/axes/autoscale.py +++ b/galleries/users_explain/axes/autoscale.py @@ -62,10 +62,13 @@ ax.margins(y=-0.2) # %% +# +# .. _autoscale_sticky_edges: +# # Sticky edges # ------------ # There are plot elements (`.Artist`\s) that are usually used without margins. -# For example false-color images (e.g. created with `.Axes.imshow`) are not +# For example, false-color images (e.g. created with `.Axes.imshow`) are not # considered in the margins calculation. # @@ -166,3 +169,152 @@ ax.autoscale(enable=None, axis="x", tight=True) print(ax.margins()) + +# %% +# Technical background +# -------------------- +# +# This section explains the internal pipeline that runs when autoscaling +# computes axis limits from data. Understanding the mechanics helps when +# you encounter surprising behaviour or need to update limits manually. +# +# Data limits and view limits +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# Matplotlib maintains two sets of limits: +# +# - **Data limits** (`.Axes.dataLim`): the tight bounding box of the raw data. +# - **View limits** (`.Axes.viewLim`): the displayed axis limits. By default, +# computed from the data limits through the autoscaling mechanism outlined +# below, but they can be set independently. View limits can alternatively +# be set explicitly through `~.axes.Axes.set_xlim` / `~.axes.Axes.set_ylim`, +# which also disables autoscaling so that the set limits remain fixed. +# +# The following shows the input and output of this process — ``dataLim`` holds +# the raw data bounds, ``viewLim`` the final displayed axis limits. + + +fig, ax = plt.subplots() +x = np.linspace(-6, 6, 201) +y = np.sin(x) +ax.plot(x, y) +print(f"dataLim x: ({ax.dataLim.x0:.3f}, {ax.dataLim.x1:.3f})") +print(f"dataLim y: ({ax.dataLim.y0:.3f}, {ax.dataLim.y1:.3f})") +print(f"viewLim x: ({ax.viewLim.x0:.3f}, {ax.viewLim.x1:.3f})") +print(f"viewLim y: ({ax.viewLim.y0:.3f}, {ax.viewLim.y1:.3f})") + +# %% +# The x data range is [-6, 6] and the default 5% margin adds roughly 0.6 on +# each side, widening the view to about [-6.6, 6.6]. The same applies to the +# y axis. +# +# Update logic +# ~~~~~~~~~~~~ +# +# Data and view limit updates are handled as separate stages. +# +# **Data limits**: When an artist is added to an Axes through one of the +# plotting methods, the data limits are updated through `.Axes.update_datalim` +# to include the new data. This only ever increases the data limits. It is +# also possible to update `.Axes.dataLim` manually, but this is not common. +# Removal of an artist or change of its data does not trigger any update of +# the data limits, so they can become out of date. In such cases, it is +# necessary to explicitly recompute the data limit through `.Axes.relim`. +# +# **View limits**: When autoscaling is enabled, the view limits are +# automatically computed from the data limit. This update is lazy and only +# triggered when the view limits are queried or drawn, so that they don't have +# to be recomputed for every added artist. This is transparent to the user. +# Explicit changes of the data limits through `.Axes.dataLim` or `.Axes.relim` +# do not trigger an update of the view limits, so they can also become out of +# date. In such cases, it is necessary to explicitly recompute the view limits +# through `.Axes.autoscale_view`. +# +# View limit calculation +# ~~~~~~~~~~~~~~~~~~~~~~ +# +# Given the data limits, the view limits are derived through these steps: +# +# - scale domain clamping +# - margin expansion +# - sticky edge clamping +# - optional limit rounding +# +# Scale domain clamping +# ~~~~~~~~~~~~~~~~~~~~~ +# +# Before margins are applied, the data limits are clipped to the valid domain +# of the axis scale. This matters for scales like log (positive values only) +# and logit (values strictly between 0 and 1): if a bound lies outside the +# domain, it is replaced with a value at the domain boundary. +# +# For this purpose, `.Axes.dataLim` tracks not just the ordinary min/max of +# the data but also ``minpos`` — the smallest strictly positive value seen. +# A log-scale lower bound of zero or less is replaced with ``minpos`` rather +# than the actual minimum, because only positive values can be displayed. +# +# For a logit scale, the upper bound is approximated as ``1 - minpos``, since +# the largest data value below 1 is not tracked separately. This means the +# autoscaled upper limit may include slightly more headroom than necessary +# when the data maximum is well below 1. +# +# Margin expansion +# ~~~~~~~~~~~~~~~~ +# +# The first step is to apply the margins, i.e. widen the view limits beyond the +# data limits so that data is not at the very edge of the plot. Margins are +# specified as a fraction of the data span in screen coordinates so that +# the data-free border area always has the same visual size, irrespective of +# data ranges or axis scales. The margin is applied symmetrically to both sides +# of the data limits, so the view is expanded equally in both directions. +# +# This is illustrated in the following example, where the data limits and +# axis scales are different, but the visual margin is the same in both cases. + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 4)) +fig.suptitle("Margins are visually constant, " + "even with different data limits and axis scales") + +ax1.plot([0, 10], [0, 1]) +ax1.margins(0.2) + +x = np.linspace(1, 20) +ax2.semilogy(x, np.exp(x)) +ax2.margins(0.2) + +# %% +# Sticky edges clamping +# ~~~~~~~~~~~~~~~~~~~~~ +# +# Sticky edges are axis values at which margin expansion is clamped. After +# computing the margin-expanded limits, if an expanded limit would extend +# beyond a sticky edge, it is pulled back to that edge instead. +# +# Artists register sticky edges to prevent blank margins at natural data +# boundaries. `~.Axes.imshow`, for example, registers sticky edges at its +# four pixel boundaries, which is why images fill the Axes by default without +# any surrounding margin (as shown in the :ref:`autoscale_sticky_edges` +# section above). Sticky edges only suppress *outward expansion past the data +# boundary* — they never shrink limits into the data, and negative margins +# are not affected. Setting ``Axes.use_sticky_edges = False`` disables sticky +# edge clamping on that Axes. +# +# Limit rounding +# ~~~~~~~~~~~~~~ +# +# As a final step, the view limits can optionally be expanded outward to the +# nearest "nice" tick position, so that the axis edges coincide with tick +# marks. This is disabled by default, but can be turned on with the +# "round_numbers" mode of :rc:`axes.autolimit_mode`: +# +# - ``'data'`` (default): keep the limits at the margin-expanded values. +# - ``'round_numbers'``: expand the limits outward to the nearest "nice" tick +# position, so the axis edges coincide with tick marks. + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) +ax1.plot([0.3, 4.7], [0.3, 4.7]) +ax1.set_title("autolimit_mode='data' (default)") +with plt.rc_context({'axes.autolimit_mode': 'round_numbers'}): + ax2.plot([0.3, 4.7], [0.3, 4.7]) + ax2.set_title("autolimit_mode='round_numbers'") + ax2.autoscale_view() # force autoscale while round_numbers is active From 81a1e03fba3ab3f6f26767feb32bc4a7c3a8381a Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Fri, 15 May 2026 20:27:49 +0200 Subject: [PATCH 80/99] PERF: Defer tick materialization during Axes init/clear (#31525) * Defer tick materialization during Axes init/clear Co-Authored-By: Claude Opus 4.7 (1M context) * fix * fix * review comments * review comments: docstrings, comment style, spine helper Co-Authored-By: Claude Opus 4.7 (1M context) * extract _rc_context_raw; simplify _LazyTickList.__get__ Co-Authored-By: Claude Opus 4.7 (1M context) * move clip-state propagation onto Tick._configure_for_axis Co-Authored-By: Claude Opus 4.7 (1M context) * review comments part 1 * refactor --------- Co-authored-by: Claude Opus 4.7 (1M context) --- lib/matplotlib/axes/_base.py | 5 ++ lib/matplotlib/axis.py | 148 +++++++++++++++++++++++++++-------- lib/matplotlib/spines.py | 7 ++ 3 files changed, 126 insertions(+), 34 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 0ddf18b12ec2..d2589cfc74d3 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1435,6 +1435,11 @@ def __clear(self): self.xaxis.set_clip_path(self.patch) self.yaxis.set_clip_path(self.patch) + # Lazy tick lists no longer trigger spine transform setup as a + # side effect, so nudge each spine explicitly. + for spine in self.spines.values(): + spine._ensure_transform_is_set() + if self._sharex is not None: self.xaxis.set_visible(xaxis_visible) self.patch.set_visible(patch_visible) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index c526b8a2aa6a..223eb5e7a34f 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -2,6 +2,7 @@ Classes for the ticks and x- and y-axis. """ +import contextlib import datetime import functools import logging @@ -243,6 +244,25 @@ def set_clip_path(self, path, transform=None): self.gridline.set_clip_path(path, transform) self.stale = True + def _configure_for_axis(self, axis, major): + """ + Apply axis-level configuration to a freshly-materialized Tick. + + Used by `_LazyTickList` to apply ``set_tick_params()`` overrides + held on the Axis and to stamp the clip state set via + ``Axis.set_clip_path`` onto the Tick and its gridline. + """ + # Subclasses of Axis (e.g. SkewXAxis in the skewt gallery example) + # may override _get_tick() without forwarding _{major,minor}_tick_kw, + # so apply them here. + tick_kw = axis._major_tick_kw if major else axis._minor_tick_kw + if tick_kw: + self._apply_params(**tick_kw) + for artist in (self, self.gridline): + artist.clipbox = axis.clipbox + artist._clippath = axis._clippath + artist._clipon = axis._clipon + def contains(self, mouseevent): """ Test whether the mouse event occurred in the Tick marks. @@ -536,6 +556,26 @@ def formatter(self, formatter): self._formatter = formatter +@contextlib.contextmanager +def _rc_context_raw(snapshot): + """ + Like ``mpl.rc_context(snapshot)`` but bypasses ``RcParams`` validators + on entry and exit; re-applying a snapshot to its own values must not + re-trigger one-shot validator warnings (e.g. ``toolbar='toolmanager'``). + ``snapshot=None`` is a no-op. + """ + if snapshot is None: + yield + return + rc = mpl.rcParams + orig = dict(rc) + rc._update_raw(snapshot) + try: + yield + finally: + rc._update_raw(orig) + + class _LazyTickList: """ A descriptor for lazy instantiation of tick lists. @@ -548,26 +588,26 @@ def __init__(self, major): self._major = major def __get__(self, instance, owner): + """Materialize the descriptor to a list with one configured tick.""" if instance is None: return self - else: - # instance._get_tick() can itself try to access the majorTicks - # attribute (e.g. in certain projection classes which override - # e.g. get_xaxis_text1_transform). In order to avoid infinite - # recursion, first set the majorTicks on the instance temporarily - # to an empty list. Then create the tick; note that _get_tick() - # may call reset_ticks(). Therefore, the final tick list is - # created and assigned afterwards. - if self._major: - instance.majorTicks = [] - tick = instance._get_tick(major=True) - instance.majorTicks = [tick] - return instance.majorTicks - else: - instance.minorTicks = [] - tick = instance._get_tick(major=False) - instance.minorTicks = [tick] - return instance.minorTicks + # 1. Bind a placeholder so reentrant access via _get_tick() (e.g. + # projections overriding get_xaxis_text1_transform) does not + # recurse back into this descriptor. + # 2. Build the tick under the rcParams snapshot from the last + # Axis.clear() so its sub-artists pick up the right rcParams. + # 3. Apply set_tick_params() overrides and axis state. + # 4. Re-bind the final list; _get_tick() may have called + # reset_ticks(), which pops the attribute, so this assignment + # is what makes future accesses skip the descriptor. + attr = 'majorTicks' if self._major else 'minorTicks' + setattr(instance, attr, ()) # placeholder; not appended to + with _rc_context_raw(instance._tick_rcParams): + tick = instance._get_tick(major=self._major) + tick._configure_for_axis(instance, self._major) + tick_list = [tick] + setattr(instance, attr, tick_list) + return tick_list class Axis(martist.Artist): @@ -672,6 +712,12 @@ def __init__(self, axes, *, pickradius=15, clear=True): # Initialize here for testing; later add API self._major_tick_kw = dict() self._minor_tick_kw = dict() + # Snapshot of rcParams from the last Axis.clear() (or + # set_tick_params(reset=True)); re-applied by _LazyTickList when + # it lazily materializes a Tick. Kept separate from + # _major_tick_kw/_minor_tick_kw, which hold user-provided + # set_tick_params() overrides rather than ambient rcParams. + self._tick_rcParams = None if clear: self.clear() @@ -860,12 +906,14 @@ def _reset_major_tick_kw(self): self._major_tick_kw['gridOn'] = ( mpl.rcParams['axes.grid'] and mpl.rcParams['axes.grid.which'] in ('both', 'major')) + self._tick_rcParams = dict(mpl.rcParams) def _reset_minor_tick_kw(self): self._minor_tick_kw.clear() self._minor_tick_kw['gridOn'] = ( mpl.rcParams['axes.grid'] and mpl.rcParams['axes.grid.which'] in ('both', 'minor')) + self._tick_rcParams = dict(mpl.rcParams) def clear(self): """ @@ -896,6 +944,11 @@ def clear(self): # Clear the callback registry for this axis, or it may "leak" self.callbacks = cbook.CallbackRegistry(signals=["units"]) + # Snapshot current rcParams so that a Tick materialized later by + # _LazyTickList (possibly outside any rc_context() active now) + # sees the same rcParams an eager pre-lazy tick would have. + self._tick_rcParams = dict(mpl.rcParams) + # whether the grids are on self._major_tick_kw['gridOn'] = ( mpl.rcParams['axes.grid'] and @@ -916,19 +969,46 @@ def reset_ticks(self): Each list starts with a single fresh Tick. """ - # Restore the lazy tick lists. - try: - del self.majorTicks - except AttributeError: - pass - try: - del self.minorTicks - except AttributeError: - pass - try: - self.set_clip_path(self.axes.patch) - except AttributeError: - pass + # Drop any materialized tick lists so the _LazyTickList descriptor is + # reactivated on next access. If ticks were already materialized, + # re-apply the axes-patch clip path; otherwise skip. + had_major = bool(self.__dict__.pop('majorTicks', None)) + had_minor = bool(self.__dict__.pop('minorTicks', None)) + if had_major or had_minor: + try: + self.set_clip_path(self.axes.patch) + except AttributeError: + pass + + def _existing_ticks(self, major=None): + """ + Yield already-materialized ticks without triggering the lazy descriptor. + + `majorTicks` and `minorTicks` are `_LazyTickList` descriptors that + create a fresh `.Tick` on first access. Several internal methods + (`set_clip_path`, `set_tick_params`) need to touch every + *already-materialized* tick without forcing materialization, because + doing so would + + (a) create throwaway Tick objects during ``Axes.__init__`` and + ``Axes.__clear`` + (b) risk re-entering the + ``Spine.set_position -> Axis.reset_ticks -> Axis.set_clip_path + -> _LazyTickList.__get__ -> Tick.__init__ -> Spine.set_position`` + cascade. + + Reading the instance ``__dict__`` directly bypasses the descriptor. + + Parameters + ---------- + major : bool, optional + If True, yield only major ticks; if False, only minor ticks; + if None (default), yield major followed by minor. + """ + if major is None or major: + yield from self.__dict__.get('majorTicks', ()) + if major is None or not major: + yield from self.__dict__.get('minorTicks', ()) def minorticks_on(self): """ @@ -997,11 +1077,11 @@ def set_tick_params(self, which='major', reset=False, **kwargs): else: if which in ['major', 'both']: self._major_tick_kw.update(kwtrans) - for tick in self.majorTicks: + for tick in self._existing_ticks(major=True): tick._apply_params(**kwtrans) if which in ['minor', 'both']: self._minor_tick_kw.update(kwtrans) - for tick in self.minorTicks: + for tick in self._existing_ticks(major=False): tick._apply_params(**kwtrans) # labelOn and labelcolor also apply to the offset text. if 'label1On' in kwtrans or 'label2On' in kwtrans: @@ -1140,7 +1220,7 @@ def _translate_tick_params(cls, kw, reverse=False): def set_clip_path(self, path, transform=None): super().set_clip_path(path, transform) - for child in self.majorTicks + self.minorTicks: + for child in self._existing_ticks(): child.set_clip_path(path, transform) self.stale = True diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 741491b3dc58..9aafb8336520 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -206,6 +206,13 @@ def _ensure_position_is_set(self): self._position = ('outward', 0.0) # in points self.set_position(self._position) + def _ensure_transform_is_set(self): + # Install the default blended transform if the spine still carries + # the placeholder from Spine.__init__. No-op for spines whose + # transform was set explicitly (e.g. via set_patch_arc/_circle). + if self._position is None and self._transform is self.axes.transData: + self.set_position(('outward', 0.0)) + def register_axis(self, axis): """ Register an axis. From 9fa7d8b24120fee60d59b0b7917df7ccdba43ff2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 15 May 2026 15:05:26 -0400 Subject: [PATCH 81/99] MNT: Make a note that setuptools-scm can be unpinned This is fine for non-developers and downstream distributors. --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f3c38512a2c9..eef7f82fb810 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,8 @@ requires = [ # you really need it and aren't using an sdist. "meson-python>=0.13.2,!=0.17.*", "pybind11>=2.13.2,!=2.13.3", + # setuptools_scm 10 breaks versioning in editable installs. You can remove this pin + # if you're a downstream distributor just building wheels or your equivalent. "setuptools_scm>=7,<10", ] From 7e280774911f9ba72e1d74cd23901791ffec543c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 May 2026 19:12:47 +0000 Subject: [PATCH 82/99] Bump the actions group with 2 updates Bumps the actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [j178/prek-action](https://github.com/j178/prek-action). Updates `github/codeql-action` from 4.35.4 to 4.35.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba) Updates `j178/prek-action` from 2.0.3 to 2.0.4 - [Release notes](https://github.com/j178/prek-action/releases) - [Commits](https://github.com/j178/prek-action/compare/6ad80277337ad479fe43bd70701c3f7f8aa74db3...bdca6f102f98e2b4c7029491a53dfd366469e33d) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: j178/prek-action dependency-version: 2.0.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/linting.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 71425e9cc3e9..04609e6a868f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 with: languages: ${{ matrix.language }} @@ -45,4 +45,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 0d6e71198817..51593f607653 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 + - uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4 with: extra-args: --hook-stage manual --all-files From 7499f38d2ac097f2d2d1e594dbc3e74360db5aa3 Mon Sep 17 00:00:00 2001 From: jaya prajapati Date: Sat, 16 May 2026 11:47:30 +0530 Subject: [PATCH 83/99] DOC: Clarify SVG hyperlink behavior in gallery hyperlinks example (#31497) * Enable SVG hyperlinks in sgskip example * Address review feedback --- doc/_static/image.svg | 381 ++++++++++++ doc/_static/scatter.svg | 591 +++++++++++++++++++ galleries/examples/misc/hyperlinks_sgskip.py | 10 + 3 files changed, 982 insertions(+) create mode 100644 doc/_static/image.svg create mode 100644 doc/_static/scatter.svg diff --git a/doc/_static/image.svg b/doc/_static/image.svg new file mode 100644 index 000000000000..c101e6aea399 --- /dev/null +++ b/doc/_static/image.svg @@ -0,0 +1,381 @@ + + + + + + + + 2026-04-26T10:16:40.198456 + image/svg+xml + + + Matplotlib v3.11.0.dev2285+ge1329a5eb.d20260419, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/_static/scatter.svg b/doc/_static/scatter.svg new file mode 100644 index 000000000000..6db3c159c093 --- /dev/null +++ b/doc/_static/scatter.svg @@ -0,0 +1,591 @@ + + + + + + + + 2026-04-26T10:16:39.958872 + image/svg+xml + + + Matplotlib v3.11.0.dev2285+ge1329a5eb.d20260419, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/galleries/examples/misc/hyperlinks_sgskip.py b/galleries/examples/misc/hyperlinks_sgskip.py index 26421c941573..ea2870aeae3d 100644 --- a/galleries/examples/misc/hyperlinks_sgskip.py +++ b/galleries/examples/misc/hyperlinks_sgskip.py @@ -20,6 +20,11 @@ s.set_urls(['https://www.bbc.com/news', 'https://www.google.com/', None]) fig.savefig('scatter.svg') +# %% +# .. raw:: html +# +# + # %% fig = plt.figure() @@ -35,3 +40,8 @@ im.set_url('https://www.google.com/') fig.savefig('image.svg') + +# %% +# .. raw:: html +# +# From e81ff913c3c3a5f4bfdf7b34fa2cfed560b78e1d Mon Sep 17 00:00:00 2001 From: Pieter Eendebak Date: Sat, 16 May 2026 11:07:46 +0200 Subject: [PATCH 84/99] Fix Axes.clear() crash for custom spine types PR #31525 made Axes.__clear call Spine._ensure_transform_is_set on every spine, which calls set_position(('outward', 0.0)) for a spine that still carries the placeholder transform from Spine.__init__. Custom spines such as cartopy's GeoSpine have a non-cartesian spine_type, manage their own transform, and may reject set_position, so this raised NotImplementedError on plain subplot creation. Restrict the spine nudge to the four standard cartesian spine types, the only ones set_position / get_spine_transform support. See SciTools/cartopy#2674. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/matplotlib/spines.py | 10 +++++++--- lib/matplotlib/tests/test_spines.py | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 9aafb8336520..35c1879a345c 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -208,9 +208,13 @@ def _ensure_position_is_set(self): def _ensure_transform_is_set(self): # Install the default blended transform if the spine still carries - # the placeholder from Spine.__init__. No-op for spines whose - # transform was set explicitly (e.g. via set_patch_arc/_circle). - if self._position is None and self._transform is self.axes.transData: + # the placeholder from Spine.__init__. Restricted to the standard + # cartesian spines: set_position/get_spine_transform only support + # those, and other spines (polar, cartopy's GeoSpine) manage their + # own transform. + if (self.spine_type in ('left', 'right', 'top', 'bottom') + and self._position is None + and self._transform is self.axes.transData): self.set_position(('outward', 0.0)) def register_axis(self, axis): diff --git a/lib/matplotlib/tests/test_spines.py b/lib/matplotlib/tests/test_spines.py index b652b1f78867..1f0122f27aff 100644 --- a/lib/matplotlib/tests/test_spines.py +++ b/lib/matplotlib/tests/test_spines.py @@ -1,8 +1,9 @@ import numpy as np import pytest +import matplotlib.path as mpath import matplotlib.pyplot as plt -from matplotlib.spines import Spines +from matplotlib.spines import Spine, Spines from matplotlib.testing.decorators import check_figures_equal, image_comparison @@ -197,3 +198,19 @@ def test_spine_set_bounds_with_none(): "left bound should be numeric" assert np.isclose(left_bound[0], ylim[0]), "Lower bound should match original value" assert np.isclose(left_bound[1], ylim[1]), "Upper bound should match original value" + + +def test_clear_with_custom_spine_type(): + # Spines with a non-cartesian spine_type (e.g. cartopy's GeoSpine) manage + # their own transform and may reject set_position(); Axes.clear() must not + # call _ensure_transform_is_set() on them. See SciTools/cartopy#2674. + class NoPositionSpine(Spine): + def __init__(self, axes, **kwargs): + super().__init__(axes, 'geo', mpath.Path(np.empty((0, 2))), **kwargs) + + def set_position(self, position): + raise NotImplementedError('spine does not support set_position') + + fig, ax = plt.subplots() + ax.spines['geo'] = NoPositionSpine(ax) + ax.clear() # must not raise From c6377d0f0009b845947c7eccf10f5a59b7f46ce1 Mon Sep 17 00:00:00 2001 From: Valentin Bruch <46252273+stiglers-eponym@users.noreply.github.com> Date: Sun, 17 May 2026 03:47:39 +0200 Subject: [PATCH 85/99] plt.stairs: fix unit handling for orientation="horizontal" (#31666) --- lib/matplotlib/axes/_axes.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 75dcb4653c52..dd1eb8433888 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -7827,8 +7827,12 @@ def stairs(self, values, edges=None, *, if edges is None: edges = np.arange(len(values) + 1) - edges, values, baseline = self._process_unit_info( - [("x", edges), ("y", values), ("y", baseline)], kwargs) + if orientation == "vertical": + edges, values, baseline = self._process_unit_info( + [("x", edges), ("y", values), ("y", baseline)], kwargs) + else: + edges, values, baseline = self._process_unit_info( + [("y", edges), ("x", values), ("x", baseline)], kwargs) patch = mpatches.StepPatch(values, edges, From 087ffde6769e50b413efb3e6a649ad8abc33e68d Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 17 May 2026 13:45:12 +0200 Subject: [PATCH 86/99] DOC: Remove Tick object details from artist tutorial Closes #31682. Don't explain the object structure and accessor methods to Tick objects and their constituent artists. Users should rarely use them as they are dynamically created and modified. - Remove the complete section on "Tick containers". Users really should access the individual parts through a Tick instance. The technical API still remains accessible in https://matplotlib.org/stable/api/axis_api.html#matplotlib.axis.Tick - Remove all the tick parts getter functions and instead point users to formatters, locators and tick_params. --- galleries/tutorials/artists.py | 110 +++------------------------------ 1 file changed, 10 insertions(+), 100 deletions(-) diff --git a/galleries/tutorials/artists.py b/galleries/tutorials/artists.py index 4f93f7c71a6e..9fc8065f112f 100644 --- a/galleries/tutorials/artists.py +++ b/galleries/tutorials/artists.py @@ -596,40 +596,16 @@ class in the Matplotlib API, and the one you will be working with most # the ticks are placed and how they are represented as strings. # # Each ``Axis`` object contains a :attr:`~matplotlib.axis.Axis.label` attribute -# (this is what :mod:`.pyplot` modifies in calls to `~.pyplot.xlabel` and -# `~.pyplot.ylabel`) as well as a list of major and minor ticks. The ticks are +# (this is what `~.Axes.set_xlabel` / `~.Axes.set_ylabel` modifies internally) +# as well as a list of major and minor ticks. The ticks are # `.axis.XTick` and `.axis.YTick` instances, which contain the actual line and # text primitives that render the ticks and ticklabels. Because the ticks are -# dynamically created as needed (e.g., when panning and zooming), you should -# access the lists of major and minor ticks through their accessor methods -# `.axis.Axis.get_major_ticks` and `.axis.Axis.get_minor_ticks`. Although -# the ticks contain all the primitives and will be covered below, ``Axis`` -# instances have accessor methods that return the tick lines, tick labels, tick -# locations etc.: - -fig, ax = plt.subplots() -axis = ax.xaxis -axis.get_ticklocs() - -# %% - -axis.get_ticklabels() - -# %% -# note there are twice as many ticklines as labels because by default there are -# tick lines at the top and bottom but only tick labels below the xaxis; -# however, this can be customized. - -axis.get_ticklines() - -# %% -# And with the above methods, you only get lists of major ticks back by -# default, but you can also ask for the minor ticks: - -axis.get_ticklabels(minor=True) -axis.get_ticklines(minor=True) - -# %% +# dynamically created and modified as needed (e.g., when panning and zooming), +# directly working on the ticks and their parts (tick lines, tick labels, grid lines) +# is discouraged. Instead, the high-level concepts tick locators, tick formatters +# and style configuration via `~.Axes.tick_params` should be used. See +# :ref:'user_axes_ticks` for details. +# # Here is a summary of some of the useful accessor methods of the ``Axis`` # (these have corresponding setters where useful, such as # :meth:`~matplotlib.axis.Axis.set_major_formatter`.) @@ -640,82 +616,16 @@ class in the Matplotlib API, and the one you will be working with most # `~.Axis.get_scale` The scale of the Axis, e.g., 'log' or 'linear' # `~.Axis.get_view_interval` The interval instance of the Axis view limits # `~.Axis.get_data_interval` The interval instance of the Axis data limits -# `~.Axis.get_gridlines` A list of grid lines for the Axis # `~.Axis.get_label` The Axis label - a `.Text` instance -# `~.Axis.get_offset_text` The Axis offset text - a `.Text` instance -# `~.Axis.get_ticklabels` A list of `.Text` instances - -# keyword minor=True|False -# `~.Axis.get_ticklines` A list of `.Line2D` instances - -# keyword minor=True|False -# `~.Axis.get_ticklocs` A list of Tick locations - -# keyword minor=True|False # `~.Axis.get_major_locator` The `.ticker.Locator` instance for major ticks # `~.Axis.get_major_formatter` The `.ticker.Formatter` instance for major # ticks # `~.Axis.get_minor_locator` The `.ticker.Locator` instance for minor ticks # `~.Axis.get_minor_formatter` The `.ticker.Formatter` instance for minor # ticks -# `~.axis.Axis.get_major_ticks` A list of `.Tick` instances for major ticks -# `~.axis.Axis.get_minor_ticks` A list of `.Tick` instances for minor ticks +# `~.Axis.get_tick_params` Styling of ticks, ticklabels and gridlines # `~.Axis.grid` Turn the grid on or off for the major or minor # ticks # ============================= ============================================== # -# Here is an example, not recommended for its beauty, which customizes -# the Axes and Tick properties. - -# plt.figure creates a matplotlib.figure.Figure instance -fig = plt.figure() -rect = fig.patch # a rectangle instance -rect.set_facecolor('lightgoldenrodyellow') - -ax1 = fig.add_axes((0.1, 0.3, 0.4, 0.4)) -rect = ax1.patch -rect.set_facecolor('lightslategray') - - -for label in ax1.xaxis.get_ticklabels(): - # label is a Text instance - label.set_color('red') - label.set_rotation(45) - label.set_fontsize(16) - -for line in ax1.yaxis.get_ticklines(): - # line is a Line2D instance - line.set_color('green') - line.set_markersize(25) - line.set_markeredgewidth(3) - -plt.show() - -# %% -# .. _tick-container: -# -# Tick containers -# --------------- -# -# The :class:`matplotlib.axis.Tick` is the final container object in our -# descent from the :class:`~matplotlib.figure.Figure` to the -# :class:`~matplotlib.axes.Axes` to the :class:`~matplotlib.axis.Axis` -# to the :class:`~matplotlib.axis.Tick`. The ``Tick`` contains the tick -# and grid line instances, as well as the label instances for the upper -# and lower ticks. Each of these is accessible directly as an attribute -# of the ``Tick``. -# -# ============== ========================================================== -# Tick attribute Description -# ============== ========================================================== -# tick1line A `.Line2D` instance -# tick2line A `.Line2D` instance -# gridline A `.Line2D` instance -# label1 A `.Text` instance -# label2 A `.Text` instance -# ============== ========================================================== -# -# Here is an example which sets the formatter for the right side ticks with -# dollar signs and colors them green on the right side of the yaxis. -# -# -# .. include:: ../gallery/ticks/dollar_ticks.rst -# :start-after: .. redirect-from:: /gallery/pyplots/dollar_ticks -# :end-before: .. admonition:: References +# The full Axis API can be found at :doc:`/api/axis_api`. From 9792ba319b6a1e1ec84693a5cb41b8da45f5e13e Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 17 May 2026 20:30:50 +0100 Subject: [PATCH 87/99] DOC: correct some outdated points in Artist tutorial --- galleries/tutorials/artists.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/galleries/tutorials/artists.py b/galleries/tutorials/artists.py index 4f93f7c71a6e..44e6da9fe4bd 100644 --- a/galleries/tutorials/artists.py +++ b/galleries/tutorials/artists.py @@ -38,12 +38,9 @@ helper methods to create the primitives. In the example below, we create a ``Figure`` instance using :func:`matplotlib.pyplot.figure`, which is a convenience method for instantiating ``Figure`` instances and connecting them -with your user interface or drawing toolkit ``FigureCanvas``. As we will -discuss below, this is not necessary -- you can work directly with PostScript, -PDF Gtk+, or wxPython ``FigureCanvas`` instances, instantiate your ``Figures`` -directly and connect them yourselves -- but since we are focusing here on the -``Artist`` API we'll let :mod:`~matplotlib.pyplot` handle some of those details -for us:: +with your user interface. This is not always necessary -- you can instantiate +the ``Figure`` instance directly if you do not require the +`matplotlib.pyplot.show` functionality:: import matplotlib.pyplot as plt fig = plt.figure() @@ -94,9 +91,8 @@ class in the Matplotlib API, and the one you will be working with most In [102]: line Out[102]: -If you make subsequent calls to ``ax.plot`` (and the hold state is "on" -which is the default) then additional lines will be added to the list. -You can remove a line later by calling its ``remove`` method:: +If you make subsequent calls to ``ax.plot`` then additional lines will be added +to the list. You can remove a line later by calling its ``remove`` method:: line = ax.lines[0] line.remove() @@ -301,7 +297,7 @@ class in the Matplotlib API, and the one you will be working with most # Out[159]: # # In [160]: print(fig.axes) -# [, ] +# [, ] # # Because the figure maintains the concept of the "current Axes" (see # :meth:`Figure.gca ` and From 69be02c7a42281959da27730ba665eefb78df629 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 17 May 2026 21:56:50 +0200 Subject: [PATCH 88/99] Update galleries/tutorials/artists.py Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- galleries/tutorials/artists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/galleries/tutorials/artists.py b/galleries/tutorials/artists.py index 9fc8065f112f..734805bf7e19 100644 --- a/galleries/tutorials/artists.py +++ b/galleries/tutorials/artists.py @@ -604,7 +604,7 @@ class in the Matplotlib API, and the one you will be working with most # directly working on the ticks and their parts (tick lines, tick labels, grid lines) # is discouraged. Instead, the high-level concepts tick locators, tick formatters # and style configuration via `~.Axes.tick_params` should be used. See -# :ref:'user_axes_ticks` for details. +# :ref:`user_axes_ticks` for details. # # Here is a summary of some of the useful accessor methods of the ``Axis`` # (these have corresponding setters where useful, such as From d96a9056bd7ddfd23bf1a73d743cc36d33b28019 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 17 May 2026 21:52:13 +0100 Subject: [PATCH 89/99] Apply suggestion from @timhoffm Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- galleries/tutorials/artists.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/galleries/tutorials/artists.py b/galleries/tutorials/artists.py index 44e6da9fe4bd..557ded9f1b14 100644 --- a/galleries/tutorials/artists.py +++ b/galleries/tutorials/artists.py @@ -38,9 +38,7 @@ helper methods to create the primitives. In the example below, we create a ``Figure`` instance using :func:`matplotlib.pyplot.figure`, which is a convenience method for instantiating ``Figure`` instances and connecting them -with your user interface. This is not always necessary -- you can instantiate -the ``Figure`` instance directly if you do not require the -`matplotlib.pyplot.show` functionality:: +with a GUI framework so that they can be shown in a window on the screen:: import matplotlib.pyplot as plt fig = plt.figure() From 5ffcca9358ec645f64dbfc15234d19464620b1ff Mon Sep 17 00:00:00 2001 From: tinezivic <43860297+tinezivic@users.noreply.github.com> Date: Tue, 19 May 2026 09:25:26 +0200 Subject: [PATCH 90/99] ENH: Add PolarAxes.get_rlim() and get_thetalim() for API symmetry (#31695) Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- .../polar_get_rlim_thetalim.rst | 15 ++++++++++ lib/matplotlib/projections/polar.py | 30 +++++++++++++++++++ lib/matplotlib/projections/polar.pyi | 2 ++ lib/matplotlib/tests/test_polar.py | 23 ++++++++++++++ 4 files changed, 70 insertions(+) create mode 100644 doc/release/next_whats_new/polar_get_rlim_thetalim.rst diff --git a/doc/release/next_whats_new/polar_get_rlim_thetalim.rst b/doc/release/next_whats_new/polar_get_rlim_thetalim.rst new file mode 100644 index 000000000000..57586d2a32ce --- /dev/null +++ b/doc/release/next_whats_new/polar_get_rlim_thetalim.rst @@ -0,0 +1,15 @@ +``PolarAxes.get_rlim()`` and ``get_thetalim()`` added +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:class:`~matplotlib.projections.polar.PolarAxes` now provides +`~matplotlib.projections.polar.PolarAxes.get_rlim` and +`~matplotlib.projections.polar.PolarAxes.get_thetalim` to complement the +existing `~matplotlib.projections.polar.PolarAxes.set_rlim` and +`~matplotlib.projections.polar.PolarAxes.set_thetalim`. Previously, one +had to use `.Axes.get_ylim`, `.Axes.get_xlim` as a workaround. + +:: + + ax = plt.subplot(projection="polar") + ax.set_rlim(1, 5) + rmin, rmax = ax.get_rlim() # was: AttributeError diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 9d999dde2f6f..6908e9b45b65 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -1069,6 +1069,21 @@ def set_thetalim(self, *args, **kwargs): raise ValueError("The angle range must be less than a full circle") return tuple(np.rad2deg((new_min, new_max))) + def get_thetalim(self): + """ + Get the minimum and maximum theta values. + + Returns + ------- + thetamin, thetamax : float + The minimum and maximum theta limit values in degrees. + + See Also + -------- + set_thetalim + """ + return tuple(np.rad2deg(self.get_xlim())) + def set_theta_offset(self, offset): """ Set the offset for the location of 0 in radians. @@ -1228,6 +1243,21 @@ def set_rlim(self, bottom=None, top=None, *, return self.set_ylim(bottom=bottom, top=top, emit=emit, auto=auto, **kwargs) + def get_rlim(self): + """ + Get the radial axis view limits. + + Returns + ------- + bottom, top : float + The lower and upper radial axis limits. + + See Also + -------- + set_rlim + """ + return self.get_ylim() + def get_rlabel_position(self): """ Returns diff --git a/lib/matplotlib/projections/polar.pyi b/lib/matplotlib/projections/polar.pyi index de1cbc293900..b3f18587c237 100644 --- a/lib/matplotlib/projections/polar.pyi +++ b/lib/matplotlib/projections/polar.pyi @@ -141,6 +141,7 @@ class PolarAxes(Axes): def set_thetalim(self, minval: float, maxval: float, /) -> tuple[float, float]: ... @overload def set_thetalim(self, *, thetamin: float, thetamax: float) -> tuple[float, float]: ... + def get_thetalim(self) -> tuple[float, float]: ... def set_theta_offset(self, offset: float) -> None: ... def get_theta_offset(self) -> float: ... def set_theta_zero_location( @@ -169,6 +170,7 @@ class PolarAxes(Axes): auto: bool = ..., **kwargs, ) -> tuple[float, float]: ... + def get_rlim(self) -> tuple[float, float]: ... def get_rlabel_position(self) -> float: ... def set_rlabel_position(self, value: float) -> None: ... def set_rscale(self, *args, **kwargs) -> None: ... diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index 6bb534b96f25..3fbf9aea16d5 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -398,6 +398,29 @@ def test_axvspan(): assert span.get_path()._interpolation_steps > 1 +def test_polar_get_rlim(): + # PolarAxes.get_rlim() should mirror set_rlim() + ax = plt.figure().add_subplot(projection='polar') + ax.set_rlim(1.5, 8.0) + assert ax.get_rlim() == (1.5, 8.0) + + +def test_polar_get_rlim_after_plot(): + # get_rlim() should work after autoscaling via plot() + ax = plt.figure().add_subplot(projection='polar') + theta = np.linspace(0, 2 * np.pi, 10) + ax.plot(theta, np.ones(10) * 5.0) + rmin, rmax = ax.get_rlim() + assert rmax >= 5.0 + + +def test_polar_get_thetalim(): + # PolarAxes.get_thetalim() should mirror set_thetalim() + ax = plt.figure().add_subplot(projection='polar') + ax.set_thetalim(thetamin=30, thetamax=90) + assert_allclose(ax.get_thetalim(), (30, 90)) + + @check_figures_equal() def test_remove_shared_polar(fig_ref, fig_test): # Removing shared polar axes used to crash. Test removing them, keeping in From 6cde20074263d3a8a45442d6ae6a3cc21f672ea0 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 19 May 2026 11:57:25 +0200 Subject: [PATCH 91/99] DOC: Remove pyplot text example Motivation: This is part of the general approach to consolidate examples. Why we don't need the pyplot text example: - We don't have to explicitly discuss mathtext for the case of pyplot - Title and axkis labels are better described already in the basic plot example - `plt.text()` is not so common as to justify a specific pyplot example. As with most other functions it's sufficient to have examples for the Axes interface (which for text are in the section "Text, labels and annoations") --- galleries/examples/pyplots/pyplot_simple.py | 28 ++++++++++---- galleries/examples/pyplots/pyplot_text.py | 41 --------------------- 2 files changed, 20 insertions(+), 49 deletions(-) delete mode 100644 galleries/examples/pyplots/pyplot_text.py diff --git a/galleries/examples/pyplots/pyplot_simple.py b/galleries/examples/pyplots/pyplot_simple.py index 48a862c7fee3..8da1e346c296 100644 --- a/galleries/examples/pyplots/pyplot_simple.py +++ b/galleries/examples/pyplots/pyplot_simple.py @@ -1,20 +1,30 @@ """ -=========== -Simple plot -=========== +========== +Basic plot +========== -A simple plot where a list of numbers are plotted against their index, -resulting in a straight line. Use a format string (here, 'o-r') to set the -markers (circles), linestyle (solid line) and color (red). +A basic plot using the :ref:`pyplot_interface`. + +- `~.pyplot.plot` plots the data y versus x as lines and/or markers. +- `~.pyplot.title`, `~.pyplot.xlabel` and `~.pyplot.ylabel` set the title, + x-axis label and y-axis label. +- `~.pyplot.show` displays the plot. .. redirect-from:: /gallery/pyplots/fig_axes_labels_simple .. redirect-from:: /gallery/pyplots/pyplot_formatstr +.. redirect-from:: /gallery/pyplots/pyplot_text """ import matplotlib.pyplot as plt +import numpy as np + +x = np.arange(0.0, 2.0, 0.01) +y = np.sin(2 * np.pi * x) -plt.plot([1, 2, 3, 4], 'o-r') -plt.ylabel('some numbers') +plt.plot(x, y) +plt.title("A basic plot using pyplot") +plt.xlabel('Time [s]') +plt.ylabel('Voltage [mV]') plt.show() # %% @@ -25,5 +35,7 @@ # in this example: # # - `matplotlib.pyplot.plot` +# - `matplotlib.pyplot.title` +# - `matplotlib.pyplot.ylabel` # - `matplotlib.pyplot.ylabel` # - `matplotlib.pyplot.show` diff --git a/galleries/examples/pyplots/pyplot_text.py b/galleries/examples/pyplots/pyplot_text.py deleted file mode 100644 index 72f977c2f985..000000000000 --- a/galleries/examples/pyplots/pyplot_text.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -============================== -Text and mathtext using pyplot -============================== - -Set the special text objects `~.pyplot.title`, `~.pyplot.xlabel`, and -`~.pyplot.ylabel` through the dedicated pyplot functions. Additional text -objects can be placed in the Axes using `~.pyplot.text`. - -You can use TeX-like mathematical typesetting in all texts; see also -:ref:`mathtext`. - -.. redirect-from:: /gallery/pyplots/pyplot_mathtext -""" - -import matplotlib.pyplot as plt -import numpy as np - -t = np.arange(0.0, 2.0, 0.01) -s = np.sin(2*np.pi*t) - -plt.plot(t, s) -plt.text(0, -1, r'Hello, world!', fontsize=15) -plt.title(r'$\mathcal{A}\sin(\omega t)$', fontsize=20) -plt.xlabel('Time [s]') -plt.ylabel('Voltage [mV]') -plt.show() - -# %% -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.pyplot.hist` -# - `matplotlib.pyplot.xlabel` -# - `matplotlib.pyplot.ylabel` -# - `matplotlib.pyplot.text` -# - `matplotlib.pyplot.grid` -# - `matplotlib.pyplot.show` From 672aa488544fb8970c9c7d78cf7034e51869b16f Mon Sep 17 00:00:00 2001 From: Gabriel Silva Date: Tue, 19 May 2026 17:39:52 +0100 Subject: [PATCH 92/99] DOC: Correct test path in boilerplate.py docstring --- tools/boilerplate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/boilerplate.py b/tools/boilerplate.py index c312929b67a2..2ffe86080143 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -11,7 +11,7 @@ of Figure and Axes. Whenever the API of one of the wrapped methods changes, this script has to be rerun to keep pyplot.py up to date. -The test ``lib/matplotlib/test_pyplot.py::test_pyplot_up_to_date`` checks +The test ``lib/matplotlib/tests/test_pyplot.py::test_pyplot_up_to_date`` checks that the autogenerated part of pyplot.py is up to date. It will fail in the case of an API mismatch and remind the developer to rerun this script. """ From 6fc102094ec456148ebe13d869873950277ac55e Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 19 May 2026 12:15:12 +0200 Subject: [PATCH 93/99] DOC: Remove "Multiple lines using pyplot" We should not promote the specific `plot(x, y, fmt, x2, y2, fmt)` anymore. This is a Matlabism and hard to read and reason about. Separate `plot()` calls are the better option. They don't need a dedicated pyplot example. Therefore, a redirect to the pyplot subplot example is sufficient, which as a side-topic has two plot calls in one subplot. Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- galleries/examples/pyplots/pyplot_three.py | 26 ------------------- .../examples/pyplots/pyplot_two_subplots.py | 15 ++++++++++- 2 files changed, 14 insertions(+), 27 deletions(-) delete mode 100644 galleries/examples/pyplots/pyplot_three.py diff --git a/galleries/examples/pyplots/pyplot_three.py b/galleries/examples/pyplots/pyplot_three.py deleted file mode 100644 index b14998cca4c9..000000000000 --- a/galleries/examples/pyplots/pyplot_three.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -=========================== -Multiple lines using pyplot -=========================== - -Plot three datasets with a single call to `~matplotlib.pyplot.plot`. -""" - -import matplotlib.pyplot as plt -import numpy as np - -# evenly sampled time at 200ms intervals -t = np.arange(0., 5., 0.2) - -# red dashes, blue squares and green triangles -plt.plot(t, t, 'r--', t, t**2, 'bs', t, t**3, 'g^') -plt.show() - -# %% -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` diff --git a/galleries/examples/pyplots/pyplot_two_subplots.py b/galleries/examples/pyplots/pyplot_two_subplots.py index 2eb0237d5521..b532c7b1534d 100644 --- a/galleries/examples/pyplots/pyplot_two_subplots.py +++ b/galleries/examples/pyplots/pyplot_two_subplots.py @@ -3,7 +3,18 @@ Two subplots using pyplot ========================= -Create a figure with two subplots using `.pyplot.subplot`. +A typical pyplot usage pattern is to create subplots incrementally through +`~.pyplot.subplot`. + +The three-digit number passed to `~.pyplot.subplot` specifies the position of +the subplot in the grid of subplots. ``211`` means "in a grid of 2 rows and 1 column, +create this subplot in the 1st position". ``212`` likewise means "in a grid of 2 +rows and 1 column, create this subplot in the 2nd position". + +After calling ``subplot()`` all following pyplot commands will modify that subplot +until a new subplot is created. + +.. redirect-from:: /gallery/pyplots/pyplot_three """ import matplotlib.pyplot as plt @@ -21,9 +32,11 @@ def f(t): plt.subplot(211) plt.plot(t1, f(t1), color='tab:blue', marker='o') plt.plot(t2, f(t2), color='black') +plt.title("Subplot 1") plt.subplot(212) plt.plot(t2, np.cos(2*np.pi*t2), color='tab:orange', linestyle='--') +plt.title("Subplot 2") plt.show() # %% From 319240d3ae3059aec46cc0124782097ba2893a06 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 21 May 2026 11:19:06 +0200 Subject: [PATCH 94/99] Improve some example titles. date_demo_convert and fig_axes_customize_simple should actually be likely candidates for deletion or maybe merging with some other example, but in the meantime, let's give them more sensible titles. --- .../ticks/colorbar_tick_labelling_demo.py | 2 +- galleries/examples/ticks/date_demo_convert.py | 15 +++++++-------- .../examples/ticks/fig_axes_customize_simple.py | 6 +++--- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/galleries/examples/ticks/colorbar_tick_labelling_demo.py b/galleries/examples/ticks/colorbar_tick_labelling_demo.py index 6436748a46ec..30d2dbe274ce 100644 --- a/galleries/examples/ticks/colorbar_tick_labelling_demo.py +++ b/galleries/examples/ticks/colorbar_tick_labelling_demo.py @@ -1,6 +1,6 @@ """ ======================= -Colorbar Tick Labelling +Colorbar Tick labelling ======================= Vertical colorbars have ticks, tick labels, and labels visible on the *y* axis, diff --git a/galleries/examples/ticks/date_demo_convert.py b/galleries/examples/ticks/date_demo_convert.py index c22edf54df9a..a3c7a25b5fc0 100644 --- a/galleries/examples/ticks/date_demo_convert.py +++ b/galleries/examples/ticks/date_demo_convert.py @@ -1,9 +1,9 @@ """ -================= -Date Demo Convert -================= - +=================== +Date converter demo +=================== """ + import datetime import matplotlib.pyplot as plt @@ -21,14 +21,13 @@ fig, ax = plt.subplots() ax.plot(dates, y**2, 'o') -# this is superfluous, since the autoscaler should get it right, but +# This is superfluous, since the autoscaler should get it right, but # use date2num and num2date to convert between dates and floats if -# you want; both date2num and num2date convert an instance or sequence +# you want; both date2num and num2date convert an instance or sequence. ax.set_xlim(dates[0], dates[-1]) # The hour locator takes the hour or sequence of hours you want to -# tick, not the base multiple - +# tick, not the base multiple. ax.xaxis.set_major_locator(DayLocator()) ax.xaxis.set_minor_locator(HourLocator(range(0, 25, 6))) ax.xaxis.set_major_formatter(DateFormatter('%Y-%m-%d')) diff --git a/galleries/examples/ticks/fig_axes_customize_simple.py b/galleries/examples/ticks/fig_axes_customize_simple.py index 07a569e3d31d..72c36c7a96cc 100644 --- a/galleries/examples/ticks/fig_axes_customize_simple.py +++ b/galleries/examples/ticks/fig_axes_customize_simple.py @@ -1,7 +1,7 @@ """ -========================= -Fig Axes Customize Simple -========================= +====================================== +Customizing figure and axes appearance +====================================== Customize the background, labels and ticks of a simple plot. From 3f4e85fdf62feb0ee240c83b03e75f134083976f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Weber=20Mendon=C3=A7a?= Date: Thu, 21 May 2026 17:07:02 -0300 Subject: [PATCH 95/99] Pin first-contribution action Workaround for https://github.com/plbstl/first-contribution/issues/106 --- .github/workflows/pr_welcome.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_welcome.yml b/.github/workflows/pr_welcome.yml index 48691e61d87b..70388a3507b1 100644 --- a/.github/workflows/pr_welcome.yml +++ b/.github/workflows/pr_welcome.yml @@ -16,7 +16,7 @@ jobs: issues: write pull-requests: write steps: - - uses: plbstl/first-contribution@7c31f41b0e7a70adfcae06cf964679f61af6780b # v4.3.0 + - uses: plbstl/first-contribution@4fb1541ce2706255850d56c5684552607be1ae9b # v4.2.0 with: labels: first-contribution pr-opened-msg: >+ From 4a6e12681d6bbe3b066f0363e281324352ed1469 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 21 May 2026 11:29:36 +0200 Subject: [PATCH 96/99] Log import failure tracebacks during backend autodetection fallback. This should help troubleshooting backend autodetection issues. --- lib/matplotlib/pyplot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 57f5ac08e398..c94045621b64 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -429,6 +429,8 @@ def switch_backend(newbackend: str) -> None: try: switch_backend(candidate) except ImportError: + _log.debug("Skipping backend candidate %r as loading failed.", + candidate, exc_info=True) continue else: rcParamsOrig['backend'] = candidate From 4320f31a1a949c4aba41269d7a6ca8cf5f7c9594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Weber=20Mendon=C3=A7a?= Date: Fri, 22 May 2026 08:57:54 -0300 Subject: [PATCH 97/99] DOC: Update triage team nomination instructions (#31089) * DOC: Update triage team nomination instructions * DOC: More updates to triage triage doc * DOC: Try to fix linting issue in triage.rst * Address review comments * Update nomination instructions and categories * Add @ to staff groups * Add nomination steps and follow-up for objections * Lint * Add triager to discourse group --- doc/devel/triage.rst | 161 ++++++++++++++++++++++++++----------------- 1 file changed, 98 insertions(+), 63 deletions(-) diff --git a/doc/devel/triage.rst b/doc/devel/triage.rst index de27afcab111..f151fa1faf64 100644 --- a/doc/devel/triage.rst +++ b/doc/devel/triage.rst @@ -30,35 +30,29 @@ are not part of the Matplotlib organization do not have `permissions to change milestones, add labels, or close issue `_. -If you do not have enough GitHub permissions do something (e.g. add a -label, close an issue), please leave a comment with your -recommendations! +If you do not have enough GitHub permissions to do something (e.g. add a +label, close an issue), please leave a comment with your recommendations! The following actions are typically useful: -- documenting issues that are missing elements to reproduce the problem - such as code samples - -- suggesting better use of code formatting (e.g. triple back ticks in the - markdown). - -- suggesting to reformulate the title and description to make them more - explicit about the problem to be solved - -- linking to related issues or discussions while briefly describing +* documenting issues that are missing elements to reproduce the problem, + such as code samples; +* suggesting better use of code formatting (e.g. triple back ticks in the + markdown); +* suggesting to reformulate the title and description to make them more + explicit about the problem to be solved; +* linking to related issues or discussions while briefly describing how they are related, for instance "See also #xyz for a similar attempt at this" or "See also #xyz where the same thing was - reported" provides context and helps the discussion - -- verifying that the issue is reproducible - -- classify the issue as a feature request, a long standing bug or a - regression + reported", which provides context and helps the discussion; +* verifying that the issue is reproducible; +* classifying the issue as a feature request, a long standing bug or a + regression. .. topic:: Fruitful discussions - Online discussions may be harder than it seems at first glance, in - particular given that a person new to open-source may have a very + Online discussions may be harder than they seem at first glance, in + particular given that a person new to open source may have a very different understanding of the process than a seasoned maintainer. Overall, it is useful to stay positive and assume good will. `The @@ -73,31 +67,26 @@ Maintainers and triage team members In addition to the above, maintainers and the triage team can do the following important tasks: -- Update labels for issues and PRs: see the list of `available GitHub +* Update labels for issues and PRs: see the list of `available GitHub labels `_. +* Triage issues: -- Triage issues: - - - **reproduce the issue**, if the posted code is a bug label the issue - with "status: confirmed bug". - - - **identify regressions**, determine if the reported bug used to + * **reproduce the issue**, and if the posted code is a bug label the issue + with `status: confirmed bug `_. + * **identify regressions**, determine if the reported bug used to work as expected in a recent version of Matplotlib and if so determine the last working version. Regressions should be milestoned for the next bug-fix release and may be labeled as "Release critical". - - - **close usage questions** and politely point the reporter to use - `discourse `_ or Stack Overflow - instead and label as "community support". - - - **close duplicate issues**, after checking that they are + * **close duplicate issues**, after checking that they are indeed duplicate. Ideally, the original submitter moves the - discussion to the older, duplicate issue - - - **close issues that cannot be replicated**, after leaving time (at - least a week) to add extra information - + discussion to the older, duplicate issue. + * **close issues that cannot be replicated**, after leaving time (at + least a week) to add extra information. + * **invite contributors to engage with the community** if the issue requires + more information or discussion. These discussions can take place in the + `weekly community meetings `__, or + on `discourse `__. .. topic:: Closing issues: a tough call @@ -107,13 +96,6 @@ important tasks: question or has been considered as unclear for many years, then it should be closed. -Prepare PRs for review -====================== - -Reviewing code is also encouraged. Contributors and users are welcome to -participate to the review process following our :ref:`review guidelines -`. - .. _triage_workflow: Triage workflow @@ -127,13 +109,19 @@ The following workflow is a good way to approach issue triaging: Matplotlib project itself, beyond just using the library. As such, we want it to be a welcoming, pleasant experience. -#. Is this a usage question? If so close it with a polite message. +#. Is this a usage question? + + If so, close it with a polite message, point the reporter to use + `discourse `__ or Stack Overflow instead + and use the + `community support `__ + label, if you have the necessary permissions. #. Is the necessary information provided? Check that the poster has filled in the issue template. If crucial information (the version of Python, the version of Matplotlib used, - the OS, and the backend), is missing politely ask the original + the OS, and the backend) is missing, politely ask the original poster to provide the information. #. Is the issue minimal and reproducible? @@ -154,7 +142,7 @@ The following workflow is a good way to approach issue triaging: OS, Python, and Matplotlib versions. If we need more information from either this or the previous step - please label the issue with "status: needs clarification". + please label the issue with `status: needs clarification `_. #. Is this a regression? @@ -169,7 +157,6 @@ The following workflow is a good way to approach issue triaging: `_ to find the first commit where it was broken. - #. Is this a duplicate issue? We have many open issues. If a new issue seems to be a duplicate, @@ -182,32 +169,69 @@ The following workflow is a good way to approach issue triaging: slightly different example, add it to the original issue as a comment or an edit to the original post. - Label the closed issue with "status: duplicate" + Label the closed issue with `status: duplicate `__. #. Make sure that the title accurately reflects the issue. If you have the necessary permissions edit it yourself if it's not clear. -#. Add the relevant labels, such as "Documentation" when the issue is - about documentation, "Bug" if it is clearly a bug, "New feature" if it - is a new feature request, ... +#. Add the relevant labels, such as `Documentation `__ + when the issue is about documentation, `status: confirmed bug `__ + if it is clearly a bug, `New feature `__ + if it is a new feature request, etc. + + An additional useful step can be to tag with the relevant "topic: ..." label, + e.g. "topic: widgets/UI" or "topic: animation". + + Take some time to familiarize yourself with the available labels and their + meaning, and try to use them consistently. + +.. topic:: Good first issues - If the issue is clearly defined and the fix seems relatively - straightforward, label the issue as “Good first issue” (and - possibly a description of the fix or a hint as to where in the - code base to look to get started). + If the issue is clearly defined, the fix seems relatively straightforward, + and there is consensus on what the solution is among maintainers, label the + issue as + `Good first issue `_ + (and possibly a description of the fix or a hint as to where in the + code base to look to get started). - An additional useful step can be to tag the corresponding module e.g. - the "GUI/Qt" label when relevant. + Note that good first issues are intended to onboard newcomers with a genuine + interest in improving Matplotlib, in the hopes that they will continue to + participate in our development community; therefore, the use of AI tools to + resolve these issues is not appropriate. + +Preparing PRs for review +======================== + +Doing initial reviews of contributions is also encouraged. Contributors and +users are welcome to participate to the review process following our +:ref:`review guidelines `. In particular, if you identify a PR +that needs maintainer attention, you can add the +`status: needs review `_ +label to it, or add it to the next community meeting agenda for discussion. You +can: + +* Suggest fixes to CI check failures, such as failing tests or documentation + builds; +* Help with :ref:`rebasing instructions `; +* Suggest improvements to the PR description, including filling out the AI + Disclosure section if it is missing. + +AI-generated contributions +-------------------------- + +Make sure PRs comply with our :ref:`AI policy `. If you identify +a PR that does not comply with the policy, ask the contributor to clarify the AI +tools used and the contribution of the author, and to update the PR description +accordingly to comply with our AI policy. .. _triage_team: Triage team =========== - If you would like to join the triage team: -1. Correctly triage 2-3 issues. +1. Correctly triage 2-3 issues or review 2-3 pull requests, as described above. 2. Ask someone on in the Matplotlib organization (publicly or privately) to recommend you to the triage team (look for "Member" on the top-right of comments on GitHub). If you worked with someone on the issues triaged, they @@ -215,4 +239,15 @@ If you would like to join the triage team: 3. Responsibly exercise your new power! Anyone with commit or triage rights may nominate a user to be invited to join -the triage team by emailing matplotlib-steering-council@numfocus.org . +the triage team by nominating them through the private "Triage team nominations" +category on `Discourse `__ (Note that only +``@maintainers`` and ``@triage`` members can see this category). The nomination +will then be confirmed by the Steering Council and the user, if accepted, will +be added to the triage team on GitHub. + +If no objections are raised within one week of the nomination, a member with the ``owner`` role on GitHub will: +1. Send an invitation email to the nominee following a template. +2. Once the nominee responds affirmatively, they will add the nominee to the Triage group on GitHub, and to the ``@triage`` group on Discourse. +3. Close the Discourse thread with a confirmation that the nomination was accepted (or turned down). + +If objections are raised, no action will be taken and the nomination can be revisited in the future. From a94275af4216e8d6f01026a0dcdf5bcc99706e7b Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Fri, 22 May 2026 07:58:44 -0400 Subject: [PATCH 98/99] Adds `plot_skip_execution` config to temporarily disable plot_directive. (#31270) * squashed commit adding plot_skip_execution config * MNT: will be in mpl 3.12 --------- Co-authored-by: Thomas A Caswell --- .../next_whats_new/plot_skip_execution.rst | 11 +++++ lib/matplotlib/sphinxext/plot_directive.py | 30 ++++++++---- lib/matplotlib/tests/test_sphinxext.py | 48 +++++++++++++++++++ 3 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 doc/release/next_whats_new/plot_skip_execution.rst diff --git a/doc/release/next_whats_new/plot_skip_execution.rst b/doc/release/next_whats_new/plot_skip_execution.rst new file mode 100644 index 000000000000..75d95bbada17 --- /dev/null +++ b/doc/release/next_whats_new/plot_skip_execution.rst @@ -0,0 +1,11 @@ +New config option for ``matplotlib.sphinxext.plot_directive``: ``plot_skip_execution`` +-------------------------------------------------------------------------------------- + +This configuration option allows users to temporarily skip the execution of all +plot directives, not running the code or generating the plots. It is intended to +be used during development to speed up building documentation that contains many +plot directives. + +It can be temporarily enabled from the command line by passing ``-D +plot_skip_execution=1`` to ``sphinx-build``, e.g.,: ``make html O="-D +plot_skip_execution=1"``. diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 7b46b3145e2b..142752f23007 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -162,6 +162,12 @@ The plot_srcset option is incompatible with *singlehtml* builds, and an error will be raised. +plot_skip_execution + If True, will not run any plot directives. Code, captions, etc. will all + still be rendered, but no plots will be created. + + .. versionadded:: 3.12 + Notes on how it works --------------------- @@ -323,6 +329,7 @@ def setup(app): app.add_config_value('plot_working_directory', None, True) app.add_config_value('plot_template', None, True) app.add_config_value('plot_srcset', [], True) + app.add_config_value('plot_skip_execution', False, True) app.connect('doctree-read', mark_plot_labels) app.add_css_file('plot_directive.css') app.connect('build-finished', _copy_css_file) @@ -925,16 +932,19 @@ def run(arguments, content, options, state_machine, state, lineno): # make figures try: - results = render_figures(code=code, - code_path=source_file_name, - output_dir=build_dir, - output_base=output_base, - context=keep_context, - function_name=function_name, - config=config, - context_reset=context_opt == 'reset', - close_figs=context_opt == 'close-figs', - code_includes=source_file_includes) + if config.plot_skip_execution: + results = [(code, [])] + else: + results = render_figures(code=code, + code_path=source_file_name, + output_dir=build_dir, + output_base=output_base, + context=keep_context, + function_name=function_name, + config=config, + context_reset=context_opt == 'reset', + close_figs=context_opt == 'close-figs', + code_includes=source_file_includes) errors = [] except PlotError as err: reporter = state.memo.reporter diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index c6f4e13c74c2..5b0eacd50e40 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -269,3 +269,51 @@ def plot_file(num, suff=''): st = ('srcset="../_images/nestedpage2-index-2.png, ' '../_images/nestedpage2-index-2.2x.png 2.00x"') assert st in (html_dir / 'nestedpage2/index.html').read_text(encoding='utf-8') + + +def test_plot_skip_execution(tmp_path): + # test that modifying plot_exclude_patterns in config leads to skipping files + shutil.copyfile(tinypages / 'conf.py', tmp_path / 'conf.py') + shutil.copytree(tinypages / '_static', tmp_path / '_static') + shutil.copyfile(tinypages / 'range4.py', tmp_path / 'range4.py') + shutil.copyfile(tinypages / 'range6.py', tmp_path / 'range6.py') + + html_dir = tmp_path / '_build' / 'html' + img_dir = html_dir / '_images' + doctree_dir = tmp_path / 'doctrees' + + (tmp_path / 'index.rst').write_text(""" +.. plot:: + + plt.plot(range(2)) + +.. toctree:: + + script_func + script_nofunc +""") + (tmp_path / 'script_func.rst').write_text(""" +########## +Some plots +########## + +.. plot:: range6.py range6 + +.. plot:: range6.py range10 +""") + (tmp_path / 'script_nofunc.rst').write_text(""" +########## +Some plots +########## + +.. plot:: range4.py +""") + + # Build the pages with warnings turned into errors + build_sphinx_html(tmp_path, doctree_dir, html_dir, + extra_args=["-D", "plot_skip_execution=1"]) + + assert not (img_dir / "index-1.png").exists() + assert not (img_dir / "range6_range6.png").exists() + assert not (img_dir / "range6_range10.png").exists() + assert not (img_dir / "range4.png").exists() From f4cf12592f0bffc8a3f578a8984c33cbef2b7525 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Sun, 24 May 2026 14:47:56 -0400 Subject: [PATCH 99/99] Drop duplicate 'the the' in two doc comments (#31741) Two unrelated spots have the same minor typo: - lib/matplotlib/_mathtext.py: "greater than the the actual" in the ComputerModernFontConstants x-height note. - lib/matplotlib/colors.py: "mapped through the the corresponding norm" in the structured-array branch of the to_rgba docstring. Signed-off-by: Charlie Tonneslan --- lib/matplotlib/_mathtext.py | 2 +- lib/matplotlib/colors.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 17dc1b8fb462..9f23e5e3ab08 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -1010,7 +1010,7 @@ class FontConstantsBase: class ComputerModernFontConstants(FontConstantsBase): # Previously, the x-height of Computer Modern was obtained from the font - # table. However, that x-height was greater than the the actual (rendered) + # table. However, that x-height was greater than the actual (rendered) # x-height by a factor of 1.771484375 (at font size 12, DPI 100 and hinting # type 32). Now that we're using the rendered x-height, some font constants # have been increased by the same factor to compensate. diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 685a96cc7803..53471e0f0a17 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -3522,7 +3522,7 @@ def inverse(self, values): - If iterable, must be of length `n_components`. Each element can be a scalar or array-like and is mapped through the corresponding norm. - If structured array, must have `n_components` fields. Each field - is mapped through the the corresponding norm. + is mapped through the corresponding norm. """ values = self._iterable_components_in_data(values, self.n_components)