axes3d.py 101 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955
  1. """
  2. axes3d.py, original mplot3d version by John Porter
  3. Created: 23 Sep 2005
  4. Parts fixed by Reinier Heeres <reinier@heeres.eu>
  5. Minor additions by Ben Axelrod <baxelrod@coroware.com>
  6. Significant updates and revisions by Ben Root <ben.v.root@gmail.com>
  7. Module containing Axes3D, an object which can plot 3D objects on a
  8. 2D matplotlib figure.
  9. """
  10. from collections import defaultdict
  11. from functools import reduce
  12. import math
  13. import textwrap
  14. import numpy as np
  15. from matplotlib import artist
  16. import matplotlib.axes as maxes
  17. import matplotlib.cbook as cbook
  18. import matplotlib.collections as mcoll
  19. import matplotlib.colors as mcolors
  20. import matplotlib.docstring as docstring
  21. import matplotlib.scale as mscale
  22. import matplotlib.transforms as mtransforms
  23. from matplotlib.axes import Axes, rcParams
  24. from matplotlib.axes._base import _axis_method_wrapper
  25. from matplotlib.transforms import Bbox
  26. from matplotlib.tri.triangulation import Triangulation
  27. from . import art3d
  28. from . import proj3d
  29. from . import axis3d
  30. @cbook.deprecated("3.2", alternative="Bbox.unit()")
  31. def unit_bbox():
  32. box = Bbox(np.array([[0, 0], [1, 1]]))
  33. return box
  34. @cbook._define_aliases({
  35. "xlim3d": ["xlim"], "ylim3d": ["ylim"], "zlim3d": ["zlim"]})
  36. class Axes3D(Axes):
  37. """
  38. 3D axes object.
  39. """
  40. name = '3d'
  41. _shared_z_axes = cbook.Grouper()
  42. def __init__(
  43. self, fig, rect=None, *args,
  44. azim=-60, elev=30, sharez=None, proj_type='persp',
  45. box_aspect=None,
  46. **kwargs):
  47. """
  48. Parameters
  49. ----------
  50. fig : Figure
  51. The parent figure.
  52. rect : (float, float, float, float)
  53. The ``(left, bottom, width, height)`` axes position.
  54. azim : float, default: -60
  55. Azimuthal viewing angle.
  56. elev : float, default: 30
  57. Elevation viewing angle.
  58. sharez : Axes3D, optional
  59. Other axes to share z-limits with.
  60. proj_type : {'persp', 'ortho'}
  61. The projection type, default 'persp'.
  62. **kwargs
  63. Other optional keyword arguments:
  64. %(Axes3D)s
  65. Notes
  66. -----
  67. .. versionadded:: 1.2.1
  68. The *sharez* parameter.
  69. """
  70. if rect is None:
  71. rect = [0.0, 0.0, 1.0, 1.0]
  72. self.initial_azim = azim
  73. self.initial_elev = elev
  74. self.set_proj_type(proj_type)
  75. self.xy_viewLim = Bbox.unit()
  76. self.zz_viewLim = Bbox.unit()
  77. self.xy_dataLim = Bbox.unit()
  78. self.zz_dataLim = Bbox.unit()
  79. # inhibit autoscale_view until the axes are defined
  80. # they can't be defined until Axes.__init__ has been called
  81. self.view_init(self.initial_elev, self.initial_azim)
  82. self._sharez = sharez
  83. if sharez is not None:
  84. self._shared_z_axes.join(self, sharez)
  85. self._adjustable = 'datalim'
  86. super().__init__(
  87. fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs
  88. )
  89. # Disable drawing of axes by base class
  90. super().set_axis_off()
  91. # Enable drawing of axes by Axes3D class
  92. self.set_axis_on()
  93. self.M = None
  94. # func used to format z -- fall back on major formatters
  95. self.fmt_zdata = None
  96. if self.zaxis is not None:
  97. self._zcid = self.zaxis.callbacks.connect(
  98. 'units finalize', lambda: self._on_units_changed(scalez=True))
  99. else:
  100. self._zcid = None
  101. self.mouse_init()
  102. self.figure.canvas.mpl_connect(
  103. 'motion_notify_event', self._on_move),
  104. self.figure.canvas.mpl_connect(
  105. 'button_press_event', self._button_press),
  106. self.figure.canvas.mpl_connect(
  107. 'button_release_event', self._button_release),
  108. self.set_top_view()
  109. self.patch.set_linewidth(0)
  110. # Calculate the pseudo-data width and height
  111. pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)])
  112. self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0]
  113. self.figure.add_axes(self)
  114. # mplot3d currently manages its own spines and needs these turned off
  115. # for bounding box calculations
  116. for k in self.spines.keys():
  117. self.spines[k].set_visible(False)
  118. def set_axis_off(self):
  119. self._axis3don = False
  120. self.stale = True
  121. def set_axis_on(self):
  122. self._axis3don = True
  123. self.stale = True
  124. def convert_zunits(self, z):
  125. """
  126. For artists in an axes, if the zaxis has units support,
  127. convert *z* using zaxis unit type
  128. .. versionadded:: 1.2.1
  129. """
  130. return self.zaxis.convert_units(z)
  131. def _process_unit_info(self, xdata=None, ydata=None, zdata=None,
  132. kwargs=None):
  133. """Update the axis instances based on unit *kwargs* if given."""
  134. super()._process_unit_info(xdata=xdata, ydata=ydata, kwargs=kwargs)
  135. if self.xaxis is None or self.yaxis is None or self.zaxis is None:
  136. return
  137. if zdata is not None:
  138. # we only need to update if there is nothing set yet.
  139. if not self.zaxis.have_units():
  140. self.zaxis.update_units(xdata)
  141. # process kwargs 2nd since these will override default units
  142. if kwargs is not None:
  143. zunits = kwargs.pop('zunits', self.zaxis.units)
  144. if zunits != self.zaxis.units:
  145. self.zaxis.set_units(zunits)
  146. # If the units being set imply a different converter,
  147. # we need to update.
  148. if zdata is not None:
  149. self.zaxis.update_units(zdata)
  150. def set_top_view(self):
  151. # this happens to be the right view for the viewing coordinates
  152. # moved up and to the left slightly to fit labels and axes
  153. xdwl = 0.95 / self.dist
  154. xdw = 0.9 / self.dist
  155. ydwl = 0.95 / self.dist
  156. ydw = 0.9 / self.dist
  157. # This is purposely using the 2D Axes's set_xlim and set_ylim,
  158. # because we are trying to place our viewing pane.
  159. super().set_xlim(-xdwl, xdw, auto=None)
  160. super().set_ylim(-ydwl, ydw, auto=None)
  161. def _init_axis(self):
  162. """Init 3D axes; overrides creation of regular X/Y axes."""
  163. self.xaxis = axis3d.XAxis('x', self.xy_viewLim.intervalx,
  164. self.xy_dataLim.intervalx, self)
  165. self.yaxis = axis3d.YAxis('y', self.xy_viewLim.intervaly,
  166. self.xy_dataLim.intervaly, self)
  167. self.zaxis = axis3d.ZAxis('z', self.zz_viewLim.intervalx,
  168. self.zz_dataLim.intervalx, self)
  169. for ax in self.xaxis, self.yaxis, self.zaxis:
  170. ax.init3d()
  171. def get_zaxis(self):
  172. """Return the ``ZAxis`` (`~.axis3d.Axis`) instance."""
  173. return self.zaxis
  174. get_zgridlines = _axis_method_wrapper("zaxis", "get_gridlines")
  175. get_zticklines = _axis_method_wrapper("zaxis", "get_ticklines")
  176. @cbook.deprecated("3.1", alternative="xaxis", pending=True)
  177. @property
  178. def w_xaxis(self):
  179. return self.xaxis
  180. @cbook.deprecated("3.1", alternative="yaxis", pending=True)
  181. @property
  182. def w_yaxis(self):
  183. return self.yaxis
  184. @cbook.deprecated("3.1", alternative="zaxis", pending=True)
  185. @property
  186. def w_zaxis(self):
  187. return self.zaxis
  188. def _get_axis_list(self):
  189. return super()._get_axis_list() + (self.zaxis, )
  190. def unit_cube(self, vals=None):
  191. minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims()
  192. return [(minx, miny, minz),
  193. (maxx, miny, minz),
  194. (maxx, maxy, minz),
  195. (minx, maxy, minz),
  196. (minx, miny, maxz),
  197. (maxx, miny, maxz),
  198. (maxx, maxy, maxz),
  199. (minx, maxy, maxz)]
  200. def tunit_cube(self, vals=None, M=None):
  201. if M is None:
  202. M = self.M
  203. xyzs = self.unit_cube(vals)
  204. tcube = proj3d.proj_points(xyzs, M)
  205. return tcube
  206. def tunit_edges(self, vals=None, M=None):
  207. tc = self.tunit_cube(vals, M)
  208. edges = [(tc[0], tc[1]),
  209. (tc[1], tc[2]),
  210. (tc[2], tc[3]),
  211. (tc[3], tc[0]),
  212. (tc[0], tc[4]),
  213. (tc[1], tc[5]),
  214. (tc[2], tc[6]),
  215. (tc[3], tc[7]),
  216. (tc[4], tc[5]),
  217. (tc[5], tc[6]),
  218. (tc[6], tc[7]),
  219. (tc[7], tc[4])]
  220. return edges
  221. def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
  222. """
  223. Set the aspect ratios.
  224. Axes 3D does not current support any aspect but 'auto' which fills
  225. the axes with the data limits.
  226. To simulate having equal aspect in data space, set the ratio
  227. of your data limits to match the value of `~.get_box_aspect`.
  228. To control box aspect ratios use `~.Axes3D.set_box_aspect`.
  229. Parameters
  230. ----------
  231. aspect : {'auto'}
  232. Possible values:
  233. ========= ==================================================
  234. value description
  235. ========= ==================================================
  236. 'auto' automatic; fill the position rectangle with data.
  237. ========= ==================================================
  238. adjustable : None
  239. Currently ignored by Axes3D
  240. If not *None*, this defines which parameter will be adjusted to
  241. meet the required aspect. See `.set_adjustable` for further
  242. details.
  243. anchor : None or str or 2-tuple of float, optional
  244. If not *None*, this defines where the Axes will be drawn if there
  245. is extra space due to aspect constraints. The most common way to
  246. to specify the anchor are abbreviations of cardinal directions:
  247. ===== =====================
  248. value description
  249. ===== =====================
  250. 'C' centered
  251. 'SW' lower left corner
  252. 'S' middle of bottom edge
  253. 'SE' lower right corner
  254. etc.
  255. ===== =====================
  256. See `.set_anchor` for further details.
  257. share : bool, default: False
  258. If ``True``, apply the settings to all shared Axes.
  259. See Also
  260. --------
  261. mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
  262. """
  263. if aspect != 'auto':
  264. raise NotImplementedError(
  265. "Axes3D currently only supports the aspect argument "
  266. f"'auto'. You passed in {aspect!r}."
  267. )
  268. if share:
  269. axes = {*self._shared_x_axes.get_siblings(self),
  270. *self._shared_y_axes.get_siblings(self),
  271. *self._shared_z_axes.get_siblings(self),
  272. }
  273. else:
  274. axes = {self}
  275. for ax in axes:
  276. ax._aspect = aspect
  277. ax.stale = True
  278. if anchor is not None:
  279. self.set_anchor(anchor, share=share)
  280. def set_anchor(self, anchor, share=False):
  281. # docstring inherited
  282. if not (anchor in mtransforms.Bbox.coefs or len(anchor) == 2):
  283. raise ValueError('anchor must be among %s' %
  284. ', '.join(mtransforms.Bbox.coefs))
  285. if share:
  286. axes = {*self._shared_x_axes.get_siblings(self),
  287. *self._shared_y_axes.get_siblings(self),
  288. *self._shared_z_axes.get_siblings(self),
  289. }
  290. else:
  291. axes = {self}
  292. for ax in axes:
  293. ax._anchor = anchor
  294. ax.stale = True
  295. def set_box_aspect(self, aspect, *, zoom=1):
  296. """
  297. Set the axes box aspect.
  298. The box aspect is the ratio of height to width in display
  299. units for each face of the box when viewed perpendicular to
  300. that face. This is not to be confused with the data aspect
  301. (which for Axes3D is always 'auto'). The default ratios are
  302. 4:4:3 (x:y:z).
  303. To simulate having equal aspect in data space, set the box
  304. aspect to match your data range in each dimension.
  305. *zoom* controls the overall size of the Axes3D in the figure.
  306. Parameters
  307. ----------
  308. aspect : 3-tuple of floats or None
  309. Changes the physical dimensions of the Axes3D, such that the ratio
  310. of the axis lengths in display units is x:y:z.
  311. If None, defaults to 4:4:3
  312. zoom : float
  313. Control overall size of the Axes3D in the figure.
  314. """
  315. if aspect is None:
  316. aspect = np.asarray((4, 4, 3), dtype=float)
  317. else:
  318. orig_aspect = aspect
  319. aspect = np.asarray(aspect, dtype=float)
  320. if aspect.shape != (3,):
  321. raise ValueError(
  322. "You must pass a 3-tuple that can be cast to floats. "
  323. f"You passed {orig_aspect!r}"
  324. )
  325. # default scale tuned to match the mpl32 appearance.
  326. aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect)
  327. self._box_aspect = aspect
  328. self.stale = True
  329. def apply_aspect(self, position=None):
  330. if position is None:
  331. position = self.get_position(original=True)
  332. # in the superclass, we would go through and actually deal with axis
  333. # scales and box/datalim. Those are all irrelevant - all we need to do
  334. # is make sure our coordinate system is square.
  335. figW, figH = self.get_figure().get_size_inches()
  336. fig_aspect = figH / figW
  337. box_aspect = 1
  338. pb = position.frozen()
  339. pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
  340. self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
  341. @artist.allow_rasterization
  342. def draw(self, renderer):
  343. # draw the background patch
  344. self.patch.draw(renderer)
  345. self._frameon = False
  346. # first, set the aspect
  347. # this is duplicated from `axes._base._AxesBase.draw`
  348. # but must be called before any of the artist are drawn as
  349. # it adjusts the view limits and the size of the bounding box
  350. # of the axes
  351. locator = self.get_axes_locator()
  352. if locator:
  353. pos = locator(self, renderer)
  354. self.apply_aspect(pos)
  355. else:
  356. self.apply_aspect()
  357. # add the projection matrix to the renderer
  358. self.M = self.get_proj()
  359. renderer.M = self.M
  360. renderer.vvec = self.vvec
  361. renderer.eye = self.eye
  362. renderer.get_axis_position = self.get_axis_position
  363. # Calculate projection of collections and patches and zorder them.
  364. # Make sure they are drawn above the grids.
  365. zorder_offset = max(axis.get_zorder()
  366. for axis in self._get_axis_list()) + 1
  367. for i, col in enumerate(
  368. sorted(self.collections,
  369. key=lambda col: col.do_3d_projection(renderer),
  370. reverse=True)):
  371. col.zorder = zorder_offset + i
  372. for i, patch in enumerate(
  373. sorted(self.patches,
  374. key=lambda patch: patch.do_3d_projection(renderer),
  375. reverse=True)):
  376. patch.zorder = zorder_offset + i
  377. if self._axis3don:
  378. # Draw panes first
  379. for axis in self._get_axis_list():
  380. axis.draw_pane(renderer)
  381. # Then axes
  382. for axis in self._get_axis_list():
  383. axis.draw(renderer)
  384. # Then rest
  385. super().draw(renderer)
  386. def get_axis_position(self):
  387. vals = self.get_w_lims()
  388. tc = self.tunit_cube(vals, self.M)
  389. xhigh = tc[1][2] > tc[2][2]
  390. yhigh = tc[3][2] > tc[2][2]
  391. zhigh = tc[0][2] > tc[2][2]
  392. return xhigh, yhigh, zhigh
  393. def _on_units_changed(self, scalex=False, scaley=False, scalez=False):
  394. """
  395. Callback for processing changes to axis units.
  396. Currently forces updates of data limits and view limits.
  397. """
  398. self.relim()
  399. self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez)
  400. def update_datalim(self, xys, **kwargs):
  401. pass
  402. def get_autoscale_on(self):
  403. """
  404. Get whether autoscaling is applied for all axes on plot commands
  405. .. versionadded:: 1.1.0
  406. This function was added, but not tested. Please report any bugs.
  407. """
  408. return super().get_autoscale_on() and self.get_autoscalez_on()
  409. def get_autoscalez_on(self):
  410. """
  411. Get whether autoscaling for the z-axis is applied on plot commands
  412. .. versionadded:: 1.1.0
  413. This function was added, but not tested. Please report any bugs.
  414. """
  415. return self._autoscaleZon
  416. def set_autoscale_on(self, b):
  417. """
  418. Set whether autoscaling is applied on plot commands
  419. .. versionadded:: 1.1.0
  420. This function was added, but not tested. Please report any bugs.
  421. Parameters
  422. ----------
  423. b : bool
  424. """
  425. super().set_autoscale_on(b)
  426. self.set_autoscalez_on(b)
  427. def set_autoscalez_on(self, b):
  428. """
  429. Set whether autoscaling for the z-axis is applied on plot commands
  430. .. versionadded:: 1.1.0
  431. Parameters
  432. ----------
  433. b : bool
  434. """
  435. self._autoscaleZon = b
  436. def set_zmargin(self, m):
  437. """
  438. Set padding of Z data limits prior to autoscaling.
  439. *m* times the data interval will be added to each
  440. end of that interval before it is used in autoscaling.
  441. accepts: float in range 0 to 1
  442. .. versionadded:: 1.1.0
  443. """
  444. if m < 0 or m > 1:
  445. raise ValueError("margin must be in range 0 to 1")
  446. self._zmargin = m
  447. self.stale = True
  448. def margins(self, *margins, x=None, y=None, z=None, tight=True):
  449. """
  450. Convenience method to set or retrieve autoscaling margins.
  451. Call signatures::
  452. margins()
  453. returns xmargin, ymargin, zmargin
  454. ::
  455. margins(margin)
  456. margins(xmargin, ymargin, zmargin)
  457. margins(x=xmargin, y=ymargin, z=zmargin)
  458. margins(..., tight=False)
  459. All forms above set the xmargin, ymargin and zmargin
  460. parameters. All keyword parameters are optional. A single
  461. positional argument specifies xmargin, ymargin and zmargin.
  462. Passing both positional and keyword arguments for xmargin,
  463. ymargin, and/or zmargin is invalid.
  464. The *tight* parameter
  465. is passed to :meth:`autoscale_view`, which is executed after
  466. a margin is changed; the default here is *True*, on the
  467. assumption that when margins are specified, no additional
  468. padding to match tick marks is usually desired. Setting
  469. *tight* to *None* will preserve the previous setting.
  470. Specifying any margin changes only the autoscaling; for example,
  471. if *xmargin* is not None, then *xmargin* times the X data
  472. interval will be added to each end of that interval before
  473. it is used in autoscaling.
  474. .. versionadded:: 1.1.0
  475. """
  476. if margins and x is not None and y is not None and z is not None:
  477. raise TypeError('Cannot pass both positional and keyword '
  478. 'arguments for x, y, and/or z.')
  479. elif len(margins) == 1:
  480. x = y = z = margins[0]
  481. elif len(margins) == 3:
  482. x, y, z = margins
  483. elif margins:
  484. raise TypeError('Must pass a single positional argument for all '
  485. 'margins, or one for each margin (x, y, z).')
  486. if x is None and y is None and z is None:
  487. if tight is not True:
  488. cbook._warn_external(f'ignoring tight={tight!r} in get mode')
  489. return self._xmargin, self._ymargin, self._zmargin
  490. if x is not None:
  491. self.set_xmargin(x)
  492. if y is not None:
  493. self.set_ymargin(y)
  494. if z is not None:
  495. self.set_zmargin(z)
  496. self.autoscale_view(
  497. tight=tight, scalex=(x is not None), scaley=(y is not None),
  498. scalez=(z is not None)
  499. )
  500. def autoscale(self, enable=True, axis='both', tight=None):
  501. """
  502. Convenience method for simple axis view autoscaling.
  503. See :meth:`matplotlib.axes.Axes.autoscale` for full explanation.
  504. Note that this function behaves the same, but for all
  505. three axes. Therefore, 'z' can be passed for *axis*,
  506. and 'both' applies to all three axes.
  507. .. versionadded:: 1.1.0
  508. """
  509. if enable is None:
  510. scalex = True
  511. scaley = True
  512. scalez = True
  513. else:
  514. if axis in ['x', 'both']:
  515. self._autoscaleXon = scalex = bool(enable)
  516. else:
  517. scalex = False
  518. if axis in ['y', 'both']:
  519. self._autoscaleYon = scaley = bool(enable)
  520. else:
  521. scaley = False
  522. if axis in ['z', 'both']:
  523. self._autoscaleZon = scalez = bool(enable)
  524. else:
  525. scalez = False
  526. self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
  527. scalez=scalez)
  528. def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
  529. # This updates the bounding boxes as to keep a record as to what the
  530. # minimum sized rectangular volume holds the data.
  531. X = np.reshape(X, -1)
  532. Y = np.reshape(Y, -1)
  533. self.xy_dataLim.update_from_data_xy(
  534. np.column_stack([X, Y]), not had_data)
  535. if Z is not None:
  536. Z = np.reshape(Z, -1)
  537. self.zz_dataLim.update_from_data_xy(
  538. np.column_stack([Z, Z]), not had_data)
  539. # Let autoscale_view figure out how to use this data.
  540. self.autoscale_view()
  541. def autoscale_view(self, tight=None, scalex=True, scaley=True,
  542. scalez=True):
  543. """
  544. Autoscale the view limits using the data limits.
  545. See :meth:`matplotlib.axes.Axes.autoscale_view` for documentation.
  546. Note that this function applies to the 3D axes, and as such
  547. adds the *scalez* to the function arguments.
  548. .. versionchanged:: 1.1.0
  549. Function signature was changed to better match the 2D version.
  550. *tight* is now explicitly a kwarg and placed first.
  551. .. versionchanged:: 1.2.1
  552. This is now fully functional.
  553. """
  554. # This method looks at the rectangular volume (see above)
  555. # of data and decides how to scale the view portal to fit it.
  556. if tight is None:
  557. # if image data only just use the datalim
  558. _tight = self._tight or (
  559. len(self.images) > 0
  560. and len(self.lines) == len(self.patches) == 0)
  561. else:
  562. _tight = self._tight = bool(tight)
  563. if scalex and self._autoscaleXon:
  564. self._shared_x_axes.clean()
  565. x0, x1 = self.xy_dataLim.intervalx
  566. xlocator = self.xaxis.get_major_locator()
  567. x0, x1 = xlocator.nonsingular(x0, x1)
  568. if self._xmargin > 0:
  569. delta = (x1 - x0) * self._xmargin
  570. x0 -= delta
  571. x1 += delta
  572. if not _tight:
  573. x0, x1 = xlocator.view_limits(x0, x1)
  574. self.set_xbound(x0, x1)
  575. if scaley and self._autoscaleYon:
  576. self._shared_y_axes.clean()
  577. y0, y1 = self.xy_dataLim.intervaly
  578. ylocator = self.yaxis.get_major_locator()
  579. y0, y1 = ylocator.nonsingular(y0, y1)
  580. if self._ymargin > 0:
  581. delta = (y1 - y0) * self._ymargin
  582. y0 -= delta
  583. y1 += delta
  584. if not _tight:
  585. y0, y1 = ylocator.view_limits(y0, y1)
  586. self.set_ybound(y0, y1)
  587. if scalez and self._autoscaleZon:
  588. self._shared_z_axes.clean()
  589. z0, z1 = self.zz_dataLim.intervalx
  590. zlocator = self.zaxis.get_major_locator()
  591. z0, z1 = zlocator.nonsingular(z0, z1)
  592. if self._zmargin > 0:
  593. delta = (z1 - z0) * self._zmargin
  594. z0 -= delta
  595. z1 += delta
  596. if not _tight:
  597. z0, z1 = zlocator.view_limits(z0, z1)
  598. self.set_zbound(z0, z1)
  599. def get_w_lims(self):
  600. """Get 3D world limits."""
  601. minx, maxx = self.get_xlim3d()
  602. miny, maxy = self.get_ylim3d()
  603. minz, maxz = self.get_zlim3d()
  604. return minx, maxx, miny, maxy, minz, maxz
  605. def set_xlim3d(self, left=None, right=None, emit=True, auto=False,
  606. *, xmin=None, xmax=None):
  607. """
  608. Set 3D x limits.
  609. See :meth:`matplotlib.axes.Axes.set_xlim` for full documentation.
  610. """
  611. if right is None and np.iterable(left):
  612. left, right = left
  613. if xmin is not None:
  614. if left is not None:
  615. raise TypeError('Cannot pass both `xmin` and `left`')
  616. left = xmin
  617. if xmax is not None:
  618. if right is not None:
  619. raise TypeError('Cannot pass both `xmax` and `right`')
  620. right = xmax
  621. self._process_unit_info(xdata=(left, right))
  622. left = self._validate_converted_limits(left, self.convert_xunits)
  623. right = self._validate_converted_limits(right, self.convert_xunits)
  624. old_left, old_right = self.get_xlim()
  625. if left is None:
  626. left = old_left
  627. if right is None:
  628. right = old_right
  629. if left == right:
  630. cbook._warn_external(
  631. f"Attempting to set identical left == right == {left} results "
  632. f"in singular transformations; automatically expanding.")
  633. reverse = left > right
  634. left, right = self.xaxis.get_major_locator().nonsingular(left, right)
  635. left, right = self.xaxis.limit_range_for_scale(left, right)
  636. # cast to bool to avoid bad interaction between python 3.8 and np.bool_
  637. left, right = sorted([left, right], reverse=bool(reverse))
  638. self.xy_viewLim.intervalx = (left, right)
  639. if auto is not None:
  640. self._autoscaleXon = bool(auto)
  641. if emit:
  642. self.callbacks.process('xlim_changed', self)
  643. # Call all of the other x-axes that are shared with this one
  644. for other in self._shared_x_axes.get_siblings(self):
  645. if other is not self:
  646. other.set_xlim(self.xy_viewLim.intervalx,
  647. emit=False, auto=auto)
  648. if other.figure != self.figure:
  649. other.figure.canvas.draw_idle()
  650. self.stale = True
  651. return left, right
  652. def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False,
  653. *, ymin=None, ymax=None):
  654. """
  655. Set 3D y limits.
  656. See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation.
  657. """
  658. if top is None and np.iterable(bottom):
  659. bottom, top = bottom
  660. if ymin is not None:
  661. if bottom is not None:
  662. raise TypeError('Cannot pass both `ymin` and `bottom`')
  663. bottom = ymin
  664. if ymax is not None:
  665. if top is not None:
  666. raise TypeError('Cannot pass both `ymax` and `top`')
  667. top = ymax
  668. self._process_unit_info(ydata=(bottom, top))
  669. bottom = self._validate_converted_limits(bottom, self.convert_yunits)
  670. top = self._validate_converted_limits(top, self.convert_yunits)
  671. old_bottom, old_top = self.get_ylim()
  672. if bottom is None:
  673. bottom = old_bottom
  674. if top is None:
  675. top = old_top
  676. if bottom == top:
  677. cbook._warn_external(
  678. f"Attempting to set identical bottom == top == {bottom} "
  679. f"results in singular transformations; automatically "
  680. f"expanding.")
  681. swapped = bottom > top
  682. bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top)
  683. bottom, top = self.yaxis.limit_range_for_scale(bottom, top)
  684. if swapped:
  685. bottom, top = top, bottom
  686. self.xy_viewLim.intervaly = (bottom, top)
  687. if auto is not None:
  688. self._autoscaleYon = bool(auto)
  689. if emit:
  690. self.callbacks.process('ylim_changed', self)
  691. # Call all of the other y-axes that are shared with this one
  692. for other in self._shared_y_axes.get_siblings(self):
  693. if other is not self:
  694. other.set_ylim(self.xy_viewLim.intervaly,
  695. emit=False, auto=auto)
  696. if other.figure != self.figure:
  697. other.figure.canvas.draw_idle()
  698. self.stale = True
  699. return bottom, top
  700. def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False,
  701. *, zmin=None, zmax=None):
  702. """
  703. Set 3D z limits.
  704. See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation
  705. """
  706. if top is None and np.iterable(bottom):
  707. bottom, top = bottom
  708. if zmin is not None:
  709. if bottom is not None:
  710. raise TypeError('Cannot pass both `zmin` and `bottom`')
  711. bottom = zmin
  712. if zmax is not None:
  713. if top is not None:
  714. raise TypeError('Cannot pass both `zmax` and `top`')
  715. top = zmax
  716. self._process_unit_info(zdata=(bottom, top))
  717. bottom = self._validate_converted_limits(bottom, self.convert_zunits)
  718. top = self._validate_converted_limits(top, self.convert_zunits)
  719. old_bottom, old_top = self.get_zlim()
  720. if bottom is None:
  721. bottom = old_bottom
  722. if top is None:
  723. top = old_top
  724. if bottom == top:
  725. cbook._warn_external(
  726. f"Attempting to set identical bottom == top == {bottom} "
  727. f"results in singular transformations; automatically "
  728. f"expanding.")
  729. swapped = bottom > top
  730. bottom, top = self.zaxis.get_major_locator().nonsingular(bottom, top)
  731. bottom, top = self.zaxis.limit_range_for_scale(bottom, top)
  732. if swapped:
  733. bottom, top = top, bottom
  734. self.zz_viewLim.intervalx = (bottom, top)
  735. if auto is not None:
  736. self._autoscaleZon = bool(auto)
  737. if emit:
  738. self.callbacks.process('zlim_changed', self)
  739. # Call all of the other y-axes that are shared with this one
  740. for other in self._shared_z_axes.get_siblings(self):
  741. if other is not self:
  742. other.set_zlim(self.zz_viewLim.intervalx,
  743. emit=False, auto=auto)
  744. if other.figure != self.figure:
  745. other.figure.canvas.draw_idle()
  746. self.stale = True
  747. return bottom, top
  748. def get_xlim3d(self):
  749. return tuple(self.xy_viewLim.intervalx)
  750. get_xlim3d.__doc__ = maxes.Axes.get_xlim.__doc__
  751. if get_xlim3d.__doc__ is not None:
  752. get_xlim3d.__doc__ += """
  753. .. versionchanged:: 1.1.0
  754. This function now correctly refers to the 3D x-limits
  755. """
  756. def get_ylim3d(self):
  757. return tuple(self.xy_viewLim.intervaly)
  758. get_ylim3d.__doc__ = maxes.Axes.get_ylim.__doc__
  759. if get_ylim3d.__doc__ is not None:
  760. get_ylim3d.__doc__ += """
  761. .. versionchanged:: 1.1.0
  762. This function now correctly refers to the 3D y-limits.
  763. """
  764. def get_zlim3d(self):
  765. """Get 3D z limits."""
  766. return tuple(self.zz_viewLim.intervalx)
  767. def get_zscale(self):
  768. """
  769. Return the zaxis scale string %s
  770. """ % (", ".join(mscale.get_scale_names()))
  771. return self.zaxis.get_scale()
  772. # We need to slightly redefine these to pass scalez=False
  773. # to their calls of autoscale_view.
  774. def set_xscale(self, value, **kwargs):
  775. self.xaxis._set_scale(value, **kwargs)
  776. self.autoscale_view(scaley=False, scalez=False)
  777. self._update_transScale()
  778. self.stale = True
  779. def set_yscale(self, value, **kwargs):
  780. self.yaxis._set_scale(value, **kwargs)
  781. self.autoscale_view(scalex=False, scalez=False)
  782. self._update_transScale()
  783. self.stale = True
  784. def set_zscale(self, value, **kwargs):
  785. self.zaxis._set_scale(value, **kwargs)
  786. self.autoscale_view(scalex=False, scaley=False)
  787. self._update_transScale()
  788. self.stale = True
  789. set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map(
  790. """
  791. Set the {}-axis scale.
  792. Parameters
  793. ----------
  794. value : {{"linear"}}
  795. The axis scale type to apply. 3D axes currently only support
  796. linear scales; other scales yield nonsensical results.
  797. **kwargs
  798. Keyword arguments are nominally forwarded to the scale class, but
  799. none of them is applicable for linear scales.
  800. """.format,
  801. ["x", "y", "z"])
  802. get_zticks = _axis_method_wrapper("zaxis", "get_ticklocs")
  803. set_zticks = _axis_method_wrapper("zaxis", "set_ticks")
  804. get_zmajorticklabels = _axis_method_wrapper("zaxis", "get_majorticklabels")
  805. get_zminorticklabels = _axis_method_wrapper("zaxis", "get_minorticklabels")
  806. get_zticklabels = _axis_method_wrapper("zaxis", "get_ticklabels")
  807. set_zticklabels = _axis_method_wrapper(
  808. "zaxis", "_set_ticklabels",
  809. doc_sub={"Axis.set_ticks": "Axes3D.set_zticks"})
  810. zaxis_date = _axis_method_wrapper("zaxis", "axis_date")
  811. if zaxis_date.__doc__:
  812. zaxis_date.__doc__ += textwrap.dedent("""
  813. Notes
  814. -----
  815. This function is merely provided for completeness, but 3d axes do not
  816. support dates for ticks, and so this may not work as expected.
  817. """)
  818. def clabel(self, *args, **kwargs):
  819. """Currently not implemented for 3D axes, and returns *None*."""
  820. return None
  821. def view_init(self, elev=None, azim=None):
  822. """
  823. Set the elevation and azimuth of the axes in degrees (not radians).
  824. This can be used to rotate the axes programmatically.
  825. 'elev' stores the elevation angle in the z plane (in degrees).
  826. 'azim' stores the azimuth angle in the (x, y) plane (in degrees).
  827. if 'elev' or 'azim' are None (default), then the initial value
  828. is used which was specified in the :class:`Axes3D` constructor.
  829. """
  830. self.dist = 10
  831. if elev is None:
  832. self.elev = self.initial_elev
  833. else:
  834. self.elev = elev
  835. if azim is None:
  836. self.azim = self.initial_azim
  837. else:
  838. self.azim = azim
  839. def set_proj_type(self, proj_type):
  840. """
  841. Set the projection type.
  842. Parameters
  843. ----------
  844. proj_type : {'persp', 'ortho'}
  845. """
  846. self._projection = cbook._check_getitem({
  847. 'persp': proj3d.persp_transformation,
  848. 'ortho': proj3d.ortho_transformation,
  849. }, proj_type=proj_type)
  850. def get_proj(self):
  851. """Create the projection matrix from the current viewing position."""
  852. # elev stores the elevation angle in the z plane
  853. # azim stores the azimuth angle in the x,y plane
  854. #
  855. # dist is the distance of the eye viewing point from the object
  856. # point.
  857. relev, razim = np.pi * self.elev/180, np.pi * self.azim/180
  858. xmin, xmax = self.get_xlim3d()
  859. ymin, ymax = self.get_ylim3d()
  860. zmin, zmax = self.get_zlim3d()
  861. # transform to uniform world coordinates 0-1, 0-1, 0-1
  862. worldM = proj3d.world_transformation(xmin, xmax,
  863. ymin, ymax,
  864. zmin, zmax,
  865. pb_aspect=self._box_aspect)
  866. # look into the middle of the new coordinates
  867. R = self._box_aspect / 2
  868. xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
  869. yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist
  870. zp = R[2] + np.sin(relev) * self.dist
  871. E = np.array((xp, yp, zp))
  872. self.eye = E
  873. self.vvec = R - E
  874. self.vvec = self.vvec / np.linalg.norm(self.vvec)
  875. if abs(relev) > np.pi/2:
  876. # upside down
  877. V = np.array((0, 0, -1))
  878. else:
  879. V = np.array((0, 0, 1))
  880. zfront, zback = -self.dist, self.dist
  881. viewM = proj3d.view_transformation(E, R, V)
  882. projM = self._projection(zfront, zback)
  883. M0 = np.dot(viewM, worldM)
  884. M = np.dot(projM, M0)
  885. return M
  886. def mouse_init(self, rotate_btn=1, zoom_btn=3):
  887. """
  888. Set the mouse buttons for 3D rotation and zooming.
  889. Parameters
  890. ----------
  891. rotate_btn : int or list of int, default: 1
  892. The mouse button or buttons to use for 3D rotation of the axes.
  893. zoom_btn : int or list of int, default: 3
  894. The mouse button or buttons to use to zoom the 3D axes.
  895. """
  896. self.button_pressed = None
  897. # coerce scalars into array-like, then convert into
  898. # a regular list to avoid comparisons against None
  899. # which breaks in recent versions of numpy.
  900. self._rotate_btn = np.atleast_1d(rotate_btn).tolist()
  901. self._zoom_btn = np.atleast_1d(zoom_btn).tolist()
  902. def disable_mouse_rotation(self):
  903. """Disable mouse buttons for 3D rotation and zooming."""
  904. self.mouse_init(rotate_btn=[], zoom_btn=[])
  905. def can_zoom(self):
  906. """
  907. Return *True* if this axes supports the zoom box button functionality.
  908. 3D axes objects do not use the zoom box button.
  909. """
  910. return False
  911. def can_pan(self):
  912. """
  913. Return *True* if this axes supports the pan/zoom button functionality.
  914. 3D axes objects do not use the pan/zoom button.
  915. """
  916. return False
  917. def cla(self):
  918. # docstring inherited.
  919. super().cla()
  920. self.zaxis.cla()
  921. if self._sharez is not None:
  922. self.zaxis.major = self._sharez.zaxis.major
  923. self.zaxis.minor = self._sharez.zaxis.minor
  924. z0, z1 = self._sharez.get_zlim()
  925. self.set_zlim(z0, z1, emit=False, auto=None)
  926. self.zaxis._set_scale(self._sharez.zaxis.get_scale())
  927. else:
  928. self.zaxis._set_scale('linear')
  929. try:
  930. self.set_zlim(0, 1)
  931. except TypeError:
  932. pass
  933. self._autoscaleZon = True
  934. self._zmargin = 0
  935. self.grid(rcParams['axes3d.grid'])
  936. def _button_press(self, event):
  937. if event.inaxes == self:
  938. self.button_pressed = event.button
  939. self.sx, self.sy = event.xdata, event.ydata
  940. toolbar = getattr(self.figure.canvas, "toolbar")
  941. if toolbar and toolbar._nav_stack() is None:
  942. self.figure.canvas.toolbar.push_current()
  943. def _button_release(self, event):
  944. self.button_pressed = None
  945. toolbar = getattr(self.figure.canvas, "toolbar")
  946. if toolbar:
  947. self.figure.canvas.toolbar.push_current()
  948. def _get_view(self):
  949. # docstring inherited
  950. return (self.get_xlim(), self.get_ylim(), self.get_zlim(),
  951. self.elev, self.azim)
  952. def _set_view(self, view):
  953. # docstring inherited
  954. xlim, ylim, zlim, elev, azim = view
  955. self.set(xlim=xlim, ylim=ylim, zlim=zlim)
  956. self.elev = elev
  957. self.azim = azim
  958. def format_zdata(self, z):
  959. """
  960. Return *z* string formatted. This function will use the
  961. :attr:`fmt_zdata` attribute if it is callable, else will fall
  962. back on the zaxis major formatter
  963. """
  964. try:
  965. return self.fmt_zdata(z)
  966. except (AttributeError, TypeError):
  967. func = self.zaxis.get_major_formatter().format_data_short
  968. val = func(z)
  969. return val
  970. def format_coord(self, xd, yd):
  971. """
  972. Given the 2D view coordinates attempt to guess a 3D coordinate.
  973. Looks for the nearest edge to the point and then assumes that
  974. the point is at the same z location as the nearest point on the edge.
  975. """
  976. if self.M is None:
  977. return ''
  978. if self.button_pressed in self._rotate_btn:
  979. return 'azimuth={:.0f} deg, elevation={:.0f} deg '.format(
  980. self.azim, self.elev)
  981. # ignore xd and yd and display angles instead
  982. # nearest edge
  983. p0, p1 = min(self.tunit_edges(),
  984. key=lambda edge: proj3d._line2d_seg_dist(
  985. edge[0], edge[1], (xd, yd)))
  986. # scale the z value to match
  987. x0, y0, z0 = p0
  988. x1, y1, z1 = p1
  989. d0 = np.hypot(x0-xd, y0-yd)
  990. d1 = np.hypot(x1-xd, y1-yd)
  991. dt = d0+d1
  992. z = d1/dt * z0 + d0/dt * z1
  993. x, y, z = proj3d.inv_transform(xd, yd, z, self.M)
  994. xs = self.format_xdata(x)
  995. ys = self.format_ydata(y)
  996. zs = self.format_zdata(z)
  997. return 'x=%s, y=%s, z=%s' % (xs, ys, zs)
  998. def _on_move(self, event):
  999. """
  1000. Mouse moving.
  1001. By default, button-1 rotates and button-3 zooms; these buttons can be
  1002. modified via `mouse_init`.
  1003. """
  1004. if not self.button_pressed:
  1005. return
  1006. if self.M is None:
  1007. return
  1008. x, y = event.xdata, event.ydata
  1009. # In case the mouse is out of bounds.
  1010. if x is None:
  1011. return
  1012. dx, dy = x - self.sx, y - self.sy
  1013. w = self._pseudo_w
  1014. h = self._pseudo_h
  1015. self.sx, self.sy = x, y
  1016. # Rotation
  1017. if self.button_pressed in self._rotate_btn:
  1018. # rotate viewing point
  1019. # get the x and y pixel coords
  1020. if dx == 0 and dy == 0:
  1021. return
  1022. self.elev = art3d._norm_angle(self.elev - (dy/h)*180)
  1023. self.azim = art3d._norm_angle(self.azim - (dx/w)*180)
  1024. self.get_proj()
  1025. self.stale = True
  1026. self.figure.canvas.draw_idle()
  1027. # elif self.button_pressed == 2:
  1028. # pan view
  1029. # project xv, yv, zv -> xw, yw, zw
  1030. # pan
  1031. # pass
  1032. # Zoom
  1033. elif self.button_pressed in self._zoom_btn:
  1034. # zoom view
  1035. # hmmm..this needs some help from clipping....
  1036. minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
  1037. df = 1-((h - dy)/h)
  1038. dx = (maxx-minx)*df
  1039. dy = (maxy-miny)*df
  1040. dz = (maxz-minz)*df
  1041. self.set_xlim3d(minx - dx, maxx + dx)
  1042. self.set_ylim3d(miny - dy, maxy + dy)
  1043. self.set_zlim3d(minz - dz, maxz + dz)
  1044. self.get_proj()
  1045. self.figure.canvas.draw_idle()
  1046. def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
  1047. """
  1048. Set zlabel. See doc for `.set_ylabel` for description.
  1049. """
  1050. if labelpad is not None:
  1051. self.zaxis.labelpad = labelpad
  1052. return self.zaxis.set_label_text(zlabel, fontdict, **kwargs)
  1053. def get_zlabel(self):
  1054. """
  1055. Get the z-label text string.
  1056. .. versionadded:: 1.1.0
  1057. This function was added, but not tested. Please report any bugs.
  1058. """
  1059. label = self.zaxis.get_label()
  1060. return label.get_text()
  1061. # Axes rectangle characteristics
  1062. def get_frame_on(self):
  1063. """Get whether the 3D axes panels are drawn."""
  1064. return self._frameon
  1065. def set_frame_on(self, b):
  1066. """
  1067. Set whether the 3D axes panels are drawn.
  1068. Parameters
  1069. ----------
  1070. b : bool
  1071. """
  1072. self._frameon = bool(b)
  1073. self.stale = True
  1074. def grid(self, b=True, **kwargs):
  1075. """
  1076. Set / unset 3D grid.
  1077. .. note::
  1078. Currently, this function does not behave the same as
  1079. :meth:`matplotlib.axes.Axes.grid`, but it is intended to
  1080. eventually support that behavior.
  1081. .. versionadded:: 1.1.0
  1082. """
  1083. # TODO: Operate on each axes separately
  1084. if len(kwargs):
  1085. b = True
  1086. self._draw_grid = b
  1087. self.stale = True
  1088. def locator_params(self, axis='both', tight=None, **kwargs):
  1089. """
  1090. Convenience method for controlling tick locators.
  1091. See :meth:`matplotlib.axes.Axes.locator_params` for full
  1092. documentation. Note that this is for Axes3D objects,
  1093. therefore, setting *axis* to 'both' will result in the
  1094. parameters being set for all three axes. Also, *axis*
  1095. can also take a value of 'z' to apply parameters to the
  1096. z axis.
  1097. .. versionadded:: 1.1.0
  1098. This function was added, but not tested. Please report any bugs.
  1099. """
  1100. _x = axis in ['x', 'both']
  1101. _y = axis in ['y', 'both']
  1102. _z = axis in ['z', 'both']
  1103. if _x:
  1104. self.xaxis.get_major_locator().set_params(**kwargs)
  1105. if _y:
  1106. self.yaxis.get_major_locator().set_params(**kwargs)
  1107. if _z:
  1108. self.zaxis.get_major_locator().set_params(**kwargs)
  1109. self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z)
  1110. def tick_params(self, axis='both', **kwargs):
  1111. """
  1112. Convenience method for changing the appearance of ticks and
  1113. tick labels.
  1114. See :meth:`matplotlib.axes.Axes.tick_params` for more complete
  1115. documentation.
  1116. The only difference is that setting *axis* to 'both' will
  1117. mean that the settings are applied to all three axes. Also,
  1118. the *axis* parameter also accepts a value of 'z', which
  1119. would mean to apply to only the z-axis.
  1120. Also, because of how Axes3D objects are drawn very differently
  1121. from regular 2D axes, some of these settings may have
  1122. ambiguous meaning. For simplicity, the 'z' axis will
  1123. accept settings as if it was like the 'y' axis.
  1124. .. note::
  1125. Axes3D currently ignores some of these settings.
  1126. .. versionadded:: 1.1.0
  1127. """
  1128. cbook._check_in_list(['x', 'y', 'z', 'both'], axis=axis)
  1129. if axis in ['x', 'y', 'both']:
  1130. super().tick_params(axis, **kwargs)
  1131. if axis in ['z', 'both']:
  1132. zkw = dict(kwargs)
  1133. zkw.pop('top', None)
  1134. zkw.pop('bottom', None)
  1135. zkw.pop('labeltop', None)
  1136. zkw.pop('labelbottom', None)
  1137. self.zaxis.set_tick_params(**zkw)
  1138. # data limits, ticks, tick labels, and formatting
  1139. def invert_zaxis(self):
  1140. """
  1141. Invert the z-axis.
  1142. .. versionadded:: 1.1.0
  1143. This function was added, but not tested. Please report any bugs.
  1144. """
  1145. bottom, top = self.get_zlim()
  1146. self.set_zlim(top, bottom, auto=None)
  1147. def zaxis_inverted(self):
  1148. """
  1149. Returns True if the z-axis is inverted.
  1150. .. versionadded:: 1.1.0
  1151. """
  1152. bottom, top = self.get_zlim()
  1153. return top < bottom
  1154. def get_zbound(self):
  1155. """
  1156. Return the lower and upper z-axis bounds, in increasing order.
  1157. .. versionadded:: 1.1.0
  1158. """
  1159. bottom, top = self.get_zlim()
  1160. if bottom < top:
  1161. return bottom, top
  1162. else:
  1163. return top, bottom
  1164. def set_zbound(self, lower=None, upper=None):
  1165. """
  1166. Set the lower and upper numerical bounds of the z-axis.
  1167. This method will honor axes inversion regardless of parameter order.
  1168. It will not change the autoscaling setting (`.get_autoscalez_on()`).
  1169. .. versionadded:: 1.1.0
  1170. """
  1171. if upper is None and np.iterable(lower):
  1172. lower, upper = lower
  1173. old_lower, old_upper = self.get_zbound()
  1174. if lower is None:
  1175. lower = old_lower
  1176. if upper is None:
  1177. upper = old_upper
  1178. self.set_zlim(sorted((lower, upper),
  1179. reverse=bool(self.zaxis_inverted())),
  1180. auto=None)
  1181. def text(self, x, y, z, s, zdir=None, **kwargs):
  1182. """
  1183. Add text to the plot. kwargs will be passed on to Axes.text,
  1184. except for the *zdir* keyword, which sets the direction to be
  1185. used as the z direction.
  1186. """
  1187. text = super().text(x, y, s, **kwargs)
  1188. art3d.text_2d_to_3d(text, z, zdir)
  1189. return text
  1190. text3D = text
  1191. text2D = Axes.text
  1192. def plot(self, xs, ys, *args, zdir='z', **kwargs):
  1193. """
  1194. Plot 2D or 3D data.
  1195. Parameters
  1196. ----------
  1197. xs : 1D array-like
  1198. x coordinates of vertices.
  1199. ys : 1D array-like
  1200. y coordinates of vertices.
  1201. zs : float or 1D array-like
  1202. z coordinates of vertices; either one for all points or one for
  1203. each point.
  1204. zdir : {'x', 'y', 'z'}, default: 'z'
  1205. When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
  1206. **kwargs
  1207. Other arguments are forwarded to `matplotlib.axes.Axes.plot`.
  1208. """
  1209. had_data = self.has_data()
  1210. # `zs` can be passed positionally or as keyword; checking whether
  1211. # args[0] is a string matches the behavior of 2D `plot` (via
  1212. # `_process_plot_var_args`).
  1213. if args and not isinstance(args[0], str):
  1214. zs, *args = args
  1215. if 'zs' in kwargs:
  1216. raise TypeError("plot() for multiple values for argument 'z'")
  1217. else:
  1218. zs = kwargs.pop('zs', 0)
  1219. # Match length
  1220. zs = np.broadcast_to(zs, np.shape(xs))
  1221. lines = super().plot(xs, ys, *args, **kwargs)
  1222. for line in lines:
  1223. art3d.line_2d_to_3d(line, zs=zs, zdir=zdir)
  1224. xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir)
  1225. self.auto_scale_xyz(xs, ys, zs, had_data)
  1226. return lines
  1227. plot3D = plot
  1228. def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
  1229. vmax=None, lightsource=None, **kwargs):
  1230. """
  1231. Create a surface plot.
  1232. By default it will be colored in shades of a solid color, but it also
  1233. supports color mapping by supplying the *cmap* argument.
  1234. .. note::
  1235. The *rcount* and *ccount* kwargs, which both default to 50,
  1236. determine the maximum number of samples used in each direction. If
  1237. the input data is larger, it will be downsampled (by slicing) to
  1238. these numbers of points.
  1239. .. note::
  1240. To maximize rendering speed consider setting *rstride* and *cstride*
  1241. to divisors of the number of rows minus 1 and columns minus 1
  1242. respectively. For example, given 51 rows rstride can be any of the
  1243. divisors of 50.
  1244. Similarly, a setting of *rstride* and *cstride* equal to 1 (or
  1245. *rcount* and *ccount* equal the number of rows and columns) can use
  1246. the optimized path.
  1247. Parameters
  1248. ----------
  1249. X, Y, Z : 2d arrays
  1250. Data values.
  1251. rcount, ccount : int
  1252. Maximum number of samples used in each direction. If the input
  1253. data is larger, it will be downsampled (by slicing) to these
  1254. numbers of points. Defaults to 50.
  1255. .. versionadded:: 2.0
  1256. rstride, cstride : int
  1257. Downsampling stride in each direction. These arguments are
  1258. mutually exclusive with *rcount* and *ccount*. If only one of
  1259. *rstride* or *cstride* is set, the other defaults to 10.
  1260. 'classic' mode uses a default of ``rstride = cstride = 10`` instead
  1261. of the new default of ``rcount = ccount = 50``.
  1262. color : color-like
  1263. Color of the surface patches.
  1264. cmap : Colormap
  1265. Colormap of the surface patches.
  1266. facecolors : array-like of colors.
  1267. Colors of each individual patch.
  1268. norm : Normalize
  1269. Normalization for the colormap.
  1270. vmin, vmax : float
  1271. Bounds for the normalization.
  1272. shade : bool, default: True
  1273. Whether to shade the facecolors. Shading is always disabled when
  1274. *cmap* is specified.
  1275. lightsource : `~matplotlib.colors.LightSource`
  1276. The lightsource to use when *shade* is True.
  1277. **kwargs
  1278. Other arguments are forwarded to `.Poly3DCollection`.
  1279. """
  1280. had_data = self.has_data()
  1281. if Z.ndim != 2:
  1282. raise ValueError("Argument Z must be 2-dimensional.")
  1283. if np.any(np.isnan(Z)):
  1284. cbook._warn_external(
  1285. "Z contains NaN values. This may result in rendering "
  1286. "artifacts.")
  1287. # TODO: Support masked arrays
  1288. X, Y, Z = np.broadcast_arrays(X, Y, Z)
  1289. rows, cols = Z.shape
  1290. has_stride = 'rstride' in kwargs or 'cstride' in kwargs
  1291. has_count = 'rcount' in kwargs or 'ccount' in kwargs
  1292. if has_stride and has_count:
  1293. raise ValueError("Cannot specify both stride and count arguments")
  1294. rstride = kwargs.pop('rstride', 10)
  1295. cstride = kwargs.pop('cstride', 10)
  1296. rcount = kwargs.pop('rcount', 50)
  1297. ccount = kwargs.pop('ccount', 50)
  1298. if rcParams['_internal.classic_mode']:
  1299. # Strides have priority over counts in classic mode.
  1300. # So, only compute strides from counts
  1301. # if counts were explicitly given
  1302. compute_strides = has_count
  1303. else:
  1304. # If the strides are provided then it has priority.
  1305. # Otherwise, compute the strides from the counts.
  1306. compute_strides = not has_stride
  1307. if compute_strides:
  1308. rstride = int(max(np.ceil(rows / rcount), 1))
  1309. cstride = int(max(np.ceil(cols / ccount), 1))
  1310. if 'facecolors' in kwargs:
  1311. fcolors = kwargs.pop('facecolors')
  1312. else:
  1313. color = kwargs.pop('color', None)
  1314. if color is None:
  1315. color = self._get_lines.get_next_color()
  1316. color = np.array(mcolors.to_rgba(color))
  1317. fcolors = None
  1318. cmap = kwargs.get('cmap', None)
  1319. shade = kwargs.pop('shade', cmap is None)
  1320. if shade is None:
  1321. cbook.warn_deprecated(
  1322. "3.1",
  1323. message="Passing shade=None to Axes3D.plot_surface() is "
  1324. "deprecated since matplotlib 3.1 and will change its "
  1325. "semantic or raise an error in matplotlib 3.3. "
  1326. "Please use shade=False instead.")
  1327. colset = [] # the sampled facecolor
  1328. if (rows - 1) % rstride == 0 and \
  1329. (cols - 1) % cstride == 0 and \
  1330. fcolors is None:
  1331. polys = np.stack(
  1332. [cbook._array_patch_perimeters(a, rstride, cstride)
  1333. for a in (X, Y, Z)],
  1334. axis=-1)
  1335. else:
  1336. # evenly spaced, and including both endpoints
  1337. row_inds = list(range(0, rows-1, rstride)) + [rows-1]
  1338. col_inds = list(range(0, cols-1, cstride)) + [cols-1]
  1339. polys = []
  1340. for rs, rs_next in zip(row_inds[:-1], row_inds[1:]):
  1341. for cs, cs_next in zip(col_inds[:-1], col_inds[1:]):
  1342. ps = [
  1343. # +1 ensures we share edges between polygons
  1344. cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
  1345. for a in (X, Y, Z)
  1346. ]
  1347. # ps = np.stack(ps, axis=-1)
  1348. ps = np.array(ps).T
  1349. polys.append(ps)
  1350. if fcolors is not None:
  1351. colset.append(fcolors[rs][cs])
  1352. # note that the striding causes some polygons to have more coordinates
  1353. # than others
  1354. polyc = art3d.Poly3DCollection(polys, *args, **kwargs)
  1355. if fcolors is not None:
  1356. if shade:
  1357. colset = self._shade_colors(
  1358. colset, self._generate_normals(polys), lightsource)
  1359. polyc.set_facecolors(colset)
  1360. polyc.set_edgecolors(colset)
  1361. elif cmap:
  1362. # can't always vectorize, because polys might be jagged
  1363. if isinstance(polys, np.ndarray):
  1364. avg_z = polys[..., 2].mean(axis=-1)
  1365. else:
  1366. avg_z = np.array([ps[:, 2].mean() for ps in polys])
  1367. polyc.set_array(avg_z)
  1368. if vmin is not None or vmax is not None:
  1369. polyc.set_clim(vmin, vmax)
  1370. if norm is not None:
  1371. polyc.set_norm(norm)
  1372. else:
  1373. if shade:
  1374. colset = self._shade_colors(
  1375. color, self._generate_normals(polys), lightsource)
  1376. else:
  1377. colset = color
  1378. polyc.set_facecolors(colset)
  1379. self.add_collection(polyc)
  1380. self.auto_scale_xyz(X, Y, Z, had_data)
  1381. return polyc
  1382. def _generate_normals(self, polygons):
  1383. """
  1384. Compute the normals of a list of polygons.
  1385. Normals point towards the viewer for a face with its vertices in
  1386. counterclockwise order, following the right hand rule.
  1387. Uses three points equally spaced around the polygon.
  1388. This normal of course might not make sense for polygons with more than
  1389. three points not lying in a plane, but it's a plausible and fast
  1390. approximation.
  1391. Parameters
  1392. ----------
  1393. polygons: list of (M_i, 3) array-like, or (..., M, 3) array-like
  1394. A sequence of polygons to compute normals for, which can have
  1395. varying numbers of vertices. If the polygons all have the same
  1396. number of vertices and array is passed, then the operation will
  1397. be vectorized.
  1398. Returns
  1399. -------
  1400. normals: (..., 3) array-like
  1401. A normal vector estimated for the polygon.
  1402. """
  1403. if isinstance(polygons, np.ndarray):
  1404. # optimization: polygons all have the same number of points, so can
  1405. # vectorize
  1406. n = polygons.shape[-2]
  1407. i1, i2, i3 = 0, n//3, 2*n//3
  1408. v1 = polygons[..., i1, :] - polygons[..., i2, :]
  1409. v2 = polygons[..., i2, :] - polygons[..., i3, :]
  1410. else:
  1411. # The subtraction doesn't vectorize because polygons is jagged.
  1412. v1 = np.empty((len(polygons), 3))
  1413. v2 = np.empty((len(polygons), 3))
  1414. for poly_i, ps in enumerate(polygons):
  1415. n = len(ps)
  1416. i1, i2, i3 = 0, n//3, 2*n//3
  1417. v1[poly_i, :] = ps[i1, :] - ps[i2, :]
  1418. v2[poly_i, :] = ps[i2, :] - ps[i3, :]
  1419. return np.cross(v1, v2)
  1420. def _shade_colors(self, color, normals, lightsource=None):
  1421. """
  1422. Shade *color* using normal vectors given by *normals*.
  1423. *color* can also be an array of the same length as *normals*.
  1424. """
  1425. if lightsource is None:
  1426. # chosen for backwards-compatibility
  1427. lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
  1428. with np.errstate(invalid="ignore"):
  1429. shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
  1430. @ lightsource.direction)
  1431. mask = ~np.isnan(shade)
  1432. if mask.any():
  1433. # convert dot product to allowed shading fractions
  1434. in_norm = mcolors.Normalize(-1, 1)
  1435. out_norm = mcolors.Normalize(0.3, 1).inverse
  1436. def norm(x):
  1437. return out_norm(in_norm(x))
  1438. shade[~mask] = 0
  1439. color = mcolors.to_rgba_array(color)
  1440. # shape of color should be (M, 4) (where M is number of faces)
  1441. # shape of shade should be (M,)
  1442. # colors should have final shape of (M, 4)
  1443. alpha = color[:, 3]
  1444. colors = norm(shade)[:, np.newaxis] * color
  1445. colors[:, 3] = alpha
  1446. else:
  1447. colors = np.asanyarray(color).copy()
  1448. return colors
  1449. def plot_wireframe(self, X, Y, Z, *args, **kwargs):
  1450. """
  1451. Plot a 3D wireframe.
  1452. .. note::
  1453. The *rcount* and *ccount* kwargs, which both default to 50,
  1454. determine the maximum number of samples used in each direction. If
  1455. the input data is larger, it will be downsampled (by slicing) to
  1456. these numbers of points.
  1457. Parameters
  1458. ----------
  1459. X, Y, Z : 2d arrays
  1460. Data values.
  1461. rcount, ccount : int
  1462. Maximum number of samples used in each direction. If the input
  1463. data is larger, it will be downsampled (by slicing) to these
  1464. numbers of points. Setting a count to zero causes the data to be
  1465. not sampled in the corresponding direction, producing a 3D line
  1466. plot rather than a wireframe plot. Defaults to 50.
  1467. .. versionadded:: 2.0
  1468. rstride, cstride : int
  1469. Downsampling stride in each direction. These arguments are
  1470. mutually exclusive with *rcount* and *ccount*. If only one of
  1471. *rstride* or *cstride* is set, the other defaults to 1. Setting a
  1472. stride to zero causes the data to be not sampled in the
  1473. corresponding direction, producing a 3D line plot rather than a
  1474. wireframe plot.
  1475. 'classic' mode uses a default of ``rstride = cstride = 1`` instead
  1476. of the new default of ``rcount = ccount = 50``.
  1477. **kwargs
  1478. Other arguments are forwarded to `.Line3DCollection`.
  1479. """
  1480. had_data = self.has_data()
  1481. if Z.ndim != 2:
  1482. raise ValueError("Argument Z must be 2-dimensional.")
  1483. # FIXME: Support masked arrays
  1484. X, Y, Z = np.broadcast_arrays(X, Y, Z)
  1485. rows, cols = Z.shape
  1486. has_stride = 'rstride' in kwargs or 'cstride' in kwargs
  1487. has_count = 'rcount' in kwargs or 'ccount' in kwargs
  1488. if has_stride and has_count:
  1489. raise ValueError("Cannot specify both stride and count arguments")
  1490. rstride = kwargs.pop('rstride', 1)
  1491. cstride = kwargs.pop('cstride', 1)
  1492. rcount = kwargs.pop('rcount', 50)
  1493. ccount = kwargs.pop('ccount', 50)
  1494. if rcParams['_internal.classic_mode']:
  1495. # Strides have priority over counts in classic mode.
  1496. # So, only compute strides from counts
  1497. # if counts were explicitly given
  1498. if has_count:
  1499. rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
  1500. cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
  1501. else:
  1502. # If the strides are provided then it has priority.
  1503. # Otherwise, compute the strides from the counts.
  1504. if not has_stride:
  1505. rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
  1506. cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
  1507. # We want two sets of lines, one running along the "rows" of
  1508. # Z and another set of lines running along the "columns" of Z.
  1509. # This transpose will make it easy to obtain the columns.
  1510. tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z)
  1511. if rstride:
  1512. rii = list(range(0, rows, rstride))
  1513. # Add the last index only if needed
  1514. if rows > 0 and rii[-1] != (rows - 1):
  1515. rii += [rows-1]
  1516. else:
  1517. rii = []
  1518. if cstride:
  1519. cii = list(range(0, cols, cstride))
  1520. # Add the last index only if needed
  1521. if cols > 0 and cii[-1] != (cols - 1):
  1522. cii += [cols-1]
  1523. else:
  1524. cii = []
  1525. if rstride == 0 and cstride == 0:
  1526. raise ValueError("Either rstride or cstride must be non zero")
  1527. # If the inputs were empty, then just
  1528. # reset everything.
  1529. if Z.size == 0:
  1530. rii = []
  1531. cii = []
  1532. xlines = [X[i] for i in rii]
  1533. ylines = [Y[i] for i in rii]
  1534. zlines = [Z[i] for i in rii]
  1535. txlines = [tX[i] for i in cii]
  1536. tylines = [tY[i] for i in cii]
  1537. tzlines = [tZ[i] for i in cii]
  1538. lines = ([list(zip(xl, yl, zl))
  1539. for xl, yl, zl in zip(xlines, ylines, zlines)]
  1540. + [list(zip(xl, yl, zl))
  1541. for xl, yl, zl in zip(txlines, tylines, tzlines)])
  1542. linec = art3d.Line3DCollection(lines, *args, **kwargs)
  1543. self.add_collection(linec)
  1544. self.auto_scale_xyz(X, Y, Z, had_data)
  1545. return linec
  1546. def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
  1547. lightsource=None, **kwargs):
  1548. """
  1549. Plot a triangulated surface.
  1550. The (optional) triangulation can be specified in one of two ways;
  1551. either::
  1552. plot_trisurf(triangulation, ...)
  1553. where triangulation is a `~matplotlib.tri.Triangulation` object, or::
  1554. plot_trisurf(X, Y, ...)
  1555. plot_trisurf(X, Y, triangles, ...)
  1556. plot_trisurf(X, Y, triangles=triangles, ...)
  1557. in which case a Triangulation object will be created. See
  1558. `.Triangulation` for a explanation of these possibilities.
  1559. The remaining arguments are::
  1560. plot_trisurf(..., Z)
  1561. where *Z* is the array of values to contour, one per point
  1562. in the triangulation.
  1563. Parameters
  1564. ----------
  1565. X, Y, Z : array-like
  1566. Data values as 1D arrays.
  1567. color
  1568. Color of the surface patches.
  1569. cmap
  1570. A colormap for the surface patches.
  1571. norm : Normalize
  1572. An instance of Normalize to map values to colors.
  1573. vmin, vmax : float, default: None
  1574. Minimum and maximum value to map.
  1575. shade : bool, default: True
  1576. Whether to shade the facecolors. Shading is always disabled when
  1577. *cmap* is specified.
  1578. lightsource : `~matplotlib.colors.LightSource`
  1579. The lightsource to use when *shade* is True.
  1580. **kwargs
  1581. All other arguments are passed on to
  1582. :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`
  1583. Examples
  1584. --------
  1585. .. plot:: gallery/mplot3d/trisurf3d.py
  1586. .. plot:: gallery/mplot3d/trisurf3d_2.py
  1587. .. versionadded:: 1.2.0
  1588. """
  1589. had_data = self.has_data()
  1590. # TODO: Support custom face colours
  1591. if color is None:
  1592. color = self._get_lines.get_next_color()
  1593. color = np.array(mcolors.to_rgba(color))
  1594. cmap = kwargs.get('cmap', None)
  1595. shade = kwargs.pop('shade', cmap is None)
  1596. tri, args, kwargs = \
  1597. Triangulation.get_from_args_and_kwargs(*args, **kwargs)
  1598. try:
  1599. z = kwargs.pop('Z')
  1600. except KeyError:
  1601. # We do this so Z doesn't get passed as an arg to PolyCollection
  1602. z, *args = args
  1603. z = np.asarray(z)
  1604. triangles = tri.get_masked_triangles()
  1605. xt = tri.x[triangles]
  1606. yt = tri.y[triangles]
  1607. zt = z[triangles]
  1608. verts = np.stack((xt, yt, zt), axis=-1)
  1609. polyc = art3d.Poly3DCollection(verts, *args, **kwargs)
  1610. if cmap:
  1611. # average over the three points of each triangle
  1612. avg_z = verts[:, :, 2].mean(axis=1)
  1613. polyc.set_array(avg_z)
  1614. if vmin is not None or vmax is not None:
  1615. polyc.set_clim(vmin, vmax)
  1616. if norm is not None:
  1617. polyc.set_norm(norm)
  1618. else:
  1619. if shade:
  1620. normals = self._generate_normals(verts)
  1621. colset = self._shade_colors(color, normals, lightsource)
  1622. else:
  1623. colset = color
  1624. polyc.set_facecolors(colset)
  1625. self.add_collection(polyc)
  1626. self.auto_scale_xyz(tri.x, tri.y, z, had_data)
  1627. return polyc
  1628. def _3d_extend_contour(self, cset, stride=5):
  1629. """
  1630. Extend a contour in 3D by creating
  1631. """
  1632. levels = cset.levels
  1633. colls = cset.collections
  1634. dz = (levels[1] - levels[0]) / 2
  1635. for z, linec in zip(levels, colls):
  1636. paths = linec.get_paths()
  1637. if not paths:
  1638. continue
  1639. topverts = art3d._paths_to_3d_segments(paths, z - dz)
  1640. botverts = art3d._paths_to_3d_segments(paths, z + dz)
  1641. color = linec.get_color()[0]
  1642. polyverts = []
  1643. normals = []
  1644. nsteps = round(len(topverts[0]) / stride)
  1645. if nsteps <= 1:
  1646. if len(topverts[0]) > 1:
  1647. nsteps = 2
  1648. else:
  1649. continue
  1650. stepsize = (len(topverts[0]) - 1) / (nsteps - 1)
  1651. for i in range(int(round(nsteps)) - 1):
  1652. i1 = int(round(i * stepsize))
  1653. i2 = int(round((i + 1) * stepsize))
  1654. polyverts.append([topverts[0][i1],
  1655. topverts[0][i2],
  1656. botverts[0][i2],
  1657. botverts[0][i1]])
  1658. # all polygons have 4 vertices, so vectorize
  1659. polyverts = np.array(polyverts)
  1660. normals = self._generate_normals(polyverts)
  1661. colors = self._shade_colors(color, normals)
  1662. colors2 = self._shade_colors(color, normals)
  1663. polycol = art3d.Poly3DCollection(polyverts,
  1664. facecolors=colors,
  1665. edgecolors=colors2)
  1666. polycol.set_sort_zpos(z)
  1667. self.add_collection3d(polycol)
  1668. for col in colls:
  1669. self.collections.remove(col)
  1670. def add_contour_set(
  1671. self, cset, extend3d=False, stride=5, zdir='z', offset=None):
  1672. zdir = '-' + zdir
  1673. if extend3d:
  1674. self._3d_extend_contour(cset, stride)
  1675. else:
  1676. for z, linec in zip(cset.levels, cset.collections):
  1677. if offset is not None:
  1678. z = offset
  1679. art3d.line_collection_2d_to_3d(linec, z, zdir=zdir)
  1680. def add_contourf_set(self, cset, zdir='z', offset=None):
  1681. zdir = '-' + zdir
  1682. for z, linec in zip(cset.levels, cset.collections):
  1683. if offset is not None:
  1684. z = offset
  1685. art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir)
  1686. linec.set_sort_zpos(z)
  1687. def contour(self, X, Y, Z, *args,
  1688. extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
  1689. """
  1690. Create a 3D contour plot.
  1691. Parameters
  1692. ----------
  1693. X, Y, Z : array-like
  1694. Input data.
  1695. extend3d : bool, default: False
  1696. Whether to extend contour in 3D.
  1697. stride : int
  1698. Step size for extending contour.
  1699. zdir : {'x', 'y', 'z'}, default: 'z'
  1700. The direction to use.
  1701. offset : float, optional
  1702. If specified, plot a projection of the contour lines at this
  1703. position in a plane normal to zdir.
  1704. *args, **kwargs
  1705. Other arguments are forwarded to `matplotlib.axes.Axes.contour`.
  1706. Returns
  1707. -------
  1708. matplotlib.contour.QuadContourSet
  1709. """
  1710. had_data = self.has_data()
  1711. jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
  1712. cset = super().contour(jX, jY, jZ, *args, **kwargs)
  1713. self.add_contour_set(cset, extend3d, stride, zdir, offset)
  1714. self.auto_scale_xyz(X, Y, Z, had_data)
  1715. return cset
  1716. contour3D = contour
  1717. def tricontour(self, *args,
  1718. extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
  1719. """
  1720. Create a 3D contour plot.
  1721. .. versionchanged:: 1.3.0
  1722. Added support for custom triangulations
  1723. .. note::
  1724. This method currently produces incorrect output due to a
  1725. longstanding bug in 3D PolyCollection rendering.
  1726. Parameters
  1727. ----------
  1728. X, Y, Z : array-like
  1729. Input data.
  1730. extend3d : bool, default: False
  1731. Whether to extend contour in 3D.
  1732. stride : int
  1733. Step size for extending contour.
  1734. zdir : {'x', 'y', 'z'}, default: 'z'
  1735. The direction to use.
  1736. offset : float, optional
  1737. If specified, plot a projection of the contour lines at this
  1738. position in a plane normal to zdir.
  1739. *args, **kwargs
  1740. Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`.
  1741. Returns
  1742. -------
  1743. matplotlib.tri.tricontour.TriContourSet
  1744. """
  1745. had_data = self.has_data()
  1746. tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
  1747. *args, **kwargs)
  1748. X = tri.x
  1749. Y = tri.y
  1750. if 'Z' in kwargs:
  1751. Z = kwargs.pop('Z')
  1752. else:
  1753. # We do this so Z doesn't get passed as an arg to Axes.tricontour
  1754. Z, *args = args
  1755. jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
  1756. tri = Triangulation(jX, jY, tri.triangles, tri.mask)
  1757. cset = super().tricontour(tri, jZ, *args, **kwargs)
  1758. self.add_contour_set(cset, extend3d, stride, zdir, offset)
  1759. self.auto_scale_xyz(X, Y, Z, had_data)
  1760. return cset
  1761. def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs):
  1762. """
  1763. Create a 3D filled contour plot.
  1764. Parameters
  1765. ----------
  1766. X, Y, Z : array-like
  1767. Input data.
  1768. zdir : {'x', 'y', 'z'}, default: 'z'
  1769. The direction to use.
  1770. offset : float, optional
  1771. If specified, plot a projection of the contour lines at this
  1772. position in a plane normal to zdir.
  1773. *args, **kwargs
  1774. Other arguments are forwarded to `matplotlib.axes.Axes.contourf`.
  1775. Returns
  1776. -------
  1777. matplotlib.contour.QuadContourSet
  1778. Notes
  1779. -----
  1780. .. versionadded:: 1.1.0
  1781. The *zdir* and *offset* parameters.
  1782. """
  1783. had_data = self.has_data()
  1784. jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
  1785. cset = super().contourf(jX, jY, jZ, *args, **kwargs)
  1786. self.add_contourf_set(cset, zdir, offset)
  1787. self.auto_scale_xyz(X, Y, Z, had_data)
  1788. return cset
  1789. contourf3D = contourf
  1790. def tricontourf(self, *args, zdir='z', offset=None, **kwargs):
  1791. """
  1792. Create a 3D filled contour plot.
  1793. .. note::
  1794. This method currently produces incorrect output due to a
  1795. longstanding bug in 3D PolyCollection rendering.
  1796. Parameters
  1797. ----------
  1798. X, Y, Z : array-like
  1799. Input data.
  1800. zdir : {'x', 'y', 'z'}, default: 'z'
  1801. The direction to use.
  1802. offset : float, optional
  1803. If specified, plot a projection of the contour lines at this
  1804. position in a plane normal to zdir.
  1805. *args, **kwargs
  1806. Other arguments are forwarded to
  1807. `matplotlib.axes.Axes.tricontourf`.
  1808. Returns
  1809. -------
  1810. matplotlib.tri.tricontour.TriContourSet
  1811. Notes
  1812. -----
  1813. .. versionadded:: 1.1.0
  1814. The *zdir* and *offset* parameters.
  1815. .. versionchanged:: 1.3.0
  1816. Added support for custom triangulations
  1817. """
  1818. had_data = self.has_data()
  1819. tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
  1820. *args, **kwargs)
  1821. X = tri.x
  1822. Y = tri.y
  1823. if 'Z' in kwargs:
  1824. Z = kwargs.pop('Z')
  1825. else:
  1826. # We do this so Z doesn't get passed as an arg to Axes.tricontourf
  1827. Z, *args = args
  1828. jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
  1829. tri = Triangulation(jX, jY, tri.triangles, tri.mask)
  1830. cset = super().tricontourf(tri, jZ, *args, **kwargs)
  1831. self.add_contourf_set(cset, zdir, offset)
  1832. self.auto_scale_xyz(X, Y, Z, had_data)
  1833. return cset
  1834. def add_collection3d(self, col, zs=0, zdir='z'):
  1835. """
  1836. Add a 3D collection object to the plot.
  1837. 2D collection types are converted to a 3D version by
  1838. modifying the object and adding z coordinate information.
  1839. Supported are:
  1840. - PolyCollection
  1841. - LineCollection
  1842. - PatchCollection
  1843. """
  1844. zvals = np.atleast_1d(zs)
  1845. zsortval = (np.min(zvals) if zvals.size
  1846. else 0) # FIXME: arbitrary default
  1847. # FIXME: use issubclass() (although, then a 3D collection
  1848. # object would also pass.) Maybe have a collection3d
  1849. # abstract class to test for and exclude?
  1850. if type(col) is mcoll.PolyCollection:
  1851. art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir)
  1852. col.set_sort_zpos(zsortval)
  1853. elif type(col) is mcoll.LineCollection:
  1854. art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir)
  1855. col.set_sort_zpos(zsortval)
  1856. elif type(col) is mcoll.PatchCollection:
  1857. art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir)
  1858. col.set_sort_zpos(zsortval)
  1859. super().add_collection(col)
  1860. def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True,
  1861. *args, **kwargs):
  1862. """
  1863. Create a scatter plot.
  1864. Parameters
  1865. ----------
  1866. xs, ys : array-like
  1867. The data positions.
  1868. zs : float or array-like, default: 0
  1869. The z-positions. Either an array of the same length as *xs* and
  1870. *ys* or a single value to place all points in the same plane.
  1871. zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z'
  1872. The axis direction for the *zs*. This is useful when plotting 2D
  1873. data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting
  1874. *zdir* to 'y' then plots the data to the x-z-plane.
  1875. See also :doc:`/gallery/mplot3d/2dcollections3d`.
  1876. s : float or array-like, default: 20
  1877. The marker size in points**2. Either an array of the same length
  1878. as *xs* and *ys* or a single value to make all markers the same
  1879. size.
  1880. c : color, sequence, or sequence of colors, optional
  1881. The marker color. Possible values:
  1882. - A single color format string.
  1883. - A sequence of colors of length n.
  1884. - A sequence of n numbers to be mapped to colors using *cmap* and
  1885. *norm*.
  1886. - A 2-D array in which the rows are RGB or RGBA.
  1887. For more details see the *c* argument of `~.axes.Axes.scatter`.
  1888. depthshade : bool, default: True
  1889. Whether to shade the scatter markers to give the appearance of
  1890. depth. Each call to ``scatter()`` will perform its depthshading
  1891. independently.
  1892. **kwargs
  1893. All other arguments are passed on to `~.axes.Axes.scatter`.
  1894. Returns
  1895. -------
  1896. paths : `~matplotlib.collections.PathCollection`
  1897. """
  1898. had_data = self.has_data()
  1899. zs_orig = zs
  1900. xs, ys, zs = np.broadcast_arrays(
  1901. *[np.ravel(np.ma.filled(t, np.nan)) for t in [xs, ys, zs]])
  1902. s = np.ma.ravel(s) # This doesn't have to match x, y in size.
  1903. xs, ys, zs, s, c = cbook.delete_masked_points(xs, ys, zs, s, c)
  1904. # For xs and ys, 2D scatter() will do the copying.
  1905. if np.may_share_memory(zs_orig, zs): # Avoid unnecessary copies.
  1906. zs = zs.copy()
  1907. patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs)
  1908. art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir,
  1909. depthshade=depthshade)
  1910. if self._zmargin < 0.05 and xs.size > 0:
  1911. self.set_zmargin(0.05)
  1912. self.auto_scale_xyz(xs, ys, zs, had_data)
  1913. return patches
  1914. scatter3D = scatter
  1915. def bar(self, left, height, zs=0, zdir='z', *args, **kwargs):
  1916. """
  1917. Add 2D bar(s).
  1918. Parameters
  1919. ----------
  1920. left : 1D array-like
  1921. The x coordinates of the left sides of the bars.
  1922. height : 1D array-like
  1923. The height of the bars.
  1924. zs : float or 1D array-like
  1925. Z coordinate of bars; if a single value is specified, it will be
  1926. used for all bars.
  1927. zdir : {'x', 'y', 'z'}, default: 'z'
  1928. When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
  1929. **kwargs
  1930. Other arguments are forwarded to `matplotlib.axes.Axes.bar`.
  1931. Returns
  1932. -------
  1933. mpl_toolkits.mplot3d.art3d.Patch3DCollection
  1934. """
  1935. had_data = self.has_data()
  1936. patches = super().bar(left, height, *args, **kwargs)
  1937. zs = np.broadcast_to(zs, len(left))
  1938. verts = []
  1939. verts_zs = []
  1940. for p, z in zip(patches, zs):
  1941. vs = art3d._get_patch_verts(p)
  1942. verts += vs.tolist()
  1943. verts_zs += [z] * len(vs)
  1944. art3d.patch_2d_to_3d(p, z, zdir)
  1945. if 'alpha' in kwargs:
  1946. p.set_alpha(kwargs['alpha'])
  1947. if len(verts) > 0:
  1948. # the following has to be skipped if verts is empty
  1949. # NOTE: Bugs could still occur if len(verts) > 0,
  1950. # but the "2nd dimension" is empty.
  1951. xs, ys = zip(*verts)
  1952. else:
  1953. xs, ys = [], []
  1954. xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir)
  1955. self.auto_scale_xyz(xs, ys, verts_zs, had_data)
  1956. return patches
  1957. def bar3d(self, x, y, z, dx, dy, dz, color=None,
  1958. zsort='average', shade=True, lightsource=None, *args, **kwargs):
  1959. """
  1960. Generate a 3D barplot.
  1961. This method creates three dimensional barplot where the width,
  1962. depth, height, and color of the bars can all be uniquely set.
  1963. Parameters
  1964. ----------
  1965. x, y, z : array-like
  1966. The coordinates of the anchor point of the bars.
  1967. dx, dy, dz : float or array-like
  1968. The width, depth, and height of the bars, respectively.
  1969. color : sequence of colors, optional
  1970. The color of the bars can be specified globally or
  1971. individually. This parameter can be:
  1972. - A single color, to color all bars the same color.
  1973. - An array of colors of length N bars, to color each bar
  1974. independently.
  1975. - An array of colors of length 6, to color the faces of the
  1976. bars similarly.
  1977. - An array of colors of length 6 * N bars, to color each face
  1978. independently.
  1979. When coloring the faces of the boxes specifically, this is
  1980. the order of the coloring:
  1981. 1. -Z (bottom of box)
  1982. 2. +Z (top of box)
  1983. 3. -Y
  1984. 4. +Y
  1985. 5. -X
  1986. 6. +X
  1987. zsort : str, optional
  1988. The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection`
  1989. shade : bool, default: True
  1990. When true, this shades the dark sides of the bars (relative
  1991. to the plot's source of light).
  1992. lightsource : `~matplotlib.colors.LightSource`
  1993. The lightsource to use when *shade* is True.
  1994. **kwargs
  1995. Any additional keyword arguments are passed onto
  1996. `~.art3d.Poly3DCollection`.
  1997. Returns
  1998. -------
  1999. collection : `~.art3d.Poly3DCollection`
  2000. A collection of three dimensional polygons representing
  2001. the bars.
  2002. """
  2003. had_data = self.has_data()
  2004. x, y, z, dx, dy, dz = np.broadcast_arrays(
  2005. np.atleast_1d(x), y, z, dx, dy, dz)
  2006. minx = np.min(x)
  2007. maxx = np.max(x + dx)
  2008. miny = np.min(y)
  2009. maxy = np.max(y + dy)
  2010. minz = np.min(z)
  2011. maxz = np.max(z + dz)
  2012. # shape (6, 4, 3)
  2013. # All faces are oriented facing outwards - when viewed from the
  2014. # outside, their vertices are in a counterclockwise ordering.
  2015. cuboid = np.array([
  2016. # -z
  2017. (
  2018. (0, 0, 0),
  2019. (0, 1, 0),
  2020. (1, 1, 0),
  2021. (1, 0, 0),
  2022. ),
  2023. # +z
  2024. (
  2025. (0, 0, 1),
  2026. (1, 0, 1),
  2027. (1, 1, 1),
  2028. (0, 1, 1),
  2029. ),
  2030. # -y
  2031. (
  2032. (0, 0, 0),
  2033. (1, 0, 0),
  2034. (1, 0, 1),
  2035. (0, 0, 1),
  2036. ),
  2037. # +y
  2038. (
  2039. (0, 1, 0),
  2040. (0, 1, 1),
  2041. (1, 1, 1),
  2042. (1, 1, 0),
  2043. ),
  2044. # -x
  2045. (
  2046. (0, 0, 0),
  2047. (0, 0, 1),
  2048. (0, 1, 1),
  2049. (0, 1, 0),
  2050. ),
  2051. # +x
  2052. (
  2053. (1, 0, 0),
  2054. (1, 1, 0),
  2055. (1, 1, 1),
  2056. (1, 0, 1),
  2057. ),
  2058. ])
  2059. # indexed by [bar, face, vertex, coord]
  2060. polys = np.empty(x.shape + cuboid.shape)
  2061. # handle each coordinate separately
  2062. for i, p, dp in [(0, x, dx), (1, y, dy), (2, z, dz)]:
  2063. p = p[..., np.newaxis, np.newaxis]
  2064. dp = dp[..., np.newaxis, np.newaxis]
  2065. polys[..., i] = p + dp * cuboid[..., i]
  2066. # collapse the first two axes
  2067. polys = polys.reshape((-1,) + polys.shape[2:])
  2068. facecolors = []
  2069. if color is None:
  2070. color = [self._get_patches_for_fill.get_next_color()]
  2071. color = list(mcolors.to_rgba_array(color))
  2072. if len(color) == len(x):
  2073. # bar colors specified, need to expand to number of faces
  2074. for c in color:
  2075. facecolors.extend([c] * 6)
  2076. else:
  2077. # a single color specified, or face colors specified explicitly
  2078. facecolors = color
  2079. if len(facecolors) < len(x):
  2080. facecolors *= (6 * len(x))
  2081. if shade:
  2082. normals = self._generate_normals(polys)
  2083. sfacecolors = self._shade_colors(facecolors, normals, lightsource)
  2084. else:
  2085. sfacecolors = facecolors
  2086. col = art3d.Poly3DCollection(polys,
  2087. zsort=zsort,
  2088. facecolor=sfacecolors,
  2089. *args, **kwargs)
  2090. self.add_collection(col)
  2091. self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
  2092. return col
  2093. def set_title(self, label, fontdict=None, loc='center', **kwargs):
  2094. # docstring inherited
  2095. ret = super().set_title(label, fontdict=fontdict, loc=loc, **kwargs)
  2096. (x, y) = self.title.get_position()
  2097. self.title.set_y(0.92 * y)
  2098. return ret
  2099. def quiver(self, *args,
  2100. length=1, arrow_length_ratio=.3, pivot='tail', normalize=False,
  2101. **kwargs):
  2102. """
  2103. ax.quiver(X, Y, Z, U, V, W, /, length=1, arrow_length_ratio=.3, \
  2104. pivot='tail', normalize=False, **kwargs)
  2105. Plot a 3D field of arrows.
  2106. The arguments could be array-like or scalars, so long as they
  2107. they can be broadcast together. The arguments can also be
  2108. masked arrays. If an element in any of argument is masked, then
  2109. that corresponding quiver element will not be plotted.
  2110. Parameters
  2111. ----------
  2112. X, Y, Z : array-like
  2113. The x, y and z coordinates of the arrow locations (default is
  2114. tail of arrow; see *pivot* kwarg).
  2115. U, V, W : array-like
  2116. The x, y and z components of the arrow vectors.
  2117. length : float, default: 1
  2118. The length of each quiver.
  2119. arrow_length_ratio : float, default: 0.3
  2120. The ratio of the arrow head with respect to the quiver.
  2121. pivot : {'tail', 'middle', 'tip'}, default: 'tail'
  2122. The part of the arrow that is at the grid point; the arrow
  2123. rotates about this point, hence the name *pivot*.
  2124. normalize : bool, default: False
  2125. Whether all arrows are normalized to have the same length, or keep
  2126. the lengths defined by *u*, *v*, and *w*.
  2127. **kwargs
  2128. Any additional keyword arguments are delegated to
  2129. :class:`~matplotlib.collections.LineCollection`
  2130. """
  2131. def calc_arrows(UVW, angle=15):
  2132. # get unit direction vector perpendicular to (u, v, w)
  2133. x = UVW[:, 0]
  2134. y = UVW[:, 1]
  2135. norm = np.linalg.norm(UVW[:, :2], axis=1)
  2136. x_p = np.divide(y, norm, where=norm != 0, out=np.zeros_like(x))
  2137. y_p = np.divide(-x, norm, where=norm != 0, out=np.ones_like(x))
  2138. # compute the two arrowhead direction unit vectors
  2139. ra = math.radians(angle)
  2140. c = math.cos(ra)
  2141. s = math.sin(ra)
  2142. # construct the rotation matrices of shape (3, 3, n)
  2143. Rpos = np.array(
  2144. [[c + (x_p ** 2) * (1 - c), x_p * y_p * (1 - c), y_p * s],
  2145. [y_p * x_p * (1 - c), c + (y_p ** 2) * (1 - c), -x_p * s],
  2146. [-y_p * s, x_p * s, np.full_like(x_p, c)]])
  2147. # opposite rotation negates all the sin terms
  2148. Rneg = Rpos.copy()
  2149. Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] *= -1
  2150. # Batch n (3, 3) x (3) matrix multiplications ((3, 3, n) x (n, 3)).
  2151. Rpos_vecs = np.einsum("ij...,...j->...i", Rpos, UVW)
  2152. Rneg_vecs = np.einsum("ij...,...j->...i", Rneg, UVW)
  2153. # Stack into (n, 2, 3) result.
  2154. head_dirs = np.stack([Rpos_vecs, Rneg_vecs], axis=1)
  2155. return head_dirs
  2156. had_data = self.has_data()
  2157. # handle args
  2158. argi = 6
  2159. if len(args) < argi:
  2160. raise ValueError('Wrong number of arguments. Expected %d got %d' %
  2161. (argi, len(args)))
  2162. # first 6 arguments are X, Y, Z, U, V, W
  2163. input_args = args[:argi]
  2164. # extract the masks, if any
  2165. masks = [k.mask for k in input_args
  2166. if isinstance(k, np.ma.MaskedArray)]
  2167. # broadcast to match the shape
  2168. bcast = np.broadcast_arrays(*input_args, *masks)
  2169. input_args = bcast[:argi]
  2170. masks = bcast[argi:]
  2171. if masks:
  2172. # combine the masks into one
  2173. mask = reduce(np.logical_or, masks)
  2174. # put mask on and compress
  2175. input_args = [np.ma.array(k, mask=mask).compressed()
  2176. for k in input_args]
  2177. else:
  2178. input_args = [np.ravel(k) for k in input_args]
  2179. if any(len(v) == 0 for v in input_args):
  2180. # No quivers, so just make an empty collection and return early
  2181. linec = art3d.Line3DCollection([], *args[argi:], **kwargs)
  2182. self.add_collection(linec)
  2183. return linec
  2184. shaft_dt = np.array([0., length], dtype=float)
  2185. arrow_dt = shaft_dt * arrow_length_ratio
  2186. cbook._check_in_list(['tail', 'middle', 'tip'], pivot=pivot)
  2187. if pivot == 'tail':
  2188. shaft_dt -= length
  2189. elif pivot == 'middle':
  2190. shaft_dt -= length / 2
  2191. XYZ = np.column_stack(input_args[:3])
  2192. UVW = np.column_stack(input_args[3:argi]).astype(float)
  2193. # Normalize rows of UVW
  2194. norm = np.linalg.norm(UVW, axis=1)
  2195. # If any row of UVW is all zeros, don't make a quiver for it
  2196. mask = norm > 0
  2197. XYZ = XYZ[mask]
  2198. if normalize:
  2199. UVW = UVW[mask] / norm[mask].reshape((-1, 1))
  2200. else:
  2201. UVW = UVW[mask]
  2202. if len(XYZ) > 0:
  2203. # compute the shaft lines all at once with an outer product
  2204. shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1)
  2205. # compute head direction vectors, n heads x 2 sides x 3 dimensions
  2206. head_dirs = calc_arrows(UVW)
  2207. # compute all head lines at once, starting from the shaft ends
  2208. heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs)
  2209. # stack left and right head lines together
  2210. heads = heads.reshape((len(arrow_dt), -1, 3))
  2211. # transpose to get a list of lines
  2212. heads = heads.swapaxes(0, 1)
  2213. lines = [*shafts, *heads]
  2214. else:
  2215. lines = []
  2216. linec = art3d.Line3DCollection(lines, *args[argi:], **kwargs)
  2217. self.add_collection(linec)
  2218. self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data)
  2219. return linec
  2220. quiver3D = quiver
  2221. def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
  2222. lightsource=None, **kwargs):
  2223. """
  2224. ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \
  2225. **kwargs)
  2226. Plot a set of filled voxels
  2227. All voxels are plotted as 1x1x1 cubes on the axis, with
  2228. ``filled[0, 0, 0]`` placed with its lower corner at the origin.
  2229. Occluded faces are not plotted.
  2230. .. versionadded:: 2.1
  2231. Parameters
  2232. ----------
  2233. filled : 3D np.array of bool
  2234. A 3d array of values, with truthy values indicating which voxels
  2235. to fill
  2236. x, y, z : 3D np.array, optional
  2237. The coordinates of the corners of the voxels. This should broadcast
  2238. to a shape one larger in every dimension than the shape of
  2239. *filled*. These can be used to plot non-cubic voxels.
  2240. If not specified, defaults to increasing integers along each axis,
  2241. like those returned by :func:`~numpy.indices`.
  2242. As indicated by the ``/`` in the function signature, these
  2243. arguments can only be passed positionally.
  2244. facecolors, edgecolors : array-like, optional
  2245. The color to draw the faces and edges of the voxels. Can only be
  2246. passed as keyword arguments.
  2247. This parameter can be:
  2248. - A single color value, to color all voxels the same color. This
  2249. can be either a string, or a 1D rgb/rgba array
  2250. - ``None``, the default, to use a single color for the faces, and
  2251. the style default for the edges.
  2252. - A 3D ndarray of color names, with each item the color for the
  2253. corresponding voxel. The size must match the voxels.
  2254. - A 4D ndarray of rgb/rgba data, with the components along the
  2255. last axis.
  2256. shade : bool, default: True
  2257. Whether to shade the facecolors. Shading is always disabled when
  2258. *cmap* is specified.
  2259. .. versionadded:: 3.1
  2260. lightsource : `~matplotlib.colors.LightSource`
  2261. The lightsource to use when *shade* is True.
  2262. .. versionadded:: 3.1
  2263. **kwargs
  2264. Additional keyword arguments to pass onto
  2265. `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`.
  2266. Returns
  2267. -------
  2268. faces : dict
  2269. A dictionary indexed by coordinate, where ``faces[i, j, k]`` is a
  2270. `.Poly3DCollection` of the faces drawn for the voxel
  2271. ``filled[i, j, k]``. If no faces were drawn for a given voxel,
  2272. either because it was not asked to be drawn, or it is fully
  2273. occluded, then ``(i, j, k) not in faces``.
  2274. Examples
  2275. --------
  2276. .. plot:: gallery/mplot3d/voxels.py
  2277. .. plot:: gallery/mplot3d/voxels_rgb.py
  2278. .. plot:: gallery/mplot3d/voxels_torus.py
  2279. .. plot:: gallery/mplot3d/voxels_numpy_logo.py
  2280. """
  2281. # work out which signature we should be using, and use it to parse
  2282. # the arguments. Name must be voxels for the correct error message
  2283. if len(args) >= 3:
  2284. # underscores indicate position only
  2285. def voxels(__x, __y, __z, filled, **kwargs):
  2286. return (__x, __y, __z), filled, kwargs
  2287. else:
  2288. def voxels(filled, **kwargs):
  2289. return None, filled, kwargs
  2290. xyz, filled, kwargs = voxels(*args, **kwargs)
  2291. # check dimensions
  2292. if filled.ndim != 3:
  2293. raise ValueError("Argument filled must be 3-dimensional")
  2294. size = np.array(filled.shape, dtype=np.intp)
  2295. # check xyz coordinates, which are one larger than the filled shape
  2296. coord_shape = tuple(size + 1)
  2297. if xyz is None:
  2298. x, y, z = np.indices(coord_shape)
  2299. else:
  2300. x, y, z = (np.broadcast_to(c, coord_shape) for c in xyz)
  2301. def _broadcast_color_arg(color, name):
  2302. if np.ndim(color) in (0, 1):
  2303. # single color, like "red" or [1, 0, 0]
  2304. return np.broadcast_to(color, filled.shape + np.shape(color))
  2305. elif np.ndim(color) in (3, 4):
  2306. # 3D array of strings, or 4D array with last axis rgb
  2307. if np.shape(color)[:3] != filled.shape:
  2308. raise ValueError(
  2309. "When multidimensional, {} must match the shape of "
  2310. "filled".format(name))
  2311. return color
  2312. else:
  2313. raise ValueError("Invalid {} argument".format(name))
  2314. # broadcast and default on facecolors
  2315. if facecolors is None:
  2316. facecolors = self._get_patches_for_fill.get_next_color()
  2317. facecolors = _broadcast_color_arg(facecolors, 'facecolors')
  2318. # broadcast but no default on edgecolors
  2319. edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')
  2320. # scale to the full array, even if the data is only in the center
  2321. self.auto_scale_xyz(x, y, z)
  2322. # points lying on corners of a square
  2323. square = np.array([
  2324. [0, 0, 0],
  2325. [1, 0, 0],
  2326. [1, 1, 0],
  2327. [0, 1, 0],
  2328. ], dtype=np.intp)
  2329. voxel_faces = defaultdict(list)
  2330. def permutation_matrices(n):
  2331. """Generate cyclic permutation matrices."""
  2332. mat = np.eye(n, dtype=np.intp)
  2333. for i in range(n):
  2334. yield mat
  2335. mat = np.roll(mat, 1, axis=0)
  2336. # iterate over each of the YZ, ZX, and XY orientations, finding faces
  2337. # to render
  2338. for permute in permutation_matrices(3):
  2339. # find the set of ranges to iterate over
  2340. pc, qc, rc = permute.T.dot(size)
  2341. pinds = np.arange(pc)
  2342. qinds = np.arange(qc)
  2343. rinds = np.arange(rc)
  2344. square_rot_pos = square.dot(permute.T)
  2345. square_rot_neg = square_rot_pos[::-1]
  2346. # iterate within the current plane
  2347. for p in pinds:
  2348. for q in qinds:
  2349. # iterate perpendicularly to the current plane, handling
  2350. # boundaries. We only draw faces between a voxel and an
  2351. # empty space, to avoid drawing internal faces.
  2352. # draw lower faces
  2353. p0 = permute.dot([p, q, 0])
  2354. i0 = tuple(p0)
  2355. if filled[i0]:
  2356. voxel_faces[i0].append(p0 + square_rot_neg)
  2357. # draw middle faces
  2358. for r1, r2 in zip(rinds[:-1], rinds[1:]):
  2359. p1 = permute.dot([p, q, r1])
  2360. p2 = permute.dot([p, q, r2])
  2361. i1 = tuple(p1)
  2362. i2 = tuple(p2)
  2363. if filled[i1] and not filled[i2]:
  2364. voxel_faces[i1].append(p2 + square_rot_pos)
  2365. elif not filled[i1] and filled[i2]:
  2366. voxel_faces[i2].append(p2 + square_rot_neg)
  2367. # draw upper faces
  2368. pk = permute.dot([p, q, rc-1])
  2369. pk2 = permute.dot([p, q, rc])
  2370. ik = tuple(pk)
  2371. if filled[ik]:
  2372. voxel_faces[ik].append(pk2 + square_rot_pos)
  2373. # iterate over the faces, and generate a Poly3DCollection for each
  2374. # voxel
  2375. polygons = {}
  2376. for coord, faces_inds in voxel_faces.items():
  2377. # convert indices into 3D positions
  2378. if xyz is None:
  2379. faces = faces_inds
  2380. else:
  2381. faces = []
  2382. for face_inds in faces_inds:
  2383. ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
  2384. face = np.empty(face_inds.shape)
  2385. face[:, 0] = x[ind]
  2386. face[:, 1] = y[ind]
  2387. face[:, 2] = z[ind]
  2388. faces.append(face)
  2389. # shade the faces
  2390. facecolor = facecolors[coord]
  2391. edgecolor = edgecolors[coord]
  2392. if shade:
  2393. normals = self._generate_normals(faces)
  2394. facecolor = self._shade_colors(facecolor, normals, lightsource)
  2395. if edgecolor is not None:
  2396. edgecolor = self._shade_colors(
  2397. edgecolor, normals, lightsource
  2398. )
  2399. poly = art3d.Poly3DCollection(
  2400. faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs)
  2401. self.add_collection3d(poly)
  2402. polygons[coord] = poly
  2403. return polygons
  2404. def get_tightbbox(self, renderer, call_axes_locator=True,
  2405. bbox_extra_artists=None, *, for_layout_only=False):
  2406. ret = super().get_tightbbox(renderer,
  2407. call_axes_locator=call_axes_locator,
  2408. bbox_extra_artists=bbox_extra_artists,
  2409. for_layout_only=for_layout_only)
  2410. batch = [ret]
  2411. if self._axis3don:
  2412. for axis in self._get_axis_list():
  2413. if axis.get_visible():
  2414. try:
  2415. axis_bb = axis.get_tightbbox(
  2416. renderer,
  2417. for_layout_only=for_layout_only
  2418. )
  2419. except TypeError:
  2420. # in case downstream library has redefined axis:
  2421. axis_bb = axis.get_tightbbox(renderer)
  2422. if axis_bb:
  2423. batch.append(axis_bb)
  2424. return mtransforms.Bbox.union(batch)
  2425. docstring.interpd.update(Axes3D=artist.kwdoc(Axes3D))
  2426. docstring.dedent_interpd(Axes3D.__init__)
  2427. def get_test_data(delta=0.05):
  2428. """Return a tuple X, Y, Z with a test data set."""
  2429. x = y = np.arange(-3.0, 3.0, delta)
  2430. X, Y = np.meshgrid(x, y)
  2431. Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
  2432. Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
  2433. (2 * np.pi * 0.5 * 1.5))
  2434. Z = Z2 - Z1
  2435. X = X * 10
  2436. Y = Y * 10
  2437. Z = Z * 500
  2438. return X, Y, Z