From e7ddffb84b4e22bac001b156b2efdfaba1973728 Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Fri, 23 Jan 2026 15:41:12 +0200 Subject: [PATCH 1/3] *Buttons: share self._buttons_xs --- lib/matplotlib/widgets.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index c9c5a79ae3a3..7096019bfc58 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1057,6 +1057,7 @@ def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs): self._useblit = useblit + self._buttons_xs = [0.15] * len(labels) self._buttons_ys = np.linspace(1, 0, len(labels)+2)[1:-1] label_props = _expand_text_props(label_props) @@ -1219,7 +1220,7 @@ def _init_props(self, text_size, actives, frame_props, check_props): frame_props.setdefault('facecolor', frame_props.get('color', 'none')) frame_props.setdefault('edgecolor', frame_props.pop('color', 'black')) self._frames = self.ax.scatter( - [0.15] * len(self._buttons_ys), + self._buttons_xs, self._buttons_ys, **frame_props, ) @@ -1236,7 +1237,7 @@ def _init_props(self, text_size, actives, frame_props, check_props): } check_props.setdefault('facecolor', check_props.pop('color', 'black')) self._buttons = self.ax.scatter( - [0.15] * len(self._buttons_ys), + self._buttons_xs, self._buttons_ys, **check_props ) @@ -1746,7 +1747,7 @@ def _init_props(self, text_size, active, activecolor, radio_props): radio_props.setdefault('facecolor', radio_props.pop('color', activecolor)) self._buttons = self.ax.scatter( - [.15] * len(self._buttons_ys), + self._buttons_xs, self._buttons_ys, **radio_props, ) From f6273a0faffe0234c5ba14251814966a0ac0b49d Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Fri, 23 Jan 2026 16:41:36 +0200 Subject: [PATCH 2/3] _Buttons: take layout logic to a dedicated _init function --- lib/matplotlib/tests/test_widgets.py | 3 +-- lib/matplotlib/widgets.py | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index ea1bdbe27f23..5a4569ae95cb 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1229,8 +1229,7 @@ def test__buttons_callbacks(ax, widget): "button_press_event", ax, ax.transData.inverted().transform(ax.transAxes.transform( - # (x, y) of the 0th button defined at - # `{Check,Radio}Buttons._init_props` + # (x, y) of the 0th button defined at `_Buttons._init_layout` (0.15, 0.5), )), 1, diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 7096019bfc58..64a960727256 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1057,16 +1057,7 @@ def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs): self._useblit = useblit - self._buttons_xs = [0.15] * len(labels) - self._buttons_ys = np.linspace(1, 0, len(labels)+2)[1:-1] - - label_props = _expand_text_props(label_props) - - self.labels = [ - ax.text(0.25, y, label, transform=ax.transAxes, - horizontalalignment="left", verticalalignment="center", - **props) - for y, label, props in zip(self._buttons_ys, labels, label_props)] + self._init_layout(labels, label_props) text_size = np.array([text.get_fontsize() for text in self.labels]) / 2 self._init_props(text_size, **kwargs) @@ -1077,6 +1068,18 @@ def __init__(self, ax, labels, *, useblit=True, label_props=None, **kwargs): self._observers = cbook.CallbackRegistry(signals=["clicked"]) + def _init_layout(self, labels, label_props): + self._buttons_xs = [0.15] * len(labels) + self._buttons_ys = np.linspace(1, 0, len(labels)+2)[1:-1] + + label_props = _expand_text_props(label_props) + + self.labels = [ + self.ax.text(0.25, y, label, transform=self.ax.transAxes, + horizontalalignment="left", verticalalignment="center", + **props) + for y, label, props in zip(self._buttons_ys, labels, label_props)] + def _init_props(self, text_size, **kwargs): raise NotImplementedError("This method should be defined in subclasses") From 0cc8ba593179dcde6f955664a4398963955552a6 Mon Sep 17 00:00:00 2001 From: Doron Behar Date: Sat, 24 Jan 2026 22:28:17 +0200 Subject: [PATCH 3/3] *Buttons: support more layouts --- .../next_whats_new/radio_buttons_2d_grid.rst | 54 ++++++ .../examples/widgets/radio_buttons_grid.py | 73 ++++++++ .../test_widgets/check_radio_grid_buttons.png | Bin 0 -> 22050 bytes lib/matplotlib/tests/test_widgets.py | 23 +++ lib/matplotlib/widgets.py | 160 ++++++++++++++++-- lib/matplotlib/widgets.pyi | 2 + 6 files changed, 297 insertions(+), 15 deletions(-) create mode 100644 doc/release/next_whats_new/radio_buttons_2d_grid.rst create mode 100644 galleries/examples/widgets/radio_buttons_grid.py create mode 100644 lib/matplotlib/tests/baseline_images/test_widgets/check_radio_grid_buttons.png diff --git a/doc/release/next_whats_new/radio_buttons_2d_grid.rst b/doc/release/next_whats_new/radio_buttons_2d_grid.rst new file mode 100644 index 000000000000..3840eb69211e --- /dev/null +++ b/doc/release/next_whats_new/radio_buttons_2d_grid.rst @@ -0,0 +1,54 @@ +RadioButtons and CheckButtons widgets support flexible layouts +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `.widgets.RadioButtons` and `.widgets.CheckButtons` widgets now support +arranging buttons in different layouts via the new *layout* parameter. You can +arrange buttons vertically (default), horizontally, or in a 2D grid by passing +a ``(rows, cols)`` tuple. + +See :doc:`/gallery/widgets/radio_buttons_grid` for a ``(rows, cols)`` example. + +.. plot:: + :include-source: true + :alt: Multiple sine waves with checkboxes to toggle their visibility. + + import matplotlib.pyplot as plt + import numpy as np + from matplotlib.widgets import CheckButtons + + t = np.arange(0.0, 2.0, 0.01) + s0 = np.sin(2*np.pi*t) + s1 = np.sin(4*np.pi*t) + s2 = np.sin(6*np.pi*t) + s3 = np.sin(8*np.pi*t) + + fig, axes = plt.subplot_mosaic( + [['main'], ['buttons']], + height_ratios=[8, 1], + layout="constrained", + ) + + l0, = axes['main'].plot(t, s0, lw=2, color='red', label='2 Hz') + l1, = axes['main'].plot(t, s1, lw=2, color='green', label='4 Hz') + l2, = axes['main'].plot(t, s2, lw=2, color='blue', label='6 Hz') + l3, = axes['main'].plot(t, s3, lw=2, color='purple', label='8 Hz') + axes['main'].set_xlabel('Time (s)') + axes['main'].set_ylabel('Amplitude') + + lines_by_label = {l.get_label(): l for l in [l0, l1, l2, l3]} + + axes['buttons'].set_facecolor('0.9') + check = CheckButtons( + axes['buttons'], + labels=lines_by_label.keys(), + actives=[l.get_visible() for l in lines_by_label.values()], + layout='horizontal' + ) + + def callback(label): + ln = lines_by_label[label] + ln.set_visible(not ln.get_visible()) + fig.canvas.draw_idle() + + check.on_clicked(callback) + plt.show() diff --git a/galleries/examples/widgets/radio_buttons_grid.py b/galleries/examples/widgets/radio_buttons_grid.py new file mode 100644 index 000000000000..1f6199b8e06a --- /dev/null +++ b/galleries/examples/widgets/radio_buttons_grid.py @@ -0,0 +1,73 @@ +""" +================== +Radio Buttons Grid +================== + +Using radio buttons in a 2D grid layout. + +Radio buttons can be arranged in a 2D grid by passing a ``(rows, cols)`` +tuple to the *layout* parameter. This is useful when you have multiple +related options that are best displayed in a grid format rather than a +vertical list. + +In this example, we create a color picker using a 2D grid of radio buttons +to select the line color of a plot. +""" + +import matplotlib.pyplot as plt +import numpy as np + +from matplotlib.widgets import RadioButtons + +# Generate sample data +t = np.arange(0.0, 2.0, 0.01) +s = np.sin(2 * np.pi * t) + +fig, (ax_plot, ax_buttons) = plt.subplots( + 1, + 2, + figsize=(8, 4), + width_ratios=[4, 1.4], +) + +# Create initial plot +(line,) = ax_plot.plot(t, s, lw=2, color="red") +ax_plot.set_xlabel("Time (s)") +ax_plot.set_ylabel("Amplitude") +ax_plot.set_title("Sine Wave - Click a color!") +ax_plot.grid(True, alpha=0.3) + +# Configure the radio buttons axes +ax_buttons.set_facecolor("0.9") +ax_buttons.set_title("Line Color", fontsize=12, pad=10) +# Create a 2D grid of color options (3 rows x 2 columns) +colors = ["red", "yellow", "green", "purple", "brown", "gray"] +radio = RadioButtons(ax_buttons, colors, layout=(3, 2)) + + +def color_func(label): + """Update the line color based on selected button.""" + line.set_color(label) + fig.canvas.draw() + + +radio.on_clicked(color_func) + +plt.show() + +# %% +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.widgets.RadioButtons` +# +# .. tags:: +# +# styling: color +# styling: conditional +# plot-type: line +# level: intermediate +# purpose: showcase diff --git a/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_grid_buttons.png b/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_grid_buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..c569e6afcf045a02062ce7c74c0eb86e2277eb04 GIT binary patch literal 22050 zcmeIacU08rx-M84yG?W(K@_w_ObC*L1VgKoBqfRnsN_%}S+d=>(kgO3oQ1-{-UUoi%6X%$Yf}?ppVs-D|mfO9NHEFTC%Q-nw&6<;?mu+t*Mi zl=W1F-_<6vceb(fQ| zoxQn@wTOU_kiZcEBV#8a31QJgrjioEMndA^SETrNnK?LIvzHbWr2QX1Bw%A_Dj2-5 zw+z4d=W7Z&_7n>H74nBIPCnM0LeX@i{(fBJ`r|>C%VU+!_NCG3B$HzPkSiQ>!Y9tI zGL9eEot8Lb^hj^dsbHf+md{S@-hAuq!L%(W?z|E-mZ0+&aoh~OG8NiC?HxJSZ*3Ct zYm9Vicl+1TYbo}6jX700S<{d0Q|6qG6mzgqD6fTg*t_C=P_DoH=?4C_^(18lh4PYP z*)9s@#`|9=_!j=#l%FV+%R(!T;lJP8O<6{vTv*5U3x4+MzklT4EB5aL^Y8oe?{e{f ztCm$#vNs+F1_#HCj9hbZaVdTCW_PUj&D9kgFFt?1uyVzU51$?u-l9+%uJlqkxVclF zpSssIapXZ!)nlm_zn!|wSX`7-}bN`77*FY6~tYip~N%yty0tmI9<{POh14GMEC ztIadfu*BEA=i@V-)~ct+Z`LL0h}6fccUybA>T0K4I&WqcC!m+h>*eKDpP+d!Qgu1y zi-8a2I?wx(66US6jt--}d-oo29%`Yd?LK+i?A_nL(mPLjZ!n*m8fVvnVcIgWG+Tx6PB;u&@(&P-!#;n z+UnYWSUF6T8tJu|^Tcb>Mb`ySpp|rCG;3}(g(7v+jgsEmSD(0D+$sbw%O@x(Sf8S& zTONG4+(~|JdRQB8+;QZ~@l4A`6+gMzIF-ke&uj{2PPZ4|Pp^0Dj&RPn_Vq;N6Iop` zi@I|W(vG5Di|&kqRqHsOuICb0@OPaaI!xUz>zeWB&6{HG->(gReswb9sI%7Dvv>6v zr|xa-t(0i1xaR&y(4ZlT8Zc3IDE0F#d1``AdQ7n&k07-<-P8}y_gsGQ`$<}J#-|s4 zatZd`RaF0i@88ynnSDH_nW&|iWzBM!oiJy#OJ05VQ+=v|!SUnAEgDlI=rpEDR>ZTD z-WD@so!jL-^Afa@6K=2PvcSWgRZ^;Zd8J4r(?T=Ts!8pPA9v-LE!JbjYpXlHz74gi z^2vBwh01V=#<=q@jC{`D+j@B7t2d8HV(Qv$0zLhyg$+xK)AD4g_MG;4@aWN_#qaIx zx*-8k_qil2KEJ$jvCuOguN8woiMxL;0AUytFGKj``RsI!WJ=S;M;L(kJy z>$mq{aAAsBTv=JUL-N{dEqsf~i4!M$!o$OJ9C~YL%)+@L zMsZ_fBZpxH&gp&Lbi2;84Cx|oc8SUU#;n5o~9K;!b6R z%DT~oibKACzrLq*eu`=1v@q4*SS@N^^Nv1`rQq1MZQG__e|`A%LYv#LZ2Ollmp5+Q zXw6V9eEk`Z-Y>dlz z7rhfzsU4Tqb!x!;&{3DMsE0-yc1k-iZB2L?e)1H`(w3IxA?w@R$M2InqZxPZjaJu} zFHa@f+|_Gilxx$B%ZIKP52U6E+h>_nhS^=a_VVzfgPLc%t0Su$oN(xq&juaro#?GS zsFf6gZ-@>)tebG`k7b+<&JGTNLZ=TLIDidmq|G@wIMk`D${Opo7J3z?7!;;_{CIA# zIa9HiL)T#Sh8-{K=Vtp;C+oEhdIy@)$)QEX=xlE{Y;?ZAUCe4}dSXJOzL3L=b`z+>(|GqWi<ryA#ZeZ)zB#LbeSa9xIR(4(SNU^sCA1ZsyPDPmjke)Wxfx!86T%xydeS)0*edziH#fnp^4N%a$)U z_BsFJ^nH!!vq9_n`E}_^JC8bPbXP^tjPXC}&raO3&T^wrzHon8cUgSh4#{(|DiLbo z;rShci0KEHZlQ5Q$*FJG*;>3FN#u=!9+&e-H+CPP9HXTPr{Cmo$7%^~$F%68`U(W{3q zW+-@idX_~T%^K?{eSy1?fNqpUn?v=8JNN9wLCu7~HJkVE!b70KSBbCveqW;XoRLw~ z{K7(zpn;sn!epcKz$OZ%g=<4GM?me(4+qea$3{obqY%Uxc>6z$isDVy%~H`$)z|DS z3tG|)V3&3ph|A8F^q6KjH(5A$RdAB?w79r<@WRW>!OT;qPW7UN;Z18EJ9dMmQxX`s zd8fSRB^)n$=??Z9mfRwRGSB>#a(E-9>WOUPVO@G0-g5KHD>q5mtUXy~tK~Y+M#

R=Q(k}j>BiPqXsD05!P>riU)~5N_n*?bD;qy74-5&3!*B1Y z2o)jglc1fNa^uDg{>+8G#MI5eD1R(p-P(N~S7}MQiXyj5F_{1SosE0z(u@_ABjxn= zoblCob@hE+fyW$wW?QD^BXO(G*CNmSOra#*pnRuTeSUeI#-wXL63}x%m8CVMokBNl zd}UbD@$H*`ZPD@{OsXQLu111Ndff^=z2kD6LyIZhTIfmVe;m$4Sm@9@?sM zGg0KJ$S=jNdD)nI!o@guYa!D${&Zq(*N)b_a|Th5vxsG zwnPsyvl0~_31khr;VhAU8lw`C(EY?sCADBS!KC_0#1+=PZGs8tVr$shwSl7cQU&uT z{JSbbUtmovDnf+F&fwR@W@Ka(W>(|U6?#*5HH|cxM)m@!kXug{ld5*<(&J`2Epcj) z*^j-jb9TQ2CTQjF|T4+nDx>+eW+7rF8kqPM5PLprnzOBo)HI$tjNGDaG zqo*fM)@{n{>)Tt>t`l)*9|;_$y7tA#Y9w6{TKL}XhZfM=#&V=kMtDw8DsGdDpp$Mw zAK6Fc=H@29K!a|jlYHUT3Ec2>%l4k@?Yx$1YBjXE)>nq!G0NdFFJB&cUtD}pTwLqt zpMQ38#LH`>n<$gcd-C?WS3x#Ry7H4JPe>K;5%T9X^l52narp9@jR*@bi}rr8l08eQiNNYVjVn;_u)QdBICe6bS_ngr{sR$C#yQm(kQvdD!J+aHLDSP?& zn(OTy9fJ}R6RRSQnyz*A-^|H*0OdO;C#RIf(!bpH3>{{kq8#cEO|Oa@af<)}^e)dxgI zMlQ~et42uLo~XG$JRdIS;d1pl86g|hzvMs2y zNl6zo%beJMLpM(*)9TC>GrVb<}mUT&RJVVgnFA#Ig# z37ZDAoym_U*~#)*TU+(q!lw>y)@)Iu{0^?h-D^jmYb?F+g! zD*?x}{Qdh=mJLataEdy*yJP=cxwZ_y2cTQYnUfQ5Zt48Yc$+4VmTlG#km9uB*s>eOqo{mIcNJfQ3H^VSvkF(bM@GoF zsFjwM9@M1|c(gUVzqc*ZxP0rWGiMI%+I1|`qTZjunDb~8KpA!3O=&g7u713=cH6{w zwR~F^jd$NZOZ4F|Igi}Cn-4rcacfO)wfs^YijbIL@w$a!uO)^Jlbk}i6|WiB`c>bo zT)8qH zKA_}9x>YFI8yqABZ+4z4JD$SD*~E)k)&yh4>*u@9juf%e`uzg}Cebc`Y#)x)WV2=J z0X{yy+5#Z%6kK(JYMs8S`af~$;siBO^3S+Zv}!%akz+UwS^&Qq@oIa);SL=>tjyyz z|B4i2K!Qua&YEb4$$sPA)s`b)-Xy5U`jY)4#p?AR%bl3LEJhYe|I;f)w-4;!ub><* zUcb07w}p!<@xg;%aHUSYdGkivdFWEG->(!(w%1QLnqFRgzb1dCD||Z;slcV|r??9{ zWv-vYVbTI_4GIo+Dv=B3*Le<*ABS@nCgYMu5M^zwDzl+*qDC=H#MF0oYA{jAu()UB zYY{5EevC(|e*Q&sbMwCD%zj#y8x9+&iz)!f`9!T`(d#3Z39iSExq`9PMJsLtOHnz0 z{yZUCfM$EC1O;8Z7^d$rsY%Epz+J-m7qE;rMV-`J|<#6(_mGER68ho}{>scU7MD*U5Kv%ymRh`G*8N zI-p*hH$PU+IaTO3ZQb6}W9GZ_sL3IQ=R)r2a&-M(+%zI4DMT^&2Sv;(*_p%OM&0?p)$^-AI5mGpt(VH*PM8&i2j>^c;5 zn}EK3cFCA)UM;lx@O))9@TI4}9@;!x0g@qV3K3bL}Ca|iJ@4zZ?CN@MVU6#e;B9>%eO~CL>NDYK8`)}U1Emm9KrhU&HM0QZ zrYvBepVM$_GHLm`Ja`M()yYAogx-a6C6K00tQou9_hXy(sVHU1p{(k?W;nVvrW$B_ zZ`i33rSR|{xK7CGTEVu<>15@kq4dBT<(zqx{igu2A1-YgTT;g;&qX4}(inB?M-`(e%&w z?s&*MGc$wTR|ZiI7CPNt;-~67vkk3~fem}p)T9IQTtl=Z^oG8-x7MPZ1fXQZ;(Z&( z`{CG;7L}l%pX=`75p>kqnutIC9YaGYVwMd(1Rjr%tH1ekWhcG@7#hRqa81O&`vO$u zKCR|)e=6fLwjUZ(84es)Bl^piFG@^u!?DX}m6bKYayo{G1%VJI2b$Ee#E*FMLD|uG zAB}Zar{bs0s-MVmw%!9gUX12ELz*aOW7~+J!vfhzx{UN*I$1{dwzuFX61qx5Ll02 z$>}@Uc5ok$7lmTCk?jHHvW^T8e&V-{ko^8rZ2o_-T&9~KbYFD=97gg!^9L(#M2HFX<`;aO^XdKQ1l3|bEs%O!60(sB;vxtD2En0e@)pI;-iOCm86#LjRY zrY9>ZE7wCHst0J+1n{VqoqmjZ^Dx;%Nl~#j!@O35;Y5>#iUmm~5<6W}ubX8ZLAoXR z?xNf4)93QP2^M@m0Tfr--~=Xd^EiMsDnq-HaZQ2 zdvc~bk{R4x86Jyr$E?@Mv5Q7Ow$`S5F1QZ16}F`eOg0*5gTzkay_eS3s-rPA07mSk z-nnxJMeoPG#byJ^KT$&!&U6}V<{$39!M0q~xNNgye}gh=q9#BkxM4hwoNfek)BLGs z%RaPaeXj*akJ0zr`q~N$X$#=%lUN*MmvAtVH(xhi6IXg7b85qey+#$z{+AZL%vVRp zxSAE+7H`h6k0poW3UizO_2a2|V|RUbNIie|?ssyY!F3yOr_*G8aq-MSa{Qwpw3atE zY7>xsbJhBeuV1e;nzC{ow}3*b$}eaDY~&fXmH6>ALJ!I`EZKndY-%_YDr}qqf}Uy9 zrtj|nFiUI6J`W8hkauQwmQWMYS|JMt4tjvfvTp!s17u7H45ZH%jyS8Ym6FUh!*K~+ zb z?2@B^TA&Ygaj9RV5vU>`=;SQ^aW*h8@e%4R0|y!i-nnyUP*~VO>TKsDgCFEMnX`QDJa*3MlGW3y=m7VMeP>s=z+^5Q|m+x9n zxAOUfDng`Uem`3%GKAEy7Otar>R6Ljg>D%SnKnx{0A*DQ(ES6Gsq61B@L33Qemtsx@EU1tr zhgv{n+pAZv5_{z5O?*akN0(4CN#*LPiB|Ntb}wmzuo%!e1vYsO)Pt}cy}aw^Ywqj3 zfhMGW;X+8VJ{0rY8+Il@F9093`0!wN!o|!4ZkLXSR`=0&WUhn#j6jw#>p8uidj}AC za4E~fCqdi!Becqas%%zOrD8NECsh7#?d?JGUY;&)Zv7|+PSbP7ry}J%G@c$~+j97# z2BhGRU*6oT&$4Ob&vJHhYVea^jQi`a6l!M`uX`ifANC$NZ9@&04A{y(B9f6jqZj&@~y3@ z(TGtBWpFFv2LJZkZ=`8Ku3XpuSl&yYcmgNcWqD1zy&89}TzXf+L%Ff~4~hciB|o

TF=DB5#eC3_~j&7B)LfGxorZoGQ+%0G%&RwD`z z0>8zGc-!~e5QciGmo>hbx9zn!vk#geam zG!R-0P4$UPJVg8bkhT<=t`N~Cp&qvc*mg%jeI%MBwA_z4zGg>FbVgD6dm*DQ=rUDd zZ2)mP%zqy(tY-&D-k^Io~a7uq{a^y)N$j z9@KQ`Fj&F*c0c(P)S?gd_4Qi%&wu|Jbg`PgW9QCf&xKjSV@aO_Or`ac8w;z2q4*js zMFX&L3qS<~B(v8y*w97FudO585t?PD>!bx$4NYa$h8=OhQwadiguAM#tJ5ai;8KxQ zOgF98g3F*0tFp5)Qhq7LmkH!CF;*^!0<7m$k_>5JyQtZ@+cZ?EEnBz7K=CYZnpyHp z%f${QoO|{suQ?7HvCaPc^Uu1c$Cu|%x4JpajcBiC=hi6nT+m3=IxIW-ZWGY!BchdQ z?*m{2MyWUSlZ^p(=D55`XyoZ8Ifa7f|@ za9!{C`DFzSvKWn^uwfu<_;I{*FF|M?jm`%1oAYwQ2d{1a$Xmx9$gxJF#e$t?y-MmenI+x9IX3nZOP@=w-Fr~H`g3M)^)6OUnX=f zn2!64awV!S0%x#Zwc+`i7KgjL_u=&;&;3kYv!h3%_0_YtH-n+F=h}B zxRdM-gg~?j^m1dLS98waN3Lwyf9{w+QKzLOB|kR@{o_j(wtE`!LGZ=z3E;iILrM)d zzHpHDK3=gGrKTA&x42Df5>8A4V0GA$D}NHWK^PCXtMs)mH_*km@bIK!Ss{~^vskiv zUU05vdtUfuShJ+bu>eFwMM$qKXoYPLX!7gK+#Iu6)avs{%JeoImV-lVdDRf@_U_vk z%g<8m>gqDd8tCZ>>6N7whd4ZPT_P$fl$2hRm#1Me78RX@Gz&Z65#$NG?yBU`_L4+6 zfX-r1=H7q$be^0meYZg|a!rstr~$0q0^fr0qLh>r@&sECUyQ;JDXPU71l2N}Z4T?_ zU09f%tc7~@%%*Tb3xFo0-tXQ$#cey1MSE9KF7JHImbVsnK^p*d0t#Y0j*mraUQV$O zXTTLbRaI3|R3XG^D=7TU`fC;Stb#&zqEO&rT`f?o0$KzlJ2RO7*sS}I)#_iIVW+BH zzWnrUQ4t)>kBsIi7D`7LG5Fcp+1Kxso=BbQV#uR{g zY2ZG33woOX6%s@k#KOzE1_>GmDnmuiV6pA&?9hb^j)TNo5-atG?y=_>(Gp;{xh^eC z6T%Y@eK3?B4tWCzi_n8xw{B5$M^_essA}Z9P7Yb{A~!-u$H!|Cmq6G8OnP>D*oNsvB|b7( zji~4RbxuQIvlp;P7Ax~tQ@%ucLzjZO;BWnS3%X4(;l{XI(8oSG#e66)KcvTipl1&8 zF&9mnm=HM(MtNnWGO4}j|9EWl?^+xhU|jcFN1WM)MiDa%yD{Sww)lDw)7t%?` zI?H1bmWl)GpYGWY_)z`zvh@?Ww5K5<9B{|?!=y%TO2=Xo8t3QFpVu|wsiNnk0}8>; z(;(9x!T}EA*Qv4gf6r1-Qqlmgz&q4<{`(!K%9@!&hCZa%;|PKWh!*W~N;Pt78FHw= zL3W%P&>=j??9+3Unhag9uWwhv$31Z5$j70hTSe^cYt9g{s=q(nJOWZ!BEAn|VD-+q zJYDDUc_gItcTvXJe0IDYXQw<+&t9?a|0Q3WSFHkUhn$cFB!pR%U`LoFj7h*u9KM@_ z(`Hu$1q4)3_-aALCjey*%E-(=nPfpEy>@%}&}kCTcb zWAPRfT_^hv02KHK`ujgewd(^^kUIL&>nZACBJwLlctO*Thlro3@bI@;G@4=RQk&OO zK33;puPZkf#N#c6E9WUP3lp`ZyYHnE@PV5|bas!$nQpkr&bg5`ML%Rye0_q5Af6{@ zaFp))`FZVPcE4hy%xl9m3x|!E0mi#-i8UY5#*7ERxtL*gWo|ta+pu!=YG%ZpyLa!_ zAc2qqws;9$rdClfKMG8>sllc>gGx{uK{n8xEOFAOJX(nl0!^YLr?``#?{6Y=i%1oG~;_Tm#Kj!LWXgCqewOiV904QARqt-bU$$~;0Anx z8AIzAcC~^RB)E%WauVmJEckFDe#Gc|Dm*HkHGk|=dHix=9ns@abB!1hM?fclseYh$ zuzmBEY+I7!Lj~JKB}Up0T`B3pd5B4$HmS-y!%MeL>C+!OG1`NhgzTN2wPCfBz!f4B^oFx-~=T?(JbfK^UuA8 zUC}bRD6_vw)*9|!4OH(8Xi>D*JRK~bChn5hmA5NMOY$9r>6!$&F^*gsm=*OI=IXGd zrCH+E=s0SCKaPv@GfBg6ewR*MTINaN_Lw+N>SLmIY6Gen)F@%)2x+mR++q%(UZdO-XDpa??2Iq5V6H z5QV{}^fQR`&BCbJ45$QFL#9BcMIVY@*-efi`6`koBru?uRL{>IrxdNMta9C!!F-AX zYOn-BBmtZ{I!$*yx6B17BiCQ=A)V)@$PMSO=z_f_>D+*C<%6IH`=FSS7*KBmk=)@B zeA2bakH%r5lBITls3avV-3JM*0bpy;WRV*nUmgXdt?kv^Kx)aLQwcu~ErBt(%RwO_ z`>CYviNQ^+PuA7muwlcRHEU|194C;3DFh&}FOoW&97XkO_P8?+XxrJ>cTi9;buKFa zE3)<)EP3lyXmrvouc`(+sv>krZiAd~Z3B<`AHjnOyqyG_wgQrkL|+R+K?L-R2gD-% z{oA*1U{oIHnuo;1H1Q4yMFD}BpLQCN%c-^fkF5^eCGe28z+PTn;w#Zp?Oce{2n3Co z>KXFGFgwxoZTk0!ifSNr6NnfBls!^}k;j7#zaTfT7YB-aLsvt13_qOP|LI0tt2uQV zWl}gIv#+x=`t|GAZo+h^U_dbl86O8S4@544H4Dy9h!b&9;R@C0on4GA9mr<&hG4b& zNOdNFo^WnM0ccD8BisRa8>6W_BAXS0^7I! zbigDA#RSK_jAvYOr1$W&DO11VLiW~(3M26?$j48DS$c=0#F}A{l z3{P+H8;DHCd84;G?F02@RKH+DKIbMSMA>Wl(XoTtDMVZ70oD2emOp$@AT9&?Tpf;e zP>Ls@6N#oc_SI4Gk5fPHHF9_|K=%DC=eGLP!o>s<4gwG$(TXrpbGzxIuIxb5%D5Wm z@q&Y%iOzsESR)cmO>b!=*98ElA6rG99R1XhaC^9X4Iduv`KNn1IAriCg*?Ss2mjE6 zj!*-JN-GT62*5*ZJQ&2_tLH&F31CF(yoDKo3(NC2pdz{o&5;-kiDlhi=wNXMMkc4!lw`2997<07iMVzZz*&I1ZkCHm~+s19Ff$RXOB;{~OvRGB9%}6~Z78!#Y6NE6%lgS5*<@DR z_Ue36aj0t8Es)rOh#bfZ$-)xCD`^6-8juHxA+1>r zcomJSqrczHpcnvAe!VH&`|@(dn)?P3+3FDkBy|eCzdlx#2l?F+t=1T2F6GCPpSFtb z+_@x}4s5E6TBZ)+F$2+vEdrM=pr#yDi~1W+J?@vY!reY*2%93D*F9|Qr(*(XDIQ`J z2{00NP6D=|QSp#Oh@uI}lTcCO{D8ibGYT&KK`;#iE=6xR4DDQhk;29QS6eJ-_b`@N zU=6e78PwF&IE-~rISm&dPQNk+XPXu}3Rx4sPBTj3SKVLo56H+|f}D9!Bklowry(YX zu)RdJJdEp=%Xaa+{;->LI;mT3|4W7+ZL50OI-h8|kT_sHhH&KQAw@8N0`9PZRZz@M&r2KE3%Y@~BOa;W`KfGTy%7wp#PQxu)^V3fcUhL_euyQ9V!hc~>;YH?&{URYUk2#Kszz!7#Uio{f70RrJQ`xB zKXDt9LTto9KVGm6H0a~UkB_-mU90|; zGEDCJKi>U2rG9wzClLZ+AoM^C4Rn%z;0+Rg29+a82{KdzG#xBd++Yn-Mbh>p76Q9NzDvm%-JGf4&R-R@*@HsPj~^h>ZMq}!l3K85 zkY_a}wnLleTrx`%(Kxc0r(gqVLL3%F*=IBl15$(HDq;WVZeBB~3RLQO@O-F7;k75( z7K4ymbif!Gsj}#cBp{1e&{ODx2OtdTv4Chu;1&ogiA1@fUjjm2BJo0%NV#NSGg9+N z#H#gr`+8jl$(G}I5RDc>Un;4I2s}H>b|Jz6uJu{m1Zq2JNdN`eGzO4D55Z!X>A=+1 zNusg%M*)*g4!4nEFCzaDVuMGZ) z_$$I#_8VUv@{24ekXQ$pE5 z^oo70`3#s*Bv0RiTj12szEe7i>OtiXLBgK&`40MAHuy8yL98he5eeeOvQQ9Pk)6@)2<1{e(ENw2o9(E@P_dWoI zgXJ`9NGuvSPUJZWS4Jbk^SQ+we~H~iI}aWdy2NlwE?8Q0BRNL8G!etmMTsB-@NWrP z3b5}FXCx6N<8zfOFt94&WaQOBq<^Fb3DqLA4Mf+6jrT9_FnN2pa!5wqoJQ>Jt8+E$x9?a;k7guiPa}=$0XdGsnsvRs&8N|2@d~a3uS)6?G!K#Fk+la@W)d?-BrhIHvBjt7x3p6ABL+QkOF3R3 z+86)(osHx?-`#pRfkYa)8Vy*Lb2_IHQ@J`F0zlE9oYksE%uINWgjPY@E9_rb&du>3 zJTaVWP6GpjafHo7vGEm228FHbIF7uQl#&XCUTyq22A9a>%j-Yro$zBr5cvrKG|_E> z?|t=>o?a!&q{u)!kkV=Y927h8siCD6t5W;E)crziWMtD{{H*# zbxaH&)n@1BhW)zdbO-=_7;bC`UL_P4L>;AI9Kb3ZXX#Bv#L>o%>d3Y{1g2oSK?*d) z%9;|MzEx1)3j1Pm;^awX@Pq@}DUWsXuD{a3=3_(;K4>VyrQtX`;p7FlWnDw?cjlA` z486^Vv??;Jnrf1e=W$2mJq97>E;8piC8VIDA`ac_2-?00%~hbuV@l~C9L!u`xKmNJ8rR0;R% zK4gFy+%g)I146~<$jA|#dr=_%bYebMFop|Y)bR-jRBaa2ZxJ>7_&VvrtG7y#ax5g7 z!kk-ON*W44mSPMFJ?Wje{-K;ACLw4(p-8=>aFvB%sAapMPYxVia^)&>!AUpr>4SaS4T}V2^%7Mj&Cf zKD@%*_vHH9Jn(ILC&oUQv$-S>*vF}XQ5g_ zp0iiCz2|$Bt&@`wbVDU=`Q5wUkTV5d6#@XCqDAyR(TS;a--69Nf?A|-{)s^yJ%=kcVj}MQ0Yj(Dg zOmGqhgsX=}u*QgNZAg@7Zm)UUcKC`;LoK0K!Wka&B3;FX4VY^+thir&1N}Z6=ZHK# ztYK1y$tG0bi7O#dRw7+%j;3&}WZ}b5H~Y2JUn%LM%LWkTfHNfm0PWOCIEwAHYe&$Q z6dJoy*OPqM8>PbaJ@OG zCK#4#YAwlkn}*{J5yYtzfz#sJ+FJYyVZCq^`v6FlBG?#*esT{QoA72W+vuR&*v&`6sJx)9gBuaT;MYleryxvZ0lk^ZcCz4%G3L(YhKqs}OrO+hZQZ)ngg2Cg?G1f6g^m7p>3Qa@ z1T7@F3S-ks@DuQGzn?l4dMU>)6tjy)+4oU0IWf`5kY7h6koLhr+FBE@^*6jS5zr=E zvNqblV=@To{>(­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