From d78afb31f7f3fc6135172989f47a3a25ff5cc427 Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Thu, 13 Aug 2026 12:53:47 +0530 Subject: [PATCH 01/12] Implement Figure-level overlay architecture with two-pass drawing --- lib/matplotlib/figure.py | 147 ++++++++++++++---- lib/matplotlib/figure.pyi | 2 +- .../test_figure/two_pass_base_only.png | Bin 0 -> 13390 bytes .../test_figure/two_pass_overlay_only.png | Bin 0 -> 3687 bytes lib/matplotlib/tests/test_figure.py | 61 ++++++++ 5 files changed, 179 insertions(+), 31 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_figure/two_pass_base_only.png create mode 100644 lib/matplotlib/tests/baseline_images/test_figure/two_pass_overlay_only.png diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 9920f6d908b3..48c2c54e6418 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -209,6 +209,9 @@ def __init__(self, **kwargs): self._localaxes = [] # track all Axes self.subfigs = [] self._children = [] # All artists except SubFigure and Axes + self._children_by_layer = {"base": self._children, "overlay": []} + # Note: "patch" layer is added by Figure/SubFigure.__init__ after + # self.patch is created, since FigureBase has no self.patch of its own. self.stale = True self.suppressComposite = None self.set(**kwargs) @@ -238,14 +241,8 @@ def patches(self): def texts(self): return _FigureArtistList(self, 'texts', valid_types=Text) - def _get_draw_artists(self, renderer): - """Also runs apply_aspect""" - artists = self.get_children() - - artists.remove(self.patch) - artists = sorted( - (artist for artist in artists if not artist.get_animated()), - key=lambda artist: artist.get_zorder()) + def _apply_aspects(self, renderer): + """Apply aspect ratios to all axes and children.""" for ax in self._localaxes: locator = ax.get_axes_locator() ax.apply_aspect(locator(ax, renderer) if locator else None) @@ -255,8 +252,33 @@ def _get_draw_artists(self, renderer): locator = child.get_axes_locator() child.apply_aspect( locator(child, renderer) if locator else None) + + def _get_draw_artists(self, renderer, layer): + artists = self.get_children(layer=layer) + artists = sorted( + (artist for artist in artists if not artist.get_animated()), + key=lambda artist: artist.get_zorder()) return artists + def _draw_layer(self, renderer, layer): + """ + Draw the specified layer of artists. + + Parameters + ---------- + renderer : `.RendererBase` + layer : str + The layer to draw. + """ + artists = self._get_draw_artists(renderer, layer=layer) + if not artists: + return + + renderer.open_group(layer) + mimage._draw_list_compositing_images( + renderer, self, artists, self.suppressComposite) + renderer.close_group(layer) + def autofmt_xdate( self, bottom=0.2, rotation=30, ha='right', which='major'): """ @@ -302,17 +324,61 @@ def autofmt_xdate( self.subplots_adjust(bottom=bottom) self.stale = True - def get_children(self): + def get_children(self, *, layer=None): """Get a list of artists contained in the figure.""" - return [self.patch, - *self.artists, - *self._localaxes, - *self.lines, - *self.patches, - *self.texts, - *self.images, - *self.legends, - *self.subfigs] + patch_list = self._children_by_layer.get("patch", []) + + if layer is None: + sources = [ + a for key, lst in self._children_by_layer.items() + if key != "patch" + for a in lst + ] + elif layer == "patch": + return list(patch_list) + else: + sources = self._children_by_layer.get(layer, []) + + artists = [] + lines = [] + patches = [] + texts = [] + images = [] + legends = [] + + for a in sources: + if isinstance(a, mimage.FigureImage): + images.append(a) + elif isinstance(a, mlegend.Legend): + legends.append(a) + elif isinstance(a, Line2D): + lines.append(a) + elif isinstance(a, Patch): + patches.append(a) + elif isinstance(a, Text): + texts.append(a) + else: + artists.append(a) + + prefix = patch_list if layer is None else [] + + if layer is None or layer == "base": + return [*prefix, + *artists, + *self._localaxes, + *lines, + *patches, + *texts, + *images, + *legends, + *self.subfigs] + else: + return [*artists, + *lines, + *patches, + *texts, + *images, + *legends] def get_figure(self, root=None): """ @@ -585,7 +651,7 @@ def set_frameon(self, b): frameon = property(get_frameon, set_frameon) - def add_artist(self, artist, clip=False): + def add_artist(self, artist, clip=False, *, layer=None): """ Add an `.Artist` to the figure. @@ -601,6 +667,8 @@ def add_artist(self, artist, clip=False): ``figure.transSubfigure``. clip : bool, default: False Whether the added artist should be clipped by the figure patch. + layer : str, default: None + The layer to add the artist to. If None, the base layer is used. Returns ------- @@ -608,8 +676,9 @@ def add_artist(self, artist, clip=False): The added artist. """ artist.set_figure(self) - self._children.append(artist) - artist._remove_method = self._children.remove + target = self._children_by_layer[layer or "base"] + target.append(artist) + artist._remove_method = target.remove if not artist.is_transform_set(): artist.set_transform(self.transSubfigure) @@ -1076,6 +1145,11 @@ def clear(self, keep_observers=False): self.delaxes(ax) # Remove ax from self._axstack. self._children = [] + self._children_by_layer = { + "patch": [self.patch], + "base": self._children, + "overlay": [], + } self.subplotpars.reset() if not keep_observers: self._axobservers = cbook.CallbackRegistry() @@ -2385,6 +2459,9 @@ def __init__(self, parent, subplotspec, *, in_layout=False, transform=self.transSubfigure) self._set_artist_props(self.patch) self.patch.set_antialiased(False) + # Now that self.patch exists, register it as its own layer so that + # _draw_layer(renderer, "patch") draws it first before "base". + self._children_by_layer["patch"] = [self.patch] @property def canvas(self): @@ -2494,13 +2571,15 @@ def draw(self, renderer): if not self.get_visible(): return - artists = self._get_draw_artists(renderer) - try: renderer.open_group('subfigure', gid=self.get_gid()) - self.patch.draw(renderer) - mimage._draw_list_compositing_images( - renderer, self, artists, self.get_figure(root=True).suppressComposite) + self._apply_aspects(renderer) + # Patch layer: always first (figure background) + self._draw_layer(renderer, "patch") + # Pass 1: base layer + self._draw_layer(renderer, "base") + # Pass 2: overlay layer + self._draw_layer(renderer, "overlay") renderer.close_group('subfigure') finally: @@ -2731,6 +2810,9 @@ def __init__(self, in_layout=False) self._set_artist_props(self.patch) self.patch.set_antialiased(False) + # even if this line is removed it not cause any error + # as self.clear() below rebuilds the dictionary + self._children_by_layer["patch"] = [self.patch] self._set_base_canvas() @@ -3348,7 +3430,6 @@ def draw(self, renderer): with self._render_lock: - artists = self._get_draw_artists(renderer) try: renderer.open_group('figure', gid=self.get_gid()) if self.axes and self.get_layout_engine() is not None: @@ -3358,9 +3439,15 @@ def draw(self, renderer): pass # ValueError can occur when resizing a window. - self.patch.draw(renderer) - mimage._draw_list_compositing_images( - renderer, self, artists, self.suppressComposite) + self._apply_aspects(renderer) + + # Patch layer: always first (figure background) + self._draw_layer(renderer, "patch") + # Pass 1: base layer + self._draw_layer(renderer, "base") + + # Pass 2: overlay layer + self._draw_layer(renderer, "overlay") renderer.close_group('figure') finally: diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index cf17f4694dbd..16bbff88ee99 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -81,7 +81,7 @@ class FigureBase(Artist): def frameon(self) -> bool: ... @frameon.setter def frameon(self, b: bool) -> None: ... - def add_artist(self, artist: Artist, clip: bool = ...) -> Artist: ... + def add_artist(self, artist: Artist, clip: bool = ..., *, layer: str | None = ...) -> Artist: ... @overload def add_axes(self, ax: Axes) -> Axes: ... @overload diff --git a/lib/matplotlib/tests/baseline_images/test_figure/two_pass_base_only.png b/lib/matplotlib/tests/baseline_images/test_figure/two_pass_base_only.png new file mode 100644 index 0000000000000000000000000000000000000000..4df3c7d46950cc565e6a91406b26e8cd17329e97 GIT binary patch literal 13390 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 DGK6%k literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/baseline_images/test_figure/two_pass_overlay_only.png b/lib/matplotlib/tests/baseline_images/test_figure/two_pass_overlay_only.png new file mode 100644 index 0000000000000000000000000000000000000000..1e61c6c9ad96c7480e08a8a1fbaec6474c0508b0 GIT binary patch literal 3687 zcmeH}`8U*W8^=Fmlw?mdW64sIlJ8`fT_&lqWdGX7G8EsCViwCJNm)uGk*z3OmQ=>1 z%nV~+hA2xh(O9Cwn8_#;PwI!B=RbI!=X9UzT<5;-^LoF}ectE(;l65bYbCN>W;*}? zBG$((9RPq24*;NvZQvg#d}8WDKYFn}!U?|dV*$XQ`jfzaZ31sesRKV=?ZkNpTSoxI z2j<_hRbZRob|GOAQ896e9g;hxq<2A}vU2hYib~3R_Nwet-LH1wp!y+=zhIhL+B&-W z28Kq)re@|AM=jyUtd5^JiLkM?vv+WEc5!w4+r!h#+ZX8<5Qqv2J`)o9&)Kl>h;xzW zFGNLSa51rQ@%W3E5-%rRNxph5B{l6v#!W(I*6r+^JGpuJ1%-F-6&2rqP*Pe}Uh%N9 zy5>>sc|=1%B0pI)2WfI$GfNknG;LBV_lYQCuR z1c=XdJ;TNuh3JWh!a-Uo71K?+t1yf@qaaqRe~`NbM-(fO@BSuz#uuxB2mywYK{miF|>qSoGMCw z>?>W@Kh|-+*4ca~1S+8I%3ga21`qY!5cEpm1*tO=_}k3#E1+QpV=Zt!o-^7 zauv`M-Pc{!Gm#O6n(ESBk4Sb5O<{B~-2 zpK^d3IJ4}|GUttWvfYifB2Su8Dd@!BT^;6%udK=Z6_IjQcbk~8ndisoQ$nL2P40F# zJh(kD&M+mZcAA&cXBuZmIpQGzI1^&7n-#a?vOv0GROY**;1O;1J|t*iNro0_ZozhaJGc zo`Cv|>Q%EaS#7i1Rr|E|N8bL#Hzb9TYSUMJ$m0rrU9mqJ!-b4W2`?d)>LTv)WW3i{ z?-?Sjw$HAp9PgRgai%;38ds4wp%qYFYA}SoB*%wu&<@fAL6mWszV#{{rTYi1RzPUl z-1Jf%L2bj>+GJVp@}0xBtvvwd{=mEN1fw%EypUBzuW0<9bNBBUiToV>ye6U;=t5Pmh-KEuLrAPH&<~$kmfX+^m?&pSU>r%6}7T+rj zBV^R#8625?taBME0TB8f4CI?iRw5_g;A^7`r`XE84G9H?LJJ{CTfP zV<}O?#LO8}l58C5wuO|pH5)A(!uy!1>L|#VKkM#m7D(m~`P_XAEF4C2N&vnj&5P!m zSBX#TDcOUm%Ae??6Z?3k;yj1&Vi(g{E!v+OwaMdk4f2t`MXy+gt6xL~4->)d8kMH$Uq&Fa?E_we7d%WL-HWE@ zxK+_(YyAg(FKPmj<|XTdGVsZLgUlNTAGHL?A2v` zgDQRfHXM$KIDh0EvWJI>LAqb)?y6BOb*^v3T&To`6F_5ZT2kX=oz9)0Wx&>MUDqW) zyJW38wYBO=;L@nYO8ZJe?mvdud7eW2#_AlKi}?5gOS7H^*Zj4<+}B!TM+px`5X~ajbq*>M58TO_5aC@VU(bFG<;3D7pc=EnifTrTd zWEm7AMuBf{%%VskggL=4+q1}6qNgx>_#e*XPu8@d(Fr<{^d)_RCL1*QlArg>_Y^cd z1*M5lq;UvHK4?jyuw)19OF*_+(wr)dUlU;p-FE{y-wC@YJZP6B7?2&eiN5-ce&0mP zf1}?vQODotFV})i*PrMwSL03BpXe`F?4Jw#ea+`Q2o=|H;AAkq;m21Tu!h@OR$KTc{0qK2tIz-d literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index d8bf1954df94..1d59e39655bc 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -1966,3 +1966,64 @@ def test_artist_sublist_deprecations(): del fig.lines[-1] with pytest.warns(mpl.MatplotlibDeprecationWarning, match=match): del fig.lines[1:] + + +@image_comparison( + baseline_images=['two_pass_base_only'], extensions=['png'], style='mpl20' +) +def test_two_pass_base_only(): + """Verify that bypassing the overlay pass leaves only the base layer.""" + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1], color='blue', lw=5) + + # Add overlay elements + from matplotlib.text import Text + overlay_text = Text( + 0.5, 0.5, "Overlay Text", color='red', fontsize=20, ha='center', + transform=fig.transFigure, figure=fig + ) + fig.add_artist(overlay_text, layer="overlay") + import matplotlib.lines as mlines + overlay_line = mlines.Line2D( + [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure + ) + fig.add_artist(overlay_line, layer="overlay") + + # Mock _draw_layer to skip the overlay layer + original_draw_layer = fig._draw_layer + def mock_draw_layer(renderer, layer): + if layer == "overlay": + return + original_draw_layer(renderer, layer) + fig._draw_layer = mock_draw_layer + + +@image_comparison( + baseline_images=['two_pass_overlay_only'], extensions=['png'], style='mpl20' +) +def test_two_pass_overlay_only(): + """ + Verify that bypassing the base pass leaves only the overlay layer (transparent). + """ + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1], color='blue', lw=5) + + # Add overlay elements + from matplotlib.text import Text + overlay_text = Text( + 0.5, 0.5, "Overlay Text", color='red', fontsize=20, ha='center', + transform=fig.transFigure, figure=fig + ) + fig.add_artist(overlay_text, layer="overlay") + import matplotlib.lines as mlines + overlay_line = mlines.Line2D( + [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure + ) + fig.add_artist(overlay_line, layer="overlay") + + original_draw_layer = fig._draw_layer + def mock_draw_layer(renderer, layer): + if (layer == "base" or layer == "patch"): + return + original_draw_layer(renderer, layer) + fig._draw_layer = mock_draw_layer From 8efcda9784b98f49debed8b60144cf01a12fa660 Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Sun, 16 Aug 2026 13:54:34 +0530 Subject: [PATCH 02/12] Fix get_children mypy stub --- lib/matplotlib/figure.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index 16bbff88ee99..65b21a950cc4 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -55,7 +55,7 @@ class FigureBase(Artist): @property def texts(self) -> ArtistList[Text]: ... - def get_children(self) -> list[Artist]: ... + def get_children(self, *, layer: str | None = ...) -> list[Artist]: ... def contains(self, mouseevent: MouseEvent) -> tuple[bool, dict[Any, Any]]: ... def suptitle(self, t: str, **kwargs) -> Text: ... def get_suptitle(self) -> str: ... From 1f58dd50ebd222111288fff644dba31340f80637 Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Sun, 16 Aug 2026 15:09:16 +0530 Subject: [PATCH 03/12] Add composite test case for Figure draw layers --- .../test_figure/two_pass_composite.png | Bin 0 -> 16649 bytes lib/matplotlib/tests/test_figure.py | 23 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 lib/matplotlib/tests/baseline_images/test_figure/two_pass_composite.png diff --git a/lib/matplotlib/tests/baseline_images/test_figure/two_pass_composite.png b/lib/matplotlib/tests/baseline_images/test_figure/two_pass_composite.png new file mode 100644 index 0000000000000000000000000000000000000000..964a624bbddfa2b459b709601a2d6f134cfa67bd GIT binary patch literal 16649 zcmaL9bySqk_c*?ENTZaLpolcm9RdqTOGzUs-Q6i73rcsaNO$)F0@Bi55=(=0^L-XS z@7FoMe}0_f?z1y@YAP}yctdtrEgd6|@AuVGf11*hJZ>xYW zQ5R`#mv{E&F7C!oW*|jl7Y7@A7n_eCXx+@5oIl#zaj^-oaj==1xpA=2TDrJ6I193~ z+x|C-&ECm^-RwsEBY+9ZLH4~f2!wBp_=l7)mIpM7JIYB(sC&%rPFL6>OAv&O#FyeQ zGv6y=Bm zGRi<|oi``G_6324sf`3EKpP_;nr`*KMHZ1bJEJ zO9a?j+ykpbg%4l_k0}sf!dAG4wgOkSWCn%?Lp`hH0clEkV=E3G76(euD#imoh%)00 z(ktVrb`=#YX;d^cNhDJ2S{2m$&yQK8q;)E^BQ3jdJGu;MSK}a@9&B|iFV~+VyB$x|ox$(TJw1Cn` z+^g{skP$cXG)E6c_uT)_;{}dW+mh+s&1I2h$+tWa?=u#?`a@{6C=)w-*(L0v5)YLB z#;z_YT@i$#Y-hKh#A%{aV-+74$L+Kx>)QaP@#Pj3ZB@<`ef8>9x{$l5$Dt7}Mt7ns zNL>GYh2(xg-W!Z{I57;V%6h8QBGZ>wOw1n^fJBXy79aoQ$&(tRc4Rp;G&DOK-2szlbr$M7 z@W1zW-|lB8h6S$Dz;3V4X#`#J3ybv{MybNQSu|a{y1FL(yk*v57yE!^Dju}tC9o=2 zC=wG@dvYOBvfLrfw|JkY+BM2)R900j7&qVFT^HINt@SFo?*9X!T3=6jEq%J}yT#kG zjQr#=0SxI64Q#(jt1}#5E562hs9`07DT`!xE|0nNuyV>4}n4sn+OjKisX!?$tV z7DH+FzB{@C0s@6=t;oKAyGW-TjBc3TfcTyJ`dpTLKYu>bNrR^W)AWaRbJ=sRW@FM<@HRtfL3 ze0TfIZHGf6D$xN_N7T`Bk= z06SYwJ(|?DnjN=RlmYKl^gNC@)QFS7T@LVbXLRKT`9zGWE%??AKg(5oTB6hCEnVqstZdd1R!7anep&+~)1P>t-d70azZ+%D4uXYx?tbd7?g@JfK7y(w_ z0k4^InS5_x&CaViEDV+XneTgthCCe?s0v2GjQaE8_69B*4msO9&Ad-)7%l>q6E2{S zfIZPmDN@7T*jB#zER!CbB{*F`)KfA*9`C_qXc%tGq^?i1fsg9fPfxDXK)m({O2zNA z^E2rSwi$3fB2Q+XF7h~oev5tDd zupO`UecNUZj#}=C0tK95aS-x?OYHEooOds6XL4ev>xZ$7=n;y5ai{VYn1C}+sDv~_ zLj_-0p9>(_EF#p9WHQOp)#F`KgpPYM)z%k0_#YkQgK5qvigjo0`b|1Bi~iU>M2{1K z?I5dLuiV_r?+3PZ1^KeAk`(}%V|0KFtq~oe$aV3bKh2Y)?=7|t{Iv!{%rl8$8wdr7 z8`VE`yl}{T)BYmS^4e`eJLwEyVhzE>cukG9>wbgimUj;o)2oq>SPOt21`)~&5G0D& zRQ6X>_%e0%abEio31bqbQKwHRVlK*uN1%;oI+@}3)FRc+dB~9+sYK`1bC+&~V8*o!k0H z-BQB)FEjs}lS4(}0es5MV-Eirv)MZ3ZqASY;hdVLdG$)P=6J1_i9Io5;J;~fxWb*a z_gfvys{~w3r)9u0AOH1(V7DF*Aep+a9BcS=@Pk`}do-b(?X&QLv7>L|n zP4VtxRUv?=f#qSC)2xRtvbIq)`{1-8i_<(L>cbCclXXDA=w$0>wmRxMy zj!?T4puoPSgF?W&_#VzJ&DP?xOaszXsmQmEeoT`95^@0$l`i zRbbz%3*nIaO|YPgtY)PVLOp;{c36zi@z`#=E`h;2`2H%->2%+h16(t!fKjqlbqWn{ z``~W&V>0(wiM!?q`nD2OxFO<7kkK(>4pnZlq<QTzwEt#tRrCJpw6@5ti?)?TKLUaWfY9#hq7YkzpNZW2l<4L;JP1+?2!c~> zo7(6GVERfQpP2Xip?yW13OC7Qh?2)<)3vR;XD5`jLt99^gba zb~Zbh*X}VwR}M*l+(@;qMS|{Ua!XI`)%{wHdI0G<0U0ne+HRyHA`H&WB!dh*rOOeP zk`2rTs*FUSUF!_oZFgKww-z&D&l#D&7*IKAx>}A(2=Irjb@}BgL2RbBo0 zbPF&w0EactiAJ1OTh;yd<3aS~r)d?X;SveISI^PdM`Bf(s5jEjvWrlAqDovj99$lhGt%a<>gT&MH} zdzbFc>aI?A%CswTj-tX$P|$JeH*+G3X|!JelSlUTllhRkP4U$2czp#43ju)W{IC(^{1-C7$*N{h>*^}MUI@wVp-3=A(`NQ1%mhv8xagM$-tpCcp1 z$|}9j?ZzC~&x1_Zwl5xEU}YNrx^x-%sNiA_!}*ft-| zpdux8zgY^vz^9%s(XIO&`@sm*KjyAP6tbxHAlZ zSJqhcgi`_g!jAV74dBJ1vQ`}p%~HKB{vlwZ_>ta$QC!zoqvKPyV_Rbx3W^nI1E|o3 z2a>r~S63I_lrSkKaXjPUIrT_sl-u=_rx5k=SZejvSTCttbPBJqo?>gl*a#M+$ftku z#A}UkqU^xcA8?3Wk$^(#k@j?V0QqM_X_Y%LlSamG7)B*v=@>HyR>h_c2EV=qn;g1#cN&ZZD{G|4vP{ae5)8yJeO{**=0lA` zr6b3y3#K&m+FGf7)xUmnphGyf6&Ma6jY)j`R))6^UTZ9P{olGJdOp@5!4gJ>@3`lb*FYf|R7sXhr zwXHFaTK+%ws(>Dt+pfy_zGx!6+*r^{{jlpFdw=txgtEu*B1e<5qSw5d$|BNxdhh

vy&0jA(XvyB=Y5Huhj%LJ%`SFNYZB|d%>O&qRwWMU#xFhv**q0RUR zzwg&i8Qw~cjs{waP?;1VEsjJ4Oc)j*yNRbK1!o6*d9J3Y)|U9oLkKm-+_wB>sF^S% zwgi9OaO?Jj^wEzhL_FreaOD=Zh9`49dAj53%57JCd5k9XAW2e~AjL}oFZIr_OG=+6 z_bCWf5YK}aaiplCh>PDtYi)hm!i5gh$xGhfB5Xe7HC9RzM&q{vRqZ6_q@VfAP3_W`%Of_ffTW&4=;8E&(rARw`OJ z=jycO$3k@h2|LNZy@2tf?M(uH7ujHRfUE1XiN%(n(+5;6l2o=e0}3^EdpvXbj3S2t zeSIMu

eWAGuaX%@-(T?sH9$u%7HUNqRg8IQ>a*mM+uxM^Wj~7Efuj+V%OP><~}CZ`s>%WJ&c<`LmbO=KnR=tWFpr`z}l z`fS^hZM^SQBofM6?HDHO!@Lt@L3sPVy|5bTfPejX&Yxa3!`hYp2&D2mO2!Rg-4*gG7G?{C6UrxCm95FA$*T2_N%^TB3wHDD2YN`3tV zvPM|eVKxNsTjlEo=jdT#RjWeH(#J+K2s6tZdcb;7rRe}!;p~j`}fF_ zl>qwzf>&SWTJ`mtfX&G#!oGfDf($@wDB~j*lg*x1w*>hg^}c))h9f5@*KpYJDAVVtm)-E&6J_DQQgwCxB|lL8SU^cqFvdbg^q?d}Q)-oxoae4?U$8w1IL z@f9XL3@={-?K?o=aJ=7Ga9^^$g&np5NIss~@K`RNa=n%P{gxdIh`narU&tdOB0@t$ z3)i{}y&prQ=nD5c@x@#yEw)B;3;*0v0p#Ch0dvwJx!@EzOx%w~YB|P$gzB)Y=e<`0 z+$U(ozj&L#52^CKcGKW9?m*>B>g46{02Jq}TU);Z#95Q>{48-0(k`RN=-8P5QEX;@ z9}tTvdwK^3)Lnzoaj)9({WyS~i}SEZo$vJ--;&R9tM9eTw?BXW3>WP#w#=Obwm))p zN%y)md-2@k)@EMqYC;Q*00!*WGDN)1O-<7d6IXm_$XQc+Q{kE9hpkK;yZF;S?I7Vr$PsSMnwCg9~5?l9d`3E4%P0 z%arI0OW^_oH&im9ru(=#;C8|Lpr!8nchlReQ(#Wp4qC6(KMtnw*zq=H7T#>t*~~cg z#l5oQg~Q?aG@{Llsl33vx#8pF1qxm%^!1PZ`YNhStKVcb$BTqo&WW_EQ)`oaZJ^=e z;^O0TV_UbNsj2xxo>{BZg|g>XJ=b+-Qs1ff1t%w`qoZS@q2J{X#nfUNGNq@kC*40;6>Dv>n5Nw{&2iQOgv5&8rlbDjQv~??tIVy&&UjJY!pl!s9N|LA0NMK!hws6d)C$1i$q+C zCI@!6n)MuGIN3HksS68TS&e>%NoPbuQclUyZm~(dwV6Wk{d>SKO9}xZ_>cs_^Nh%V zK{r?kTU5c=NKZ69+fcCY*r-vBkbam+4~Ih* z#x&KiofPzchc?9pu%-^5I;LDAVx)!v8u&YbEi3A1xFz?+DGWAV^dzX_m0evah zl5hB?Wu0zgX19O=&(xN#3MvcZgNoc?s9J8j%UP7f!QY5(6Czfp$+gQt=z0e()i06U zBl=H+6C8~meGWi7mlY;ON<9d`DlHgYHX?XN3+n1}0GpUtul}@e3l*JOvKO|U4%m^9 z4ux}tRG2ufvjP0!BDC>C$Ot*j^U@6Ri@g=o#0_a+|2nJ+WM zb+NHyQa)dJlwN+0>}>YDf+HQY4Y&Di$H{}ww@geMs73TrU2iVvfm4Z>f~>eDBMXU? z44nYEeM{(-*VJgiv^K%BS6KV`g@44hdtV}LEK4X&G%YdKRO#vjIEEqL-MYK7$b1p^ z;J2$sy1h2pR$tr58Pqm!{w5vyJ7H8UBs5Sqop)jq8HPbaW;;IuC1|8!_yngjYEN|h z@@5AfUiq4=-TY4|yM9pc6K4J((kFqAs}c6Cfn1u*8$%QEfeV7$y8Ea%o%l(d!G_-N zsq{6?%abbalDQlhxMIba7z5a>r;d=p5KvzMIk{UK#m%SD`~*@yA_vWcxe6>A?}5|z zFKiie^_%o$4!aLm(k2*LP-&4#1}Fdx1`VsJw*Tc5rC#K)CSh;$=A%p#%JWeOdGR)@ zKVOwrZKHN(dEgEH-@^UYr`KoMKV-}r@M*e}IA@hNA6G9e5T)*x11z z=nTBDJUuVgNl)KsXz+Uyu{Q_A5D*yuA}~|Us}|OE7^2+Fb|WPW!$lcCe!trD)CM^Z zhfNP;Lc3g|t83qFVNvwfxai4?7anKtGsOHw(+bMq$9XXXHGHsX($)2RK@EV`_cY%vUp{EkDa?4g7&p6V$asREwqHiKx?1m#%8CnoQHQy#y ztVxP=d@^H^J}IO#heG4~-MNsE>{EZ@O2?GpvDTvB+}-Qfqn*)ZI$dJ>5-|AiF-WeW zqS$&t2<`IHVIwaSST~JKt~^W;^F`3C|4g zKHIZx`Nd)|z$d2P$bv_OO&1cD)2QSAUC&@;C|wY__5Mz2pGw4o<5Rgq_D(t0)v2D( zp4}sy8~ZCWF)`@Um(QbKW?s*htGS`@fYek%s**g7MJ^u#gAZUSP$H^&pmL zRkapuW~TY(Xnp9ms(#U8TV2cIWT%H*JBC^i_jOmL-?2Ad=%f==1U}`ISf&!7T zgmHBPQ`& z1<0l9;nY+}G@>}}#kv;XrJ_4rR$9(4pv}UTrl$PnkSvq^$?>X}uek*Fc1r?mc|~6) zarYh%)#3oHIS-oqYG5+;}09K+Bm zg(as8)xe#0Co7cCNUe^JSLKN1KIA|G9zDv&KMy(+jv^0U^0VeyD>rUL)=c#sQ>5+q zLhd8ypPS3{99`-@Ps)9#_0qf~t6l`Tv|vTR&u>19NU_2D5&3zsubwcUALvoQWfY3ALHo#8a$PWO}m@$#+$ARc=v9OGtOB3 z@uPay{)9kGl1IRKy|3H_({OAUs@Q0qE=;bW0h(oBP9Ui*=6m-(xoHkIi}IGvC_DAR z`}SKBdUj!xL31Iou~DOKOaAzvBCWbs*OdYCJIfwCk3SS>KFa}#6C-IT4lkDt{axp^ zL`8P|2?>AYV$l}nX3-YfUT+>DrX!nQteI~pR43-pl@O#H5EC08n-iA*rg~7V z8Jodc<4gc3S0?x?HZc)*%NCzit@gz?ts>{vTQ_LOYFu2K5^PZu+|%zMo?-K1* z@&}VGIEmWpMTG*k9WriNZ~lA~WP|d6qWsz1?l5%lDTx@iP0fcjS!(swpIA^;zU`f7 zF5U_R2>nGv8*G=wpZuIt)5>E?j+idnPvVWTzuH*;+hN~|rC(XeADj6M{RjMdIyr53-QIqc~h=k@Tk9} zP}D{B%FEOKkKh6H)Dsg^+)EG}Tjd?}L`5y=bVnd@=Oe%5-Uge+J600C=FKxVCwXk4 zixIGfQ&SG|;9!2Cq$6kR)#ub_@O4)e*g(3w7m0&p(A$-x{ROs`x~ z8BEuI)t_-hvWCBy*Y&=3%T+6Sf{WWX2!`aNa2g}magPXEY~iWaSl} z2M=d_`UH!4#IwPsUeRL&uT@jbCph)ZetX6hOwE z<%By_;Mhq8rQIn!vPv?h&sj)NG3eRwpkR7dTtlDs+Y&2lPnTci9B~QTZbOy+p~nQ5 zm!9aR(&$fYbdAgutzqOuaW5^==+?^q z(d58-(3`~JtLA*jP%ajs`j$uC=LA7>C326Pa)KZuihriIY|ZZle#*PU2yKi0@kT;j zSB{XDY2RB*p9-~IA&C|dMcVoPra|@{KZhO7vA2cLCO!H;4xUFvT?`NO=^e#DHa^=) za8UgpMG>XU4OOz6O)J5cMTM{yC7yOf?bY3wvu|IF00l4EFK28rOrhbi zGq=kvnYOW!?1hDBe=rxgQ}h8_cEYf=*DNP1KK{GcaOFi++@bj4t^s zacu4T>K`G&!a3bbBZ!O%CxWhuHylnu>B7C*5wy6p4p7DI5zlxB6KD0z;N=~krBMI0 zupr1E1JLDWe2|PMc97|-gPxBG=fYAyf^Oo2T(h!y6H7D~wa!%u3F?f$5FIHG9ahZo z)wLUpt&0X!R0uweo+0L;Fe77r%wD)uK}IwQibRi?fC*=>!q(QTlt`)S%A60Xh;|`F zd!bT$uG(j*AZ#{@iQb4H4lqg>LLR356$uj)NQAqOUUamRF$8^gO#lr?ee!Qbj%rVd zlj|vQo&1I+Oa>uL?&C-u15IaVUlMX>?vB9+0i{h}f9|1rxBpYBK2LAMG9>AN+cI~L z{EUpOS*wpDS6(9X)X7jn%?I_hs%2+&N%!mub7WOl!#FEjfn(d zxBY}csKZ7oD0F%>KL2GWoxXt+L?B&H2a&bB1zv|VpxWB(I zPFvp6G6;GnjT%fw!p@G*rl z5or^ZKV9j*4=cR0kcLbnK9<f+@2_60&qaDye5+ zknTE)d=IDx*t^)1q@tAZ(?jRG`q%LmQ-{lC8-YL&!{ z?m0FteIlu#pfFvDj)4IRn^tc4e9MAvsG*Xn)81-XXj3xOkNxNw~U}JkD5H(nls6SDBh%Q}*xlXzp@OWkw5%+Y;HVGuE?Gp_$nsZKD;`EhrfM6&;XRp=848Ou zbR;q|65sH)d)2YQB0=jhHhM5{4{_>Xq)w>xa(4El@&oVca#qG9z*(#)y7m_67?w}b z+-&4HVw69|J--kLpz^=)90E-(2dWAzEOmak?1WGSOD*&`Qo^6}H2jnzcqvDOL;tK@ z_E&aRZhKY`CDZR`;gn9lo{^H0X11e0wc+@cjT{_e<>27JK|qAlDNFPwhT`XPV}H}w zwJ`qh;o;BYllz5E(76?%3GRGA-Nm z6ccOTVU$t#2qzbb@rLl{>h`{y@S=`RbtN4z-2W^5ad~@i_V0wmiTPd2rA31mM99wE zeqn&G#-+vyjqSDkAo=8p0gvkYf|tsu8MH z_xMs%^U+ojaHC59_>W&ohJ-hr|YkuURZCG9; zARG-L+uU5E&&=OVEBmRd#zVlpu6_O|L7R!O1THS*jEYQ!>BY?_029Jt24i%x4-Y}1 zOsuD$&r>K`d|ut4I&)Wo0q;hFA7)rylRKo;vsws!^Ym#DG0-6abfDvh=?xASwJtIb z(-Y$2F{P7Kc2eT<{gZpG+7j|*6NXeqR#q}G@Fj*G&=mbYO`S#OKO&`!&abklX?S%( z>ddVG2Rv)=#s*~rRfMIbY*SOv5+}IR4j{o{X0P1Y30bc!RjR6zG~xhvS^}-*tNoM-AVlIy={#V*nhQz-Y|o1$|uHg6}^oRE7qz z0(&TN-vDl(ewvrhb)PaiCkB9yKxoW}i>t82b?xvMrhpBhv&-f~bPSABvUX7U`bq0% za?0AzfFn{sa2^K;1YeeCrd7b`ctqy*r@ zO|c>gXyxESL~oz9t#=xlUhAgPp$rU3>Ft!bj{x0uk`iH^o(cY`Xn5b>j|^-*5EHxG zc78B1`p0r<-_G}%AwYn_9FV$OG&N0~dpkaSB%fM@2v`*+0JzB#u;#WLe=bK5>%1of z8od#X{>|Qbt36bE-p4 zcs^aY0G|f+CJLZ+3~2aovu}|}59OZsPC~0c=^#CKcTg)?bU^90N;kCb{{Z2w5C44TLyf+*ji$$Cawuc_X3dav4dNy zzoD6#fq>gZd4?Ei3^RZov3)}OdsR?W2W%P+d03eP!?m?8e{<-645f{9hXn)}Lxykx z#pntr*|+$WTzQqbe3o)KFCG1eB+*5g&hu}X(ag?`(fC)IoU;{l8B@Z_CMVTZ(sIHW z^K-LaSYZi;kV7WM3DyY4>DchlDP#ypDN*_OFm6%wo=Z|ls#3a)jnqMLJM?{?oqGFw z2oOyrj;UPCc? zs;WR8TSS|;tfWL=Uw_7?c5iR*^z_uJq4+8=>Pf9}LgEuEizcc;229nbh0rG|pks1a zw#p|F>(|MszwPSYqetd^qK=Tt@jjijs+pM3sG30lMrb~Yj>hH-pj}=^3&|&$7ztJd z9aF%jrly31gkmml{i0-mofJphh^4TIh{i3fTcQD{#lU!vk}6vr`56<~^@R2@EDil| zWo6~^Qo!`DM_P*$3rVNZrH`MvdH1?gHn_#$p1yzGsTtp9t;3jw4R4NDhn1Q---A9j zWP4MTG2jwsd0|06wioq1L#%NbC0;nEc#Y1&+?)xRi#cbc)2{CRD&#ycN~J2|+S{_G z^K4+0$iPtI*UxOhD=`G=a>maV78WrTbBQgln}SF1AL+Lkv^6xm-{#J6LGmY-3u0s{ zCJjb9IG{C^RHeElN8`vt^6~a=s;NOs9)_o;qR@+3Bjm7RTk~OxC>f+`9oBRucPFPHhz+lGjm`|IICRA5zUqXPBr=w( zoaXFrV3Uv~cjsAeANJy+IA*qBVZ@!>3;*?< zP~6NxfB)N}fz`kHenw)iGJD$F4c+$ms{4+jS1;_hgq=%xCM7=ya;iR#42>kgQ<5-~ zKqKe0kxYIu4#Oty!+1eh@p2|7{{>0S*LKQ{UxA{GQGPixmC^|n6_vdBwkz{~*2ihL z0`OxYZ4}m)vg_h>Ps6EOKapwZ^~mMo)VZ@Ejkz{GzHn9`5>ghV6-cqzgEE<&?sN?e zm!_xr`T6r-RcIHNma^SyjRHA`9X4PSx^@FIA8hqydi8gfx}gC`_vWf0i&NdYqn1wq z6%=fVe;r#6gI4}{cj8`MStGDA0=d22Wmx6jlwpliuCc!GNens$8ju?#LexA&qVyLI zi@tNLm`X_EQ?_qY+ynA}PZ3#l42*;qCML7hy$*l%t7Us2SRmJGLLg`9=3q2)csvXw z^gDI(OG(6@ic{_3BibE}xc0}CT~8+r=4gpVybn}`fK($nA{&f{XM_HNXtsU?KvgNV zpnJS$)EC;E8Ej|G`$|nAD@{##sAWJNGdJNwQ#?Ma&QliUF z2xKcn(K`isIHXHv{U^8Q$cHbtH;o=To{i2F965Dz`rkN4eZhoX=0mxD0=ZOu%IRv~ zn~Kur(H$2!yr@79#1<$hh=M3~Xb)8E+{_0`*c|@0Y%$zkKtVv>?F>;oA;e# zz|Kbr$9?K@Rt&n3pxQ(ECnovj;hS~1clw_oWZ$C*kSUPU4tRj!$h^CF6D?|0f3fIO z)!2wio0SjMUjnoo+3R}ILcb0#S3X>ln83L-%ro9T{ z7CpgVKTQ>|FsWG=fwZFqXR`r@=4>rx@x^QaZroD+E~P<~0Qx=vJ?q&Z9}_bdW#?cj zk0={R3BdjVz^*Bud$N(*H+U-OJEaG$cyiPT&EI`jnpsce1$2=&hY2(ag-;XDD#`XMLMzL@~*(J~tgfNOI*L!>mo_X1P zjpqipcTFJL0auqpL;>d*O!Qvw5effg0X7UMZ14umpw`@onAmIU#rm2@I%HH0sQdu| z>E|OryS5bCEI%yAf8#Isw<%?k049y$iI5xDW=S+Y&Lcgv!MVmZ6^OAQSC<$B_CXi} z!^xk>F!qtqN3JLNpa4O^UkC~xjYLEW3txXf*q#t{mH~-VQLX(Gy2V>VZ})U)I5n?s zF~JCh$xr9cFhNTpCLqyB*onA^U(MBr3YN@iKN3{n79 zkPc#e?>0;Ze7Y7J#@_YRemsI08Psp83D#{Jet|rl5tz>Z6;JEUmECk!l?56|vo8x$ z>z$eT2BiDCJX%!EuXRf3y-HTP5E{tsSRXvsG|U53onOIZWKO0{!q_{GoLt?oL6j>^ zkXq|Z!%rZ*GZ=(sYBU$nREdT#8_41Ff#7U{?;Sp>g9=81{Kp==^>xn{@6b@703Ni= zu;l6jJHN~g5e!gH7o3=!bh|iQ>Ft%vj2;|oQ$UNg zwsSX^$IubWj~_pJdV2c%w+;7Hij+g}Sl@q;U9!m3pnC?wL|by@MzWCrxx2Wmt*uqp z*2+^*Q7OK(R#c1vM)Q@c%OA8B$^P7-VrCl65J3(Ad@7(RAi)ph;@NeoW#84>F9Exa zQNo?$3L6|7OWBEuF!ts?`E=n|Ak0??UtlB_$pB#ByUV&3@1j>3bwPSgYI1Yd~zE1!F2H07nfJ8T|AiPGv84(mf zm{JVIO#)5@pk?CWvj06YFB*WKasb?Kge6vS8<+Hf&63UN{a9)Z@u zB={YZ(Om5W8nI+E3P`x%@@Nzvl~T}yL*(}Ok7QiGRmNN`Yt4EYGKr)D5~z7^ZhRac zu3s(4AJ4NFO zZP-cd-{gY1ACHnrQ$U3i!>;q7JW@!`^k30G#-M-nIW(f!&xB0kq76hzQGA!6`2IH) zJmzrkQmx6j+%{=f$J(z}MucdWptyg9OH&F3L=0dU&;bLw0Qr(tLw*Dxff_kKP$>dx z;{JXwn1W2r9NX(dqvl^PVqWUh;N$M@?&+DVz(C~(fBscg&(KgzRCMFm^JA$_$=S_~ z&3jF5ioH`We;@kvjEwFNoFDU*@`xj(hK7gVWUD|&Vpg9-c3odzr>3UrvAx9QA;bCe zx8CgiHe0@Ck<%CRDssH=3EW)dUh}F^J*&t0{>LjT5`yx1V_I5Tb#-;2lI>zUme?28 z?M4o6Zf^GW_Wu6<=H@brBy~Lsaq_tva5yk38Ui69CB?_bUw;`dZ|3UiYHhu8jmf&{(VwlG5JdLU7PNi%M)Y2B&E$|%mm{Lxh(r5J+=$eB7I~RmZ5)|b zu>;thw{3!g_IX2|EgPvzMuHanb?}C=oz}FM_w#n=>rT&J{0?#z)ECVR=U++Ka*?N$ z%T?2+p0MH}iA)|emo9P?Ob0IcKL57ijqIY+!7xplMgjxQsQh_3hmR=DEgm{qi$S!= zw?T7N-1-Up2xuE}@l7T!dN2|S4HhhpJW7W0y*K=I0{o`gfSo%Yb>mn7@6eX_>7@!| zP3eN2R?&n(nbrreU^*Tua&*t?tL{xwGP6z6*U31P5laVfCRn;sk5$c|Ie#Trw!|W} zf|xH8R=H{g46#^PSzbwD5mZ$(IMG-mzL1U)%g1+LOleVoBc52e!}e+8g568EEM&}q zLo6X=B)=6)9*RLrZaSo58h*0k80Ak0PC-u6yE!!U{-T-B1XllEEcAN>%0$3*SV*2D zA1_*RZ@%bXCvvC4!}x=yAkqQ)m_ZWVLmYwnijxxWS7Yd zkdYZ#`&-4*fi3Ucah*dq@)y>CC~>;rcF?IO@4HvOR{GbEyjtvPbby)JVMfkY!9xfL#-epy1xD=1 zKq+}SdDY4!Q`+7ROW`H>)P(ERrV~(avb5t?3OBi$chBtFa`s(>HxFI=O!s`WvPwxx z+B`V02OM})@Y*f%K(MNoR^qJ;MsW994P_&g6=qLMSXfwyO%7C6y?ghLriGOixTC(I z&dA7+GyoP6u(%i@vI!CQlardVL+k4=ppGss>bsnu*a;W;kB^V_^7I4bmfyEO_SVTU zZ1bC#n&M)(5ub)borA)kKMx-q0^SYyp!`K`fj6WdUi7Q=uUw3pyl zKKXswLbJ#>ZeWl;*R$20Xd2)y0Ko-tLT{Fu&cpHqZUS6VLg=R9Cg+LqDzYPkgSgO& zmKLfb;J*47{-aI{>aZnG;0?*?ZwgIA%VgC($cZ`@9N91bE7+ZrZkCd*7o+m%X7auUW-HulCcmJce;Y{I>v%*hF59U2IyrS%mYm6Sv!gLhBOxcLL(cAjSG3uh1-nRi#mXB5ZecOWYy zf}j%0peewyHuLe1k{f)1Kq~V(73wHOCykN(=4NLpUcSdLICMB1j_L8_!TUnm!YU2i zbPTX2)xPuI)LJ|I;D$6lHI>LL-7IDK<0Ad9vO_whk}z?8Uq%tmKvCtH?be@|Cz<5N4YA(AQY`-O&m0}S zy@hlG?r~nxsjp4bfnfqqs9w9dyU(EFq9r6G)N?)moJ)=C{srCQ@WZ|Hz)Dqt&d{=PS><`T9~3Npt<&wQkt_B$l}YWMX1^ zRzA+I_70QfA6>dtqiKIrM;Cnp)(v&*BicS`KbK499H)TiIJQdP!dIH ziy#J?%k^{w7NGuQkCDf&05Z&A*>Y|9-N6 zdI0qeen2&v*!2K&xB2hq)ISvR#1Ak`?;j8saQ_1x{rNCfp5$Mr;{T`f+do9||5##@ s_{Wmimj`OMWgq_it{Lqb@kfb2On+loU_cK1j|3n&X(g#rNu!|u2QB#vzyJUM literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 1d59e39655bc..33fc8faa3f6d 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -2027,3 +2027,26 @@ def mock_draw_layer(renderer, layer): return original_draw_layer(renderer, layer) fig._draw_layer = mock_draw_layer + + +@image_comparison( + baseline_images=['two_pass_composite'], extensions=['png'], style='mpl20' +) +def test_two_pass_composite(): + """ + Verify that the base and overlay layers compose correctly when drawn together. + """ + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1], color='blue', lw=5) + + from matplotlib.text import Text + overlay_text = Text( + 0.5, 0.5, "Overlay Text", color='red', fontsize=20, ha='center', + transform=fig.transFigure, figure=fig + ) + fig.add_artist(overlay_text, layer="overlay") + import matplotlib.lines as mlines + overlay_line = mlines.Line2D( + [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure + ) + fig.add_artist(overlay_line, layer="overlay") From 6e899bd21819259418f29f56660376583ed7c309 Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Thu, 20 Aug 2026 17:58:59 +0530 Subject: [PATCH 04/12] Refactor get_children and add_artist layer handling --- lib/matplotlib/figure.py | 121 +++++++++++----------------- lib/matplotlib/tests/test_figure.py | 8 +- 2 files changed, 48 insertions(+), 81 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 48c2c54e6418..27bccc1b9df9 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -209,7 +209,7 @@ def __init__(self, **kwargs): self._localaxes = [] # track all Axes self.subfigs = [] self._children = [] # All artists except SubFigure and Axes - self._children_by_layer = {"base": self._children, "overlay": []} + self._children_by_layer = {"base": self._children} # Note: "patch" layer is added by Figure/SubFigure.__init__ after # self.patch is created, since FigureBase has no self.patch of its own. self.stale = True @@ -326,59 +326,39 @@ def autofmt_xdate( def get_children(self, *, layer=None): """Get a list of artists contained in the figure.""" - patch_list = self._children_by_layer.get("patch", []) - - if layer is None: - sources = [ - a for key, lst in self._children_by_layer.items() - if key != "patch" - for a in lst - ] - elif layer == "patch": - return list(patch_list) - else: - sources = self._children_by_layer.get(layer, []) - - artists = [] - lines = [] - patches = [] - texts = [] - images = [] - legends = [] - - for a in sources: - if isinstance(a, mimage.FigureImage): - images.append(a) - elif isinstance(a, mlegend.Legend): - legends.append(a) - elif isinstance(a, Line2D): - lines.append(a) - elif isinstance(a, Patch): - patches.append(a) - elif isinstance(a, Text): - texts.append(a) - else: - artists.append(a) - - prefix = patch_list if layer is None else [] - - if layer is None or layer == "base": - return [*prefix, - *artists, - *self._localaxes, - *lines, - *patches, - *texts, - *images, - *legends, - *self.subfigs] - else: - return [*artists, - *lines, - *patches, - *texts, - *images, - *legends] + layers = list(self._children_by_layer) if layer is None else [layer] + result = [] + for name in layers: + layer_children = self._children_by_layer.get(name, []) + artists = [] + lines = [] + patches = [] + texts = [] + images = [] + legends = [] + + for a in layer_children: + if isinstance(a, mimage.FigureImage): + images.append(a) + elif isinstance(a, mlegend.Legend): + legends.append(a) + elif isinstance(a, Line2D): + lines.append(a) + elif isinstance(a, Patch): + patches.append(a) + elif isinstance(a, Text): + texts.append(a) + else: + artists.append(a) + + result += artists + if name == "base": + result += self._localaxes + result += [*lines, *patches, *texts, *images, *legends] + if name == "base": + result += self.subfigs + + return result def get_figure(self, root=None): """ @@ -676,7 +656,7 @@ def add_artist(self, artist, clip=False, *, layer=None): The added artist. """ artist.set_figure(self) - target = self._children_by_layer[layer or "base"] + target = self._children_by_layer.setdefault(layer or "base", []) target.append(artist) artist._remove_method = target.remove @@ -1148,7 +1128,6 @@ def clear(self, keep_observers=False): self._children_by_layer = { "patch": [self.patch], "base": self._children, - "overlay": [], } self.subplotpars.reset() if not keep_observers: @@ -2459,9 +2438,9 @@ def __init__(self, parent, subplotspec, *, in_layout=False, transform=self.transSubfigure) self._set_artist_props(self.patch) self.patch.set_antialiased(False) - # Now that self.patch exists, register it as its own layer so that - # _draw_layer(renderer, "patch") draws it first before "base". - self._children_by_layer["patch"] = [self.patch] + # Rebuild the dict with "patch" as the first key so that iterating + # _children_by_layer in insertion order always draws patch first. + self._children_by_layer = {"patch": [self.patch], **self._children_by_layer} @property def canvas(self): @@ -2574,12 +2553,10 @@ def draw(self, renderer): try: renderer.open_group('subfigure', gid=self.get_gid()) self._apply_aspects(renderer) - # Patch layer: always first (figure background) - self._draw_layer(renderer, "patch") - # Pass 1: base layer - self._draw_layer(renderer, "base") - # Pass 2: overlay layer - self._draw_layer(renderer, "overlay") + # Draw all layers in dict insertion order: + # patch (background) → base → any additional layers. + for _layer in self._children_by_layer: + self._draw_layer(renderer, _layer) renderer.close_group('subfigure') finally: @@ -2810,9 +2787,6 @@ def __init__(self, in_layout=False) self._set_artist_props(self.patch) self.patch.set_antialiased(False) - # even if this line is removed it not cause any error - # as self.clear() below rebuilds the dictionary - self._children_by_layer["patch"] = [self.patch] self._set_base_canvas() @@ -3441,13 +3415,10 @@ def draw(self, renderer): self._apply_aspects(renderer) - # Patch layer: always first (figure background) - self._draw_layer(renderer, "patch") - # Pass 1: base layer - self._draw_layer(renderer, "base") - - # Pass 2: overlay layer - self._draw_layer(renderer, "overlay") + # Draw all layers in dict insertion order: + # patch (background) → base → any additional layers. + for _layer in self._children_by_layer: + self._draw_layer(renderer, _layer) renderer.close_group('figure') finally: diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 33fc8faa3f6d..1ef9b26c04e9 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -28,6 +28,7 @@ import matplotlib.patches as mpatch from matplotlib.offsetbox import AnchoredOffsetbox, TextArea import matplotlib.transforms as mtransforms +from matplotlib.text import Text @image_comparison(['figure_align_labels'], extensions=['png', 'svg'], style='mpl20', @@ -1977,13 +1978,11 @@ def test_two_pass_base_only(): ax.plot([0, 1], [0, 1], color='blue', lw=5) # Add overlay elements - from matplotlib.text import Text overlay_text = Text( 0.5, 0.5, "Overlay Text", color='red', fontsize=20, ha='center', transform=fig.transFigure, figure=fig ) fig.add_artist(overlay_text, layer="overlay") - import matplotlib.lines as mlines overlay_line = mlines.Line2D( [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure ) @@ -2009,13 +2008,12 @@ def test_two_pass_overlay_only(): ax.plot([0, 1], [0, 1], color='blue', lw=5) # Add overlay elements - from matplotlib.text import Text + overlay_text = Text( 0.5, 0.5, "Overlay Text", color='red', fontsize=20, ha='center', transform=fig.transFigure, figure=fig ) fig.add_artist(overlay_text, layer="overlay") - import matplotlib.lines as mlines overlay_line = mlines.Line2D( [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure ) @@ -2039,13 +2037,11 @@ def test_two_pass_composite(): fig, ax = plt.subplots() ax.plot([0, 1], [0, 1], color='blue', lw=5) - from matplotlib.text import Text overlay_text = Text( 0.5, 0.5, "Overlay Text", color='red', fontsize=20, ha='center', transform=fig.transFigure, figure=fig ) fig.add_artist(overlay_text, layer="overlay") - import matplotlib.lines as mlines overlay_line = mlines.Line2D( [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure ) From 370d2c8a3fb3301b2ab119dc062a8842581746cb Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Sun, 23 Aug 2026 12:27:37 +0530 Subject: [PATCH 05/12] Remove _children_by_layer initialization from FigureBase --- lib/matplotlib/figure.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 27bccc1b9df9..68c1f4f5e61d 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -209,9 +209,6 @@ def __init__(self, **kwargs): self._localaxes = [] # track all Axes self.subfigs = [] self._children = [] # All artists except SubFigure and Axes - self._children_by_layer = {"base": self._children} - # Note: "patch" layer is added by Figure/SubFigure.__init__ after - # self.patch is created, since FigureBase has no self.patch of its own. self.stale = True self.suppressComposite = None self.set(**kwargs) @@ -2440,7 +2437,10 @@ def __init__(self, parent, subplotspec, *, self.patch.set_antialiased(False) # Rebuild the dict with "patch" as the first key so that iterating # _children_by_layer in insertion order always draws patch first. - self._children_by_layer = {"patch": [self.patch], **self._children_by_layer} + self._children_by_layer = { + "patch": [self.patch], + "base": self._children, + } @property def canvas(self): From ede6a769ae7c67676d453683d82798fda38c29fc Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Tue, 25 Aug 2026 06:52:44 +0530 Subject: [PATCH 06/12] Simplify type-sorting logic in .get_children --- lib/matplotlib/figure.py | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 68c1f4f5e61d..31db12d9f86f 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -325,33 +325,33 @@ def get_children(self, *, layer=None): """Get a list of artists contained in the figure.""" layers = list(self._children_by_layer) if layer is None else [layer] result = [] + + # Define the types in the exact order they should be appended + ordered_types = (Line2D, Patch, Text, mimage.FigureImage, mlegend.Legend) + for name in layers: - layer_children = self._children_by_layer.get(name, []) + children = self._children_by_layer.get(name, []) + + buckets = {cls: [] for cls in ordered_types} artists = [] - lines = [] - patches = [] - texts = [] - images = [] - legends = [] - - for a in layer_children: - if isinstance(a, mimage.FigureImage): - images.append(a) - elif isinstance(a, mlegend.Legend): - legends.append(a) - elif isinstance(a, Line2D): - lines.append(a) - elif isinstance(a, Patch): - patches.append(a) - elif isinstance(a, Text): - texts.append(a) + + for a in children: + # Route the artist to its bucket, or artists + matched = next( + (cls for cls in ordered_types if isinstance(a, cls)), None + ) + if matched: + buckets[matched].append(a) else: artists.append(a) result += artists if name == "base": result += self._localaxes - result += [*lines, *patches, *texts, *images, *legends] + + for cls in ordered_types: + result += buckets[cls] + if name == "base": result += self.subfigs From 1f159cfb545cec8318b7ad89c28d4a6fb4fd3296 Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Thu, 27 Aug 2026 23:21:21 +0530 Subject: [PATCH 07/12] Update stale figure callback --- lib/matplotlib/figure.py | 19 +++++++++++++- lib/matplotlib/tests/test_figure.py | 40 +++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 31db12d9f86f..60e38a5e326d 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -65,6 +65,15 @@ def _stale_figure_callback(self, val): if (fig := self.get_figure(root=False)) is not None: + if val and hasattr(fig, '_stale_layers'): + for layer_name, artists in fig._children_by_layer.items(): + if self in artists: + fig._stale_layers[layer_name] = True + break + else: + if (self in getattr(fig, '_localaxes', []) or + self in getattr(fig, 'subfigs', [])): + fig._stale_layers["base"] = True fig.stale = val @@ -653,10 +662,14 @@ def add_artist(self, artist, clip=False, *, layer=None): The added artist. """ artist.set_figure(self) - target = self._children_by_layer.setdefault(layer or "base", []) + resolved_layer = layer or "base" + target = self._children_by_layer.setdefault(resolved_layer, []) target.append(artist) artist._remove_method = target.remove + self._stale_layers[resolved_layer] = True + artist.stale_callback = _stale_figure_callback + if not artist.is_transform_set(): artist.set_transform(self.transSubfigure) @@ -1126,6 +1139,7 @@ def clear(self, keep_observers=False): "patch": [self.patch], "base": self._children, } + self._stale_layers = {"patch": True, "base": True} self.subplotpars.reset() if not keep_observers: self._axobservers = cbook.CallbackRegistry() @@ -2441,6 +2455,7 @@ def __init__(self, parent, subplotspec, *, "patch": [self.patch], "base": self._children, } + self._stale_layers = {"patch": True, "base": True} @property def canvas(self): @@ -2561,6 +2576,7 @@ def draw(self, renderer): finally: self.stale = False + self._stale_layers = {k: False for k in self._stale_layers} @_docstring.interpd @@ -3423,6 +3439,7 @@ def draw(self, renderer): renderer.close_group('figure') finally: self.stale = False + self._stale_layers = {k: False for k in self._stale_layers} DrawEvent("draw_event", self.canvas, renderer)._process() diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 1ef9b26c04e9..9c11a27bd371 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -2046,3 +2046,43 @@ def test_two_pass_composite(): [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure ) fig.add_artist(overlay_line, layer="overlay") + + +def test_stale_layers_via_add_artist(): + fig = plt.figure() + overlay = Text(0.5, 0.9, "overlay") + fig.add_artist(overlay, layer="overlay") + fig.draw_without_rendering() + + assert fig._stale_layers["overlay"] is False + assert fig._stale_layers["base"] is False + assert fig._stale_layers["patch"] is False + # change only overlay + overlay.set_visible(False) + assert fig._stale_layers["overlay"] is True + assert fig._stale_layers["base"] is False + assert fig._stale_layers["patch"] is False + + +def test_stale_layers_via_text(): + fig = plt.figure() + t = fig.text(0.5, 0.5, "hello") + fig.draw_without_rendering() + + assert fig._stale_layers["base"] is False + assert fig._stale_layers["patch"] is False + t.set_color("red") + assert fig._stale_layers["base"] is True + assert fig._stale_layers["patch"] is False + + +def test_stale_layers_via_axes_text(): + fig, ax = plt.subplots() + t = ax.text(0.5, 0.5, "hello") + fig.draw_without_rendering() + + assert fig._stale_layers["base"] is False + assert fig._stale_layers["patch"] is False + t.set_color("red") + assert fig._stale_layers["base"] is True + assert fig._stale_layers["patch"] is False From d049ac1aaf21d83bff1aacd81b4be3daaf963ff6 Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Thu, 3 Sep 2026 13:34:22 +0530 Subject: [PATCH 08/12] Update _stale_figure_callback and move stale layer clearing in _draw_layer --- lib/matplotlib/figure.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 60e38a5e326d..2d58751ccfa5 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -68,12 +68,12 @@ def _stale_figure_callback(self, val): if val and hasattr(fig, '_stale_layers'): for layer_name, artists in fig._children_by_layer.items(): if self in artists: - fig._stale_layers[layer_name] = True + fig._stale_layers[layer_name] = val break else: if (self in getattr(fig, '_localaxes', []) or self in getattr(fig, 'subfigs', [])): - fig._stale_layers["base"] = True + fig._stale_layers["base"] = val fig.stale = val @@ -276,14 +276,18 @@ def _draw_layer(self, renderer, layer): layer : str The layer to draw. """ - artists = self._get_draw_artists(renderer, layer=layer) - if not artists: - return + try: + artists = self._get_draw_artists(renderer, layer=layer) + if not artists: + return - renderer.open_group(layer) - mimage._draw_list_compositing_images( - renderer, self, artists, self.suppressComposite) - renderer.close_group(layer) + renderer.open_group(layer) + mimage._draw_list_compositing_images( + renderer, self, artists, self.suppressComposite) + renderer.close_group(layer) + finally: + if hasattr(self, '_stale_layers'): + self._stale_layers[layer] = False def autofmt_xdate( self, bottom=0.2, rotation=30, ha='right', which='major'): @@ -2576,7 +2580,6 @@ def draw(self, renderer): finally: self.stale = False - self._stale_layers = {k: False for k in self._stale_layers} @_docstring.interpd @@ -3439,7 +3442,6 @@ def draw(self, renderer): renderer.close_group('figure') finally: self.stale = False - self._stale_layers = {k: False for k in self._stale_layers} DrawEvent("draw_event", self.canvas, renderer)._process() From a3965064ca7699c199274722f75043b636023e55 Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Sat, 5 Sep 2026 19:43:55 +0530 Subject: [PATCH 09/12] FEAT: Implement Layered Rendering Architecture in QtAgg Backend --- lib/matplotlib/backends/backend_qtagg.py | 83 +++++++++++++++++++----- lib/matplotlib/tests/test_backend_qt.py | 44 ++++++++++++- 2 files changed, 109 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index 54efb134c2b1..32cd7a50385f 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -4,9 +4,10 @@ from matplotlib.transforms import Bbox +from matplotlib.backend_bases import DrawEvent from .qt_compat import QT_API, QtCore, QtGui -from .backend_agg import FigureCanvasAgg +from .backend_agg import FigureCanvasAgg, RendererAgg from .backend_qt import _BackendQT, FigureCanvasQT from .backend_qt import ( # noqa: F401 # pylint: disable=W0611 FigureManagerQT, NavigationToolbar2QT) @@ -14,6 +15,49 @@ class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT): + def __init__(self, figure=None): + super().__init__(figure=figure) + self._layer_renderers = {} + self._renderer_key = None + + def draw(self): + """ + Render the figure using the per-layer caching optimization. + """ + fig = self.figure + w, h = self.get_width_height(physical=True) + dpi = fig.dpi + + # Run layout engine once before drawing + if fig.axes and fig.get_layout_engine() is not None: + try: + fig.get_layout_engine().execute(fig) + except ValueError: + pass + + key = (w, h, dpi) + is_resized = getattr(self, '_renderer_key', None) != key + if is_resized: + self._renderer_key = key + + for layer_name in fig._children_by_layer: + stale = fig._stale_layers.get(layer_name, True) + + # Re-render if: layer is stale OR canvas was resized + if stale or is_resized: + layer_renderer = RendererAgg(w, h, dpi) + fig._draw_layer(layer_renderer, layer_name) + + self._layer_renderers[layer_name] = layer_renderer + + fig.stale = False + + # Fire the draw event + base_renderer = self._layer_renderers.get("patch") + DrawEvent("draw_event", self, base_renderer)._process() + + self.update() + def paintEvent(self, event): """ Copy the image from the Agg canvas to the qt.drawable. @@ -23,9 +67,8 @@ def paintEvent(self, event): """ self._draw_idle() # Only does something if a draw is pending. - # If the canvas does not have a renderer, then give up and wait for - # FigureCanvasAgg.draw(self) to be called. - if not hasattr(self, 'renderer'): + # If the layers haven't been rendered yet, give up and wait for draw() + if not self._layer_renderers: return painter = QtGui.QPainter(self) @@ -46,21 +89,29 @@ def paintEvent(self, event): right = left + width # create a buffer using the image bounding box bbox = Bbox([[left, bottom], [right, top]]) - buf = memoryview(self.copy_from_bbox(bbox)) - - if QT_API == "PyQt6": - from PyQt6 import sip - ptr = int(sip.voidptr(buf)) - else: - ptr = buf painter.eraseRect(rect) # clear the widget canvas - qimage = QtGui.QImage(ptr, buf.shape[1], buf.shape[0], - QtGui.QImage.Format.Format_RGBA8888) - qimage.setDevicePixelRatio(self.device_pixel_ratio) - # set origin using original QT coordinates origin = QtCore.QPoint(rect.left(), rect.top()) - painter.drawImage(origin, qimage) + + for layer_name in self.figure._children_by_layer: + if layer_name in self._layer_renderers: + layer_renderer = self._layer_renderers[layer_name] + + buf = memoryview(layer_renderer.copy_from_bbox(bbox)) + + if QT_API == "PyQt6": + from PyQt6 import sip + ptr = int(sip.voidptr(buf)) + else: + ptr = buf + + qimage = QtGui.QImage(ptr, buf.shape[1], buf.shape[0], + QtGui.QImage.Format.Format_RGBA8888) + qimage.setDevicePixelRatio(self.device_pixel_ratio) + + # Qt's QPainter natively handles alpha blending! + painter.drawImage(origin, qimage) + self._draw_rect_callback(painter) finally: painter.end() diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index ae24effe505f..129e7d81db6a 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -185,8 +185,11 @@ def set_device_pixel_ratio(ratio): # The DPI and the renderer width/height change assert fig.dpi == dpi - assert qt_canvas.renderer.width == width - assert qt_canvas.renderer.height == height + + renderer = qt_canvas._layer_renderers["patch"] + + assert renderer.width == width + assert renderer.height == height # The actual widget size and figure logical size don't change. assert size.width() == 600 @@ -386,3 +389,40 @@ def custom_handler(signum, frame): def test_ipython(): from matplotlib.testing import ipython_in_subprocess ipython_in_subprocess("qt", {(8, 24): "qtagg", (8, 15): "QtAgg", (7, 0): "Qt5Agg"}) + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_qtagg_layer_caching(): + from matplotlib.text import Text + + fig, ax = plt.subplots() + + fig.add_artist(Text(0.5, 0.5, "patch"), layer="patch") + fig.add_artist(Text(0.5, 0.5, "base"), layer="base") + fig.add_artist(Text(0.5, 0.5, "overlay"), layer="overlay") + + with mock.patch.object( + fig, '_draw_layer', wraps=fig._draw_layer + ) as mock_draw_layer: + # 1. Initial draw: Everything is stale, so all layers must be rendered + fig.canvas.draw() + + drawn_layers = [call.args[1] for call in mock_draw_layer.call_args_list] + assert "patch" in drawn_layers + assert "base" in drawn_layers + assert "overlay" in drawn_layers + + # Reset the mock tracker for the next draw + mock_draw_layer.reset_mock() + + # 2. Second draw: ONLY make the overlay stale + fig._stale_layers["overlay"] = True + + fig.canvas.draw() + + # The caching logic should completely skip rendering 'patch' and 'base' + drawn_layers2 = [call.args[1] for call in mock_draw_layer.call_args_list] + assert "patch" not in drawn_layers2 + assert "base" not in drawn_layers2 + assert "overlay" in drawn_layers2 + assert drawn_layers2 == ["overlay"] From ff89e29c681c0478595541c96d03423ca8035c9b Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Tue, 8 Sep 2026 20:15:30 +0530 Subject: [PATCH 10/12] TEST: Add image comparison test for QtAgg layer compositing --- lib/matplotlib/backends/backend_qtagg.py | 6 ++--- .../test_backend_qt/two_pass_composite.png | Bin 0 -> 16675 bytes lib/matplotlib/tests/test_backend_qt.py | 23 ++++++++++++++++++ 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/test_backend_qt/two_pass_composite.png diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index 32cd7a50385f..3f4b22d4d4ec 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -36,15 +36,15 @@ def draw(self): pass key = (w, h, dpi) - is_resized = getattr(self, '_renderer_key', None) != key + is_resized = self._renderer_key != key if is_resized: self._renderer_key = key for layer_name in fig._children_by_layer: - stale = fig._stale_layers.get(layer_name, True) + layer_stale = fig._stale_layers.get(layer_name, True) # Re-render if: layer is stale OR canvas was resized - if stale or is_resized: + if layer_stale or is_resized: layer_renderer = RendererAgg(w, h, dpi) fig._draw_layer(layer_renderer, layer_name) diff --git a/lib/matplotlib/tests/baseline_images/test_backend_qt/two_pass_composite.png b/lib/matplotlib/tests/baseline_images/test_backend_qt/two_pass_composite.png new file mode 100644 index 0000000000000000000000000000000000000000..d84b18b84e81dedef866dcdfcc923bb82016a33c GIT binary patch literal 16675 zcmaL9bySqk_c*?ENTZaLpolcm9RdqTOGzUs-Q6i73rcsaNO$)F0@Bi55=(=0^L-XS z@7FoMe}0_f?z1y@YAP}yctdtrEgd6|@AuVGf11*hJZ>xYW zZ5L^6mv{E&F7C!oW*|jl7Y7@A7n_eCXx+@5oIl#zaj|i7uyL@Nnz`}taB}i8TNrZ* z@bYl*a+|Q3zUFw%%OSwQ&q8bI;^N>e$j)y2A3U4AlLfo8&5vq;29|^DduI>`-x%=^ zDPJt_BM2n!C?_SM?lHSNU15tXK@c_)Uy8$&!yJAh{VIeM^c8x{UlGb;{M4qtCD~h< ze$VajtzqP3>paI#JJPYoAlO*7<&<4hlp_wvCdg?J`L7XpZiEq^QZgoihZY$MFvm^vOMt zt63vQ;E=4u1)#;Tun=&xZD|OpWAru<`|A=xA~D~tJ#Ip#x=U@oZT#`CJu0!kxsuf|6}M%>8L96cD_bN@e&7dTFBOQv@> zmqnT--||Gf&sg;8524YbOziAsm#~XUJW&1{ySk)wMG%Ivo!x#Cr-@FDReW3=x6_)e zZv&Xdms?b{RXJ1i)vH(OLhhm-heo&<-HEOsasBrdlKTaDZ!p&3#4w~P>#0(UOkZ9x zF@IP95;am$|P(Ck3bWSlu+D69Q~psX_ub<5dmb`o0|bRRz zKR?~}W()ebhJ}frKHXt(R7CG~2TY#TS*Y*8|K8tyyPusH7Pv|SyS+ZA5p>BfEY@on zr3&+A(RA(V>YDKLmRW~g>;smmc+irUz^YuKNK91i$%RD8a)&hE;(eZK*C?k^Syi=Q z+Y{||&}eLdy1^y#+m7H`Wk@{`8|Fr+^;u>B^j&TxFK_!{e}#!iRf zb{+ww<02fSERx;1Jm%8Fvd7NOSj(Wl_{(3ud^tIB(QsVqb_#b~h6SeJd@(e`1X;`q z9prmDXTBRa@XPy*&9n(R#9an34yW=B-^OuU45iuo?&t~#2o$chBK!XBBAs$Dx?y?) z;&<-rb6N8J{P{>H4W0%}(;wE&WzW5usrjtE(cHHOi!BTH9>;@x%06R#x5WzG*ma@% z(-o`lltp;#=Ib{%H_sQn514Vj2vUGrCA`n_-R(2C9S)7CL})ypXj0c|cHCM~2E0?z^El#ABTfQ$ zIl#}I(UlwI6EUi`;9EERELZVqiB6Zh1Tjnl0c1T}^H#+B>iYrD)aEZjKXMc#A7sFU zF9Z$z^_T8yrv$%5jKnH45v|lcBZN(0$WY;e3>D=f{4U>ScFH!nU7fcDw+t(Xg79t- zJcLZ-Wp0nY^&LIG+F9tb{v}!#2IBo-1Xz6syk^Q}^1XpIJFn)jFjV$uzV96x@^oCF zDi{Sb>d%MU8@OmVK59D`*;{0#NA(4Gm%#&ghw<62%cna-rVJuJ zkG7l_7AySVF!`TgFutkc+q?5Z!zJrAaZ*(#k-4Dg#e-kmWN?ZvmU<4+D6fom#os{ z(RyX@U&KV>1G!|fqkzoghTE(!GbQbnw3Tf^#DfMVKG9- zW4rCT1P1Tm`>Q;s(|unKaLud&M#)yyDKxz8gS**}$=qKh?wTX$+e%R3hKMUcM#q4q znPTm-_Nmwc(Y_~1g*&~Id9NPJU~ulP@Fl~eV`Fxv(pL$j8!kvut|S-1pm5kwGFLN} z!h?B&HJm<+9J0u+Ey0* z2nZekLc6PrLTnL!CUWmnqMPUNAV@7B2u`(aYNH!~=_`GFV&3nE_7!m|+$57BN**QhE+3a9myT=G!IV1scBh|VV3A&%jEj_hY z_iHuk0i^2$WWda5yOEBFFgQ1p3^MSPE=O2OHZU8gG7^Dytut`9-EleHTFit!XJr0j zK;@w6YB?$)z#p>K<(I3JBSc#PtbaWHeN|k{vgVH<)j9 z-3h~e+WqsVsi|pPT->Jme%t+>OcVu=^&|@$8=HuT@7NE;NqmLzk$}L;N|!~?e;$C1 z1arADE;g2!hDOYGwi<{bdvkR!U%p&&ozfTVUAjA~yE@$|)2_%liV8D9LC2}z%!w?f z(S89;9@*DV=0oZ>#Z$NA^%W#61OTG*!%qCB@Y&CW>!Q9_C*4s`6XW9x1o^Uu-QMK% zeIe(XA1gmaxkh&7Ch!19Z#VAv*g+rsi8u(OBbq7Qf$SRQm^PToSJX%)!IuU$dEXYi%4UEiRAN z^RCXu+nzHpFuZsn4F=yIhKmgh4o=8@j*Jv5tMop%8^ds80%oaPiq7lCQ5x*Mz$YiC zQ?EiB5d1s-W+?yzpL)JTx9)T72P0tDF@&W+ zGo6?Xm>wOU)|;@4dA1g>kLPxQAiOl=&M*L8S!2-?P6g}>JKj$;fESC(T6HuuOZB$+ zhk%LVM|uZFaa~`Hj!)T+ZH;9pC{~~iph6oSNak8yU0rxn!lanQ@r;M()FY))Zr4wq zLe$4&snu6wy`*l@DZIjZimeG_BUq3kpZ>`cuQkGnvIAFtz#(=;0t%@|O3Tb#`l0x= zPR*dj>ok(Rm5iA9xZ@Ffn#X{v*b7#jYDOleKHtAqtKCudZMSYZIWH+k@9dc(14EZYjVDPHqqda2VwhReNE%@hkPczb3__7fmtb{__K^07odUh}suXUxjp{1WYEJ}>< zt+ptSxVJ5}ZF`AoEG0(X(eDD`sw0p*K#*ec74sMhJ|k1#PNR~Xs`(F<2!O9Sehw}b za3`Jjd7&~*X20ieS}MWkln6J%N}0r&4>b;zjvTKpn9|T|Yo+#8|N6;+4&mHZU^sv@ zCh_rG8Qwm4tpx>F7bYvu5wyAklL7yO-q)*IW+0ll_dC09@QD(Fk}f|7y(+kq(Y`sG zL9w}XGKp+3Nn&p|^j}vnAkCq?ybC;C6l1B@w#Gba`Ty9f0(xL>yDI1VqKWWwV?i(V z!>)hq{mq9G${xdu98JoKUh`@yi%9S3z56Hc`yfGzKvlisIs0>giwyUS%E}N5M3Cjh z8=xK^!3?mS8?PxEHsk*ga�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 T0XgtL5`g5Sm841~je`Ck5&jYP literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 129e7d81db6a..446509c9b90b 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -13,6 +13,9 @@ from matplotlib import pyplot as plt from matplotlib._pylab_helpers import Gcf from matplotlib import _c_internal_utils +from matplotlib.testing.decorators import image_comparison +import matplotlib.lines as mlines +from matplotlib.text import Text try: from matplotlib.backends.qt_compat import QtCore # type: ignore[attr-defined] @@ -426,3 +429,23 @@ def test_qtagg_layer_caching(): assert "base" not in drawn_layers2 assert "overlay" in drawn_layers2 assert drawn_layers2 == ["overlay"] + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +@image_comparison( + baseline_images=['two_pass_composite'], extensions=['png'], style='mpl20' +) +def test_two_pass_composite(): + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1], color='blue', lw=5) + + overlay_text = Text( + 0.5, 0.5, "Overlay Text", color='red', fontsize=20, ha='center', + transform=fig.transFigure, figure=fig + ) + fig.add_artist(overlay_text, layer="overlay") + + overlay_line = mlines.Line2D( + [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure + ) + fig.add_artist(overlay_line, layer="overlay") From 187e57dd74ba75e6b1365cc12dd58a2428ffe36c Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Tue, 8 Sep 2026 21:11:18 +0530 Subject: [PATCH 11/12] remove image comparison test for QtAgg layer compositing --- .../test_backend_qt/two_pass_composite.png | Bin 16675 -> 0 bytes lib/matplotlib/tests/test_backend_qt.py | 23 ------------------ 2 files changed, 23 deletions(-) delete mode 100644 lib/matplotlib/tests/baseline_images/test_backend_qt/two_pass_composite.png diff --git a/lib/matplotlib/tests/baseline_images/test_backend_qt/two_pass_composite.png b/lib/matplotlib/tests/baseline_images/test_backend_qt/two_pass_composite.png deleted file mode 100644 index d84b18b84e81dedef866dcdfcc923bb82016a33c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16675 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�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 T0XgtL5`g5Sm841~je`Ck5&jYP diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 446509c9b90b..129e7d81db6a 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -13,9 +13,6 @@ from matplotlib import pyplot as plt from matplotlib._pylab_helpers import Gcf from matplotlib import _c_internal_utils -from matplotlib.testing.decorators import image_comparison -import matplotlib.lines as mlines -from matplotlib.text import Text try: from matplotlib.backends.qt_compat import QtCore # type: ignore[attr-defined] @@ -429,23 +426,3 @@ def test_qtagg_layer_caching(): assert "base" not in drawn_layers2 assert "overlay" in drawn_layers2 assert drawn_layers2 == ["overlay"] - - -@pytest.mark.backend('QtAgg', skip_on_importerror=True) -@image_comparison( - baseline_images=['two_pass_composite'], extensions=['png'], style='mpl20' -) -def test_two_pass_composite(): - fig, ax = plt.subplots() - ax.plot([0, 1], [0, 1], color='blue', lw=5) - - overlay_text = Text( - 0.5, 0.5, "Overlay Text", color='red', fontsize=20, ha='center', - transform=fig.transFigure, figure=fig - ) - fig.add_artist(overlay_text, layer="overlay") - - overlay_line = mlines.Line2D( - [0, 1], [1, 0], color='red', lw=5, transform=fig.transFigure - ) - fig.add_artist(overlay_line, layer="overlay") From 370e5c55ca86b6ef7f8ea8a03b3be5774333fe51 Mon Sep 17 00:00:00 2001 From: Vikash Kumar Date: Sun, 13 Sep 2026 18:21:07 +0530 Subject: [PATCH 12/12] Update QtAgg base layer reference from 'patch' to 'base' for base renderer --- lib/matplotlib/backends/backend_qtagg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py index 3f4b22d4d4ec..9c3db52da529 100644 --- a/lib/matplotlib/backends/backend_qtagg.py +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -53,7 +53,7 @@ def draw(self): fig.stale = False # Fire the draw event - base_renderer = self._layer_renderers.get("patch") + base_renderer = self._layer_renderers.get("base") DrawEvent("draw_event", self, base_renderer)._process() self.update()