From 9c74f7793f067048bab2f6256daa8bd1e56938ce Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Thu, 15 Nov 2018 12:08:22 -0800 Subject: [PATCH] ENH: add arbitrary-scale functionality API: add ability to set formatter and locators from scale init FIX: rename to FuncScale DOC: add whats new FIX: simplify Mercator transform TST: simplify test --- .flake8 | 1 + doc/users/next_whats_new/2018-11-25-JMK.rst | 11 +++ examples/scales/custom_scale.py | 6 ++ examples/scales/scales.py | 71 +++++++++++++- lib/matplotlib/scale.py | 91 ++++++++++++++++++ .../test_scale/function_scales.png | Bin 0 -> 12706 bytes lib/matplotlib/tests/test_scale.py | 19 ++++ 7 files changed, 196 insertions(+), 3 deletions(-) create mode 100644 doc/users/next_whats_new/2018-11-25-JMK.rst create mode 100644 lib/matplotlib/tests/baseline_images/test_scale/function_scales.png diff --git a/.flake8 b/.flake8 index b3c38d015490..3719d82131e5 100644 --- a/.flake8 +++ b/.flake8 @@ -197,6 +197,7 @@ per-file-ignores = examples/pyplots/whats_new_99_spines.py: E231, E402 examples/recipes/placing_text_boxes.py: E501 examples/scales/power_norm.py: E402 + examples/scales/scales.py: E402 examples/shapes_and_collections/artist_reference.py: E402 examples/shapes_and_collections/collections.py: E402 examples/shapes_and_collections/compound_path.py: E402 diff --git a/doc/users/next_whats_new/2018-11-25-JMK.rst b/doc/users/next_whats_new/2018-11-25-JMK.rst new file mode 100644 index 000000000000..6915de01700a --- /dev/null +++ b/doc/users/next_whats_new/2018-11-25-JMK.rst @@ -0,0 +1,11 @@ +:orphan: + +New `~.scale.FuncScale` added for arbitrary axes scales +```````````````````````````````````````````````````````` + +A new `~.scale.FuncScale` class was added (and `~.scale.FuncTransform`) +to allow the user to have arbitrary scale transformations without having to +write a new subclass of `~.scale.ScaleBase`. This can be accessed by +``ax.set_yscale('function', functions=(forward, inverse))``, where +``forward`` and ``inverse`` are callables that return the scale transform and +its inverse. See the last example in :doc:`/gallery/scales/scales`. diff --git a/examples/scales/custom_scale.py b/examples/scales/custom_scale.py index ea73b9d45e27..b4a4ea243527 100644 --- a/examples/scales/custom_scale.py +++ b/examples/scales/custom_scale.py @@ -5,6 +5,12 @@ Create a custom scale, by implementing the scaling use for latitude data in a Mercator Projection. + +Unless you are making special use of the `~.Transform` class, you probably +don't need to use this verbose method, and instead can use +`~.matplotlib.scale.FuncScale` and the ``'function'`` option of +`~.matplotlib.axes.Axes.set_xscale` and `~.matplotlib.axes.Axes.set_yscale`. +See the last example in :doc:`/gallery/scales/scales`. """ import numpy as np diff --git a/examples/scales/scales.py b/examples/scales/scales.py index 37a783ae2d30..89352c4351a5 100644 --- a/examples/scales/scales.py +++ b/examples/scales/scales.py @@ -4,10 +4,13 @@ ====== Illustrate the scale transformations applied to axes, e.g. log, symlog, logit. + +The last two examples are examples of using the ``'function'`` scale by +supplying forward and inverse functions for the scale transformation. """ import numpy as np import matplotlib.pyplot as plt -from matplotlib.ticker import NullFormatter +from matplotlib.ticker import NullFormatter, FixedLocator # Fixing random state for reproducibility np.random.seed(19680801) @@ -19,8 +22,8 @@ x = np.arange(len(y)) # plot with various axes scales -fig, axs = plt.subplots(2, 2, sharex=True) -fig.subplots_adjust(left=0.08, right=0.98, wspace=0.3) +fig, axs = plt.subplots(3, 2, figsize=(6, 8), + constrained_layout=True) # linear ax = axs[0, 0] @@ -54,4 +57,66 @@ ax.yaxis.set_minor_formatter(NullFormatter()) +# Function x**(1/2) +def forward(x): + return x**(1/2) + + +def inverse(x): + return x**2 + + +ax = axs[2, 0] +ax.plot(x, y) +ax.set_yscale('function', functions=(forward, inverse)) +ax.set_title('function: $x^{1/2}$') +ax.grid(True) +ax.yaxis.set_major_locator(FixedLocator(np.arange(0, 1, 0.2)**2)) +ax.yaxis.set_major_locator(FixedLocator(np.arange(0, 1, 0.2))) + + +# Function Mercator transform +def forward(a): + a = np.deg2rad(a) + return np.rad2deg(np.log(np.abs(np.tan(a) + 1.0 / np.cos(a)))) + + +def inverse(a): + a = np.deg2rad(a) + return np.rad2deg(np.arctan(np.sinh(a))) + +ax = axs[2, 1] + +t = np.arange(-170.0, 170.0, 0.1) +s = t / 2. + +ax.plot(t, s, '-', lw=2) + +ax.set_yscale('function', functions=(forward, inverse)) +ax.set_title('function: Mercator') +ax.grid(True) +ax.set_xlim([-180, 180]) +ax.yaxis.set_minor_formatter(NullFormatter()) +ax.yaxis.set_major_locator(FixedLocator(np.arange(-90, 90, 30))) + plt.show() + +############################################################################# +# +# ------------ +# +# References +# """""""""" +# +# The use of the following functions, methods, classes and modules is shown +# in this example: + +import matplotlib +matplotlib.axes.Axes.set_yscale +matplotlib.axes.Axes.set_xscale +matplotlib.axis.Axis.set_major_locator +matplotlib.scale.LogitScale +matplotlib.scale.LogScale +matplotlib.scale.LinearScale +matplotlib.scale.SymmetricalLogScale +matplotlib.scale.FuncScale diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index c9f0fea1d791..9a6bef33d13d 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -94,6 +94,96 @@ def get_transform(self): return IdentityTransform() +class FuncTransform(Transform): + """ + A simple transform that takes and arbitrary function for the + forward and inverse transform. + """ + + input_dims = 1 + output_dims = 1 + is_separable = True + has_inverse = True + + def __init__(self, forward, inverse): + """ + Parameters + ---------- + + forward : callable + The forward function for the transform. This function must have + an inverse and, for best behavior, be monotonic. + It must have the signature:: + + def forward(values: array-like) -> array-like + + inverse : callable + The inverse of the forward function. Signature as ``forward``. + """ + super().__init__() + if callable(forward) and callable(inverse): + self._forward = forward + self._inverse = inverse + else: + raise ValueError('arguments to FuncTransform must ' + 'be functions') + + def transform_non_affine(self, values): + return self._forward(values) + + def inverted(self): + return FuncTransform(self._inverse, self._forward) + + +class FuncScale(ScaleBase): + """ + Provide an arbitrary scale with user-supplied function for the axis. + """ + + name = 'function' + + def __init__(self, axis, functions): + """ + Parameters + ---------- + + axis: the axis for the scale + + functions : (callable, callable) + two-tuple of the forward and inverse functions for the scale. + The forward function must have an inverse and, for best behavior, + be monotonic. + + Both functions must have the signature:: + + def forward(values: array-like) -> array-like + """ + forward, inverse = functions + transform = FuncTransform(forward, inverse) + self._transform = transform + + def get_transform(self): + """ + The transform for arbitrary scaling + """ + return self._transform + + def set_default_locators_and_formatters(self, axis): + """ + Set the locators and formatters to the same defaults as the + linear scale. + """ + axis.set_major_locator(AutoLocator()) + axis.set_major_formatter(ScalarFormatter()) + axis.set_minor_formatter(NullFormatter()) + # update the minor locator for x and y axis based on rcParams + if (axis.axis_name == 'x' and rcParams['xtick.minor.visible'] + or axis.axis_name == 'y' and rcParams['ytick.minor.visible']): + axis.set_minor_locator(AutoMinorLocator()) + else: + axis.set_minor_locator(NullLocator()) + + class LogTransformBase(Transform): input_dims = 1 output_dims = 1 @@ -557,6 +647,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos): 'log': LogScale, 'symlog': SymmetricalLogScale, 'logit': LogitScale, + 'function': FuncScale, } diff --git a/lib/matplotlib/tests/baseline_images/test_scale/function_scales.png b/lib/matplotlib/tests/baseline_images/test_scale/function_scales.png new file mode 100644 index 0000000000000000000000000000000000000000..8789aea213fd5811cc1d70e4850a3c861c2d77ef GIT binary patch literal 12706 zcmeHtXH=C-v*wGapr~L1LB%`<`NsgN!Ij2q9L={1ax=8|(b5^2cL;=Ye zM3P9(k~2*AKHuD#x$FLzyYBrnYt1?9EZ4Djzg=BbUG>yc_4b*Z)cLbiXAuNBk9s1h zh#)7t5rp73@oDfkJYB<+;E$6I;wWWe@Q*vO;T!P(XKbHnI3NhAKKzd$LnPe{{G}r1 zkvc}n#u($QZ*PQH>0@jyZ7`N*&*_|u>>bQ(tm*Evae#kqZ5%M19Gv&r42_&PSWF)9 za6d5O=6k?@kIoc>u@zuv|6e=VZ0t?gEw3K@L=ZXzB`K!t@^NXz*|AoKrEURDg%Y=w+};ux@^U~((oxpwUuv^FO&vV~6PJ$b?e2NhQe<0`p z1pMzO$`fP=Qu~}(6#P}dO@shJ?)m=zh5w35?4pI}aIeL0hDSs&^Yd%)#t}#K3)07(Hf^GkUmIw>+RS|C`!YsZQ@$3(+57AxMAz<_oN|>23zz9VjFhuTc;Jfb*C?(PGS0oV8qgI$htySCx82g z6bkirFcjQVL9^UGUWb)A{G0WCH~%@}S|{5w?WZWzZZi>r5PLmhDBc^fh+#nS$ju1LlImT<*=dsiOhO5&s^>Chj3L>m zaP<-gey#s@i}T#K+^hEdOIR1pIK@BDTjQ>fLsusY7amhd6{)I_#cfoOJT`SXVW zr!rktrgEy@FKp3%{{7s<71{+3H^rauJ&E?Z6lW;QyO&7eHqxb~vTYtGfuk{ZYL_#@9UXwMue- z((Py|G*O7xjR(bSpKXV^o2L-@he%~ zw4Mi#%mYZo#;I)^{&V}$6FfqPon z^S z`FN|CvAn-JJcrmV@m*rPkXA}nUr@7L(2Q|M<69V{^j8u?oaPT%r~Dm%ZvNPK=qMe? zN9$}Lauxh)gp zSEM(Oyge(wh&r|r1TBon*6`bW84OK24Rur(tWQ}!HF#?&BSTHqUphplMJXrLa_+7> zBe3yL*PRLU|5Ur--UCPZxA)x?mGSmh-!;{Gz3k;2LqAta5%^5UnlF^%5iL}%Pf(T0 z`SS5h@oT=AT_0_9`y<-em-u@nex>aUiM;Y-fExThYx$wq2OtJH6hp>@(%0If`pHns zLU`0-_wAm>rYXz^Ar^C}_gYK{+gL6iUlDp^b#~*SkzJQm`Yqst-kA|L`MtDbgUqP% z^@DM$0l~J(M_nQCs5GWL>rVN2ZMU5y(vURSMXN1(X*#%tiAw84Ikt&vH=2+tCMHX; zVbZkk2B#H143uW;WHvG0)5Lo))Rq!2hqi;oGc|UN?1(CHFJO2T%*b9&8n&TRg>cALQGkipQ-3-CReMm7u7OtUdRlaPMK zxFtV;o*|8FYv8nb?*m;=Dt9Sx?8J=>E5)%u$-JGQX9xbk?~Kx^}fhaztdm4 zMNJBq4%Du1xjC*EF3_XY;(Dce?$|fjq(D>E@(IjZ^$*u)_YG5Zo4w+4+~=2Qi}SgF z_l^9%-b=kIzz>(#E%dhOg&sKOeAHgOe)Xd<=^J{_8ePwt;~ya&!trk{5M=h@*LR86 z%7!^=CFHnpG2zJxG+kHBo>;f z_7BwoL7UDOO`}oc%?=q5<{O#%QMV-TdpZ&zpFZjR=sYh^VXl-ZKlk>tCWpn#;WNX_ z48NdbsQ+Bi8Ozx&RsJzmwD37^^*FDSx@`8OpS{YJ(!%J1*~~m_jq_oHp+xUy zYO!o|m2Gjf<|_dsJzr0^uf*2d&&8kzl8;+h_btB8R#BK4NXVfV;%1fSyr5X?zoSBg z)Sl2har4k{j_O~5Ebc>ie%J%BQw8o+Xy-TwbMv01N%5uUH#9j|boocVj0A+kt4Wpt z?0{w`r+N3yy60QZ8uzUs5I8>sf;-UpvubJN)OZys;kZ}o-v9GbZc@XGOJv`CJ?=%siv5{5NBC2FqS&ufcfT36fJZ{AL^g3`HC8!Y5Ta_ zVXVMirETp8IJd)b7ML-Uo;X(N-j7#Lg}HepJjt(YfVTQ{&25nCFUhiswV|~W{>hgE zFEw_b+Vi)?z2<B)mJj9?@bZidPx8;1x(voc8?w-G7bXevI&9ZGR?7_XF1k!QCpMzypUP|h;fvY3*w z(UrvSz2P0iJt}j>ONC2yaNPST)zNNGMZhLSyiO7v0px4yEQ6%Iuqf2%vgQ0QK>_+3AGS-)!xL4z-h0BSj_8fJZt=8WaVufVuPtkj}PzL3AN@{ay+>*YlO6Z&-b1Ouo5 zpTtr=n%;b!(YvVfBSAuL14SF4n`rz}6uXn!!d^PbzkI%D5+EY+Fo-IMZ-b!5@&S(k&n6A0Zo>I|#y zz(zG1{y$6LU0$V_;BX^Pg2wl5%9=ln=JZN!u56o+kIHzhCinXdABGvm(aXEhL+#rm z=)Q!-UKiF_K?Sq7AS{cA!K-tiKu&a^AU6eZ;2P?)CYQ-eZ23f6+$j=xc@8lPjueZC z+`gWFHt1J#IXk?vvYFJ~*ykeWUp;}#O|_^2)CH(2PMkJlVs1b=49(zh=SM7=!PdRs*P#PZ`^{r((%N)0J$QneWu-d{%0k25Pjb1D|JB7W-?lNx zHhx(BRFGx9ujfX(kpp9vpJ4lEGPfsffL!$*U=FRY)cNyL=OS26+j*z>=cA^8NfLlrAe(lw z!DXq;Ooj}85|Scwn;;xojy3AO!tH_)&tiAC)ISE) zFb7QRr$9;B9uofi_nCcGGWYAdJhy^pwAQ)Lv;)y09aQ;m54*uDpyrFwr}Q1XeXD*z zh;Vk*WpWc6j;p(>hmgr?zPgD%R!$uKvtO;5?SQ8?qj(Xht_%RkKH zIJxU#W?T0XB=ANfKXR?N30+N24Cw71JN&fCz}%E$s{KzZSaR+i{fGlv7dQ|6_)`&(Dkh#4U|TEp zePI{=Rliv&xNTufZNIv@S_Yiw6(5~mDD}61J&6|XNa@#m!Y5oMXJctv@oPIV)3tJj zGy;FrObefDFsSGL7CnsR56t$^zX8l%s|a)TMrQH<2ApC!Q!0Rf_}b%MG6Pw6v3y>i zTTIKXE=1wqD(-<_GYZLxO{{tNp_4#Zd(OntZQW58|}xZ9k1gV*BM0eew) zfml=%8*7Z9!m9!t@G_qpAWV~8-J&(`?QK*w*Og7imQ55bW})U|-kWaSqq_&Z!23;U zX_lB0?Y?;GsPR-9gX!S#+cj8-kPvIZk0&kN&F?ANWdV3et)^8{h~F89{W1aW>;tI> zvd_HXSe}Vhl`Jz;NPz&+lDIaviu`geDkRDZjmtD*2 zVEius4JQXFf{V~tK0MzzHaevQ9gi!97*K_Bi^;n2f-+7OZIec#lQ3lIw*a(v)W(Wk z3>CtqEYt`-6=?6D ze|d||x9a*dmE51Ka$Y!G_qPKuHVwFT28`r&;((BXFfW&AA?(sj?@Ja&Q|%RNaPg!| z;Nl!3%gxv}7x(ZBy}V5K2UF_{8BI+)uE4_~c^m z?YBG?lF-t2Y*@PNX%Ij^chFUZIc7+{x-hA&n~)6>pEsKUrSGzbTlUds_fq<~=Fq)a zHKhjw6tghKCrB5ame`vjxxP{N>kyZs`%y^p*g;dIee;wIEWQNL2GXJfY+BhNB`VGO zrDmg#Zu9>-&v6k*G^^}K1>Q@S`PJoY74RtWQOwU6JgraMz!IHiTPl3!{V!F1q5J4S z^%(boM`r??4woOoWhVcp<~$}{<|=)qcLjNLNb*|;YI6*Mt{`ZbJxOcHGyoZfOr8vV zA*G_<&F)62-@)cueJHd9>tT|5ji*sEzyR|JGC~hoL#{t0)b|y64}#~#v)9U3My#-t z6KGtDbmPoy9ElWj!`Jh1FKnR^gJVDl>WJ6PSL+XdinA7Qc$as)j4ZDb`7i;wY_$RR zRgOiysQ!t*Ju~ihfUXld^8N(zcJLs8ux;4q;ZDlEEKO6rpxA(i1Ce*gfl$9cSTb+U zH=lw94y2_>jAc7N=`aBU+FpUaRpC)oEIuGqrh1-L?0oan?m8C63^nu0!9s+Z0V;$o zKzJ;>LT4!D-AxD-L)s}Bf!e@w5yU4Cm|y5U0TkaO5|J?$RJoz{kd@B!*x~6@Z@A&v zT7CP3dvl={n_VSOQQaLKG&%7f`{Rcy5&WAwP$M#r;WimJ2T+?avL~rWqG0*Hxt(bcPTd$@Em{Q8krZ&T@`=B=t9d})y2GskvoTb!}`&Vin>6tvnRpjL5zo$X!#fG)cjlctt07_~!qul7IS0RB?k z@&3^@Ow|jCb?Mhc^e839>G}3`Ux%FQ_rA`oH!V47?XAW{dJlb_vTvHQj}jt8f~8;Q zYpAhmBq!lC`4@Q)rz`aV&gE+?Cm;tX$oqB9m~M79IYq|sOr)UjW55vg{FB%c0zit; zyO>WNKHop%KS}Dto#2Lp3=E*wL4v7;ijgdP_My=9N8>wX|OFI(u0pg zl2g|q+Zv0F75=Gt6IH%f z=ivvN2h5wiEW!X4Udki@GRKWEOO@&PQxsUIKHvDLV|2))18sUD9(!wOU<3dcXlWK2^S?OGf zgRl7(%EPeGE{@&X07M{BN;lAksBzb?qSAWV{M2p^KrX5=#x|hYTd;=F&j0{Q$i}ii z>1sU=pMGDoG%~-sJ%V5m5Gd+o%&bE5G)h!KhG#$x#C7R{2F*Rc*hqH*3~UtC{a9NH zFJGM#zhQ2v^7NyU*KQZ3hqGw}h$NEN;JPR%0Yz#`(IM``PFam_7Ox0#S$zbx^&NIX zvLK*-4jAVk67Ii3i=zjOuokGXvc6FIHMJm;EZ_?=8>7?qBWv34L8yG{#J}kmAO*Dm z+>)*tdhJWlsMxM9R>5~#xCI(>iP$@otOeM2Ps~ErAM@FxFQ|MuvskkRgtBM{d)>o7 zi#Popf63~cu4M)S%!)(%v+Jto3wVhx?@*3-HJQrNG^kV~<&bzsCzSDIRE+)1N!48p zrH9P{Qf*_ma)SaGXSiDNn*P+Od2P5BP?^D{aWgBlN~q%8bM$< zT`&s+m`r65>;aI_b@zIc_waMb;>RfxIvIF|7yu&6UM4X#`zcGKo`mI$b2gDdU4M0~ zLn8@l)qt8h34nuuNJUbd*~^HE7G_c_jlVCm`k8v>G~EICf-NO*-r?w7n#H7?W|cN!T39E!IA^z`HHKc}mz<`U@VQV8ZCuO1d&_nHQ&yAE{K`1@P# z6sV)GD{IWC1%6A@>Y&OgW89t)Cq*C-^ma1+ON80JuC>vdRdQMAG)BH#*Ngk{?SisS zLeWVO1%BVXx|rM5)TP6L$zj{(9m0xy1+ z5?Gv?{OmQ&%IEnkCiwZ zpW|t=Vjy=!aYccSh?i&8S^uHY1?T6-7-$}sep1n1AhWt0hoR|L6E_&BK=cj+5*^2I zDJwBfuIY#%5dd3#3PXo)mT#8bg7i8{z*W?D`EdNn6lOw01nCFm*C!uP$?e83cj=A} zv3b#@fD#(M!=hMDI)Yx(|W8>~g?Gb07%0O_d+$sM40V%n2*-Wht7zB(FeS!31J8sN8p{)uAT z=wdZFuZjz(nMZ!N>}XvS{mznU*1N=R(+duJii3?BU_Nt8(0z*}5Oo0vLox?Mj1Z3k zEmF@f51$=QJ`7dT3+{%nJwQGeK=wS9B&l(`_bIz<#EYG;I9oX9F#9pt6fMLDrgVetx(b zYn5ej;fHZ(n*3s}Ry)-_h_%*1)dgN&-RRz7arwkl5I#UrMJ@G@h)^bW#VJF14S~Eq ztTsy7M;pt$th-6-zBfVtrhIepYc%iYQfeoiehbXi3w#Wi^`qAmN6o+5SSJb$fOMbLl@IL2%FiaM5wGj_G7V5IW*#GyU*aXjVl#Z62rg2uZ@_ zU3cNK5o>7?&-HV9f?u@R%Cy-EO^}N=@+z;ET@3<8#}wHzmyFv*MBLu6wYR*F&2}S5 z=m9=fE@Nd7K(nKlPM-fWTlB{}z#isY-k;sOPsGd+9K%m2ss@4trW|;HboJeSLfXZ5 zrI{)?%-(OA5l*8F5R!OZZy@TV1jGqFU~a>iFVbQ(Mysf_Tmi@ZfJFm72_+(<=*kO7 z4k)rZnRsdb&O7JZ^fwEEM-KR8&#fx4a!jS4(nF-y5UBiu%CuFt$BJbS0C0wsxE8Yk zmGq6Cj)*rL zL!geF?oWvDRKRx_ii4~uTqd{Y(gf4a^_VWt5Fz@YFK#OXidA5({F>@gG$`{?Rx?|p z*nYBDo`=$)cfU^P4SR^D6aa<7l)Bx{3wqj-^qWuK_Oyi(%_UV@^7EHcAh4wWosm1L zmFzb5D~qSzj~)fmBHHb-7ursow!Z#>j1Cd<0L%e++Z3m^rtgmprjeGYKEeHYz8LMH zsEhW{^eUU+|4NA50u<5j>sIXfJ-TCKd3=1-Ve|l$q6J2iuXj*qq$UK|h>gfXO<$h(&Ab zo?66g{B0E6Y@NSUe0r#ErAREQ+-!|5X#7q)0z+->OHfMAzn`+|*f|tDkfVB+7sE)} z{bkhbPCg-m;XOh2>0RtqN9;~A8k?G-{Pi3ArW}Y3;6^aW%L3KR0^FxZ5F_n@g&j)= zgTeRNSb5rynG5Zd39BA14Wvj8=y@fnk5^0Jf8ixv|Gop-XdssJ*=>g}UkH&Lz+EH- z>QjOcR7``emIed@j(*UpyDS+y_v`*?Q`->3P>K^sD<~8dZjqgvYYQUU44JAZ0=YeN zX)>6k(Rd-cXFr>k&-OGz4SsYn?=wTf%-OAl&+<+d$BB1PRwkP|5%JmljN9RitY0Tz zBi^uDy_i)#n*WH!zu2W-2XMMo;$W8w6=T3qX*xidU!8jdz%0e z0Bwj5h9-}yB69EblUQuE3{bzLuNPPp@RDJNjqb;W4$9Y&1c2{EDa_CNZY-zSLy~~S zNsV^55;WhH7&rPECLADlZg&%@JxeT_KmwaDN-W!RQ7NEsl|aWzJgDy zFk5;*V-$bXYxYy1&H1HP$JnojN*IdffptUn|4!T0)AD z8H1x^ox%p@QtJq@M(9kyKi$|ps9i?wDO5{|P{y={S0Z>0Ln^UuAV)3W=_ZiRih1+K zu6sdCri0XWZ|Z~))dXrvTL%%9q(ffVJX;~1jRh{eR!#`Iu6^&bT@0vsV}-v|uu;e4 zuAH081LZ)O1OZ~oc!F#PmllyAJgS;F0Q&x~Sf`{$2N+QkSmz$n2IGa{_d%D+C&TtV;}7GN5WY!bIUU{yP7qDl81VhC7v92w94kr+{rtMButQbe@ZEt&F+o4 z-n?-Xv6e5X>ttTpexpxmCU7xRf!}&`fCzabaFXs`G8oQ*2xdu^X!B^=p64KcHpc$J z-s8(2i1%G7hce+2jns%*uhE`?mK*5~kGFb3OS|kP1)>Lr_mbM% z{jpW0Wq`~R&H!CduE1k=Rz6m-P(P#MN4j!Zi%vGeEPNRom}WS2DZi z|4@bZa(hh0+x@hn3ZLjl9p1Aje7WYE2pVCqMW1vj-+ybse1ahV zKv%#~!rTqzY-ydfwx0qUlMT~pm)sHLgEwf?s0{2Ed)Ab)?raTYWu$B!KdfLxfo2!g zn4s*gM>$iCP4wdpdzOAPK7CFE+2J}tHui(#9FX74AxaJ8j9=Q*J=2QuPpjIFXX1k# zfAZw0`8T*I^8A|UrD%aNId2{=StXtQl4dJUNaVusO}Rr144tTFEmh8 zzivPJhAsBk=#Wtid}Xo)h7j(Y)H}IG^pbgdPi5Xp5%34VpvC!V3T~uQx{lxYq~hp} z(i<#9x9QI>#-+edbP8#JlC|kZM^RW>LqjPVmK7^+nMWLQzR$DG0-r))-VpDi1 z6+Ik8!F->4TW*%kg^WD@gy;rxm4VU>knF$&1-MPU4j8Ba1;tsPz~sG^MuDaIxnL!B z=SusYOTB`(#j1LKy=3PT5@V{A0uyKMgeN``wxWwPJ!7Q7xM1HdVU~rpii5@M&~d%Z zyGt|5k8#r1P;8sFjhQCqTCzZgt@iqsq(i-)aC!5p;y{hW_AjZ9b0W)>AGH#IdC3|bm@ z5ZsExk3uVefnqQn-5O`W<@^+--KPh9Wi{{Re>O??0W literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index ebe5c4de9ed7..26822a7adc69 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -1,6 +1,7 @@ from matplotlib.testing.decorators import image_comparison import matplotlib.pyplot as plt from matplotlib.scale import Log10Transform, InvertedLog10Transform + import numpy as np import io import platform @@ -148,3 +149,21 @@ def test_invalid_log_lims(): with pytest.warns(UserWarning): ax.set_ylim(top=-1) assert ax.get_ylim() == original_ylim + + +@image_comparison(baseline_images=['function_scales'], remove_text=True, + extensions=['png'], style='mpl20') +def test_function_scale(): + def inverse(x): + return x**2 + + def forward(x): + return x**(1/2) + + fig, ax = plt.subplots() + + x = np.arange(1, 1000) + + ax.plot(x, x) + ax.set_xscale('function', functions=(forward, inverse)) + ax.set_xlim(1, 1000) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy