test_axes3d.py 90 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688
  1. import functools
  2. import itertools
  3. import platform
  4. import sys
  5. import pytest
  6. from mpl_toolkits.mplot3d import Axes3D, axes3d, proj3d, art3d
  7. from mpl_toolkits.mplot3d.axes3d import _Quaternion as Quaternion
  8. import matplotlib as mpl
  9. from matplotlib.backend_bases import (MouseButton, MouseEvent,
  10. NavigationToolbar2)
  11. from matplotlib import cm
  12. from matplotlib import colors as mcolors, patches as mpatch
  13. from matplotlib.testing.decorators import image_comparison, check_figures_equal
  14. from matplotlib.testing.widgets import mock_event
  15. from matplotlib.collections import LineCollection, PolyCollection
  16. from matplotlib.patches import Circle, PathPatch
  17. from matplotlib.path import Path
  18. from matplotlib.text import Text
  19. import matplotlib.pyplot as plt
  20. import numpy as np
  21. mpl3d_image_comparison = functools.partial(
  22. image_comparison, remove_text=True, style='default')
  23. def plot_cuboid(ax, scale):
  24. # plot a rectangular cuboid with side lengths given by scale (x, y, z)
  25. r = [0, 1]
  26. pts = itertools.combinations(np.array(list(itertools.product(r, r, r))), 2)
  27. for start, end in pts:
  28. if np.sum(np.abs(start - end)) == r[1] - r[0]:
  29. ax.plot3D(*zip(start*np.array(scale), end*np.array(scale)))
  30. @check_figures_equal(extensions=["png"])
  31. def test_invisible_axes(fig_test, fig_ref):
  32. ax = fig_test.subplots(subplot_kw=dict(projection='3d'))
  33. ax.set_visible(False)
  34. @mpl3d_image_comparison(['grid_off.png'], style='mpl20')
  35. def test_grid_off():
  36. fig = plt.figure()
  37. ax = fig.add_subplot(projection='3d')
  38. ax.grid(False)
  39. @mpl3d_image_comparison(['invisible_ticks_axis.png'], style='mpl20')
  40. def test_invisible_ticks_axis():
  41. fig = plt.figure()
  42. ax = fig.add_subplot(projection='3d')
  43. ax.set_xticks([])
  44. ax.set_yticks([])
  45. ax.set_zticks([])
  46. for axis in [ax.xaxis, ax.yaxis, ax.zaxis]:
  47. axis.line.set_visible(False)
  48. @mpl3d_image_comparison(['axis_positions.png'], remove_text=False, style='mpl20')
  49. def test_axis_positions():
  50. positions = ['upper', 'lower', 'both', 'none']
  51. fig, axs = plt.subplots(2, 2, subplot_kw={'projection': '3d'})
  52. for ax, pos in zip(axs.flatten(), positions):
  53. for axis in ax.xaxis, ax.yaxis, ax.zaxis:
  54. axis.set_label_position(pos)
  55. axis.set_ticks_position(pos)
  56. title = f'{pos}'
  57. ax.set(xlabel='x', ylabel='y', zlabel='z', title=title)
  58. @mpl3d_image_comparison(['aspects.png'], remove_text=False, style='mpl20')
  59. def test_aspects():
  60. aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz', 'equal')
  61. _, axs = plt.subplots(2, 3, subplot_kw={'projection': '3d'})
  62. for ax in axs.flatten()[0:-1]:
  63. plot_cuboid(ax, scale=[1, 1, 5])
  64. # plot a cube as well to cover github #25443
  65. plot_cuboid(axs[1][2], scale=[1, 1, 1])
  66. for i, ax in enumerate(axs.flatten()):
  67. ax.set_title(aspects[i])
  68. ax.set_box_aspect((3, 4, 5))
  69. ax.set_aspect(aspects[i], adjustable='datalim')
  70. axs[1][2].set_title('equal (cube)')
  71. @mpl3d_image_comparison(['aspects_adjust_box.png'],
  72. remove_text=False, style='mpl20')
  73. def test_aspects_adjust_box():
  74. aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz')
  75. fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'},
  76. figsize=(11, 3))
  77. for i, ax in enumerate(axs):
  78. plot_cuboid(ax, scale=[4, 3, 5])
  79. ax.set_title(aspects[i])
  80. ax.set_aspect(aspects[i], adjustable='box')
  81. def test_axes3d_repr():
  82. fig = plt.figure()
  83. ax = fig.add_subplot(projection='3d')
  84. ax.set_label('label')
  85. ax.set_title('title')
  86. ax.set_xlabel('x')
  87. ax.set_ylabel('y')
  88. ax.set_zlabel('z')
  89. assert repr(ax) == (
  90. "<Axes3D: label='label', "
  91. "title={'center': 'title'}, xlabel='x', ylabel='y', zlabel='z'>")
  92. @mpl3d_image_comparison(['axes3d_primary_views.png'], style='mpl20',
  93. tol=0.05 if sys.platform == "darwin" else 0)
  94. def test_axes3d_primary_views():
  95. # (elev, azim, roll)
  96. views = [(90, -90, 0), # XY
  97. (0, -90, 0), # XZ
  98. (0, 0, 0), # YZ
  99. (-90, 90, 0), # -XY
  100. (0, 90, 0), # -XZ
  101. (0, 180, 0)] # -YZ
  102. # When viewing primary planes, draw the two visible axes so they intersect
  103. # at their low values
  104. fig, axs = plt.subplots(2, 3, subplot_kw={'projection': '3d'})
  105. for i, ax in enumerate(axs.flat):
  106. ax.set_xlabel('x')
  107. ax.set_ylabel('y')
  108. ax.set_zlabel('z')
  109. ax.set_proj_type('ortho')
  110. ax.view_init(elev=views[i][0], azim=views[i][1], roll=views[i][2])
  111. plt.tight_layout()
  112. @mpl3d_image_comparison(['bar3d.png'], style='mpl20')
  113. def test_bar3d():
  114. fig = plt.figure()
  115. ax = fig.add_subplot(projection='3d')
  116. for c, z in zip(['r', 'g', 'b', 'y'], [30, 20, 10, 0]):
  117. xs = np.arange(20)
  118. ys = np.arange(20)
  119. cs = [c] * len(xs)
  120. cs[0] = 'c'
  121. ax.bar(xs, ys, zs=z, zdir='y', align='edge', color=cs, alpha=0.8)
  122. def test_bar3d_colors():
  123. fig = plt.figure()
  124. ax = fig.add_subplot(projection='3d')
  125. for c in ['red', 'green', 'blue', 'yellow']:
  126. xs = np.arange(len(c))
  127. ys = np.zeros_like(xs)
  128. zs = np.zeros_like(ys)
  129. # Color names with same length as xs/ys/zs should not be split into
  130. # individual letters.
  131. ax.bar3d(xs, ys, zs, 1, 1, 1, color=c)
  132. @mpl3d_image_comparison(['bar3d_shaded.png'], style='mpl20')
  133. def test_bar3d_shaded():
  134. x = np.arange(4)
  135. y = np.arange(5)
  136. x2d, y2d = np.meshgrid(x, y)
  137. x2d, y2d = x2d.ravel(), y2d.ravel()
  138. z = x2d + y2d + 1 # Avoid triggering bug with zero-depth boxes.
  139. views = [(30, -60, 0), (30, 30, 30), (-30, 30, -90), (300, -30, 0)]
  140. fig = plt.figure(figsize=plt.figaspect(1 / len(views)))
  141. axs = fig.subplots(
  142. 1, len(views),
  143. subplot_kw=dict(projection='3d')
  144. )
  145. for ax, (elev, azim, roll) in zip(axs, views):
  146. ax.bar3d(x2d, y2d, x2d * 0, 1, 1, z, shade=True)
  147. ax.view_init(elev=elev, azim=azim, roll=roll)
  148. fig.canvas.draw()
  149. @mpl3d_image_comparison(['bar3d_notshaded.png'], style='mpl20')
  150. def test_bar3d_notshaded():
  151. fig = plt.figure()
  152. ax = fig.add_subplot(projection='3d')
  153. x = np.arange(4)
  154. y = np.arange(5)
  155. x2d, y2d = np.meshgrid(x, y)
  156. x2d, y2d = x2d.ravel(), y2d.ravel()
  157. z = x2d + y2d
  158. ax.bar3d(x2d, y2d, x2d * 0, 1, 1, z, shade=False)
  159. fig.canvas.draw()
  160. def test_bar3d_lightsource():
  161. fig = plt.figure()
  162. ax = fig.add_subplot(1, 1, 1, projection="3d")
  163. ls = mcolors.LightSource(azdeg=0, altdeg=90)
  164. length, width = 3, 4
  165. area = length * width
  166. x, y = np.meshgrid(np.arange(length), np.arange(width))
  167. x = x.ravel()
  168. y = y.ravel()
  169. dz = x + y
  170. color = [cm.coolwarm(i/area) for i in range(area)]
  171. collection = ax.bar3d(x=x, y=y, z=0,
  172. dx=1, dy=1, dz=dz,
  173. color=color, shade=True, lightsource=ls)
  174. # Testing that the custom 90° lightsource produces different shading on
  175. # the top facecolors compared to the default, and that those colors are
  176. # precisely (within floating point rounding errors of 4 ULP) the colors
  177. # from the colormap, due to the illumination parallel to the z-axis.
  178. np.testing.assert_array_max_ulp(color, collection._facecolor3d[1::6], 4)
  179. @mpl3d_image_comparison(['contour3d.png'], style='mpl20',
  180. tol=0 if platform.machine() == 'x86_64' else 0.002)
  181. def test_contour3d():
  182. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  183. fig = plt.figure()
  184. ax = fig.add_subplot(projection='3d')
  185. X, Y, Z = axes3d.get_test_data(0.05)
  186. ax.contour(X, Y, Z, zdir='z', offset=-100, cmap=cm.coolwarm)
  187. ax.contour(X, Y, Z, zdir='x', offset=-40, cmap=cm.coolwarm)
  188. ax.contour(X, Y, Z, zdir='y', offset=40, cmap=cm.coolwarm)
  189. ax.axis(xmin=-40, xmax=40, ymin=-40, ymax=40, zmin=-100, zmax=100)
  190. @mpl3d_image_comparison(['contour3d_extend3d.png'], style='mpl20')
  191. def test_contour3d_extend3d():
  192. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  193. fig = plt.figure()
  194. ax = fig.add_subplot(projection='3d')
  195. X, Y, Z = axes3d.get_test_data(0.05)
  196. ax.contour(X, Y, Z, zdir='z', offset=-100, cmap=cm.coolwarm, extend3d=True)
  197. ax.set_xlim(-30, 30)
  198. ax.set_ylim(-20, 40)
  199. ax.set_zlim(-80, 80)
  200. @mpl3d_image_comparison(['contourf3d.png'], style='mpl20')
  201. def test_contourf3d():
  202. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  203. fig = plt.figure()
  204. ax = fig.add_subplot(projection='3d')
  205. X, Y, Z = axes3d.get_test_data(0.05)
  206. ax.contourf(X, Y, Z, zdir='z', offset=-100, cmap=cm.coolwarm)
  207. ax.contourf(X, Y, Z, zdir='x', offset=-40, cmap=cm.coolwarm)
  208. ax.contourf(X, Y, Z, zdir='y', offset=40, cmap=cm.coolwarm)
  209. ax.set_xlim(-40, 40)
  210. ax.set_ylim(-40, 40)
  211. ax.set_zlim(-100, 100)
  212. @mpl3d_image_comparison(['contourf3d_fill.png'], style='mpl20')
  213. def test_contourf3d_fill():
  214. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  215. fig = plt.figure()
  216. ax = fig.add_subplot(projection='3d')
  217. X, Y = np.meshgrid(np.arange(-2, 2, 0.25), np.arange(-2, 2, 0.25))
  218. Z = X.clip(0, 0)
  219. # This produces holes in the z=0 surface that causes rendering errors if
  220. # the Poly3DCollection is not aware of path code information (issue #4784)
  221. Z[::5, ::5] = 0.1
  222. ax.contourf(X, Y, Z, offset=0, levels=[-0.1, 0], cmap=cm.coolwarm)
  223. ax.set_xlim(-2, 2)
  224. ax.set_ylim(-2, 2)
  225. ax.set_zlim(-1, 1)
  226. @pytest.mark.parametrize('extend, levels', [['both', [2, 4, 6]],
  227. ['min', [2, 4, 6, 8]],
  228. ['max', [0, 2, 4, 6]]])
  229. @check_figures_equal(extensions=["png"])
  230. def test_contourf3d_extend(fig_test, fig_ref, extend, levels):
  231. X, Y = np.meshgrid(np.arange(-2, 2, 0.25), np.arange(-2, 2, 0.25))
  232. # Z is in the range [0, 8]
  233. Z = X**2 + Y**2
  234. # Manually set the over/under colors to be the end of the colormap
  235. cmap = mpl.colormaps['viridis'].copy()
  236. cmap.set_under(cmap(0))
  237. cmap.set_over(cmap(255))
  238. # Set vmin/max to be the min/max values plotted on the reference image
  239. kwargs = {'vmin': 1, 'vmax': 7, 'cmap': cmap}
  240. ax_ref = fig_ref.add_subplot(projection='3d')
  241. ax_ref.contourf(X, Y, Z, levels=[0, 2, 4, 6, 8], **kwargs)
  242. ax_test = fig_test.add_subplot(projection='3d')
  243. ax_test.contourf(X, Y, Z, levels, extend=extend, **kwargs)
  244. for ax in [ax_ref, ax_test]:
  245. ax.set_xlim(-2, 2)
  246. ax.set_ylim(-2, 2)
  247. ax.set_zlim(-10, 10)
  248. @mpl3d_image_comparison(['tricontour.png'], tol=0.02, style='mpl20')
  249. def test_tricontour():
  250. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  251. fig = plt.figure()
  252. np.random.seed(19680801)
  253. x = np.random.rand(1000) - 0.5
  254. y = np.random.rand(1000) - 0.5
  255. z = -(x**2 + y**2)
  256. ax = fig.add_subplot(1, 2, 1, projection='3d')
  257. ax.tricontour(x, y, z)
  258. ax = fig.add_subplot(1, 2, 2, projection='3d')
  259. ax.tricontourf(x, y, z)
  260. def test_contour3d_1d_input():
  261. # Check that 1D sequences of different length for {x, y} doesn't error
  262. fig = plt.figure()
  263. ax = fig.add_subplot(projection='3d')
  264. nx, ny = 30, 20
  265. x = np.linspace(-10, 10, nx)
  266. y = np.linspace(-10, 10, ny)
  267. z = np.random.randint(0, 2, [ny, nx])
  268. ax.contour(x, y, z, [0.5])
  269. @mpl3d_image_comparison(['lines3d.png'], style='mpl20')
  270. def test_lines3d():
  271. fig = plt.figure()
  272. ax = fig.add_subplot(projection='3d')
  273. theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
  274. z = np.linspace(-2, 2, 100)
  275. r = z ** 2 + 1
  276. x = r * np.sin(theta)
  277. y = r * np.cos(theta)
  278. ax.plot(x, y, z)
  279. @check_figures_equal(extensions=["png"])
  280. def test_plot_scalar(fig_test, fig_ref):
  281. ax1 = fig_test.add_subplot(projection='3d')
  282. ax1.plot([1], [1], "o")
  283. ax2 = fig_ref.add_subplot(projection='3d')
  284. ax2.plot(1, 1, "o")
  285. def test_invalid_line_data():
  286. with pytest.raises(RuntimeError, match='x must be'):
  287. art3d.Line3D(0, [], [])
  288. with pytest.raises(RuntimeError, match='y must be'):
  289. art3d.Line3D([], 0, [])
  290. with pytest.raises(RuntimeError, match='z must be'):
  291. art3d.Line3D([], [], 0)
  292. line = art3d.Line3D([], [], [])
  293. with pytest.raises(RuntimeError, match='x must be'):
  294. line.set_data_3d(0, [], [])
  295. with pytest.raises(RuntimeError, match='y must be'):
  296. line.set_data_3d([], 0, [])
  297. with pytest.raises(RuntimeError, match='z must be'):
  298. line.set_data_3d([], [], 0)
  299. @mpl3d_image_comparison(['mixedsubplot.png'], style='mpl20')
  300. def test_mixedsubplots():
  301. def f(t):
  302. return np.cos(2*np.pi*t) * np.exp(-t)
  303. t1 = np.arange(0.0, 5.0, 0.1)
  304. t2 = np.arange(0.0, 5.0, 0.02)
  305. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  306. fig = plt.figure(figsize=plt.figaspect(2.))
  307. ax = fig.add_subplot(2, 1, 1)
  308. ax.plot(t1, f(t1), 'bo', t2, f(t2), 'k--', markerfacecolor='green')
  309. ax.grid(True)
  310. ax = fig.add_subplot(2, 1, 2, projection='3d')
  311. X, Y = np.meshgrid(np.arange(-5, 5, 0.25), np.arange(-5, 5, 0.25))
  312. R = np.hypot(X, Y)
  313. Z = np.sin(R)
  314. ax.plot_surface(X, Y, Z, rcount=40, ccount=40,
  315. linewidth=0, antialiased=False)
  316. ax.set_zlim3d(-1, 1)
  317. @check_figures_equal(extensions=['png'])
  318. def test_tight_layout_text(fig_test, fig_ref):
  319. # text is currently ignored in tight layout. So the order of text() and
  320. # tight_layout() calls should not influence the result.
  321. ax1 = fig_test.add_subplot(projection='3d')
  322. ax1.text(.5, .5, .5, s='some string')
  323. fig_test.tight_layout()
  324. ax2 = fig_ref.add_subplot(projection='3d')
  325. fig_ref.tight_layout()
  326. ax2.text(.5, .5, .5, s='some string')
  327. @mpl3d_image_comparison(['scatter3d.png'], style='mpl20')
  328. def test_scatter3d():
  329. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  330. fig = plt.figure()
  331. ax = fig.add_subplot(projection='3d')
  332. ax.scatter(np.arange(10), np.arange(10), np.arange(10),
  333. c='r', marker='o')
  334. x = y = z = np.arange(10, 20)
  335. ax.scatter(x, y, z, c='b', marker='^')
  336. z[-1] = 0 # Check that scatter() copies the data.
  337. # Ensure empty scatters do not break.
  338. ax.scatter([], [], [], c='r', marker='X')
  339. @mpl3d_image_comparison(['scatter3d_color.png'], style='mpl20')
  340. def test_scatter3d_color():
  341. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  342. fig = plt.figure()
  343. ax = fig.add_subplot(projection='3d')
  344. # Check that 'none' color works; these two should overlay to produce the
  345. # same as setting just `color`.
  346. ax.scatter(np.arange(10), np.arange(10), np.arange(10),
  347. facecolor='r', edgecolor='none', marker='o')
  348. ax.scatter(np.arange(10), np.arange(10), np.arange(10),
  349. facecolor='none', edgecolor='r', marker='o')
  350. ax.scatter(np.arange(10, 20), np.arange(10, 20), np.arange(10, 20),
  351. color='b', marker='s')
  352. @mpl3d_image_comparison(['scatter3d_linewidth.png'], style='mpl20')
  353. def test_scatter3d_linewidth():
  354. fig = plt.figure()
  355. ax = fig.add_subplot(projection='3d')
  356. # Check that array-like linewidth can be set
  357. ax.scatter(np.arange(10), np.arange(10), np.arange(10),
  358. marker='o', linewidth=np.arange(10))
  359. @check_figures_equal(extensions=['png'])
  360. def test_scatter3d_linewidth_modification(fig_ref, fig_test):
  361. # Changing Path3DCollection linewidths with array-like post-creation
  362. # should work correctly.
  363. ax_test = fig_test.add_subplot(projection='3d')
  364. c = ax_test.scatter(np.arange(10), np.arange(10), np.arange(10),
  365. marker='o')
  366. c.set_linewidths(np.arange(10))
  367. ax_ref = fig_ref.add_subplot(projection='3d')
  368. ax_ref.scatter(np.arange(10), np.arange(10), np.arange(10), marker='o',
  369. linewidths=np.arange(10))
  370. @check_figures_equal(extensions=['png'])
  371. def test_scatter3d_modification(fig_ref, fig_test):
  372. # Changing Path3DCollection properties post-creation should work correctly.
  373. ax_test = fig_test.add_subplot(projection='3d')
  374. c = ax_test.scatter(np.arange(10), np.arange(10), np.arange(10),
  375. marker='o')
  376. c.set_facecolor('C1')
  377. c.set_edgecolor('C2')
  378. c.set_alpha([0.3, 0.7] * 5)
  379. assert c.get_depthshade()
  380. c.set_depthshade(False)
  381. assert not c.get_depthshade()
  382. c.set_sizes(np.full(10, 75))
  383. c.set_linewidths(3)
  384. ax_ref = fig_ref.add_subplot(projection='3d')
  385. ax_ref.scatter(np.arange(10), np.arange(10), np.arange(10), marker='o',
  386. facecolor='C1', edgecolor='C2', alpha=[0.3, 0.7] * 5,
  387. depthshade=False, s=75, linewidths=3)
  388. @pytest.mark.parametrize('depthshade', [True, False])
  389. @check_figures_equal(extensions=['png'])
  390. def test_scatter3d_sorting(fig_ref, fig_test, depthshade):
  391. """Test that marker properties are correctly sorted."""
  392. y, x = np.mgrid[:10, :10]
  393. z = np.arange(x.size).reshape(x.shape)
  394. sizes = np.full(z.shape, 25)
  395. sizes[0::2, 0::2] = 100
  396. sizes[1::2, 1::2] = 100
  397. facecolors = np.full(z.shape, 'C0')
  398. facecolors[:5, :5] = 'C1'
  399. facecolors[6:, :4] = 'C2'
  400. facecolors[6:, 6:] = 'C3'
  401. edgecolors = np.full(z.shape, 'C4')
  402. edgecolors[1:5, 1:5] = 'C5'
  403. edgecolors[5:9, 1:5] = 'C6'
  404. edgecolors[5:9, 5:9] = 'C7'
  405. linewidths = np.full(z.shape, 2)
  406. linewidths[0::2, 0::2] = 5
  407. linewidths[1::2, 1::2] = 5
  408. x, y, z, sizes, facecolors, edgecolors, linewidths = (
  409. a.flatten()
  410. for a in [x, y, z, sizes, facecolors, edgecolors, linewidths]
  411. )
  412. ax_ref = fig_ref.add_subplot(projection='3d')
  413. sets = (np.unique(a) for a in [sizes, facecolors, edgecolors, linewidths])
  414. for s, fc, ec, lw in itertools.product(*sets):
  415. subset = (
  416. (sizes != s) |
  417. (facecolors != fc) |
  418. (edgecolors != ec) |
  419. (linewidths != lw)
  420. )
  421. subset = np.ma.masked_array(z, subset, dtype=float)
  422. # When depth shading is disabled, the colors are passed through as
  423. # single-item lists; this triggers single path optimization. The
  424. # following reshaping is a hack to disable that, since the optimization
  425. # would not occur for the full scatter which has multiple colors.
  426. fc = np.repeat(fc, sum(~subset.mask))
  427. ax_ref.scatter(x, y, subset, s=s, fc=fc, ec=ec, lw=lw, alpha=1,
  428. depthshade=depthshade)
  429. ax_test = fig_test.add_subplot(projection='3d')
  430. ax_test.scatter(x, y, z, s=sizes, fc=facecolors, ec=edgecolors,
  431. lw=linewidths, alpha=1, depthshade=depthshade)
  432. @pytest.mark.parametrize('azim', [-50, 130]) # yellow first, blue first
  433. @check_figures_equal(extensions=['png'])
  434. def test_marker_draw_order_data_reversed(fig_test, fig_ref, azim):
  435. """
  436. Test that the draw order does not depend on the data point order.
  437. For the given viewing angle at azim=-50, the yellow marker should be in
  438. front. For azim=130, the blue marker should be in front.
  439. """
  440. x = [-1, 1]
  441. y = [1, -1]
  442. z = [0, 0]
  443. color = ['b', 'y']
  444. ax = fig_test.add_subplot(projection='3d')
  445. ax.scatter(x, y, z, s=3500, c=color)
  446. ax.view_init(elev=0, azim=azim, roll=0)
  447. ax = fig_ref.add_subplot(projection='3d')
  448. ax.scatter(x[::-1], y[::-1], z[::-1], s=3500, c=color[::-1])
  449. ax.view_init(elev=0, azim=azim, roll=0)
  450. @check_figures_equal(extensions=['png'])
  451. def test_marker_draw_order_view_rotated(fig_test, fig_ref):
  452. """
  453. Test that the draw order changes with the direction.
  454. If we rotate *azim* by 180 degrees and exchange the colors, the plot
  455. plot should look the same again.
  456. """
  457. azim = 130
  458. x = [-1, 1]
  459. y = [1, -1]
  460. z = [0, 0]
  461. color = ['b', 'y']
  462. ax = fig_test.add_subplot(projection='3d')
  463. # axis are not exactly invariant under 180 degree rotation -> deactivate
  464. ax.set_axis_off()
  465. ax.scatter(x, y, z, s=3500, c=color)
  466. ax.view_init(elev=0, azim=azim, roll=0)
  467. ax = fig_ref.add_subplot(projection='3d')
  468. ax.set_axis_off()
  469. ax.scatter(x, y, z, s=3500, c=color[::-1]) # color reversed
  470. ax.view_init(elev=0, azim=azim - 180, roll=0) # view rotated by 180 deg
  471. @mpl3d_image_comparison(['plot_3d_from_2d.png'], tol=0.019, style='mpl20')
  472. def test_plot_3d_from_2d():
  473. fig = plt.figure()
  474. ax = fig.add_subplot(projection='3d')
  475. xs = np.arange(0, 5)
  476. ys = np.arange(5, 10)
  477. ax.plot(xs, ys, zs=0, zdir='x')
  478. ax.plot(xs, ys, zs=0, zdir='y')
  479. @mpl3d_image_comparison(['fill_between_quad.png'], style='mpl20')
  480. def test_fill_between_quad():
  481. fig = plt.figure()
  482. ax = fig.add_subplot(projection='3d')
  483. theta = np.linspace(0, 2*np.pi, 50)
  484. x1 = np.cos(theta)
  485. y1 = np.sin(theta)
  486. z1 = 0.1 * np.sin(6 * theta)
  487. x2 = 0.6 * np.cos(theta)
  488. y2 = 0.6 * np.sin(theta)
  489. z2 = 2
  490. where = (theta < np.pi/2) | (theta > 3*np.pi/2)
  491. # Since none of x1 == x2, y1 == y2, or z1 == z2 is True, the fill_between
  492. # mode will map to 'quad'
  493. ax.fill_between(x1, y1, z1, x2, y2, z2,
  494. where=where, mode='auto', alpha=0.5, edgecolor='k')
  495. @mpl3d_image_comparison(['fill_between_polygon.png'], style='mpl20')
  496. def test_fill_between_polygon():
  497. fig = plt.figure()
  498. ax = fig.add_subplot(projection='3d')
  499. theta = np.linspace(0, 2*np.pi, 50)
  500. x1 = x2 = theta
  501. y1 = y2 = 0
  502. z1 = np.cos(theta)
  503. z2 = z1 + 1
  504. where = (theta < np.pi/2) | (theta > 3*np.pi/2)
  505. # Since x1 == x2 and y1 == y2, the fill_between mode will be 'polygon'
  506. ax.fill_between(x1, y1, z1, x2, y2, z2,
  507. where=where, mode='auto', edgecolor='k')
  508. @mpl3d_image_comparison(['surface3d.png'], style='mpl20')
  509. def test_surface3d():
  510. # Remove this line when this test image is regenerated.
  511. plt.rcParams['pcolormesh.snap'] = False
  512. fig = plt.figure()
  513. ax = fig.add_subplot(projection='3d')
  514. X = np.arange(-5, 5, 0.25)
  515. Y = np.arange(-5, 5, 0.25)
  516. X, Y = np.meshgrid(X, Y)
  517. R = np.hypot(X, Y)
  518. Z = np.sin(R)
  519. surf = ax.plot_surface(X, Y, Z, rcount=40, ccount=40, cmap=cm.coolwarm,
  520. lw=0, antialiased=False)
  521. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  522. ax.set_zlim(-1.01, 1.01)
  523. fig.colorbar(surf, shrink=0.5, aspect=5)
  524. @image_comparison(['surface3d_label_offset_tick_position.png'], style='mpl20')
  525. def test_surface3d_label_offset_tick_position():
  526. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  527. ax = plt.figure().add_subplot(projection="3d")
  528. x, y = np.mgrid[0:6 * np.pi:0.25, 0:4 * np.pi:0.25]
  529. z = np.sqrt(np.abs(np.cos(x) + np.cos(y)))
  530. ax.plot_surface(x * 1e5, y * 1e6, z * 1e8, cmap='autumn', cstride=2, rstride=2)
  531. ax.set_xlabel("X label")
  532. ax.set_ylabel("Y label")
  533. ax.set_zlabel("Z label")
  534. @mpl3d_image_comparison(['surface3d_shaded.png'], style='mpl20')
  535. def test_surface3d_shaded():
  536. fig = plt.figure()
  537. ax = fig.add_subplot(projection='3d')
  538. X = np.arange(-5, 5, 0.25)
  539. Y = np.arange(-5, 5, 0.25)
  540. X, Y = np.meshgrid(X, Y)
  541. R = np.sqrt(X ** 2 + Y ** 2)
  542. Z = np.sin(R)
  543. ax.plot_surface(X, Y, Z, rstride=5, cstride=5,
  544. color=[0.25, 1, 0.25], lw=1, antialiased=False)
  545. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  546. ax.set_zlim(-1.01, 1.01)
  547. @mpl3d_image_comparison(['surface3d_masked.png'], style='mpl20')
  548. def test_surface3d_masked():
  549. fig = plt.figure()
  550. ax = fig.add_subplot(projection='3d')
  551. x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
  552. y = [1, 2, 3, 4, 5, 6, 7, 8]
  553. x, y = np.meshgrid(x, y)
  554. matrix = np.array(
  555. [
  556. [-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  557. [-1, 1, 2, 3, 4, 4, 4, 3, 2, 1, 1],
  558. [-1, -1., 4, 5, 6, 8, 6, 5, 4, 3, -1.],
  559. [-1, -1., 7, 8, 11, 12, 11, 8, 7, -1., -1.],
  560. [-1, -1., 8, 9, 10, 16, 10, 9, 10, 7, -1.],
  561. [-1, -1., -1., 12, 16, 20, 16, 12, 11, -1., -1.],
  562. [-1, -1., -1., -1., 22, 24, 22, 20, 18, -1., -1.],
  563. [-1, -1., -1., -1., -1., 28, 26, 25, -1., -1., -1.],
  564. ]
  565. )
  566. z = np.ma.masked_less(matrix, 0)
  567. norm = mcolors.Normalize(vmax=z.max(), vmin=z.min())
  568. colors = mpl.colormaps["plasma"](norm(z))
  569. ax.plot_surface(x, y, z, facecolors=colors)
  570. ax.view_init(30, -80, 0)
  571. @check_figures_equal(extensions=["png"])
  572. def test_plot_scatter_masks(fig_test, fig_ref):
  573. x = np.linspace(0, 10, 100)
  574. y = np.linspace(0, 10, 100)
  575. z = np.sin(x) * np.cos(y)
  576. mask = z > 0
  577. z_masked = np.ma.array(z, mask=mask)
  578. ax_test = fig_test.add_subplot(projection='3d')
  579. ax_test.scatter(x, y, z_masked)
  580. ax_test.plot(x, y, z_masked)
  581. x[mask] = y[mask] = z[mask] = np.nan
  582. ax_ref = fig_ref.add_subplot(projection='3d')
  583. ax_ref.scatter(x, y, z)
  584. ax_ref.plot(x, y, z)
  585. @check_figures_equal(extensions=["png"])
  586. def test_plot_surface_None_arg(fig_test, fig_ref):
  587. x, y = np.meshgrid(np.arange(5), np.arange(5))
  588. z = x + y
  589. ax_test = fig_test.add_subplot(projection='3d')
  590. ax_test.plot_surface(x, y, z, facecolors=None)
  591. ax_ref = fig_ref.add_subplot(projection='3d')
  592. ax_ref.plot_surface(x, y, z)
  593. @mpl3d_image_comparison(['surface3d_masked_strides.png'], style='mpl20')
  594. def test_surface3d_masked_strides():
  595. fig = plt.figure()
  596. ax = fig.add_subplot(projection='3d')
  597. x, y = np.mgrid[-6:6.1:1, -6:6.1:1]
  598. z = np.ma.masked_less(x * y, 2)
  599. ax.plot_surface(x, y, z, rstride=4, cstride=4)
  600. ax.view_init(60, -45, 0)
  601. @mpl3d_image_comparison(['text3d.png'], remove_text=False, style='mpl20')
  602. def test_text3d():
  603. fig = plt.figure()
  604. ax = fig.add_subplot(projection='3d')
  605. zdirs = (None, 'x', 'y', 'z', (1, 1, 0), (1, 1, 1))
  606. xs = (2, 6, 4, 9, 7, 2)
  607. ys = (6, 4, 8, 7, 2, 2)
  608. zs = (4, 2, 5, 6, 1, 7)
  609. for zdir, x, y, z in zip(zdirs, xs, ys, zs):
  610. label = '(%d, %d, %d), dir=%s' % (x, y, z, zdir)
  611. ax.text(x, y, z, label, zdir)
  612. ax.text(1, 1, 1, "red", color='red')
  613. ax.text2D(0.05, 0.95, "2D Text", transform=ax.transAxes)
  614. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  615. ax.set_xlim3d(0, 10)
  616. ax.set_ylim3d(0, 10)
  617. ax.set_zlim3d(0, 10)
  618. ax.set_xlabel('X axis')
  619. ax.set_ylabel('Y axis')
  620. ax.set_zlabel('Z axis')
  621. @check_figures_equal(extensions=['png'])
  622. def test_text3d_modification(fig_ref, fig_test):
  623. # Modifying the Text position after the fact should work the same as
  624. # setting it directly.
  625. zdirs = (None, 'x', 'y', 'z', (1, 1, 0), (1, 1, 1))
  626. xs = (2, 6, 4, 9, 7, 2)
  627. ys = (6, 4, 8, 7, 2, 2)
  628. zs = (4, 2, 5, 6, 1, 7)
  629. ax_test = fig_test.add_subplot(projection='3d')
  630. ax_test.set_xlim3d(0, 10)
  631. ax_test.set_ylim3d(0, 10)
  632. ax_test.set_zlim3d(0, 10)
  633. for zdir, x, y, z in zip(zdirs, xs, ys, zs):
  634. t = ax_test.text(0, 0, 0, f'({x}, {y}, {z}), dir={zdir}')
  635. t.set_position_3d((x, y, z), zdir=zdir)
  636. ax_ref = fig_ref.add_subplot(projection='3d')
  637. ax_ref.set_xlim3d(0, 10)
  638. ax_ref.set_ylim3d(0, 10)
  639. ax_ref.set_zlim3d(0, 10)
  640. for zdir, x, y, z in zip(zdirs, xs, ys, zs):
  641. ax_ref.text(x, y, z, f'({x}, {y}, {z}), dir={zdir}', zdir=zdir)
  642. @mpl3d_image_comparison(['trisurf3d.png'], tol=0.061, style='mpl20')
  643. def test_trisurf3d():
  644. n_angles = 36
  645. n_radii = 8
  646. radii = np.linspace(0.125, 1.0, n_radii)
  647. angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)
  648. angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
  649. angles[:, 1::2] += np.pi/n_angles
  650. x = np.append(0, (radii*np.cos(angles)).flatten())
  651. y = np.append(0, (radii*np.sin(angles)).flatten())
  652. z = np.sin(-x*y)
  653. fig = plt.figure()
  654. ax = fig.add_subplot(projection='3d')
  655. ax.plot_trisurf(x, y, z, cmap=cm.jet, linewidth=0.2)
  656. @mpl3d_image_comparison(['trisurf3d_shaded.png'], tol=0.03, style='mpl20')
  657. def test_trisurf3d_shaded():
  658. n_angles = 36
  659. n_radii = 8
  660. radii = np.linspace(0.125, 1.0, n_radii)
  661. angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)
  662. angles = np.repeat(angles[..., np.newaxis], n_radii, axis=1)
  663. angles[:, 1::2] += np.pi/n_angles
  664. x = np.append(0, (radii*np.cos(angles)).flatten())
  665. y = np.append(0, (radii*np.sin(angles)).flatten())
  666. z = np.sin(-x*y)
  667. fig = plt.figure()
  668. ax = fig.add_subplot(projection='3d')
  669. ax.plot_trisurf(x, y, z, color=[1, 0.5, 0], linewidth=0.2)
  670. @mpl3d_image_comparison(['wireframe3d.png'], style='mpl20')
  671. def test_wireframe3d():
  672. fig = plt.figure()
  673. ax = fig.add_subplot(projection='3d')
  674. X, Y, Z = axes3d.get_test_data(0.05)
  675. ax.plot_wireframe(X, Y, Z, rcount=13, ccount=13)
  676. @mpl3d_image_comparison(['wireframe3dzerocstride.png'], style='mpl20')
  677. def test_wireframe3dzerocstride():
  678. fig = plt.figure()
  679. ax = fig.add_subplot(projection='3d')
  680. X, Y, Z = axes3d.get_test_data(0.05)
  681. ax.plot_wireframe(X, Y, Z, rcount=13, ccount=0)
  682. @mpl3d_image_comparison(['wireframe3dzerorstride.png'], style='mpl20')
  683. def test_wireframe3dzerorstride():
  684. fig = plt.figure()
  685. ax = fig.add_subplot(projection='3d')
  686. X, Y, Z = axes3d.get_test_data(0.05)
  687. ax.plot_wireframe(X, Y, Z, rstride=0, cstride=10)
  688. def test_wireframe3dzerostrideraises():
  689. fig = plt.figure()
  690. ax = fig.add_subplot(projection='3d')
  691. X, Y, Z = axes3d.get_test_data(0.05)
  692. with pytest.raises(ValueError):
  693. ax.plot_wireframe(X, Y, Z, rstride=0, cstride=0)
  694. def test_mixedsamplesraises():
  695. fig = plt.figure()
  696. ax = fig.add_subplot(projection='3d')
  697. X, Y, Z = axes3d.get_test_data(0.05)
  698. with pytest.raises(ValueError):
  699. ax.plot_wireframe(X, Y, Z, rstride=10, ccount=50)
  700. with pytest.raises(ValueError):
  701. ax.plot_surface(X, Y, Z, cstride=50, rcount=10)
  702. # remove tolerance when regenerating the test image
  703. @mpl3d_image_comparison(['quiver3d.png'], style='mpl20', tol=0.003)
  704. def test_quiver3d():
  705. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  706. fig = plt.figure()
  707. ax = fig.add_subplot(projection='3d')
  708. pivots = ['tip', 'middle', 'tail']
  709. colors = ['tab:blue', 'tab:orange', 'tab:green']
  710. for i, (pivot, color) in enumerate(zip(pivots, colors)):
  711. x, y, z = np.meshgrid([-0.5, 0.5], [-0.5, 0.5], [-0.5, 0.5])
  712. u = -x
  713. v = -y
  714. w = -z
  715. # Offset each set in z direction
  716. z += 2 * i
  717. ax.quiver(x, y, z, u, v, w, length=1, pivot=pivot, color=color)
  718. ax.scatter(x, y, z, color=color)
  719. ax.set_xlim(-3, 3)
  720. ax.set_ylim(-3, 3)
  721. ax.set_zlim(-1, 5)
  722. @check_figures_equal(extensions=["png"])
  723. def test_quiver3d_empty(fig_test, fig_ref):
  724. fig_ref.add_subplot(projection='3d')
  725. x = y = z = u = v = w = []
  726. ax = fig_test.add_subplot(projection='3d')
  727. ax.quiver(x, y, z, u, v, w, length=0.1, pivot='tip', normalize=True)
  728. @mpl3d_image_comparison(['quiver3d_masked.png'], style='mpl20')
  729. def test_quiver3d_masked():
  730. fig = plt.figure()
  731. ax = fig.add_subplot(projection='3d')
  732. # Using mgrid here instead of ogrid because masked_where doesn't
  733. # seem to like broadcasting very much...
  734. x, y, z = np.mgrid[-1:0.8:10j, -1:0.8:10j, -1:0.6:3j]
  735. u = np.sin(np.pi * x) * np.cos(np.pi * y) * np.cos(np.pi * z)
  736. v = -np.cos(np.pi * x) * np.sin(np.pi * y) * np.cos(np.pi * z)
  737. w = (2/3)**0.5 * np.cos(np.pi * x) * np.cos(np.pi * y) * np.sin(np.pi * z)
  738. u = np.ma.masked_where((-0.4 < x) & (x < 0.1), u, copy=False)
  739. v = np.ma.masked_where((0.1 < y) & (y < 0.7), v, copy=False)
  740. ax.quiver(x, y, z, u, v, w, length=0.1, pivot='tip', normalize=True)
  741. @mpl3d_image_comparison(['quiver3d_colorcoded.png'], style='mpl20')
  742. def test_quiver3d_colorcoded():
  743. fig = plt.figure()
  744. ax = fig.add_subplot(projection='3d')
  745. x = y = dx = dz = np.zeros(10)
  746. z = dy = np.arange(10.)
  747. color = plt.cm.Reds(dy/dy.max())
  748. ax.quiver(x, y, z, dx, dy, dz, colors=color)
  749. ax.set_ylim(0, 10)
  750. def test_patch_modification():
  751. fig = plt.figure()
  752. ax = fig.add_subplot(projection="3d")
  753. circle = Circle((0, 0))
  754. ax.add_patch(circle)
  755. art3d.patch_2d_to_3d(circle)
  756. circle.set_facecolor((1.0, 0.0, 0.0, 1))
  757. assert mcolors.same_color(circle.get_facecolor(), (1, 0, 0, 1))
  758. fig.canvas.draw()
  759. assert mcolors.same_color(circle.get_facecolor(), (1, 0, 0, 1))
  760. @check_figures_equal(extensions=['png'])
  761. def test_patch_collection_modification(fig_test, fig_ref):
  762. # Test that modifying Patch3DCollection properties after creation works.
  763. patch1 = Circle((0, 0), 0.05)
  764. patch2 = Circle((0.1, 0.1), 0.03)
  765. facecolors = np.array([[0., 0.5, 0., 1.], [0.5, 0., 0., 0.5]])
  766. c = art3d.Patch3DCollection([patch1, patch2], linewidths=3)
  767. ax_test = fig_test.add_subplot(projection='3d')
  768. ax_test.add_collection3d(c)
  769. c.set_edgecolor('C2')
  770. c.set_facecolor(facecolors)
  771. c.set_alpha(0.7)
  772. assert c.get_depthshade()
  773. c.set_depthshade(False)
  774. assert not c.get_depthshade()
  775. patch1 = Circle((0, 0), 0.05)
  776. patch2 = Circle((0.1, 0.1), 0.03)
  777. facecolors = np.array([[0., 0.5, 0., 1.], [0.5, 0., 0., 0.5]])
  778. c = art3d.Patch3DCollection([patch1, patch2], linewidths=3,
  779. edgecolor='C2', facecolor=facecolors,
  780. alpha=0.7, depthshade=False)
  781. ax_ref = fig_ref.add_subplot(projection='3d')
  782. ax_ref.add_collection3d(c)
  783. def test_poly3dcollection_verts_validation():
  784. poly = [[0, 0, 1], [0, 1, 1], [0, 1, 0], [0, 0, 0]]
  785. with pytest.raises(ValueError, match=r'list of \(N, 3\) array-like'):
  786. art3d.Poly3DCollection(poly) # should be Poly3DCollection([poly])
  787. poly = np.array(poly, dtype=float)
  788. with pytest.raises(ValueError, match=r'list of \(N, 3\) array-like'):
  789. art3d.Poly3DCollection(poly) # should be Poly3DCollection([poly])
  790. @mpl3d_image_comparison(['poly3dcollection_closed.png'], style='mpl20')
  791. def test_poly3dcollection_closed():
  792. fig = plt.figure()
  793. ax = fig.add_subplot(projection='3d')
  794. poly1 = np.array([[0, 0, 1], [0, 1, 1], [0, 0, 0]], float)
  795. poly2 = np.array([[0, 1, 1], [1, 1, 1], [1, 1, 0]], float)
  796. c1 = art3d.Poly3DCollection([poly1], linewidths=3, edgecolor='k',
  797. facecolor=(0.5, 0.5, 1, 0.5), closed=True)
  798. c2 = art3d.Poly3DCollection([poly2], linewidths=3, edgecolor='k',
  799. facecolor=(1, 0.5, 0.5, 0.5), closed=False)
  800. ax.add_collection3d(c1, autolim=False)
  801. ax.add_collection3d(c2, autolim=False)
  802. def test_poly_collection_2d_to_3d_empty():
  803. poly = PolyCollection([])
  804. art3d.poly_collection_2d_to_3d(poly)
  805. assert isinstance(poly, art3d.Poly3DCollection)
  806. assert poly.get_paths() == []
  807. fig, ax = plt.subplots(subplot_kw=dict(projection='3d'))
  808. ax.add_artist(poly)
  809. minz = poly.do_3d_projection()
  810. assert np.isnan(minz)
  811. # Ensure drawing actually works.
  812. fig.canvas.draw()
  813. @mpl3d_image_comparison(['poly3dcollection_alpha.png'], style='mpl20')
  814. def test_poly3dcollection_alpha():
  815. fig = plt.figure()
  816. ax = fig.add_subplot(projection='3d')
  817. poly1 = np.array([[0, 0, 1], [0, 1, 1], [0, 0, 0]], float)
  818. poly2 = np.array([[0, 1, 1], [1, 1, 1], [1, 1, 0]], float)
  819. c1 = art3d.Poly3DCollection([poly1], linewidths=3, edgecolor='k',
  820. facecolor=(0.5, 0.5, 1), closed=True)
  821. c1.set_alpha(0.5)
  822. c2 = art3d.Poly3DCollection([poly2], linewidths=3, closed=False)
  823. # Post-creation modification should work.
  824. c2.set_facecolor((1, 0.5, 0.5))
  825. c2.set_edgecolor('k')
  826. c2.set_alpha(0.5)
  827. ax.add_collection3d(c1, autolim=False)
  828. ax.add_collection3d(c2, autolim=False)
  829. @mpl3d_image_comparison(['add_collection3d_zs_array.png'], style='mpl20')
  830. def test_add_collection3d_zs_array():
  831. theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
  832. z = np.linspace(-2, 2, 100)
  833. r = z**2 + 1
  834. x = r * np.sin(theta)
  835. y = r * np.cos(theta)
  836. points = np.column_stack([x, y, z]).reshape(-1, 1, 3)
  837. segments = np.concatenate([points[:-1], points[1:]], axis=1)
  838. fig = plt.figure()
  839. ax = fig.add_subplot(projection='3d')
  840. norm = plt.Normalize(0, 2*np.pi)
  841. # 2D LineCollection from x & y values
  842. lc = LineCollection(segments[:, :, :2], cmap='twilight', norm=norm)
  843. lc.set_array(np.mod(theta, 2*np.pi))
  844. # Add 2D collection at z values to ax
  845. line = ax.add_collection3d(lc, zs=segments[:, :, 2])
  846. assert line is not None
  847. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  848. ax.set_xlim(-5, 5)
  849. ax.set_ylim(-4, 6)
  850. ax.set_zlim(-2, 2)
  851. @mpl3d_image_comparison(['add_collection3d_zs_scalar.png'], style='mpl20')
  852. def test_add_collection3d_zs_scalar():
  853. theta = np.linspace(0, 2 * np.pi, 100)
  854. z = 1
  855. r = z**2 + 1
  856. x = r * np.sin(theta)
  857. y = r * np.cos(theta)
  858. points = np.column_stack([x, y]).reshape(-1, 1, 2)
  859. segments = np.concatenate([points[:-1], points[1:]], axis=1)
  860. fig = plt.figure()
  861. ax = fig.add_subplot(projection='3d')
  862. norm = plt.Normalize(0, 2*np.pi)
  863. lc = LineCollection(segments, cmap='twilight', norm=norm)
  864. lc.set_array(theta)
  865. line = ax.add_collection3d(lc, zs=z)
  866. assert line is not None
  867. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  868. ax.set_xlim(-5, 5)
  869. ax.set_ylim(-4, 6)
  870. ax.set_zlim(0, 2)
  871. def test_line3dCollection_autoscaling():
  872. fig = plt.figure()
  873. ax = fig.add_subplot(projection='3d')
  874. lines = [[(0, 0, 0), (1, 4, 2)],
  875. [(1, 1, 3), (2, 0, 2)],
  876. [(1, 0, 4), (1, 4, 5)]]
  877. lc = art3d.Line3DCollection(lines)
  878. ax.add_collection3d(lc)
  879. assert np.allclose(ax.get_xlim3d(), (-0.041666666666666664, 2.0416666666666665))
  880. assert np.allclose(ax.get_ylim3d(), (-0.08333333333333333, 4.083333333333333))
  881. assert np.allclose(ax.get_zlim3d(), (-0.10416666666666666, 5.104166666666667))
  882. def test_poly3dCollection_autoscaling():
  883. fig = plt.figure()
  884. ax = fig.add_subplot(projection='3d')
  885. poly = np.array([[0, 0, 0], [1, 1, 3], [1, 0, 4]])
  886. col = art3d.Poly3DCollection([poly])
  887. ax.add_collection3d(col)
  888. assert np.allclose(ax.get_xlim3d(), (-0.020833333333333332, 1.0208333333333333))
  889. assert np.allclose(ax.get_ylim3d(), (-0.020833333333333332, 1.0208333333333333))
  890. assert np.allclose(ax.get_zlim3d(), (-0.0833333333333333, 4.083333333333333))
  891. @mpl3d_image_comparison(['axes3d_labelpad.png'],
  892. remove_text=False, style='mpl20')
  893. def test_axes3d_labelpad():
  894. fig = plt.figure()
  895. ax = fig.add_axes(Axes3D(fig))
  896. # labelpad respects rcParams
  897. assert ax.xaxis.labelpad == mpl.rcParams['axes.labelpad']
  898. # labelpad can be set in set_label
  899. ax.set_xlabel('X LABEL', labelpad=10)
  900. assert ax.xaxis.labelpad == 10
  901. ax.set_ylabel('Y LABEL')
  902. ax.set_zlabel('Z LABEL', labelpad=20)
  903. assert ax.zaxis.labelpad == 20
  904. assert ax.get_zlabel() == 'Z LABEL'
  905. # or manually
  906. ax.yaxis.labelpad = 20
  907. ax.zaxis.labelpad = -40
  908. # Tick labels also respect tick.pad (also from rcParams)
  909. for i, tick in enumerate(ax.yaxis.get_major_ticks()):
  910. tick.set_pad(tick.get_pad() + 5 - i * 5)
  911. @mpl3d_image_comparison(['axes3d_cla.png'], remove_text=False, style='mpl20')
  912. def test_axes3d_cla():
  913. # fixed in pull request 4553
  914. fig = plt.figure()
  915. ax = fig.add_subplot(1, 1, 1, projection='3d')
  916. ax.set_axis_off()
  917. ax.cla() # make sure the axis displayed is 3D (not 2D)
  918. @mpl3d_image_comparison(['axes3d_rotated.png'],
  919. remove_text=False, style='mpl20')
  920. def test_axes3d_rotated():
  921. fig = plt.figure()
  922. ax = fig.add_subplot(1, 1, 1, projection='3d')
  923. ax.view_init(90, 45, 0) # look down, rotated. Should be square
  924. def test_plotsurface_1d_raises():
  925. x = np.linspace(0.5, 10, num=100)
  926. y = np.linspace(0.5, 10, num=100)
  927. X, Y = np.meshgrid(x, y)
  928. z = np.random.randn(100)
  929. fig = plt.figure(figsize=(14, 6))
  930. ax = fig.add_subplot(1, 2, 1, projection='3d')
  931. with pytest.raises(ValueError):
  932. ax.plot_surface(X, Y, z)
  933. def _test_proj_make_M():
  934. # eye point
  935. E = np.array([1000, -1000, 2000])
  936. R = np.array([100, 100, 100])
  937. V = np.array([0, 0, 1])
  938. roll = 0
  939. u, v, w = proj3d._view_axes(E, R, V, roll)
  940. viewM = proj3d._view_transformation_uvw(u, v, w, E)
  941. perspM = proj3d._persp_transformation(100, -100, 1)
  942. M = np.dot(perspM, viewM)
  943. return M
  944. def test_proj_transform():
  945. M = _test_proj_make_M()
  946. invM = np.linalg.inv(M)
  947. xs = np.array([0, 1, 1, 0, 0, 0, 1, 1, 0, 0]) * 300.0
  948. ys = np.array([0, 0, 1, 1, 0, 0, 0, 1, 1, 0]) * 300.0
  949. zs = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) * 300.0
  950. txs, tys, tzs = proj3d.proj_transform(xs, ys, zs, M)
  951. ixs, iys, izs = proj3d.inv_transform(txs, tys, tzs, invM)
  952. np.testing.assert_almost_equal(ixs, xs)
  953. np.testing.assert_almost_equal(iys, ys)
  954. np.testing.assert_almost_equal(izs, zs)
  955. def _test_proj_draw_axes(M, s=1, *args, **kwargs):
  956. xs = [0, s, 0, 0]
  957. ys = [0, 0, s, 0]
  958. zs = [0, 0, 0, s]
  959. txs, tys, tzs = proj3d.proj_transform(xs, ys, zs, M)
  960. o, ax, ay, az = zip(txs, tys)
  961. lines = [(o, ax), (o, ay), (o, az)]
  962. fig, ax = plt.subplots(*args, **kwargs)
  963. linec = LineCollection(lines)
  964. ax.add_collection(linec)
  965. for x, y, t in zip(txs, tys, ['o', 'x', 'y', 'z']):
  966. ax.text(x, y, t)
  967. return fig, ax
  968. @mpl3d_image_comparison(['proj3d_axes_cube.png'], style='mpl20')
  969. def test_proj_axes_cube():
  970. M = _test_proj_make_M()
  971. ts = '0 1 2 3 0 4 5 6 7 4'.split()
  972. xs = np.array([0, 1, 1, 0, 0, 0, 1, 1, 0, 0]) * 300.0
  973. ys = np.array([0, 0, 1, 1, 0, 0, 0, 1, 1, 0]) * 300.0
  974. zs = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) * 300.0
  975. txs, tys, tzs = proj3d.proj_transform(xs, ys, zs, M)
  976. fig, ax = _test_proj_draw_axes(M, s=400)
  977. ax.scatter(txs, tys, c=tzs)
  978. ax.plot(txs, tys, c='r')
  979. for x, y, t in zip(txs, tys, ts):
  980. ax.text(x, y, t)
  981. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  982. ax.set_xlim(-0.2, 0.2)
  983. ax.set_ylim(-0.2, 0.2)
  984. @mpl3d_image_comparison(['proj3d_axes_cube_ortho.png'], style='mpl20')
  985. def test_proj_axes_cube_ortho():
  986. E = np.array([200, 100, 100])
  987. R = np.array([0, 0, 0])
  988. V = np.array([0, 0, 1])
  989. roll = 0
  990. u, v, w = proj3d._view_axes(E, R, V, roll)
  991. viewM = proj3d._view_transformation_uvw(u, v, w, E)
  992. orthoM = proj3d._ortho_transformation(-1, 1)
  993. M = np.dot(orthoM, viewM)
  994. ts = '0 1 2 3 0 4 5 6 7 4'.split()
  995. xs = np.array([0, 1, 1, 0, 0, 0, 1, 1, 0, 0]) * 100
  996. ys = np.array([0, 0, 1, 1, 0, 0, 0, 1, 1, 0]) * 100
  997. zs = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) * 100
  998. txs, tys, tzs = proj3d.proj_transform(xs, ys, zs, M)
  999. fig, ax = _test_proj_draw_axes(M, s=150)
  1000. ax.scatter(txs, tys, s=300-tzs)
  1001. ax.plot(txs, tys, c='r')
  1002. for x, y, t in zip(txs, tys, ts):
  1003. ax.text(x, y, t)
  1004. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  1005. ax.set_xlim(-200, 200)
  1006. ax.set_ylim(-200, 200)
  1007. def test_world():
  1008. xmin, xmax = 100, 120
  1009. ymin, ymax = -100, 100
  1010. zmin, zmax = 0.1, 0.2
  1011. M = proj3d.world_transformation(xmin, xmax, ymin, ymax, zmin, zmax)
  1012. np.testing.assert_allclose(M,
  1013. [[5e-2, 0, 0, -5],
  1014. [0, 5e-3, 0, 5e-1],
  1015. [0, 0, 1e1, -1],
  1016. [0, 0, 0, 1]])
  1017. def test_autoscale():
  1018. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1019. assert ax.get_zscale() == 'linear'
  1020. ax._view_margin = 0
  1021. ax.margins(x=0, y=.1, z=.2)
  1022. ax.plot([0, 1], [0, 1], [0, 1])
  1023. assert ax.get_w_lims() == (0, 1, -.1, 1.1, -.2, 1.2)
  1024. ax.autoscale(False)
  1025. ax.set_autoscalez_on(True)
  1026. ax.plot([0, 2], [0, 2], [0, 2])
  1027. assert ax.get_w_lims() == (0, 1, -.1, 1.1, -.4, 2.4)
  1028. ax.autoscale(axis='x')
  1029. ax.plot([0, 2], [0, 2], [0, 2])
  1030. assert ax.get_w_lims() == (0, 2, -.1, 1.1, -.4, 2.4)
  1031. @pytest.mark.parametrize('axis', ('x', 'y', 'z'))
  1032. @pytest.mark.parametrize('auto', (True, False, None))
  1033. def test_unautoscale(axis, auto):
  1034. fig = plt.figure()
  1035. ax = fig.add_subplot(projection='3d')
  1036. x = np.arange(100)
  1037. y = np.linspace(-0.1, 0.1, 100)
  1038. ax.scatter(x, y)
  1039. get_autoscale_on = getattr(ax, f'get_autoscale{axis}_on')
  1040. set_lim = getattr(ax, f'set_{axis}lim')
  1041. get_lim = getattr(ax, f'get_{axis}lim')
  1042. post_auto = get_autoscale_on() if auto is None else auto
  1043. set_lim((-0.5, 0.5), auto=auto)
  1044. assert post_auto == get_autoscale_on()
  1045. fig.canvas.draw()
  1046. np.testing.assert_array_equal(get_lim(), (-0.5, 0.5))
  1047. @check_figures_equal(extensions=["png"])
  1048. def test_culling(fig_test, fig_ref):
  1049. xmins = (-100, -50)
  1050. for fig, xmin in zip((fig_test, fig_ref), xmins):
  1051. ax = fig.add_subplot(projection='3d')
  1052. n = abs(xmin) + 1
  1053. xs = np.linspace(0, xmin, n)
  1054. ys = np.ones(n)
  1055. zs = np.zeros(n)
  1056. ax.plot(xs, ys, zs, 'k')
  1057. ax.set(xlim=(-5, 5), ylim=(-5, 5), zlim=(-5, 5))
  1058. ax.view_init(5, 180, 0)
  1059. def test_axes3d_focal_length_checks():
  1060. fig = plt.figure()
  1061. ax = fig.add_subplot(projection='3d')
  1062. with pytest.raises(ValueError):
  1063. ax.set_proj_type('persp', focal_length=0)
  1064. with pytest.raises(ValueError):
  1065. ax.set_proj_type('ortho', focal_length=1)
  1066. @mpl3d_image_comparison(['axes3d_focal_length.png'],
  1067. remove_text=False, style='mpl20')
  1068. def test_axes3d_focal_length():
  1069. fig, axs = plt.subplots(1, 2, subplot_kw={'projection': '3d'})
  1070. axs[0].set_proj_type('persp', focal_length=np.inf)
  1071. axs[1].set_proj_type('persp', focal_length=0.15)
  1072. @mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False, style='mpl20')
  1073. def test_axes3d_ortho():
  1074. fig = plt.figure()
  1075. ax = fig.add_subplot(projection='3d')
  1076. ax.set_proj_type('ortho')
  1077. @mpl3d_image_comparison(['axes3d_isometric.png'], style='mpl20')
  1078. def test_axes3d_isometric():
  1079. from itertools import combinations, product
  1080. fig, ax = plt.subplots(subplot_kw=dict(
  1081. projection='3d',
  1082. proj_type='ortho',
  1083. box_aspect=(4, 4, 4)
  1084. ))
  1085. r = (-1, 1) # stackoverflow.com/a/11156353
  1086. for s, e in combinations(np.array(list(product(r, r, r))), 2):
  1087. if abs(s - e).sum() == r[1] - r[0]:
  1088. ax.plot3D(*zip(s, e), c='k')
  1089. ax.view_init(elev=np.degrees(np.arctan(1. / np.sqrt(2))), azim=-45, roll=0)
  1090. ax.grid(True)
  1091. @check_figures_equal(extensions=["png"])
  1092. def test_axlim_clip(fig_test, fig_ref):
  1093. # With axlim clipping
  1094. ax = fig_test.add_subplot(projection="3d")
  1095. x = np.linspace(0, 1, 11)
  1096. y = np.linspace(0, 1, 11)
  1097. X, Y = np.meshgrid(x, y)
  1098. Z = X + Y
  1099. ax.plot_surface(X, Y, Z, facecolor='C1', edgecolors=None,
  1100. rcount=50, ccount=50, axlim_clip=True)
  1101. # This ax.plot is to cover the extra surface edge which is not clipped out
  1102. ax.plot([0.5, 0.5], [0, 1], [0.5, 1.5],
  1103. color='k', linewidth=3, zorder=5, axlim_clip=True)
  1104. ax.scatter(X.ravel(), Y.ravel(), Z.ravel() + 1, axlim_clip=True)
  1105. ax.quiver(X.ravel(), Y.ravel(), Z.ravel() + 2,
  1106. 0*X.ravel(), 0*Y.ravel(), 0*Z.ravel() + 1,
  1107. arrow_length_ratio=0, axlim_clip=True)
  1108. ax.plot(X[0], Y[0], Z[0] + 3, color='C2', axlim_clip=True)
  1109. ax.text(1.1, 0.5, 4, 'test', axlim_clip=True) # won't be visible
  1110. ax.set(xlim=(0, 0.5), ylim=(0, 1), zlim=(0, 5))
  1111. # With manual clipping
  1112. ax = fig_ref.add_subplot(projection="3d")
  1113. idx = (X <= 0.5)
  1114. X = X[idx].reshape(11, 6)
  1115. Y = Y[idx].reshape(11, 6)
  1116. Z = Z[idx].reshape(11, 6)
  1117. ax.plot_surface(X, Y, Z, facecolor='C1', edgecolors=None,
  1118. rcount=50, ccount=50, axlim_clip=False)
  1119. ax.plot([0.5, 0.5], [0, 1], [0.5, 1.5],
  1120. color='k', linewidth=3, zorder=5, axlim_clip=False)
  1121. ax.scatter(X.ravel(), Y.ravel(), Z.ravel() + 1, axlim_clip=False)
  1122. ax.quiver(X.ravel(), Y.ravel(), Z.ravel() + 2,
  1123. 0*X.ravel(), 0*Y.ravel(), 0*Z.ravel() + 1,
  1124. arrow_length_ratio=0, axlim_clip=False)
  1125. ax.plot(X[0], Y[0], Z[0] + 3, color='C2', axlim_clip=False)
  1126. ax.set(xlim=(0, 0.5), ylim=(0, 1), zlim=(0, 5))
  1127. @pytest.mark.parametrize('value', [np.inf, np.nan])
  1128. @pytest.mark.parametrize(('setter', 'side'), [
  1129. ('set_xlim3d', 'left'),
  1130. ('set_xlim3d', 'right'),
  1131. ('set_ylim3d', 'bottom'),
  1132. ('set_ylim3d', 'top'),
  1133. ('set_zlim3d', 'bottom'),
  1134. ('set_zlim3d', 'top'),
  1135. ])
  1136. def test_invalid_axes_limits(setter, side, value):
  1137. limit = {side: value}
  1138. fig = plt.figure()
  1139. obj = fig.add_subplot(projection='3d')
  1140. with pytest.raises(ValueError):
  1141. getattr(obj, setter)(**limit)
  1142. class TestVoxels:
  1143. @mpl3d_image_comparison(['voxels-simple.png'], style='mpl20')
  1144. def test_simple(self):
  1145. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1146. x, y, z = np.indices((5, 4, 3))
  1147. voxels = (x == y) | (y == z)
  1148. ax.voxels(voxels)
  1149. @mpl3d_image_comparison(['voxels-edge-style.png'], style='mpl20')
  1150. def test_edge_style(self):
  1151. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1152. x, y, z = np.indices((5, 5, 4))
  1153. voxels = ((x - 2)**2 + (y - 2)**2 + (z-1.5)**2) < 2.2**2
  1154. v = ax.voxels(voxels, linewidths=3, edgecolor='C1')
  1155. # change the edge color of one voxel
  1156. v[max(v.keys())].set_edgecolor('C2')
  1157. @mpl3d_image_comparison(['voxels-named-colors.png'], style='mpl20')
  1158. def test_named_colors(self):
  1159. """Test with colors set to a 3D object array of strings."""
  1160. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1161. x, y, z = np.indices((10, 10, 10))
  1162. voxels = (x == y) | (y == z)
  1163. voxels = voxels & ~(x * y * z < 1)
  1164. colors = np.full((10, 10, 10), 'C0', dtype=np.object_)
  1165. colors[(x < 5) & (y < 5)] = '0.25'
  1166. colors[(x + z) < 10] = 'cyan'
  1167. ax.voxels(voxels, facecolors=colors)
  1168. @mpl3d_image_comparison(['voxels-rgb-data.png'], style='mpl20')
  1169. def test_rgb_data(self):
  1170. """Test with colors set to a 4d float array of rgb data."""
  1171. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1172. x, y, z = np.indices((10, 10, 10))
  1173. voxels = (x == y) | (y == z)
  1174. colors = np.zeros((10, 10, 10, 3))
  1175. colors[..., 0] = x / 9
  1176. colors[..., 1] = y / 9
  1177. colors[..., 2] = z / 9
  1178. ax.voxels(voxels, facecolors=colors)
  1179. @mpl3d_image_comparison(['voxels-alpha.png'], style='mpl20')
  1180. def test_alpha(self):
  1181. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1182. x, y, z = np.indices((10, 10, 10))
  1183. v1 = x == y
  1184. v2 = np.abs(x - y) < 2
  1185. voxels = v1 | v2
  1186. colors = np.zeros((10, 10, 10, 4))
  1187. colors[v2] = [1, 0, 0, 0.5]
  1188. colors[v1] = [0, 1, 0, 0.5]
  1189. v = ax.voxels(voxels, facecolors=colors)
  1190. assert type(v) is dict
  1191. for coord, poly in v.items():
  1192. assert voxels[coord], "faces returned for absent voxel"
  1193. assert isinstance(poly, art3d.Poly3DCollection)
  1194. @mpl3d_image_comparison(['voxels-xyz.png'],
  1195. tol=0.01, remove_text=False, style='mpl20')
  1196. def test_xyz(self):
  1197. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1198. def midpoints(x):
  1199. sl = ()
  1200. for i in range(x.ndim):
  1201. x = (x[sl + np.index_exp[:-1]] +
  1202. x[sl + np.index_exp[1:]]) / 2.0
  1203. sl += np.index_exp[:]
  1204. return x
  1205. # prepare some coordinates, and attach rgb values to each
  1206. r, g, b = np.indices((17, 17, 17)) / 16.0
  1207. rc = midpoints(r)
  1208. gc = midpoints(g)
  1209. bc = midpoints(b)
  1210. # define a sphere about [0.5, 0.5, 0.5]
  1211. sphere = (rc - 0.5)**2 + (gc - 0.5)**2 + (bc - 0.5)**2 < 0.5**2
  1212. # combine the color components
  1213. colors = np.zeros(sphere.shape + (3,))
  1214. colors[..., 0] = rc
  1215. colors[..., 1] = gc
  1216. colors[..., 2] = bc
  1217. # and plot everything
  1218. ax.voxels(r, g, b, sphere,
  1219. facecolors=colors,
  1220. edgecolors=np.clip(2*colors - 0.5, 0, 1), # brighter
  1221. linewidth=0.5)
  1222. def test_calling_conventions(self):
  1223. x, y, z = np.indices((3, 4, 5))
  1224. filled = np.ones((2, 3, 4))
  1225. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1226. # all the valid calling conventions
  1227. for kw in (dict(), dict(edgecolor='k')):
  1228. ax.voxels(filled, **kw)
  1229. ax.voxels(filled=filled, **kw)
  1230. ax.voxels(x, y, z, filled, **kw)
  1231. ax.voxels(x, y, z, filled=filled, **kw)
  1232. # duplicate argument
  1233. with pytest.raises(TypeError, match='voxels'):
  1234. ax.voxels(x, y, z, filled, filled=filled)
  1235. # missing arguments
  1236. with pytest.raises(TypeError, match='voxels'):
  1237. ax.voxels(x, y)
  1238. # x, y, z are positional only - this passes them on as attributes of
  1239. # Poly3DCollection
  1240. with pytest.raises(AttributeError, match="keyword argument 'x'") as exec_info:
  1241. ax.voxels(filled=filled, x=x, y=y, z=z)
  1242. assert exec_info.value.name == 'x'
  1243. def test_line3d_set_get_data_3d():
  1244. x, y, z = [0, 1], [2, 3], [4, 5]
  1245. x2, y2, z2 = [6, 7], [8, 9], [10, 11]
  1246. fig = plt.figure()
  1247. ax = fig.add_subplot(projection='3d')
  1248. lines = ax.plot(x, y, z)
  1249. line = lines[0]
  1250. np.testing.assert_array_equal((x, y, z), line.get_data_3d())
  1251. line.set_data_3d(x2, y2, z2)
  1252. np.testing.assert_array_equal((x2, y2, z2), line.get_data_3d())
  1253. line.set_xdata(x)
  1254. line.set_ydata(y)
  1255. line.set_3d_properties(zs=z, zdir='z')
  1256. np.testing.assert_array_equal((x, y, z), line.get_data_3d())
  1257. line.set_3d_properties(zs=0, zdir='z')
  1258. np.testing.assert_array_equal((x, y, np.zeros_like(z)), line.get_data_3d())
  1259. @check_figures_equal(extensions=["png"])
  1260. def test_inverted(fig_test, fig_ref):
  1261. # Plot then invert.
  1262. ax = fig_test.add_subplot(projection="3d")
  1263. ax.plot([1, 1, 10, 10], [1, 10, 10, 10], [1, 1, 1, 10])
  1264. ax.invert_yaxis()
  1265. # Invert then plot.
  1266. ax = fig_ref.add_subplot(projection="3d")
  1267. ax.invert_yaxis()
  1268. ax.plot([1, 1, 10, 10], [1, 10, 10, 10], [1, 1, 1, 10])
  1269. def test_inverted_cla():
  1270. # GitHub PR #5450. Setting autoscale should reset
  1271. # axes to be non-inverted.
  1272. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1273. # 1. test that a new axis is not inverted per default
  1274. assert not ax.xaxis_inverted()
  1275. assert not ax.yaxis_inverted()
  1276. assert not ax.zaxis_inverted()
  1277. ax.set_xlim(1, 0)
  1278. ax.set_ylim(1, 0)
  1279. ax.set_zlim(1, 0)
  1280. assert ax.xaxis_inverted()
  1281. assert ax.yaxis_inverted()
  1282. assert ax.zaxis_inverted()
  1283. ax.cla()
  1284. assert not ax.xaxis_inverted()
  1285. assert not ax.yaxis_inverted()
  1286. assert not ax.zaxis_inverted()
  1287. def test_ax3d_tickcolour():
  1288. fig = plt.figure()
  1289. ax = Axes3D(fig)
  1290. ax.tick_params(axis='x', colors='red')
  1291. ax.tick_params(axis='y', colors='red')
  1292. ax.tick_params(axis='z', colors='red')
  1293. fig.canvas.draw()
  1294. for tick in ax.xaxis.get_major_ticks():
  1295. assert tick.tick1line._color == 'red'
  1296. for tick in ax.yaxis.get_major_ticks():
  1297. assert tick.tick1line._color == 'red'
  1298. for tick in ax.zaxis.get_major_ticks():
  1299. assert tick.tick1line._color == 'red'
  1300. @check_figures_equal(extensions=["png"])
  1301. def test_ticklabel_format(fig_test, fig_ref):
  1302. axs = fig_test.subplots(4, 5, subplot_kw={"projection": "3d"})
  1303. for ax in axs.flat:
  1304. ax.set_xlim(1e7, 1e7 + 10)
  1305. for row, name in zip(axs, ["x", "y", "z", "both"]):
  1306. row[0].ticklabel_format(
  1307. axis=name, style="plain")
  1308. row[1].ticklabel_format(
  1309. axis=name, scilimits=(-2, 2))
  1310. row[2].ticklabel_format(
  1311. axis=name, useOffset=not mpl.rcParams["axes.formatter.useoffset"])
  1312. row[3].ticklabel_format(
  1313. axis=name, useLocale=not mpl.rcParams["axes.formatter.use_locale"])
  1314. row[4].ticklabel_format(
  1315. axis=name,
  1316. useMathText=not mpl.rcParams["axes.formatter.use_mathtext"])
  1317. def get_formatters(ax, names):
  1318. return [getattr(ax, name).get_major_formatter() for name in names]
  1319. axs = fig_ref.subplots(4, 5, subplot_kw={"projection": "3d"})
  1320. for ax in axs.flat:
  1321. ax.set_xlim(1e7, 1e7 + 10)
  1322. for row, names in zip(
  1323. axs, [["xaxis"], ["yaxis"], ["zaxis"], ["xaxis", "yaxis", "zaxis"]]
  1324. ):
  1325. for fmt in get_formatters(row[0], names):
  1326. fmt.set_scientific(False)
  1327. for fmt in get_formatters(row[1], names):
  1328. fmt.set_powerlimits((-2, 2))
  1329. for fmt in get_formatters(row[2], names):
  1330. fmt.set_useOffset(not mpl.rcParams["axes.formatter.useoffset"])
  1331. for fmt in get_formatters(row[3], names):
  1332. fmt.set_useLocale(not mpl.rcParams["axes.formatter.use_locale"])
  1333. for fmt in get_formatters(row[4], names):
  1334. fmt.set_useMathText(
  1335. not mpl.rcParams["axes.formatter.use_mathtext"])
  1336. @check_figures_equal(extensions=["png"])
  1337. def test_quiver3D_smoke(fig_test, fig_ref):
  1338. pivot = "middle"
  1339. # Make the grid
  1340. x, y, z = np.meshgrid(
  1341. np.arange(-0.8, 1, 0.2),
  1342. np.arange(-0.8, 1, 0.2),
  1343. np.arange(-0.8, 1, 0.8)
  1344. )
  1345. u = v = w = np.ones_like(x)
  1346. for fig, length in zip((fig_ref, fig_test), (1, 1.0)):
  1347. ax = fig.add_subplot(projection="3d")
  1348. ax.quiver(x, y, z, u, v, w, length=length, pivot=pivot)
  1349. @image_comparison(["minor_ticks.png"], style="mpl20")
  1350. def test_minor_ticks():
  1351. ax = plt.figure().add_subplot(projection="3d")
  1352. ax.set_xticks([0.25], minor=True)
  1353. ax.set_xticklabels(["quarter"], minor=True)
  1354. ax.set_yticks([0.33], minor=True)
  1355. ax.set_yticklabels(["third"], minor=True)
  1356. ax.set_zticks([0.50], minor=True)
  1357. ax.set_zticklabels(["half"], minor=True)
  1358. # remove tolerance when regenerating the test image
  1359. @mpl3d_image_comparison(['errorbar3d_errorevery.png'], style='mpl20', tol=0.003)
  1360. def test_errorbar3d_errorevery():
  1361. """Tests errorevery functionality for 3D errorbars."""
  1362. t = np.arange(0, 2*np.pi+.1, 0.01)
  1363. x, y, z = np.sin(t), np.cos(3*t), np.sin(5*t)
  1364. fig = plt.figure()
  1365. ax = fig.add_subplot(projection='3d')
  1366. estep = 15
  1367. i = np.arange(t.size)
  1368. zuplims = (i % estep == 0) & (i // estep % 3 == 0)
  1369. zlolims = (i % estep == 0) & (i // estep % 3 == 2)
  1370. ax.errorbar(x, y, z, 0.2, zuplims=zuplims, zlolims=zlolims,
  1371. errorevery=estep)
  1372. @mpl3d_image_comparison(['errorbar3d.png'], style='mpl20',
  1373. tol=0 if platform.machine() == 'x86_64' else 0.02)
  1374. def test_errorbar3d():
  1375. """Tests limits, color styling, and legend for 3D errorbars."""
  1376. fig = plt.figure()
  1377. ax = fig.add_subplot(projection='3d')
  1378. d = [1, 2, 3, 4, 5]
  1379. e = [.5, .5, .5, .5, .5]
  1380. ax.errorbar(x=d, y=d, z=d, xerr=e, yerr=e, zerr=e, capsize=3,
  1381. zuplims=[False, True, False, True, True],
  1382. zlolims=[True, False, False, True, False],
  1383. yuplims=True,
  1384. ecolor='purple', label='Error lines')
  1385. ax.legend()
  1386. @image_comparison(['stem3d.png'], style='mpl20', tol=0.009)
  1387. def test_stem3d():
  1388. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  1389. fig, axs = plt.subplots(2, 3, figsize=(8, 6),
  1390. constrained_layout=True,
  1391. subplot_kw={'projection': '3d'})
  1392. theta = np.linspace(0, 2*np.pi)
  1393. x = np.cos(theta - np.pi/2)
  1394. y = np.sin(theta - np.pi/2)
  1395. z = theta
  1396. for ax, zdir in zip(axs[0], ['x', 'y', 'z']):
  1397. ax.stem(x, y, z, orientation=zdir)
  1398. ax.set_title(f'orientation={zdir}')
  1399. x = np.linspace(-np.pi/2, np.pi/2, 20)
  1400. y = np.ones_like(x)
  1401. z = np.cos(x)
  1402. for ax, zdir in zip(axs[1], ['x', 'y', 'z']):
  1403. markerline, stemlines, baseline = ax.stem(
  1404. x, y, z,
  1405. linefmt='C4-.', markerfmt='C1D', basefmt='C2',
  1406. orientation=zdir)
  1407. ax.set_title(f'orientation={zdir}')
  1408. markerline.set(markerfacecolor='none', markeredgewidth=2)
  1409. baseline.set_linewidth(3)
  1410. @image_comparison(["equal_box_aspect.png"], style="mpl20")
  1411. def test_equal_box_aspect():
  1412. from itertools import product, combinations
  1413. fig = plt.figure()
  1414. ax = fig.add_subplot(projection="3d")
  1415. # Make data
  1416. u = np.linspace(0, 2 * np.pi, 100)
  1417. v = np.linspace(0, np.pi, 100)
  1418. x = np.outer(np.cos(u), np.sin(v))
  1419. y = np.outer(np.sin(u), np.sin(v))
  1420. z = np.outer(np.ones_like(u), np.cos(v))
  1421. # Plot the surface
  1422. ax.plot_surface(x, y, z)
  1423. # draw cube
  1424. r = [-1, 1]
  1425. for s, e in combinations(np.array(list(product(r, r, r))), 2):
  1426. if np.sum(np.abs(s - e)) == r[1] - r[0]:
  1427. ax.plot3D(*zip(s, e), color="b")
  1428. # Make axes limits
  1429. xyzlim = np.column_stack(
  1430. [ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()]
  1431. )
  1432. XYZlim = [min(xyzlim[0]), max(xyzlim[1])]
  1433. ax.set_xlim3d(XYZlim)
  1434. ax.set_ylim3d(XYZlim)
  1435. ax.set_zlim3d(XYZlim)
  1436. ax.axis('off')
  1437. ax.set_box_aspect((1, 1, 1))
  1438. with pytest.raises(ValueError, match="Argument zoom ="):
  1439. ax.set_box_aspect((1, 1, 1), zoom=-1)
  1440. def test_colorbar_pos():
  1441. num_plots = 2
  1442. fig, axs = plt.subplots(1, num_plots, figsize=(4, 5),
  1443. constrained_layout=True,
  1444. subplot_kw={'projection': '3d'})
  1445. for ax in axs:
  1446. p_tri = ax.plot_trisurf(np.random.randn(5), np.random.randn(5),
  1447. np.random.randn(5))
  1448. cbar = plt.colorbar(p_tri, ax=axs, orientation='horizontal')
  1449. fig.canvas.draw()
  1450. # check that actually on the bottom
  1451. assert cbar.ax.get_position().extents[1] < 0.2
  1452. def test_inverted_zaxis():
  1453. fig = plt.figure()
  1454. ax = fig.add_subplot(projection='3d')
  1455. ax.set_zlim(0, 1)
  1456. assert not ax.zaxis_inverted()
  1457. assert ax.get_zlim() == (0, 1)
  1458. assert ax.get_zbound() == (0, 1)
  1459. # Change bound
  1460. ax.set_zbound((0, 2))
  1461. assert not ax.zaxis_inverted()
  1462. assert ax.get_zlim() == (0, 2)
  1463. assert ax.get_zbound() == (0, 2)
  1464. # Change invert
  1465. ax.invert_zaxis()
  1466. assert ax.zaxis_inverted()
  1467. assert ax.get_zlim() == (2, 0)
  1468. assert ax.get_zbound() == (0, 2)
  1469. # Set upper bound
  1470. ax.set_zbound(upper=1)
  1471. assert ax.zaxis_inverted()
  1472. assert ax.get_zlim() == (1, 0)
  1473. assert ax.get_zbound() == (0, 1)
  1474. # Set lower bound
  1475. ax.set_zbound(lower=2)
  1476. assert ax.zaxis_inverted()
  1477. assert ax.get_zlim() == (2, 1)
  1478. assert ax.get_zbound() == (1, 2)
  1479. def test_set_zlim():
  1480. fig = plt.figure()
  1481. ax = fig.add_subplot(projection='3d')
  1482. assert np.allclose(ax.get_zlim(), (-1/48, 49/48))
  1483. ax.set_zlim(zmax=2)
  1484. assert np.allclose(ax.get_zlim(), (-1/48, 2))
  1485. ax.set_zlim(zmin=1)
  1486. assert ax.get_zlim() == (1, 2)
  1487. with pytest.raises(
  1488. TypeError, match="Cannot pass both 'lower' and 'min'"):
  1489. ax.set_zlim(bottom=0, zmin=1)
  1490. with pytest.raises(
  1491. TypeError, match="Cannot pass both 'upper' and 'max'"):
  1492. ax.set_zlim(top=0, zmax=1)
  1493. @check_figures_equal(extensions=["png"])
  1494. def test_shared_view(fig_test, fig_ref):
  1495. elev, azim, roll = 5, 20, 30
  1496. ax1 = fig_test.add_subplot(131, projection="3d")
  1497. ax2 = fig_test.add_subplot(132, projection="3d", shareview=ax1)
  1498. ax3 = fig_test.add_subplot(133, projection="3d")
  1499. ax3.shareview(ax1)
  1500. ax2.view_init(elev=elev, azim=azim, roll=roll, share=True)
  1501. for subplot_num in (131, 132, 133):
  1502. ax = fig_ref.add_subplot(subplot_num, projection="3d")
  1503. ax.view_init(elev=elev, azim=azim, roll=roll)
  1504. def test_shared_axes_retick():
  1505. fig = plt.figure()
  1506. ax1 = fig.add_subplot(211, projection="3d")
  1507. ax2 = fig.add_subplot(212, projection="3d", sharez=ax1)
  1508. ax1.plot([0, 1], [0, 1], [0, 2])
  1509. ax2.plot([0, 1], [0, 1], [0, 2])
  1510. ax1.set_zticks([-0.5, 0, 2, 2.5])
  1511. # check that setting ticks on a shared axis is synchronized
  1512. assert ax1.get_zlim() == (-0.5, 2.5)
  1513. assert ax2.get_zlim() == (-0.5, 2.5)
  1514. def test_quaternion():
  1515. # 1:
  1516. q1 = Quaternion(1, [0, 0, 0])
  1517. assert q1.scalar == 1
  1518. assert (q1.vector == [0, 0, 0]).all
  1519. # __neg__:
  1520. assert (-q1).scalar == -1
  1521. assert ((-q1).vector == [0, 0, 0]).all
  1522. # i, j, k:
  1523. qi = Quaternion(0, [1, 0, 0])
  1524. assert qi.scalar == 0
  1525. assert (qi.vector == [1, 0, 0]).all
  1526. qj = Quaternion(0, [0, 1, 0])
  1527. assert qj.scalar == 0
  1528. assert (qj.vector == [0, 1, 0]).all
  1529. qk = Quaternion(0, [0, 0, 1])
  1530. assert qk.scalar == 0
  1531. assert (qk.vector == [0, 0, 1]).all
  1532. # i^2 = j^2 = k^2 = -1:
  1533. assert qi*qi == -q1
  1534. assert qj*qj == -q1
  1535. assert qk*qk == -q1
  1536. # identity:
  1537. assert q1*qi == qi
  1538. assert q1*qj == qj
  1539. assert q1*qk == qk
  1540. # i*j=k, j*k=i, k*i=j:
  1541. assert qi*qj == qk
  1542. assert qj*qk == qi
  1543. assert qk*qi == qj
  1544. assert qj*qi == -qk
  1545. assert qk*qj == -qi
  1546. assert qi*qk == -qj
  1547. # __mul__:
  1548. assert (Quaternion(2, [3, 4, 5]) * Quaternion(6, [7, 8, 9])
  1549. == Quaternion(-86, [28, 48, 44]))
  1550. # conjugate():
  1551. for q in [q1, qi, qj, qk]:
  1552. assert q.conjugate().scalar == q.scalar
  1553. assert (q.conjugate().vector == -q.vector).all
  1554. assert q.conjugate().conjugate() == q
  1555. assert ((q*q.conjugate()).vector == 0).all
  1556. # norm:
  1557. q0 = Quaternion(0, [0, 0, 0])
  1558. assert q0.norm == 0
  1559. assert q1.norm == 1
  1560. assert qi.norm == 1
  1561. assert qj.norm == 1
  1562. assert qk.norm == 1
  1563. for q in [q0, q1, qi, qj, qk]:
  1564. assert q.norm == (q*q.conjugate()).scalar
  1565. # normalize():
  1566. for q in [
  1567. Quaternion(2, [0, 0, 0]),
  1568. Quaternion(0, [3, 0, 0]),
  1569. Quaternion(0, [0, 4, 0]),
  1570. Quaternion(0, [0, 0, 5]),
  1571. Quaternion(6, [7, 8, 9])
  1572. ]:
  1573. assert q.normalize().norm == 1
  1574. # reciprocal():
  1575. for q in [q1, qi, qj, qk]:
  1576. assert q*q.reciprocal() == q1
  1577. assert q.reciprocal()*q == q1
  1578. # rotate():
  1579. assert (qi.rotate([1, 2, 3]) == np.array([1, -2, -3])).all
  1580. # rotate_from_to():
  1581. for r1, r2, q in [
  1582. ([1, 0, 0], [0, 1, 0], Quaternion(np.sqrt(1/2), [0, 0, np.sqrt(1/2)])),
  1583. ([1, 0, 0], [0, 0, 1], Quaternion(np.sqrt(1/2), [0, -np.sqrt(1/2), 0])),
  1584. ([1, 0, 0], [1, 0, 0], Quaternion(1, [0, 0, 0]))
  1585. ]:
  1586. assert Quaternion.rotate_from_to(r1, r2) == q
  1587. # rotate_from_to(), special case:
  1588. for r1 in [[1, 0, 0], [0, 1, 0], [0, 0, 1], [1, 1, 1]]:
  1589. r1 = np.array(r1)
  1590. with pytest.warns(UserWarning):
  1591. q = Quaternion.rotate_from_to(r1, -r1)
  1592. assert np.isclose(q.norm, 1)
  1593. assert np.dot(q.vector, r1) == 0
  1594. # from_cardan_angles(), as_cardan_angles():
  1595. for elev, azim, roll in [(0, 0, 0),
  1596. (90, 0, 0), (0, 90, 0), (0, 0, 90),
  1597. (0, 30, 30), (30, 0, 30), (30, 30, 0),
  1598. (47, 11, -24)]:
  1599. for mag in [1, 2]:
  1600. q = Quaternion.from_cardan_angles(
  1601. np.deg2rad(elev), np.deg2rad(azim), np.deg2rad(roll))
  1602. assert np.isclose(q.norm, 1)
  1603. q = Quaternion(mag * q.scalar, mag * q.vector)
  1604. np.testing.assert_allclose(np.rad2deg(Quaternion.as_cardan_angles(q)),
  1605. (elev, azim, roll), atol=1e-6)
  1606. @pytest.mark.parametrize('style',
  1607. ('azel', 'trackball', 'sphere', 'arcball'))
  1608. def test_rotate(style):
  1609. """Test rotating using the left mouse button."""
  1610. if style == 'azel':
  1611. s = 0.5
  1612. else:
  1613. s = mpl.rcParams['axes3d.trackballsize'] / 2
  1614. s *= 0.5
  1615. mpl.rcParams['axes3d.trackballborder'] = 0
  1616. with mpl.rc_context({'axes3d.mouserotationstyle': style}):
  1617. for roll, dx, dy in [
  1618. [0, 1, 0],
  1619. [30, 1, 0],
  1620. [0, 0, 1],
  1621. [30, 0, 1],
  1622. [0, 0.5, np.sqrt(3)/2],
  1623. [30, 0.5, np.sqrt(3)/2],
  1624. [0, 2, 0]]:
  1625. fig = plt.figure()
  1626. ax = fig.add_subplot(1, 1, 1, projection='3d')
  1627. ax.view_init(0, 0, roll)
  1628. ax.figure.canvas.draw()
  1629. # drag mouse to change orientation
  1630. ax._button_press(
  1631. mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0))
  1632. ax._on_move(
  1633. mock_event(ax, button=MouseButton.LEFT,
  1634. xdata=s*dx*ax._pseudo_w, ydata=s*dy*ax._pseudo_h))
  1635. ax.figure.canvas.draw()
  1636. c = np.sqrt(3)/2
  1637. expectations = {
  1638. ('azel', 0, 1, 0): (0, -45, 0),
  1639. ('azel', 0, 0, 1): (-45, 0, 0),
  1640. ('azel', 0, 0.5, c): (-38.971143, -22.5, 0),
  1641. ('azel', 0, 2, 0): (0, -90, 0),
  1642. ('azel', 30, 1, 0): (22.5, -38.971143, 30),
  1643. ('azel', 30, 0, 1): (-38.971143, -22.5, 30),
  1644. ('azel', 30, 0.5, c): (-22.5, -38.971143, 30),
  1645. ('trackball', 0, 1, 0): (0, -28.64789, 0),
  1646. ('trackball', 0, 0, 1): (-28.64789, 0, 0),
  1647. ('trackball', 0, 0.5, c): (-24.531578, -15.277726, 3.340403),
  1648. ('trackball', 0, 2, 0): (0, -180/np.pi, 0),
  1649. ('trackball', 30, 1, 0): (13.869588, -25.319385, 26.87008),
  1650. ('trackball', 30, 0, 1): (-24.531578, -15.277726, 33.340403),
  1651. ('trackball', 30, 0.5, c): (-13.869588, -25.319385, 33.129920),
  1652. ('sphere', 0, 1, 0): (0, -30, 0),
  1653. ('sphere', 0, 0, 1): (-30, 0, 0),
  1654. ('sphere', 0, 0.5, c): (-25.658906, -16.102114, 3.690068),
  1655. ('sphere', 0, 2, 0): (0, -90, 0),
  1656. ('sphere', 30, 1, 0): (14.477512, -26.565051, 26.565051),
  1657. ('sphere', 30, 0, 1): (-25.658906, -16.102114, 33.690068),
  1658. ('sphere', 30, 0.5, c): (-14.477512, -26.565051, 33.434949),
  1659. ('arcball', 0, 1, 0): (0, -60, 0),
  1660. ('arcball', 0, 0, 1): (-60, 0, 0),
  1661. ('arcball', 0, 0.5, c): (-48.590378, -40.893395, 19.106605),
  1662. ('arcball', 0, 2, 0): (0, 180, 0),
  1663. ('arcball', 30, 1, 0): (25.658906, -56.309932, 16.102114),
  1664. ('arcball', 30, 0, 1): (-48.590378, -40.893395, 49.106605),
  1665. ('arcball', 30, 0.5, c): (-25.658906, -56.309932, 43.897886)}
  1666. new_elev, new_azim, new_roll = expectations[(style, roll, dx, dy)]
  1667. np.testing.assert_allclose((ax.elev, ax.azim, ax.roll),
  1668. (new_elev, new_azim, new_roll), atol=1e-6)
  1669. def test_pan():
  1670. """Test mouse panning using the middle mouse button."""
  1671. def convert_lim(dmin, dmax):
  1672. """Convert min/max limits to center and range."""
  1673. center = (dmin + dmax) / 2
  1674. range_ = dmax - dmin
  1675. return center, range_
  1676. fig = plt.figure()
  1677. ax = fig.add_subplot(projection='3d')
  1678. ax.scatter(0, 0, 0)
  1679. fig.canvas.draw()
  1680. x_center0, x_range0 = convert_lim(*ax.get_xlim3d())
  1681. y_center0, y_range0 = convert_lim(*ax.get_ylim3d())
  1682. z_center0, z_range0 = convert_lim(*ax.get_zlim3d())
  1683. # move mouse diagonally to pan along all axis.
  1684. ax._button_press(
  1685. mock_event(ax, button=MouseButton.MIDDLE, xdata=0, ydata=0))
  1686. ax._on_move(
  1687. mock_event(ax, button=MouseButton.MIDDLE, xdata=1, ydata=1))
  1688. x_center, x_range = convert_lim(*ax.get_xlim3d())
  1689. y_center, y_range = convert_lim(*ax.get_ylim3d())
  1690. z_center, z_range = convert_lim(*ax.get_zlim3d())
  1691. # Ranges have not changed
  1692. assert x_range == pytest.approx(x_range0)
  1693. assert y_range == pytest.approx(y_range0)
  1694. assert z_range == pytest.approx(z_range0)
  1695. # But center positions have
  1696. assert x_center != pytest.approx(x_center0)
  1697. assert y_center != pytest.approx(y_center0)
  1698. assert z_center != pytest.approx(z_center0)
  1699. @pytest.mark.parametrize("tool,button,key,expected",
  1700. [("zoom", MouseButton.LEFT, None, # zoom in
  1701. ((0.00, 0.06), (0.01, 0.07), (0.02, 0.08))),
  1702. ("zoom", MouseButton.LEFT, 'x', # zoom in
  1703. ((-0.01, 0.10), (-0.03, 0.08), (-0.06, 0.06))),
  1704. ("zoom", MouseButton.LEFT, 'y', # zoom in
  1705. ((-0.07, 0.05), (-0.04, 0.08), (0.00, 0.12))),
  1706. ("zoom", MouseButton.RIGHT, None, # zoom out
  1707. ((-0.09, 0.15), (-0.08, 0.17), (-0.07, 0.18))),
  1708. ("pan", MouseButton.LEFT, None,
  1709. ((-0.70, -0.58), (-1.04, -0.91), (-1.27, -1.15))),
  1710. ("pan", MouseButton.LEFT, 'x',
  1711. ((-0.97, -0.84), (-0.58, -0.46), (-0.06, 0.06))),
  1712. ("pan", MouseButton.LEFT, 'y',
  1713. ((0.20, 0.32), (-0.51, -0.39), (-1.27, -1.15)))])
  1714. def test_toolbar_zoom_pan(tool, button, key, expected):
  1715. # NOTE: The expected zoom values are rough ballparks of moving in the view
  1716. # to make sure we are getting the right direction of motion.
  1717. # The specific values can and should change if the zoom movement
  1718. # scaling factor gets updated.
  1719. fig = plt.figure()
  1720. ax = fig.add_subplot(projection='3d')
  1721. ax.scatter(0, 0, 0)
  1722. fig.canvas.draw()
  1723. xlim0, ylim0, zlim0 = ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()
  1724. # Mouse from (0, 0) to (1, 1)
  1725. d0 = (0, 0)
  1726. d1 = (1, 1)
  1727. # Convert to screen coordinates ("s"). Events are defined only with pixel
  1728. # precision, so round the pixel values, and below, check against the
  1729. # corresponding xdata/ydata, which are close but not equal to d0/d1.
  1730. s0 = ax.transData.transform(d0).astype(int)
  1731. s1 = ax.transData.transform(d1).astype(int)
  1732. # Set up the mouse movements
  1733. start_event = MouseEvent(
  1734. "button_press_event", fig.canvas, *s0, button, key=key)
  1735. stop_event = MouseEvent(
  1736. "button_release_event", fig.canvas, *s1, button, key=key)
  1737. tb = NavigationToolbar2(fig.canvas)
  1738. if tool == "zoom":
  1739. tb.zoom()
  1740. tb.press_zoom(start_event)
  1741. tb.drag_zoom(stop_event)
  1742. tb.release_zoom(stop_event)
  1743. else:
  1744. tb.pan()
  1745. tb.press_pan(start_event)
  1746. tb.drag_pan(stop_event)
  1747. tb.release_pan(stop_event)
  1748. # Should be close, but won't be exact due to screen integer resolution
  1749. xlim, ylim, zlim = expected
  1750. assert ax.get_xlim3d() == pytest.approx(xlim, abs=0.01)
  1751. assert ax.get_ylim3d() == pytest.approx(ylim, abs=0.01)
  1752. assert ax.get_zlim3d() == pytest.approx(zlim, abs=0.01)
  1753. # Ensure that back, forward, and home buttons work
  1754. tb.back()
  1755. assert ax.get_xlim3d() == pytest.approx(xlim0)
  1756. assert ax.get_ylim3d() == pytest.approx(ylim0)
  1757. assert ax.get_zlim3d() == pytest.approx(zlim0)
  1758. tb.forward()
  1759. assert ax.get_xlim3d() == pytest.approx(xlim, abs=0.01)
  1760. assert ax.get_ylim3d() == pytest.approx(ylim, abs=0.01)
  1761. assert ax.get_zlim3d() == pytest.approx(zlim, abs=0.01)
  1762. tb.home()
  1763. assert ax.get_xlim3d() == pytest.approx(xlim0)
  1764. assert ax.get_ylim3d() == pytest.approx(ylim0)
  1765. assert ax.get_zlim3d() == pytest.approx(zlim0)
  1766. @mpl.style.context('default')
  1767. @check_figures_equal(extensions=["png"])
  1768. def test_scalarmap_update(fig_test, fig_ref):
  1769. x, y, z = np.array(list(itertools.product(*[np.arange(0, 5, 1),
  1770. np.arange(0, 5, 1),
  1771. np.arange(0, 5, 1)]))).T
  1772. c = x + y
  1773. # test
  1774. ax_test = fig_test.add_subplot(111, projection='3d')
  1775. sc_test = ax_test.scatter(x, y, z, c=c, s=40, cmap='viridis')
  1776. # force a draw
  1777. fig_test.canvas.draw()
  1778. # mark it as "stale"
  1779. sc_test.changed()
  1780. # ref
  1781. ax_ref = fig_ref.add_subplot(111, projection='3d')
  1782. sc_ref = ax_ref.scatter(x, y, z, c=c, s=40, cmap='viridis')
  1783. def test_subfigure_simple():
  1784. # smoketest that subfigures can work...
  1785. fig = plt.figure()
  1786. sf = fig.subfigures(1, 2)
  1787. ax = sf[0].add_subplot(1, 1, 1, projection='3d')
  1788. ax = sf[1].add_subplot(1, 1, 1, projection='3d', label='other')
  1789. # Update style when regenerating the test image
  1790. @image_comparison(baseline_images=['computed_zorder'], remove_text=True,
  1791. extensions=['png'], style=('mpl20'))
  1792. def test_computed_zorder():
  1793. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  1794. fig = plt.figure()
  1795. ax1 = fig.add_subplot(221, projection='3d')
  1796. ax2 = fig.add_subplot(222, projection='3d')
  1797. ax2.computed_zorder = False
  1798. # create a horizontal plane
  1799. corners = ((0, 0, 0), (0, 5, 0), (5, 5, 0), (5, 0, 0))
  1800. for ax in (ax1, ax2):
  1801. tri = art3d.Poly3DCollection([corners],
  1802. facecolors='white',
  1803. edgecolors='black',
  1804. zorder=1)
  1805. ax.add_collection3d(tri)
  1806. # plot a vector
  1807. ax.plot((2, 2), (2, 2), (0, 4), c='red', zorder=2)
  1808. # plot some points
  1809. ax.scatter((3, 3), (1, 3), (1, 3), c='red', zorder=10)
  1810. ax.set_xlim((0, 5.0))
  1811. ax.set_ylim((0, 5.0))
  1812. ax.set_zlim((0, 2.5))
  1813. ax3 = fig.add_subplot(223, projection='3d')
  1814. ax4 = fig.add_subplot(224, projection='3d')
  1815. ax4.computed_zorder = False
  1816. dim = 10
  1817. X, Y = np.meshgrid((-dim, dim), (-dim, dim))
  1818. Z = np.zeros((2, 2))
  1819. angle = 0.5
  1820. X2, Y2 = np.meshgrid((-dim, dim), (0, dim))
  1821. Z2 = Y2 * angle
  1822. X3, Y3 = np.meshgrid((-dim, dim), (-dim, 0))
  1823. Z3 = Y3 * angle
  1824. r = 7
  1825. M = 1000
  1826. th = np.linspace(0, 2 * np.pi, M)
  1827. x, y, z = r * np.cos(th), r * np.sin(th), angle * r * np.sin(th)
  1828. for ax in (ax3, ax4):
  1829. ax.plot_surface(X2, Y3, Z3,
  1830. color='blue',
  1831. alpha=0.5,
  1832. linewidth=0,
  1833. zorder=-1)
  1834. ax.plot(x[y < 0], y[y < 0], z[y < 0],
  1835. lw=5,
  1836. linestyle='--',
  1837. color='green',
  1838. zorder=0)
  1839. ax.plot_surface(X, Y, Z,
  1840. color='red',
  1841. alpha=0.5,
  1842. linewidth=0,
  1843. zorder=1)
  1844. ax.plot(r * np.sin(th), r * np.cos(th), np.zeros(M),
  1845. lw=5,
  1846. linestyle='--',
  1847. color='black',
  1848. zorder=2)
  1849. ax.plot_surface(X2, Y2, Z2,
  1850. color='blue',
  1851. alpha=0.5,
  1852. linewidth=0,
  1853. zorder=3)
  1854. ax.plot(x[y > 0], y[y > 0], z[y > 0], lw=5,
  1855. linestyle='--',
  1856. color='green',
  1857. zorder=4)
  1858. ax.view_init(elev=20, azim=-20, roll=0)
  1859. ax.axis('off')
  1860. def test_format_coord():
  1861. fig = plt.figure()
  1862. ax = fig.add_subplot(projection='3d')
  1863. x = np.arange(10)
  1864. ax.plot(x, np.sin(x))
  1865. xv = 0.1
  1866. yv = 0.1
  1867. fig.canvas.draw()
  1868. assert ax.format_coord(xv, yv) == 'x=10.5227, y pane=1.0417, z=0.1444'
  1869. # Modify parameters
  1870. ax.view_init(roll=30, vertical_axis="y")
  1871. fig.canvas.draw()
  1872. assert ax.format_coord(xv, yv) == 'x pane=9.1875, y=0.9761, z=0.1291'
  1873. # Reset parameters
  1874. ax.view_init()
  1875. fig.canvas.draw()
  1876. assert ax.format_coord(xv, yv) == 'x=10.5227, y pane=1.0417, z=0.1444'
  1877. # Check orthographic projection
  1878. ax.set_proj_type('ortho')
  1879. fig.canvas.draw()
  1880. assert ax.format_coord(xv, yv) == 'x=10.8869, y pane=1.0417, z=0.1528'
  1881. # Check non-default perspective projection
  1882. ax.set_proj_type('persp', focal_length=0.1)
  1883. fig.canvas.draw()
  1884. assert ax.format_coord(xv, yv) == 'x=9.0620, y pane=1.0417, z=0.1110'
  1885. def test_get_axis_position():
  1886. fig = plt.figure()
  1887. ax = fig.add_subplot(projection='3d')
  1888. x = np.arange(10)
  1889. ax.plot(x, np.sin(x))
  1890. fig.canvas.draw()
  1891. assert ax.get_axis_position() == (False, True, False)
  1892. def test_margins():
  1893. fig = plt.figure()
  1894. ax = fig.add_subplot(projection='3d')
  1895. ax.margins(0.2)
  1896. assert ax.margins() == (0.2, 0.2, 0.2)
  1897. ax.margins(0.1, 0.2, 0.3)
  1898. assert ax.margins() == (0.1, 0.2, 0.3)
  1899. ax.margins(x=0)
  1900. assert ax.margins() == (0, 0.2, 0.3)
  1901. ax.margins(y=0.1)
  1902. assert ax.margins() == (0, 0.1, 0.3)
  1903. ax.margins(z=0)
  1904. assert ax.margins() == (0, 0.1, 0)
  1905. def test_margin_getters():
  1906. fig = plt.figure()
  1907. ax = fig.add_subplot(projection='3d')
  1908. ax.margins(0.1, 0.2, 0.3)
  1909. assert ax.get_xmargin() == 0.1
  1910. assert ax.get_ymargin() == 0.2
  1911. assert ax.get_zmargin() == 0.3
  1912. @pytest.mark.parametrize('err, args, kwargs, match', (
  1913. (ValueError, (-1,), {}, r'margin must be greater than -0\.5'),
  1914. (ValueError, (1, -1, 1), {}, r'margin must be greater than -0\.5'),
  1915. (ValueError, (1, 1, -1), {}, r'margin must be greater than -0\.5'),
  1916. (ValueError, tuple(), {'x': -1}, r'margin must be greater than -0\.5'),
  1917. (ValueError, tuple(), {'y': -1}, r'margin must be greater than -0\.5'),
  1918. (ValueError, tuple(), {'z': -1}, r'margin must be greater than -0\.5'),
  1919. (TypeError, (1, ), {'x': 1},
  1920. 'Cannot pass both positional and keyword'),
  1921. (TypeError, (1, ), {'x': 1, 'y': 1, 'z': 1},
  1922. 'Cannot pass both positional and keyword'),
  1923. (TypeError, (1, ), {'x': 1, 'y': 1},
  1924. 'Cannot pass both positional and keyword'),
  1925. (TypeError, (1, 1), {}, 'Must pass a single positional argument for'),
  1926. ))
  1927. def test_margins_errors(err, args, kwargs, match):
  1928. with pytest.raises(err, match=match):
  1929. fig = plt.figure()
  1930. ax = fig.add_subplot(projection='3d')
  1931. ax.margins(*args, **kwargs)
  1932. @check_figures_equal(extensions=["png"])
  1933. def test_text_3d(fig_test, fig_ref):
  1934. ax = fig_ref.add_subplot(projection="3d")
  1935. txt = Text(0.5, 0.5, r'Foo bar $\int$')
  1936. art3d.text_2d_to_3d(txt, z=1)
  1937. ax.add_artist(txt)
  1938. assert txt.get_position_3d() == (0.5, 0.5, 1)
  1939. ax = fig_test.add_subplot(projection="3d")
  1940. t3d = art3d.Text3D(0.5, 0.5, 1, r'Foo bar $\int$')
  1941. ax.add_artist(t3d)
  1942. assert t3d.get_position_3d() == (0.5, 0.5, 1)
  1943. def test_draw_single_lines_from_Nx1():
  1944. # Smoke test for GH#23459
  1945. fig = plt.figure()
  1946. ax = fig.add_subplot(projection='3d')
  1947. ax.plot([[0], [1]], [[0], [1]], [[0], [1]])
  1948. @check_figures_equal(extensions=["png"])
  1949. def test_pathpatch_3d(fig_test, fig_ref):
  1950. ax = fig_ref.add_subplot(projection="3d")
  1951. path = Path.unit_rectangle()
  1952. patch = PathPatch(path)
  1953. art3d.pathpatch_2d_to_3d(patch, z=(0, 0.5, 0.7, 1, 0), zdir='y')
  1954. ax.add_artist(patch)
  1955. ax = fig_test.add_subplot(projection="3d")
  1956. pp3d = art3d.PathPatch3D(path, zs=(0, 0.5, 0.7, 1, 0), zdir='y')
  1957. ax.add_artist(pp3d)
  1958. @image_comparison(baseline_images=['scatter_spiral.png'],
  1959. remove_text=True,
  1960. style='mpl20')
  1961. def test_scatter_spiral():
  1962. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  1963. fig = plt.figure()
  1964. ax = fig.add_subplot(projection='3d')
  1965. th = np.linspace(0, 2 * np.pi * 6, 256)
  1966. sc = ax.scatter(np.sin(th), np.cos(th), th, s=(1 + th * 5), c=th ** 2)
  1967. # force at least 1 draw!
  1968. fig.canvas.draw()
  1969. def test_Poly3DCollection_get_path():
  1970. # Smoke test to see that get_path does not raise
  1971. # See GH#27361
  1972. fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
  1973. p = Circle((0, 0), 1.0)
  1974. ax.add_patch(p)
  1975. art3d.pathpatch_2d_to_3d(p)
  1976. p.get_path()
  1977. def test_Poly3DCollection_get_facecolor():
  1978. # Smoke test to see that get_facecolor does not raise
  1979. # See GH#4067
  1980. y, x = np.ogrid[1:10:100j, 1:10:100j]
  1981. z2 = np.cos(x) ** 3 - np.sin(y) ** 2
  1982. fig = plt.figure()
  1983. ax = fig.add_subplot(111, projection='3d')
  1984. r = ax.plot_surface(x, y, z2, cmap='hot')
  1985. r.get_facecolor()
  1986. def test_Poly3DCollection_get_edgecolor():
  1987. # Smoke test to see that get_edgecolor does not raise
  1988. # See GH#4067
  1989. y, x = np.ogrid[1:10:100j, 1:10:100j]
  1990. z2 = np.cos(x) ** 3 - np.sin(y) ** 2
  1991. fig = plt.figure()
  1992. ax = fig.add_subplot(111, projection='3d')
  1993. r = ax.plot_surface(x, y, z2, cmap='hot')
  1994. r.get_edgecolor()
  1995. @pytest.mark.parametrize(
  1996. "vertical_axis, proj_expected, axis_lines_expected, tickdirs_expected",
  1997. [
  1998. (
  1999. "z",
  2000. [
  2001. [0.0, 1.142857, 0.0, -0.571429],
  2002. [0.0, 0.0, 0.857143, -0.428571],
  2003. [0.0, 0.0, 0.0, -10.0],
  2004. [-1.142857, 0.0, 0.0, 10.571429],
  2005. ],
  2006. [
  2007. ([0.05617978, 0.06329114], [-0.04213483, -0.04746835]),
  2008. ([-0.06329114, 0.06329114], [-0.04746835, -0.04746835]),
  2009. ([-0.06329114, -0.06329114], [-0.04746835, 0.04746835]),
  2010. ],
  2011. [1, 0, 0],
  2012. ),
  2013. (
  2014. "y",
  2015. [
  2016. [1.142857, 0.0, 0.0, -0.571429],
  2017. [0.0, 0.857143, 0.0, -0.428571],
  2018. [0.0, 0.0, 0.0, -10.0],
  2019. [0.0, 0.0, -1.142857, 10.571429],
  2020. ],
  2021. [
  2022. ([-0.06329114, 0.06329114], [0.04746835, 0.04746835]),
  2023. ([0.06329114, 0.06329114], [-0.04746835, 0.04746835]),
  2024. ([-0.05617978, -0.06329114], [0.04213483, 0.04746835]),
  2025. ],
  2026. [2, 2, 0],
  2027. ),
  2028. (
  2029. "x",
  2030. [
  2031. [0.0, 0.0, 1.142857, -0.571429],
  2032. [0.857143, 0.0, 0.0, -0.428571],
  2033. [0.0, 0.0, 0.0, -10.0],
  2034. [0.0, -1.142857, 0.0, 10.571429],
  2035. ],
  2036. [
  2037. ([-0.06329114, -0.06329114], [0.04746835, -0.04746835]),
  2038. ([0.06329114, 0.05617978], [0.04746835, 0.04213483]),
  2039. ([0.06329114, -0.06329114], [0.04746835, 0.04746835]),
  2040. ],
  2041. [1, 2, 1],
  2042. ),
  2043. ],
  2044. )
  2045. def test_view_init_vertical_axis(
  2046. vertical_axis, proj_expected, axis_lines_expected, tickdirs_expected
  2047. ):
  2048. """
  2049. Test the actual projection, axis lines and ticks matches expected values.
  2050. Parameters
  2051. ----------
  2052. vertical_axis : str
  2053. Axis to align vertically.
  2054. proj_expected : ndarray
  2055. Expected values from ax.get_proj().
  2056. axis_lines_expected : tuple of arrays
  2057. Edgepoints of the axis line. Expected values retrieved according
  2058. to ``ax.get_[xyz]axis().line.get_data()``.
  2059. tickdirs_expected : list of int
  2060. indexes indicating which axis to create a tick line along.
  2061. """
  2062. rtol = 2e-06
  2063. ax = plt.subplot(1, 1, 1, projection="3d")
  2064. ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis)
  2065. ax.get_figure().canvas.draw()
  2066. # Assert the projection matrix:
  2067. proj_actual = ax.get_proj()
  2068. np.testing.assert_allclose(proj_expected, proj_actual, rtol=rtol)
  2069. for i, axis in enumerate([ax.get_xaxis(), ax.get_yaxis(), ax.get_zaxis()]):
  2070. # Assert black lines are correctly aligned:
  2071. axis_line_expected = axis_lines_expected[i]
  2072. axis_line_actual = axis.line.get_data()
  2073. np.testing.assert_allclose(axis_line_expected, axis_line_actual,
  2074. rtol=rtol)
  2075. # Assert ticks are correctly aligned:
  2076. tickdir_expected = tickdirs_expected[i]
  2077. tickdir_actual = axis._get_tickdir('default')
  2078. np.testing.assert_array_equal(tickdir_expected, tickdir_actual)
  2079. @pytest.mark.parametrize("vertical_axis", ["x", "y", "z"])
  2080. def test_on_move_vertical_axis(vertical_axis: str) -> None:
  2081. """
  2082. Test vertical axis is respected when rotating the plot interactively.
  2083. """
  2084. ax = plt.subplot(1, 1, 1, projection="3d")
  2085. ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis)
  2086. ax.get_figure().canvas.draw()
  2087. proj_before = ax.get_proj()
  2088. event_click = mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=1)
  2089. ax._button_press(event_click)
  2090. event_move = mock_event(ax, button=MouseButton.LEFT, xdata=0.5, ydata=0.8)
  2091. ax._on_move(event_move)
  2092. assert ax._axis_names.index(vertical_axis) == ax._vertical_axis
  2093. # Make sure plot has actually moved:
  2094. proj_after = ax.get_proj()
  2095. np.testing.assert_raises(
  2096. AssertionError, np.testing.assert_allclose, proj_before, proj_after
  2097. )
  2098. @pytest.mark.parametrize(
  2099. "vertical_axis, aspect_expected",
  2100. [
  2101. ("x", [1.190476, 0.892857, 1.190476]),
  2102. ("y", [0.892857, 1.190476, 1.190476]),
  2103. ("z", [1.190476, 1.190476, 0.892857]),
  2104. ],
  2105. )
  2106. def test_set_box_aspect_vertical_axis(vertical_axis, aspect_expected):
  2107. ax = plt.subplot(1, 1, 1, projection="3d")
  2108. ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis)
  2109. ax.get_figure().canvas.draw()
  2110. ax.set_box_aspect(None)
  2111. np.testing.assert_allclose(aspect_expected, ax._box_aspect, rtol=1e-6)
  2112. @image_comparison(baseline_images=['arc_pathpatch.png'],
  2113. remove_text=True,
  2114. style='mpl20')
  2115. def test_arc_pathpatch():
  2116. ax = plt.subplot(1, 1, 1, projection="3d")
  2117. a = mpatch.Arc((0.5, 0.5), width=0.5, height=0.9,
  2118. angle=20, theta1=10, theta2=130)
  2119. ax.add_patch(a)
  2120. art3d.pathpatch_2d_to_3d(a, z=0, zdir='z')
  2121. @image_comparison(baseline_images=['panecolor_rcparams.png'],
  2122. remove_text=True,
  2123. style='mpl20')
  2124. def test_panecolor_rcparams():
  2125. with plt.rc_context({'axes3d.xaxis.panecolor': 'r',
  2126. 'axes3d.yaxis.panecolor': 'g',
  2127. 'axes3d.zaxis.panecolor': 'b'}):
  2128. fig = plt.figure(figsize=(1, 1))
  2129. fig.add_subplot(projection='3d')
  2130. @check_figures_equal(extensions=["png"])
  2131. def test_mutating_input_arrays_y_and_z(fig_test, fig_ref):
  2132. """
  2133. Test to see if the `z` axis does not get mutated
  2134. after a call to `Axes3D.plot`
  2135. test cases came from GH#8990
  2136. """
  2137. ax1 = fig_test.add_subplot(111, projection='3d')
  2138. x = [1, 2, 3]
  2139. y = [0.0, 0.0, 0.0]
  2140. z = [0.0, 0.0, 0.0]
  2141. ax1.plot(x, y, z, 'o-')
  2142. # mutate y,z to get a nontrivial line
  2143. y[:] = [1, 2, 3]
  2144. z[:] = [1, 2, 3]
  2145. # draw the same plot without mutating x and y
  2146. ax2 = fig_ref.add_subplot(111, projection='3d')
  2147. x = [1, 2, 3]
  2148. y = [0.0, 0.0, 0.0]
  2149. z = [0.0, 0.0, 0.0]
  2150. ax2.plot(x, y, z, 'o-')
  2151. def test_scatter_masked_color():
  2152. """
  2153. Test color parameter usage with non-finite coordinate arrays.
  2154. GH#26236
  2155. """
  2156. x = [np.nan, 1, 2, 1]
  2157. y = [0, np.inf, 2, 1]
  2158. z = [0, 1, -np.inf, 1]
  2159. colors = [
  2160. [0.0, 0.0, 0.0, 1],
  2161. [0.0, 0.0, 0.0, 1],
  2162. [0.0, 0.0, 0.0, 1],
  2163. [0.0, 0.0, 0.0, 1]
  2164. ]
  2165. fig = plt.figure()
  2166. ax = fig.add_subplot(projection='3d')
  2167. path3d = ax.scatter(x, y, z, color=colors)
  2168. # Assert sizes' equality
  2169. assert len(path3d.get_offsets()) ==\
  2170. len(super(type(path3d), path3d).get_facecolors())
  2171. @mpl3d_image_comparison(['surface3d_zsort_inf.png'], style='mpl20')
  2172. def test_surface3d_zsort_inf():
  2173. plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated
  2174. fig = plt.figure()
  2175. ax = fig.add_subplot(projection='3d')
  2176. x, y = np.mgrid[-2:2:0.1, -2:2:0.1]
  2177. z = np.sin(x)**2 + np.cos(y)**2
  2178. z[x.shape[0] // 2:, x.shape[1] // 2:] = np.inf
  2179. ax.plot_surface(x, y, z, cmap='jet')
  2180. ax.view_init(elev=45, azim=145)
  2181. def test_Poly3DCollection_init_value_error():
  2182. # smoke test to ensure the input check works
  2183. # GH#26420
  2184. with pytest.raises(ValueError,
  2185. match='You must provide facecolors, edgecolors, '
  2186. 'or both for shade to work.'):
  2187. poly = np.array([[0, 0, 1], [0, 1, 1], [0, 0, 0]], float)
  2188. c = art3d.Poly3DCollection([poly], shade=True)
  2189. def test_ndarray_color_kwargs_value_error():
  2190. # smoke test
  2191. # ensures ndarray can be passed to color in kwargs for 3d projection plot
  2192. fig = plt.figure()
  2193. ax = fig.add_subplot(111, projection='3d')
  2194. ax.scatter(1, 0, 0, color=np.array([0, 0, 0, 1]))
  2195. fig.canvas.draw()