axes3d.py 154 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889389038913892389338943895389638973898389939003901390239033904390539063907390839093910391139123913391439153916391739183919392039213922392339243925392639273928392939303931393239333934393539363937393839393940394139423943394439453946394739483949395039513952395339543955395639573958395939603961396239633964396539663967396839693970397139723973397439753976397739783979398039813982398339843985398639873988398939903991399239933994399539963997399839994000400140024003400440054006400740084009401040114012401340144015401640174018401940204021402240234024402540264027402840294030403140324033403440354036403740384039404040414042404340444045404640474048404940504051405240534054405540564057405840594060406140624063406440654066406740684069407040714072407340744075407640774078407940804081408240834084408540864087408840894090409140924093409440954096409740984099410041014102410341044105410641074108410941104111411241134114411541164117411841194120412141224123412441254126412741284129413041314132413341344135413641374138413941404141414241434144414541464147414841494150415141524153415441554156415741584159416041614162
  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. import itertools
  12. import math
  13. import textwrap
  14. import warnings
  15. import numpy as np
  16. import matplotlib as mpl
  17. from matplotlib import _api, cbook, _docstring, _preprocess_data
  18. import matplotlib.artist as martist
  19. import matplotlib.collections as mcoll
  20. import matplotlib.colors as mcolors
  21. import matplotlib.image as mimage
  22. import matplotlib.lines as mlines
  23. import matplotlib.patches as mpatches
  24. import matplotlib.container as mcontainer
  25. import matplotlib.transforms as mtransforms
  26. from matplotlib.axes import Axes
  27. from matplotlib.axes._base import _axis_method_wrapper, _process_plot_format
  28. from matplotlib.transforms import Bbox
  29. from matplotlib.tri._triangulation import Triangulation
  30. from . import art3d
  31. from . import proj3d
  32. from . import axis3d
  33. @_docstring.interpd
  34. @_api.define_aliases({
  35. "xlim": ["xlim3d"], "ylim": ["ylim3d"], "zlim": ["zlim3d"]})
  36. class Axes3D(Axes):
  37. """
  38. 3D Axes object.
  39. .. note::
  40. As a user, you do not instantiate Axes directly, but use Axes creation
  41. methods instead; e.g. from `.pyplot` or `.Figure`:
  42. `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`.
  43. """
  44. name = '3d'
  45. _axis_names = ("x", "y", "z")
  46. Axes._shared_axes["z"] = cbook.Grouper()
  47. Axes._shared_axes["view"] = cbook.Grouper()
  48. def __init__(
  49. self, fig, rect=None, *args,
  50. elev=30, azim=-60, roll=0, shareview=None, sharez=None,
  51. proj_type='persp', focal_length=None,
  52. box_aspect=None,
  53. computed_zorder=True,
  54. **kwargs,
  55. ):
  56. """
  57. Parameters
  58. ----------
  59. fig : Figure
  60. The parent figure.
  61. rect : tuple (left, bottom, width, height), default: None.
  62. The ``(left, bottom, width, height)`` Axes position.
  63. elev : float, default: 30
  64. The elevation angle in degrees rotates the camera above and below
  65. the x-y plane, with a positive angle corresponding to a location
  66. above the plane.
  67. azim : float, default: -60
  68. The azimuthal angle in degrees rotates the camera about the z axis,
  69. with a positive angle corresponding to a right-handed rotation. In
  70. other words, a positive azimuth rotates the camera about the origin
  71. from its location along the +x axis towards the +y axis.
  72. roll : float, default: 0
  73. The roll angle in degrees rotates the camera about the viewing
  74. axis. A positive angle spins the camera clockwise, causing the
  75. scene to rotate counter-clockwise.
  76. shareview : Axes3D, optional
  77. Other Axes to share view angles with. Note that it is not possible
  78. to unshare axes.
  79. sharez : Axes3D, optional
  80. Other Axes to share z-limits with. Note that it is not possible to
  81. unshare axes.
  82. proj_type : {'persp', 'ortho'}
  83. The projection type, default 'persp'.
  84. focal_length : float, default: None
  85. For a projection type of 'persp', the focal length of the virtual
  86. camera. Must be > 0. If None, defaults to 1.
  87. For a projection type of 'ortho', must be set to either None
  88. or infinity (numpy.inf). If None, defaults to infinity.
  89. The focal length can be computed from a desired Field Of View via
  90. the equation: focal_length = 1/tan(FOV/2)
  91. box_aspect : 3-tuple of floats, default: None
  92. Changes the physical dimensions of the Axes3D, such that the ratio
  93. of the axis lengths in display units is x:y:z.
  94. If None, defaults to 4:4:3
  95. computed_zorder : bool, default: True
  96. If True, the draw order is computed based on the average position
  97. of the `.Artist`\\s along the view direction.
  98. Set to False if you want to manually control the order in which
  99. Artists are drawn on top of each other using their *zorder*
  100. attribute. This can be used for fine-tuning if the automatic order
  101. does not produce the desired result. Note however, that a manual
  102. zorder will only be correct for a limited view angle. If the figure
  103. is rotated by the user, it will look wrong from certain angles.
  104. **kwargs
  105. Other optional keyword arguments:
  106. %(Axes3D:kwdoc)s
  107. """
  108. if rect is None:
  109. rect = [0.0, 0.0, 1.0, 1.0]
  110. self.initial_azim = azim
  111. self.initial_elev = elev
  112. self.initial_roll = roll
  113. self.set_proj_type(proj_type, focal_length)
  114. self.computed_zorder = computed_zorder
  115. self.xy_viewLim = Bbox.unit()
  116. self.zz_viewLim = Bbox.unit()
  117. xymargin = 0.05 * 10/11 # match mpl3.8 appearance
  118. self.xy_dataLim = Bbox([[xymargin, xymargin],
  119. [1 - xymargin, 1 - xymargin]])
  120. # z-limits are encoded in the x-component of the Bbox, y is un-used
  121. self.zz_dataLim = Bbox.unit()
  122. # inhibit autoscale_view until the axes are defined
  123. # they can't be defined until Axes.__init__ has been called
  124. self.view_init(self.initial_elev, self.initial_azim, self.initial_roll)
  125. self._sharez = sharez
  126. if sharez is not None:
  127. self._shared_axes["z"].join(self, sharez)
  128. self._adjustable = 'datalim'
  129. self._shareview = shareview
  130. if shareview is not None:
  131. self._shared_axes["view"].join(self, shareview)
  132. if kwargs.pop('auto_add_to_figure', False):
  133. raise AttributeError(
  134. 'auto_add_to_figure is no longer supported for Axes3D. '
  135. 'Use fig.add_axes(ax) instead.'
  136. )
  137. super().__init__(
  138. fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs
  139. )
  140. # Disable drawing of axes by base class
  141. super().set_axis_off()
  142. # Enable drawing of axes by Axes3D class
  143. self.set_axis_on()
  144. self.M = None
  145. self.invM = None
  146. self._view_margin = 1/48 # default value to match mpl3.8
  147. self.autoscale_view()
  148. # func used to format z -- fall back on major formatters
  149. self.fmt_zdata = None
  150. self.mouse_init()
  151. fig = self.get_figure(root=True)
  152. fig.canvas.callbacks._connect_picklable(
  153. 'motion_notify_event', self._on_move)
  154. fig.canvas.callbacks._connect_picklable(
  155. 'button_press_event', self._button_press)
  156. fig.canvas.callbacks._connect_picklable(
  157. 'button_release_event', self._button_release)
  158. self.set_top_view()
  159. self.patch.set_linewidth(0)
  160. # Calculate the pseudo-data width and height
  161. pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)])
  162. self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0]
  163. # mplot3d currently manages its own spines and needs these turned off
  164. # for bounding box calculations
  165. self.spines[:].set_visible(False)
  166. def set_axis_off(self):
  167. self._axis3don = False
  168. self.stale = True
  169. def set_axis_on(self):
  170. self._axis3don = True
  171. self.stale = True
  172. def convert_zunits(self, z):
  173. """
  174. For artists in an Axes, if the zaxis has units support,
  175. convert *z* using zaxis unit type
  176. """
  177. return self.zaxis.convert_units(z)
  178. def set_top_view(self):
  179. # this happens to be the right view for the viewing coordinates
  180. # moved up and to the left slightly to fit labels and axes
  181. xdwl = 0.95 / self._dist
  182. xdw = 0.9 / self._dist
  183. ydwl = 0.95 / self._dist
  184. ydw = 0.9 / self._dist
  185. # Set the viewing pane.
  186. self.viewLim.intervalx = (-xdwl, xdw)
  187. self.viewLim.intervaly = (-ydwl, ydw)
  188. self.stale = True
  189. def _init_axis(self):
  190. """Init 3D Axes; overrides creation of regular X/Y Axes."""
  191. self.xaxis = axis3d.XAxis(self)
  192. self.yaxis = axis3d.YAxis(self)
  193. self.zaxis = axis3d.ZAxis(self)
  194. def get_zaxis(self):
  195. """Return the ``ZAxis`` (`~.axis3d.Axis`) instance."""
  196. return self.zaxis
  197. get_zgridlines = _axis_method_wrapper("zaxis", "get_gridlines")
  198. get_zticklines = _axis_method_wrapper("zaxis", "get_ticklines")
  199. def _transformed_cube(self, vals):
  200. """Return cube with limits from *vals* transformed by self.M."""
  201. minx, maxx, miny, maxy, minz, maxz = vals
  202. xyzs = [(minx, miny, minz),
  203. (maxx, miny, minz),
  204. (maxx, maxy, minz),
  205. (minx, maxy, minz),
  206. (minx, miny, maxz),
  207. (maxx, miny, maxz),
  208. (maxx, maxy, maxz),
  209. (minx, maxy, maxz)]
  210. return proj3d._proj_points(xyzs, self.M)
  211. def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
  212. """
  213. Set the aspect ratios.
  214. Parameters
  215. ----------
  216. aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'}
  217. Possible values:
  218. ========= ==================================================
  219. value description
  220. ========= ==================================================
  221. 'auto' automatic; fill the position rectangle with data.
  222. 'equal' adapt all the axes to have equal aspect ratios.
  223. 'equalxy' adapt the x and y axes to have equal aspect ratios.
  224. 'equalxz' adapt the x and z axes to have equal aspect ratios.
  225. 'equalyz' adapt the y and z axes to have equal aspect ratios.
  226. ========= ==================================================
  227. adjustable : None or {'box', 'datalim'}, optional
  228. If not *None*, this defines which parameter will be adjusted to
  229. meet the required aspect. See `.set_adjustable` for further
  230. details.
  231. anchor : None or str or 2-tuple of float, optional
  232. If not *None*, this defines where the Axes will be drawn if there
  233. is extra space due to aspect constraints. The most common way to
  234. specify the anchor are abbreviations of cardinal directions:
  235. ===== =====================
  236. value description
  237. ===== =====================
  238. 'C' centered
  239. 'SW' lower left corner
  240. 'S' middle of bottom edge
  241. 'SE' lower right corner
  242. etc.
  243. ===== =====================
  244. See `~.Axes.set_anchor` for further details.
  245. share : bool, default: False
  246. If ``True``, apply the settings to all shared Axes.
  247. See Also
  248. --------
  249. mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
  250. """
  251. _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'),
  252. aspect=aspect)
  253. super().set_aspect(
  254. aspect='auto', adjustable=adjustable, anchor=anchor, share=share)
  255. self._aspect = aspect
  256. if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'):
  257. ax_indices = self._equal_aspect_axis_indices(aspect)
  258. view_intervals = np.array([self.xaxis.get_view_interval(),
  259. self.yaxis.get_view_interval(),
  260. self.zaxis.get_view_interval()])
  261. ptp = np.ptp(view_intervals, axis=1)
  262. if self._adjustable == 'datalim':
  263. mean = np.mean(view_intervals, axis=1)
  264. scale = max(ptp[ax_indices] / self._box_aspect[ax_indices])
  265. deltas = scale * self._box_aspect
  266. for i, set_lim in enumerate((self.set_xlim3d,
  267. self.set_ylim3d,
  268. self.set_zlim3d)):
  269. if i in ax_indices:
  270. set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.,
  271. auto=True, view_margin=None)
  272. else: # 'box'
  273. # Change the box aspect such that the ratio of the length of
  274. # the unmodified axis to the length of the diagonal
  275. # perpendicular to it remains unchanged.
  276. box_aspect = np.array(self._box_aspect)
  277. box_aspect[ax_indices] = ptp[ax_indices]
  278. remaining_ax_indices = {0, 1, 2}.difference(ax_indices)
  279. if remaining_ax_indices:
  280. remaining = remaining_ax_indices.pop()
  281. old_diag = np.linalg.norm(self._box_aspect[ax_indices])
  282. new_diag = np.linalg.norm(box_aspect[ax_indices])
  283. box_aspect[remaining] *= new_diag / old_diag
  284. self.set_box_aspect(box_aspect)
  285. def _equal_aspect_axis_indices(self, aspect):
  286. """
  287. Get the indices for which of the x, y, z axes are constrained to have
  288. equal aspect ratios.
  289. Parameters
  290. ----------
  291. aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'}
  292. See descriptions in docstring for `.set_aspect()`.
  293. """
  294. ax_indices = [] # aspect == 'auto'
  295. if aspect == 'equal':
  296. ax_indices = [0, 1, 2]
  297. elif aspect == 'equalxy':
  298. ax_indices = [0, 1]
  299. elif aspect == 'equalxz':
  300. ax_indices = [0, 2]
  301. elif aspect == 'equalyz':
  302. ax_indices = [1, 2]
  303. return ax_indices
  304. def set_box_aspect(self, aspect, *, zoom=1):
  305. """
  306. Set the Axes box aspect.
  307. The box aspect is the ratio of height to width in display
  308. units for each face of the box when viewed perpendicular to
  309. that face. This is not to be confused with the data aspect (see
  310. `~.Axes3D.set_aspect`). The default ratios are 4:4:3 (x:y:z).
  311. To simulate having equal aspect in data space, set the box
  312. aspect to match your data range in each dimension.
  313. *zoom* controls the overall size of the Axes3D in the figure.
  314. Parameters
  315. ----------
  316. aspect : 3-tuple of floats or None
  317. Changes the physical dimensions of the Axes3D, such that the ratio
  318. of the axis lengths in display units is x:y:z.
  319. If None, defaults to (4, 4, 3).
  320. zoom : float, default: 1
  321. Control overall size of the Axes3D in the figure. Must be > 0.
  322. """
  323. if zoom <= 0:
  324. raise ValueError(f'Argument zoom = {zoom} must be > 0')
  325. if aspect is None:
  326. aspect = np.asarray((4, 4, 3), dtype=float)
  327. else:
  328. aspect = np.asarray(aspect, dtype=float)
  329. _api.check_shape((3,), aspect=aspect)
  330. # The scale 1.8294640721620434 is tuned to match the mpl3.2 appearance.
  331. # The 25/24 factor is to compensate for the change in automargin
  332. # behavior in mpl3.9. This comes from the padding of 1/48 on both sides
  333. # of the axes in mpl3.8.
  334. aspect *= 1.8294640721620434 * 25/24 * zoom / np.linalg.norm(aspect)
  335. self._box_aspect = self._roll_to_vertical(aspect, reverse=True)
  336. self.stale = True
  337. def apply_aspect(self, position=None):
  338. if position is None:
  339. position = self.get_position(original=True)
  340. # in the superclass, we would go through and actually deal with axis
  341. # scales and box/datalim. Those are all irrelevant - all we need to do
  342. # is make sure our coordinate system is square.
  343. trans = self.get_figure().transSubfigure
  344. bb = mtransforms.Bbox.unit().transformed(trans)
  345. # this is the physical aspect of the panel (or figure):
  346. fig_aspect = bb.height / bb.width
  347. box_aspect = 1
  348. pb = position.frozen()
  349. pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
  350. self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
  351. @martist.allow_rasterization
  352. def draw(self, renderer):
  353. if not self.get_visible():
  354. return
  355. self._unstale_viewLim()
  356. # draw the background patch
  357. self.patch.draw(renderer)
  358. self._frameon = False
  359. # first, set the aspect
  360. # this is duplicated from `axes._base._AxesBase.draw`
  361. # but must be called before any of the artist are drawn as
  362. # it adjusts the view limits and the size of the bounding box
  363. # of the Axes
  364. locator = self.get_axes_locator()
  365. self.apply_aspect(locator(self, renderer) if locator else None)
  366. # add the projection matrix to the renderer
  367. self.M = self.get_proj()
  368. self.invM = np.linalg.inv(self.M)
  369. collections_and_patches = (
  370. artist for artist in self._children
  371. if isinstance(artist, (mcoll.Collection, mpatches.Patch))
  372. and artist.get_visible())
  373. if self.computed_zorder:
  374. # Calculate projection of collections and patches and zorder
  375. # them. Make sure they are drawn above the grids.
  376. zorder_offset = max(axis.get_zorder()
  377. for axis in self._axis_map.values()) + 1
  378. collection_zorder = patch_zorder = zorder_offset
  379. for artist in sorted(collections_and_patches,
  380. key=lambda artist: artist.do_3d_projection(),
  381. reverse=True):
  382. if isinstance(artist, mcoll.Collection):
  383. artist.zorder = collection_zorder
  384. collection_zorder += 1
  385. elif isinstance(artist, mpatches.Patch):
  386. artist.zorder = patch_zorder
  387. patch_zorder += 1
  388. else:
  389. for artist in collections_and_patches:
  390. artist.do_3d_projection()
  391. if self._axis3don:
  392. # Draw panes first
  393. for axis in self._axis_map.values():
  394. axis.draw_pane(renderer)
  395. # Then gridlines
  396. for axis in self._axis_map.values():
  397. axis.draw_grid(renderer)
  398. # Then axes, labels, text, and ticks
  399. for axis in self._axis_map.values():
  400. axis.draw(renderer)
  401. # Then rest
  402. super().draw(renderer)
  403. def get_axis_position(self):
  404. tc = self._transformed_cube(self.get_w_lims())
  405. xhigh = tc[1][2] > tc[2][2]
  406. yhigh = tc[3][2] > tc[2][2]
  407. zhigh = tc[0][2] > tc[2][2]
  408. return xhigh, yhigh, zhigh
  409. def update_datalim(self, xys, **kwargs):
  410. """
  411. Not implemented in `~mpl_toolkits.mplot3d.axes3d.Axes3D`.
  412. """
  413. pass
  414. get_autoscalez_on = _axis_method_wrapper("zaxis", "_get_autoscale_on")
  415. set_autoscalez_on = _axis_method_wrapper("zaxis", "_set_autoscale_on")
  416. def get_zmargin(self):
  417. """
  418. Retrieve autoscaling margin of the z-axis.
  419. .. versionadded:: 3.9
  420. Returns
  421. -------
  422. zmargin : float
  423. See Also
  424. --------
  425. mpl_toolkits.mplot3d.axes3d.Axes3D.set_zmargin
  426. """
  427. return self._zmargin
  428. def set_zmargin(self, m):
  429. """
  430. Set padding of Z data limits prior to autoscaling.
  431. *m* times the data interval will be added to each end of that interval
  432. before it is used in autoscaling. If *m* is negative, this will clip
  433. the data range instead of expanding it.
  434. For example, if your data is in the range [0, 2], a margin of 0.1 will
  435. result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range
  436. of [0.2, 1.8].
  437. Parameters
  438. ----------
  439. m : float greater than -0.5
  440. """
  441. if m <= -0.5:
  442. raise ValueError("margin must be greater than -0.5")
  443. self._zmargin = m
  444. self._request_autoscale_view("z")
  445. self.stale = True
  446. def margins(self, *margins, x=None, y=None, z=None, tight=True):
  447. """
  448. Set or retrieve autoscaling margins.
  449. See `.Axes.margins` for full documentation. Because this function
  450. applies to 3D Axes, it also takes a *z* argument, and returns
  451. ``(xmargin, ymargin, zmargin)``.
  452. """
  453. if margins and (x is not None or y is not None or z is not None):
  454. raise TypeError('Cannot pass both positional and keyword '
  455. 'arguments for x, y, and/or z.')
  456. elif len(margins) == 1:
  457. x = y = z = margins[0]
  458. elif len(margins) == 3:
  459. x, y, z = margins
  460. elif margins:
  461. raise TypeError('Must pass a single positional argument for all '
  462. 'margins, or one for each margin (x, y, z).')
  463. if x is None and y is None and z is None:
  464. if tight is not True:
  465. _api.warn_external(f'ignoring tight={tight!r} in get mode')
  466. return self._xmargin, self._ymargin, self._zmargin
  467. if x is not None:
  468. self.set_xmargin(x)
  469. if y is not None:
  470. self.set_ymargin(y)
  471. if z is not None:
  472. self.set_zmargin(z)
  473. self.autoscale_view(
  474. tight=tight, scalex=(x is not None), scaley=(y is not None),
  475. scalez=(z is not None)
  476. )
  477. def autoscale(self, enable=True, axis='both', tight=None):
  478. """
  479. Convenience method for simple axis view autoscaling.
  480. See `.Axes.autoscale` for full documentation. Because this function
  481. applies to 3D Axes, *axis* can also be set to 'z', and setting *axis*
  482. to 'both' autoscales all three axes.
  483. """
  484. if enable is None:
  485. scalex = True
  486. scaley = True
  487. scalez = True
  488. else:
  489. if axis in ['x', 'both']:
  490. self.set_autoscalex_on(enable)
  491. scalex = self.get_autoscalex_on()
  492. else:
  493. scalex = False
  494. if axis in ['y', 'both']:
  495. self.set_autoscaley_on(enable)
  496. scaley = self.get_autoscaley_on()
  497. else:
  498. scaley = False
  499. if axis in ['z', 'both']:
  500. self.set_autoscalez_on(enable)
  501. scalez = self.get_autoscalez_on()
  502. else:
  503. scalez = False
  504. if scalex:
  505. self._request_autoscale_view("x", tight=tight)
  506. if scaley:
  507. self._request_autoscale_view("y", tight=tight)
  508. if scalez:
  509. self._request_autoscale_view("z", tight=tight)
  510. def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
  511. # This updates the bounding boxes as to keep a record as to what the
  512. # minimum sized rectangular volume holds the data.
  513. if np.shape(X) == np.shape(Y):
  514. self.xy_dataLim.update_from_data_xy(
  515. np.column_stack([np.ravel(X), np.ravel(Y)]), not had_data)
  516. else:
  517. self.xy_dataLim.update_from_data_x(X, not had_data)
  518. self.xy_dataLim.update_from_data_y(Y, not had_data)
  519. if Z is not None:
  520. self.zz_dataLim.update_from_data_x(Z, not had_data)
  521. # Let autoscale_view figure out how to use this data.
  522. self.autoscale_view()
  523. def autoscale_view(self, tight=None,
  524. scalex=True, scaley=True, scalez=True):
  525. """
  526. Autoscale the view limits using the data limits.
  527. See `.Axes.autoscale_view` for full documentation. Because this
  528. function applies to 3D Axes, it also takes a *scalez* argument.
  529. """
  530. # This method looks at the rectangular volume (see above)
  531. # of data and decides how to scale the view portal to fit it.
  532. if tight is None:
  533. _tight = self._tight
  534. if not _tight:
  535. # if image data only just use the datalim
  536. for artist in self._children:
  537. if isinstance(artist, mimage.AxesImage):
  538. _tight = True
  539. elif isinstance(artist, (mlines.Line2D, mpatches.Patch)):
  540. _tight = False
  541. break
  542. else:
  543. _tight = self._tight = bool(tight)
  544. if scalex and self.get_autoscalex_on():
  545. x0, x1 = self.xy_dataLim.intervalx
  546. xlocator = self.xaxis.get_major_locator()
  547. x0, x1 = xlocator.nonsingular(x0, x1)
  548. if self._xmargin > 0:
  549. delta = (x1 - x0) * self._xmargin
  550. x0 -= delta
  551. x1 += delta
  552. if not _tight:
  553. x0, x1 = xlocator.view_limits(x0, x1)
  554. self.set_xbound(x0, x1, self._view_margin)
  555. if scaley and self.get_autoscaley_on():
  556. y0, y1 = self.xy_dataLim.intervaly
  557. ylocator = self.yaxis.get_major_locator()
  558. y0, y1 = ylocator.nonsingular(y0, y1)
  559. if self._ymargin > 0:
  560. delta = (y1 - y0) * self._ymargin
  561. y0 -= delta
  562. y1 += delta
  563. if not _tight:
  564. y0, y1 = ylocator.view_limits(y0, y1)
  565. self.set_ybound(y0, y1, self._view_margin)
  566. if scalez and self.get_autoscalez_on():
  567. z0, z1 = self.zz_dataLim.intervalx
  568. zlocator = self.zaxis.get_major_locator()
  569. z0, z1 = zlocator.nonsingular(z0, z1)
  570. if self._zmargin > 0:
  571. delta = (z1 - z0) * self._zmargin
  572. z0 -= delta
  573. z1 += delta
  574. if not _tight:
  575. z0, z1 = zlocator.view_limits(z0, z1)
  576. self.set_zbound(z0, z1, self._view_margin)
  577. def get_w_lims(self):
  578. """Get 3D world limits."""
  579. minx, maxx = self.get_xlim3d()
  580. miny, maxy = self.get_ylim3d()
  581. minz, maxz = self.get_zlim3d()
  582. return minx, maxx, miny, maxy, minz, maxz
  583. def _set_bound3d(self, get_bound, set_lim, axis_inverted,
  584. lower=None, upper=None, view_margin=None):
  585. """
  586. Set 3D axis bounds.
  587. """
  588. if upper is None and np.iterable(lower):
  589. lower, upper = lower
  590. old_lower, old_upper = get_bound()
  591. if lower is None:
  592. lower = old_lower
  593. if upper is None:
  594. upper = old_upper
  595. set_lim(sorted((lower, upper), reverse=bool(axis_inverted())),
  596. auto=None, view_margin=view_margin)
  597. def set_xbound(self, lower=None, upper=None, view_margin=None):
  598. """
  599. Set the lower and upper numerical bounds of the x-axis.
  600. This method will honor axis inversion regardless of parameter order.
  601. It will not change the autoscaling setting (`.get_autoscalex_on()`).
  602. Parameters
  603. ----------
  604. lower, upper : float or None
  605. The lower and upper bounds. If *None*, the respective axis bound
  606. is not modified.
  607. view_margin : float or None
  608. The margin to apply to the bounds. If *None*, the margin is handled
  609. by `.set_xlim`.
  610. See Also
  611. --------
  612. get_xbound
  613. get_xlim, set_xlim
  614. invert_xaxis, xaxis_inverted
  615. """
  616. self._set_bound3d(self.get_xbound, self.set_xlim, self.xaxis_inverted,
  617. lower, upper, view_margin)
  618. def set_ybound(self, lower=None, upper=None, view_margin=None):
  619. """
  620. Set the lower and upper numerical bounds of the y-axis.
  621. This method will honor axis inversion regardless of parameter order.
  622. It will not change the autoscaling setting (`.get_autoscaley_on()`).
  623. Parameters
  624. ----------
  625. lower, upper : float or None
  626. The lower and upper bounds. If *None*, the respective axis bound
  627. is not modified.
  628. view_margin : float or None
  629. The margin to apply to the bounds. If *None*, the margin is handled
  630. by `.set_ylim`.
  631. See Also
  632. --------
  633. get_ybound
  634. get_ylim, set_ylim
  635. invert_yaxis, yaxis_inverted
  636. """
  637. self._set_bound3d(self.get_ybound, self.set_ylim, self.yaxis_inverted,
  638. lower, upper, view_margin)
  639. def set_zbound(self, lower=None, upper=None, view_margin=None):
  640. """
  641. Set the lower and upper numerical bounds of the z-axis.
  642. This method will honor axis inversion regardless of parameter order.
  643. It will not change the autoscaling setting (`.get_autoscaley_on()`).
  644. Parameters
  645. ----------
  646. lower, upper : float or None
  647. The lower and upper bounds. If *None*, the respective axis bound
  648. is not modified.
  649. view_margin : float or None
  650. The margin to apply to the bounds. If *None*, the margin is handled
  651. by `.set_zlim`.
  652. See Also
  653. --------
  654. get_zbound
  655. get_zlim, set_zlim
  656. invert_zaxis, zaxis_inverted
  657. """
  658. self._set_bound3d(self.get_zbound, self.set_zlim, self.zaxis_inverted,
  659. lower, upper, view_margin)
  660. def _set_lim3d(self, axis, lower=None, upper=None, *, emit=True,
  661. auto=False, view_margin=None, axmin=None, axmax=None):
  662. """
  663. Set 3D axis limits.
  664. """
  665. if upper is None:
  666. if np.iterable(lower):
  667. lower, upper = lower
  668. elif axmax is None:
  669. upper = axis.get_view_interval()[1]
  670. if lower is None and axmin is None:
  671. lower = axis.get_view_interval()[0]
  672. if axmin is not None:
  673. if lower is not None:
  674. raise TypeError("Cannot pass both 'lower' and 'min'")
  675. lower = axmin
  676. if axmax is not None:
  677. if upper is not None:
  678. raise TypeError("Cannot pass both 'upper' and 'max'")
  679. upper = axmax
  680. if np.isinf(lower) or np.isinf(upper):
  681. raise ValueError(f"Axis limits {lower}, {upper} cannot be infinite")
  682. if view_margin is None:
  683. if mpl.rcParams['axes3d.automargin']:
  684. view_margin = self._view_margin
  685. else:
  686. view_margin = 0
  687. delta = (upper - lower) * view_margin
  688. lower -= delta
  689. upper += delta
  690. return axis._set_lim(lower, upper, emit=emit, auto=auto)
  691. def set_xlim(self, left=None, right=None, *, emit=True, auto=False,
  692. view_margin=None, xmin=None, xmax=None):
  693. """
  694. Set the 3D x-axis view limits.
  695. Parameters
  696. ----------
  697. left : float, optional
  698. The left xlim in data coordinates. Passing *None* leaves the
  699. limit unchanged.
  700. The left and right xlims may also be passed as the tuple
  701. (*left*, *right*) as the first positional argument (or as
  702. the *left* keyword argument).
  703. .. ACCEPTS: (left: float, right: float)
  704. right : float, optional
  705. The right xlim in data coordinates. Passing *None* leaves the
  706. limit unchanged.
  707. emit : bool, default: True
  708. Whether to notify observers of limit change.
  709. auto : bool or None, default: False
  710. Whether to turn on autoscaling of the x-axis. *True* turns on,
  711. *False* turns off, *None* leaves unchanged.
  712. view_margin : float, optional
  713. The additional margin to apply to the limits.
  714. xmin, xmax : float, optional
  715. They are equivalent to left and right respectively, and it is an
  716. error to pass both *xmin* and *left* or *xmax* and *right*.
  717. Returns
  718. -------
  719. left, right : (float, float)
  720. The new x-axis limits in data coordinates.
  721. See Also
  722. --------
  723. get_xlim
  724. set_xbound, get_xbound
  725. invert_xaxis, xaxis_inverted
  726. Notes
  727. -----
  728. The *left* value may be greater than the *right* value, in which
  729. case the x-axis values will decrease from *left* to *right*.
  730. Examples
  731. --------
  732. >>> set_xlim(left, right)
  733. >>> set_xlim((left, right))
  734. >>> left, right = set_xlim(left, right)
  735. One limit may be left unchanged.
  736. >>> set_xlim(right=right_lim)
  737. Limits may be passed in reverse order to flip the direction of
  738. the x-axis. For example, suppose ``x`` represents depth of the
  739. ocean in m. The x-axis limits might be set like the following
  740. so 5000 m depth is at the left of the plot and the surface,
  741. 0 m, is at the right.
  742. >>> set_xlim(5000, 0)
  743. """
  744. return self._set_lim3d(self.xaxis, left, right, emit=emit, auto=auto,
  745. view_margin=view_margin, axmin=xmin, axmax=xmax)
  746. def set_ylim(self, bottom=None, top=None, *, emit=True, auto=False,
  747. view_margin=None, ymin=None, ymax=None):
  748. """
  749. Set the 3D y-axis view limits.
  750. Parameters
  751. ----------
  752. bottom : float, optional
  753. The bottom ylim in data coordinates. Passing *None* leaves the
  754. limit unchanged.
  755. The bottom and top ylims may also be passed as the tuple
  756. (*bottom*, *top*) as the first positional argument (or as
  757. the *bottom* keyword argument).
  758. .. ACCEPTS: (bottom: float, top: float)
  759. top : float, optional
  760. The top ylim in data coordinates. Passing *None* leaves the
  761. limit unchanged.
  762. emit : bool, default: True
  763. Whether to notify observers of limit change.
  764. auto : bool or None, default: False
  765. Whether to turn on autoscaling of the y-axis. *True* turns on,
  766. *False* turns off, *None* leaves unchanged.
  767. view_margin : float, optional
  768. The additional margin to apply to the limits.
  769. ymin, ymax : float, optional
  770. They are equivalent to bottom and top respectively, and it is an
  771. error to pass both *ymin* and *bottom* or *ymax* and *top*.
  772. Returns
  773. -------
  774. bottom, top : (float, float)
  775. The new y-axis limits in data coordinates.
  776. See Also
  777. --------
  778. get_ylim
  779. set_ybound, get_ybound
  780. invert_yaxis, yaxis_inverted
  781. Notes
  782. -----
  783. The *bottom* value may be greater than the *top* value, in which
  784. case the y-axis values will decrease from *bottom* to *top*.
  785. Examples
  786. --------
  787. >>> set_ylim(bottom, top)
  788. >>> set_ylim((bottom, top))
  789. >>> bottom, top = set_ylim(bottom, top)
  790. One limit may be left unchanged.
  791. >>> set_ylim(top=top_lim)
  792. Limits may be passed in reverse order to flip the direction of
  793. the y-axis. For example, suppose ``y`` represents depth of the
  794. ocean in m. The y-axis limits might be set like the following
  795. so 5000 m depth is at the bottom of the plot and the surface,
  796. 0 m, is at the top.
  797. >>> set_ylim(5000, 0)
  798. """
  799. return self._set_lim3d(self.yaxis, bottom, top, emit=emit, auto=auto,
  800. view_margin=view_margin, axmin=ymin, axmax=ymax)
  801. def set_zlim(self, bottom=None, top=None, *, emit=True, auto=False,
  802. view_margin=None, zmin=None, zmax=None):
  803. """
  804. Set the 3D z-axis view limits.
  805. Parameters
  806. ----------
  807. bottom : float, optional
  808. The bottom zlim in data coordinates. Passing *None* leaves the
  809. limit unchanged.
  810. The bottom and top zlims may also be passed as the tuple
  811. (*bottom*, *top*) as the first positional argument (or as
  812. the *bottom* keyword argument).
  813. .. ACCEPTS: (bottom: float, top: float)
  814. top : float, optional
  815. The top zlim in data coordinates. Passing *None* leaves the
  816. limit unchanged.
  817. emit : bool, default: True
  818. Whether to notify observers of limit change.
  819. auto : bool or None, default: False
  820. Whether to turn on autoscaling of the z-axis. *True* turns on,
  821. *False* turns off, *None* leaves unchanged.
  822. view_margin : float, optional
  823. The additional margin to apply to the limits.
  824. zmin, zmax : float, optional
  825. They are equivalent to bottom and top respectively, and it is an
  826. error to pass both *zmin* and *bottom* or *zmax* and *top*.
  827. Returns
  828. -------
  829. bottom, top : (float, float)
  830. The new z-axis limits in data coordinates.
  831. See Also
  832. --------
  833. get_zlim
  834. set_zbound, get_zbound
  835. invert_zaxis, zaxis_inverted
  836. Notes
  837. -----
  838. The *bottom* value may be greater than the *top* value, in which
  839. case the z-axis values will decrease from *bottom* to *top*.
  840. Examples
  841. --------
  842. >>> set_zlim(bottom, top)
  843. >>> set_zlim((bottom, top))
  844. >>> bottom, top = set_zlim(bottom, top)
  845. One limit may be left unchanged.
  846. >>> set_zlim(top=top_lim)
  847. Limits may be passed in reverse order to flip the direction of
  848. the z-axis. For example, suppose ``z`` represents depth of the
  849. ocean in m. The z-axis limits might be set like the following
  850. so 5000 m depth is at the bottom of the plot and the surface,
  851. 0 m, is at the top.
  852. >>> set_zlim(5000, 0)
  853. """
  854. return self._set_lim3d(self.zaxis, bottom, top, emit=emit, auto=auto,
  855. view_margin=view_margin, axmin=zmin, axmax=zmax)
  856. set_xlim3d = set_xlim
  857. set_ylim3d = set_ylim
  858. set_zlim3d = set_zlim
  859. def get_xlim(self):
  860. # docstring inherited
  861. return tuple(self.xy_viewLim.intervalx)
  862. def get_ylim(self):
  863. # docstring inherited
  864. return tuple(self.xy_viewLim.intervaly)
  865. def get_zlim(self):
  866. """
  867. Return the 3D z-axis view limits.
  868. Returns
  869. -------
  870. left, right : (float, float)
  871. The current z-axis limits in data coordinates.
  872. See Also
  873. --------
  874. set_zlim
  875. set_zbound, get_zbound
  876. invert_zaxis, zaxis_inverted
  877. Notes
  878. -----
  879. The z-axis may be inverted, in which case the *left* value will
  880. be greater than the *right* value.
  881. """
  882. return tuple(self.zz_viewLim.intervalx)
  883. get_zscale = _axis_method_wrapper("zaxis", "get_scale")
  884. # Redefine all three methods to overwrite their docstrings.
  885. set_xscale = _axis_method_wrapper("xaxis", "_set_axes_scale")
  886. set_yscale = _axis_method_wrapper("yaxis", "_set_axes_scale")
  887. set_zscale = _axis_method_wrapper("zaxis", "_set_axes_scale")
  888. set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map(
  889. """
  890. Set the {}-axis scale.
  891. Parameters
  892. ----------
  893. value : {{"linear"}}
  894. The axis scale type to apply. 3D Axes currently only support
  895. linear scales; other scales yield nonsensical results.
  896. **kwargs
  897. Keyword arguments are nominally forwarded to the scale class, but
  898. none of them is applicable for linear scales.
  899. """.format,
  900. ["x", "y", "z"])
  901. get_zticks = _axis_method_wrapper("zaxis", "get_ticklocs")
  902. set_zticks = _axis_method_wrapper("zaxis", "set_ticks")
  903. get_zmajorticklabels = _axis_method_wrapper("zaxis", "get_majorticklabels")
  904. get_zminorticklabels = _axis_method_wrapper("zaxis", "get_minorticklabels")
  905. get_zticklabels = _axis_method_wrapper("zaxis", "get_ticklabels")
  906. set_zticklabels = _axis_method_wrapper(
  907. "zaxis", "set_ticklabels",
  908. doc_sub={"Axis.set_ticks": "Axes3D.set_zticks"})
  909. zaxis_date = _axis_method_wrapper("zaxis", "axis_date")
  910. if zaxis_date.__doc__:
  911. zaxis_date.__doc__ += textwrap.dedent("""
  912. Notes
  913. -----
  914. This function is merely provided for completeness, but 3D Axes do not
  915. support dates for ticks, and so this may not work as expected.
  916. """)
  917. def clabel(self, *args, **kwargs):
  918. """Currently not implemented for 3D Axes, and returns *None*."""
  919. return None
  920. def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z",
  921. share=False):
  922. """
  923. Set the elevation and azimuth of the Axes in degrees (not radians).
  924. This can be used to rotate the Axes programmatically.
  925. To look normal to the primary planes, the following elevation and
  926. azimuth angles can be used. A roll angle of 0, 90, 180, or 270 deg
  927. will rotate these views while keeping the axes at right angles.
  928. ========== ==== ====
  929. view plane elev azim
  930. ========== ==== ====
  931. XY 90 -90
  932. XZ 0 -90
  933. YZ 0 0
  934. -XY -90 90
  935. -XZ 0 90
  936. -YZ 0 180
  937. ========== ==== ====
  938. Parameters
  939. ----------
  940. elev : float, default: None
  941. The elevation angle in degrees rotates the camera above the plane
  942. pierced by the vertical axis, with a positive angle corresponding
  943. to a location above that plane. For example, with the default
  944. vertical axis of 'z', the elevation defines the angle of the camera
  945. location above the x-y plane.
  946. If None, then the initial value as specified in the `Axes3D`
  947. constructor is used.
  948. azim : float, default: None
  949. The azimuthal angle in degrees rotates the camera about the
  950. vertical axis, with a positive angle corresponding to a
  951. right-handed rotation. For example, with the default vertical axis
  952. of 'z', a positive azimuth rotates the camera about the origin from
  953. its location along the +x axis towards the +y axis.
  954. If None, then the initial value as specified in the `Axes3D`
  955. constructor is used.
  956. roll : float, default: None
  957. The roll angle in degrees rotates the camera about the viewing
  958. axis. A positive angle spins the camera clockwise, causing the
  959. scene to rotate counter-clockwise.
  960. If None, then the initial value as specified in the `Axes3D`
  961. constructor is used.
  962. vertical_axis : {"z", "x", "y"}, default: "z"
  963. The axis to align vertically. *azim* rotates about this axis.
  964. share : bool, default: False
  965. If ``True``, apply the settings to all Axes with shared views.
  966. """
  967. self._dist = 10 # The camera distance from origin. Behaves like zoom
  968. if elev is None:
  969. elev = self.initial_elev
  970. if azim is None:
  971. azim = self.initial_azim
  972. if roll is None:
  973. roll = self.initial_roll
  974. vertical_axis = _api.check_getitem(
  975. {name: idx for idx, name in enumerate(self._axis_names)},
  976. vertical_axis=vertical_axis,
  977. )
  978. if share:
  979. axes = {sibling for sibling
  980. in self._shared_axes['view'].get_siblings(self)}
  981. else:
  982. axes = [self]
  983. for ax in axes:
  984. ax.elev = elev
  985. ax.azim = azim
  986. ax.roll = roll
  987. ax._vertical_axis = vertical_axis
  988. def set_proj_type(self, proj_type, focal_length=None):
  989. """
  990. Set the projection type.
  991. Parameters
  992. ----------
  993. proj_type : {'persp', 'ortho'}
  994. The projection type.
  995. focal_length : float, default: None
  996. For a projection type of 'persp', the focal length of the virtual
  997. camera. Must be > 0. If None, defaults to 1.
  998. The focal length can be computed from a desired Field Of View via
  999. the equation: focal_length = 1/tan(FOV/2)
  1000. """
  1001. _api.check_in_list(['persp', 'ortho'], proj_type=proj_type)
  1002. if proj_type == 'persp':
  1003. if focal_length is None:
  1004. focal_length = 1
  1005. elif focal_length <= 0:
  1006. raise ValueError(f"focal_length = {focal_length} must be "
  1007. "greater than 0")
  1008. self._focal_length = focal_length
  1009. else: # 'ortho':
  1010. if focal_length not in (None, np.inf):
  1011. raise ValueError(f"focal_length = {focal_length} must be "
  1012. f"None for proj_type = {proj_type}")
  1013. self._focal_length = np.inf
  1014. def _roll_to_vertical(
  1015. self, arr: "np.typing.ArrayLike", reverse: bool = False
  1016. ) -> np.ndarray:
  1017. """
  1018. Roll arrays to match the different vertical axis.
  1019. Parameters
  1020. ----------
  1021. arr : ArrayLike
  1022. Array to roll.
  1023. reverse : bool, default: False
  1024. Reverse the direction of the roll.
  1025. """
  1026. if reverse:
  1027. return np.roll(arr, (self._vertical_axis - 2) * -1)
  1028. else:
  1029. return np.roll(arr, (self._vertical_axis - 2))
  1030. def get_proj(self):
  1031. """Create the projection matrix from the current viewing position."""
  1032. # Transform to uniform world coordinates 0-1, 0-1, 0-1
  1033. box_aspect = self._roll_to_vertical(self._box_aspect)
  1034. worldM = proj3d.world_transformation(
  1035. *self.get_xlim3d(),
  1036. *self.get_ylim3d(),
  1037. *self.get_zlim3d(),
  1038. pb_aspect=box_aspect,
  1039. )
  1040. # Look into the middle of the world coordinates:
  1041. R = 0.5 * box_aspect
  1042. # elev: elevation angle in the z plane.
  1043. # azim: azimuth angle in the xy plane.
  1044. # Coordinates for a point that rotates around the box of data.
  1045. # p0, p1 corresponds to rotating the box only around the vertical axis.
  1046. # p2 corresponds to rotating the box only around the horizontal axis.
  1047. elev_rad = np.deg2rad(self.elev)
  1048. azim_rad = np.deg2rad(self.azim)
  1049. p0 = np.cos(elev_rad) * np.cos(azim_rad)
  1050. p1 = np.cos(elev_rad) * np.sin(azim_rad)
  1051. p2 = np.sin(elev_rad)
  1052. # When changing vertical axis the coordinates changes as well.
  1053. # Roll the values to get the same behaviour as the default:
  1054. ps = self._roll_to_vertical([p0, p1, p2])
  1055. # The coordinates for the eye viewing point. The eye is looking
  1056. # towards the middle of the box of data from a distance:
  1057. eye = R + self._dist * ps
  1058. # Calculate the viewing axes for the eye position
  1059. u, v, w = self._calc_view_axes(eye)
  1060. self._view_u = u # _view_u is towards the right of the screen
  1061. self._view_v = v # _view_v is towards the top of the screen
  1062. self._view_w = w # _view_w is out of the screen
  1063. # Generate the view and projection transformation matrices
  1064. if self._focal_length == np.inf:
  1065. # Orthographic projection
  1066. viewM = proj3d._view_transformation_uvw(u, v, w, eye)
  1067. projM = proj3d._ortho_transformation(-self._dist, self._dist)
  1068. else:
  1069. # Perspective projection
  1070. # Scale the eye dist to compensate for the focal length zoom effect
  1071. eye_focal = R + self._dist * ps * self._focal_length
  1072. viewM = proj3d._view_transformation_uvw(u, v, w, eye_focal)
  1073. projM = proj3d._persp_transformation(-self._dist,
  1074. self._dist,
  1075. self._focal_length)
  1076. # Combine all the transformation matrices to get the final projection
  1077. M0 = np.dot(viewM, worldM)
  1078. M = np.dot(projM, M0)
  1079. return M
  1080. def mouse_init(self, rotate_btn=1, pan_btn=2, zoom_btn=3):
  1081. """
  1082. Set the mouse buttons for 3D rotation and zooming.
  1083. Parameters
  1084. ----------
  1085. rotate_btn : int or list of int, default: 1
  1086. The mouse button or buttons to use for 3D rotation of the Axes.
  1087. pan_btn : int or list of int, default: 2
  1088. The mouse button or buttons to use to pan the 3D Axes.
  1089. zoom_btn : int or list of int, default: 3
  1090. The mouse button or buttons to use to zoom the 3D Axes.
  1091. """
  1092. self.button_pressed = None
  1093. # coerce scalars into array-like, then convert into
  1094. # a regular list to avoid comparisons against None
  1095. # which breaks in recent versions of numpy.
  1096. self._rotate_btn = np.atleast_1d(rotate_btn).tolist()
  1097. self._pan_btn = np.atleast_1d(pan_btn).tolist()
  1098. self._zoom_btn = np.atleast_1d(zoom_btn).tolist()
  1099. def disable_mouse_rotation(self):
  1100. """Disable mouse buttons for 3D rotation, panning, and zooming."""
  1101. self.mouse_init(rotate_btn=[], pan_btn=[], zoom_btn=[])
  1102. def can_zoom(self):
  1103. # doc-string inherited
  1104. return True
  1105. def can_pan(self):
  1106. # doc-string inherited
  1107. return True
  1108. def sharez(self, other):
  1109. """
  1110. Share the z-axis with *other*.
  1111. This is equivalent to passing ``sharez=other`` when constructing the
  1112. Axes, and cannot be used if the z-axis is already being shared with
  1113. another Axes. Note that it is not possible to unshare axes.
  1114. """
  1115. _api.check_isinstance(Axes3D, other=other)
  1116. if self._sharez is not None and other is not self._sharez:
  1117. raise ValueError("z-axis is already shared")
  1118. self._shared_axes["z"].join(self, other)
  1119. self._sharez = other
  1120. self.zaxis.major = other.zaxis.major # Ticker instances holding
  1121. self.zaxis.minor = other.zaxis.minor # locator and formatter.
  1122. z0, z1 = other.get_zlim()
  1123. self.set_zlim(z0, z1, emit=False, auto=other.get_autoscalez_on())
  1124. self.zaxis._scale = other.zaxis._scale
  1125. def shareview(self, other):
  1126. """
  1127. Share the view angles with *other*.
  1128. This is equivalent to passing ``shareview=other`` when constructing the
  1129. Axes, and cannot be used if the view angles are already being shared
  1130. with another Axes. Note that it is not possible to unshare axes.
  1131. """
  1132. _api.check_isinstance(Axes3D, other=other)
  1133. if self._shareview is not None and other is not self._shareview:
  1134. raise ValueError("view angles are already shared")
  1135. self._shared_axes["view"].join(self, other)
  1136. self._shareview = other
  1137. vertical_axis = self._axis_names[other._vertical_axis]
  1138. self.view_init(elev=other.elev, azim=other.azim, roll=other.roll,
  1139. vertical_axis=vertical_axis, share=True)
  1140. def clear(self):
  1141. # docstring inherited.
  1142. super().clear()
  1143. if self._focal_length == np.inf:
  1144. self._zmargin = mpl.rcParams['axes.zmargin']
  1145. else:
  1146. self._zmargin = 0.
  1147. xymargin = 0.05 * 10/11 # match mpl3.8 appearance
  1148. self.xy_dataLim = Bbox([[xymargin, xymargin],
  1149. [1 - xymargin, 1 - xymargin]])
  1150. # z-limits are encoded in the x-component of the Bbox, y is un-used
  1151. self.zz_dataLim = Bbox.unit()
  1152. self._view_margin = 1/48 # default value to match mpl3.8
  1153. self.autoscale_view()
  1154. self.grid(mpl.rcParams['axes3d.grid'])
  1155. def _button_press(self, event):
  1156. if event.inaxes == self:
  1157. self.button_pressed = event.button
  1158. self._sx, self._sy = event.xdata, event.ydata
  1159. toolbar = self.get_figure(root=True).canvas.toolbar
  1160. if toolbar and toolbar._nav_stack() is None:
  1161. toolbar.push_current()
  1162. if toolbar:
  1163. toolbar.set_message(toolbar._mouse_event_to_message(event))
  1164. def _button_release(self, event):
  1165. self.button_pressed = None
  1166. toolbar = self.get_figure(root=True).canvas.toolbar
  1167. # backend_bases.release_zoom and backend_bases.release_pan call
  1168. # push_current, so check the navigation mode so we don't call it twice
  1169. if toolbar and self.get_navigate_mode() is None:
  1170. toolbar.push_current()
  1171. if toolbar:
  1172. toolbar.set_message(toolbar._mouse_event_to_message(event))
  1173. def _get_view(self):
  1174. # docstring inherited
  1175. return {
  1176. "xlim": self.get_xlim(), "autoscalex_on": self.get_autoscalex_on(),
  1177. "ylim": self.get_ylim(), "autoscaley_on": self.get_autoscaley_on(),
  1178. "zlim": self.get_zlim(), "autoscalez_on": self.get_autoscalez_on(),
  1179. }, (self.elev, self.azim, self.roll)
  1180. def _set_view(self, view):
  1181. # docstring inherited
  1182. props, (elev, azim, roll) = view
  1183. self.set(**props)
  1184. self.elev = elev
  1185. self.azim = azim
  1186. self.roll = roll
  1187. def format_zdata(self, z):
  1188. """
  1189. Return *z* string formatted. This function will use the
  1190. :attr:`fmt_zdata` attribute if it is callable, else will fall
  1191. back on the zaxis major formatter
  1192. """
  1193. try:
  1194. return self.fmt_zdata(z)
  1195. except (AttributeError, TypeError):
  1196. func = self.zaxis.get_major_formatter().format_data_short
  1197. val = func(z)
  1198. return val
  1199. def format_coord(self, xv, yv, renderer=None):
  1200. """
  1201. Return a string giving the current view rotation angles, or the x, y, z
  1202. coordinates of the point on the nearest axis pane underneath the mouse
  1203. cursor, depending on the mouse button pressed.
  1204. """
  1205. coords = ''
  1206. if self.button_pressed in self._rotate_btn:
  1207. # ignore xv and yv and display angles instead
  1208. coords = self._rotation_coords()
  1209. elif self.M is not None:
  1210. coords = self._location_coords(xv, yv, renderer)
  1211. return coords
  1212. def _rotation_coords(self):
  1213. """
  1214. Return the rotation angles as a string.
  1215. """
  1216. norm_elev = art3d._norm_angle(self.elev)
  1217. norm_azim = art3d._norm_angle(self.azim)
  1218. norm_roll = art3d._norm_angle(self.roll)
  1219. coords = (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, "
  1220. f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, "
  1221. f"roll={norm_roll:.0f}\N{DEGREE SIGN}"
  1222. ).replace("-", "\N{MINUS SIGN}")
  1223. return coords
  1224. def _location_coords(self, xv, yv, renderer):
  1225. """
  1226. Return the location on the axis pane underneath the cursor as a string.
  1227. """
  1228. p1, pane_idx = self._calc_coord(xv, yv, renderer)
  1229. xs = self.format_xdata(p1[0])
  1230. ys = self.format_ydata(p1[1])
  1231. zs = self.format_zdata(p1[2])
  1232. if pane_idx == 0:
  1233. coords = f'x pane={xs}, y={ys}, z={zs}'
  1234. elif pane_idx == 1:
  1235. coords = f'x={xs}, y pane={ys}, z={zs}'
  1236. elif pane_idx == 2:
  1237. coords = f'x={xs}, y={ys}, z pane={zs}'
  1238. return coords
  1239. def _get_camera_loc(self):
  1240. """
  1241. Returns the current camera location in data coordinates.
  1242. """
  1243. cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
  1244. c = np.array([cx, cy, cz])
  1245. r = np.array([dx, dy, dz])
  1246. if self._focal_length == np.inf: # orthographic projection
  1247. focal_length = 1e9 # large enough to be effectively infinite
  1248. else: # perspective projection
  1249. focal_length = self._focal_length
  1250. eye = c + self._view_w * self._dist * r / self._box_aspect * focal_length
  1251. return eye
  1252. def _calc_coord(self, xv, yv, renderer=None):
  1253. """
  1254. Given the 2D view coordinates, find the point on the nearest axis pane
  1255. that lies directly below those coordinates. Returns a 3D point in data
  1256. coordinates.
  1257. """
  1258. if self._focal_length == np.inf: # orthographic projection
  1259. zv = 1
  1260. else: # perspective projection
  1261. zv = -1 / self._focal_length
  1262. # Convert point on view plane to data coordinates
  1263. p1 = np.array(proj3d.inv_transform(xv, yv, zv, self.invM)).ravel()
  1264. # Get the vector from the camera to the point on the view plane
  1265. vec = self._get_camera_loc() - p1
  1266. # Get the pane locations for each of the axes
  1267. pane_locs = []
  1268. for axis in self._axis_map.values():
  1269. xys, loc = axis.active_pane()
  1270. pane_locs.append(loc)
  1271. # Find the distance to the nearest pane by projecting the view vector
  1272. scales = np.zeros(3)
  1273. for i in range(3):
  1274. if vec[i] == 0:
  1275. scales[i] = np.inf
  1276. else:
  1277. scales[i] = (p1[i] - pane_locs[i]) / vec[i]
  1278. pane_idx = np.argmin(abs(scales))
  1279. scale = scales[pane_idx]
  1280. # Calculate the point on the closest pane
  1281. p2 = p1 - scale*vec
  1282. return p2, pane_idx
  1283. def _arcball(self, x: float, y: float) -> np.ndarray:
  1284. """
  1285. Convert a point (x, y) to a point on a virtual trackball.
  1286. This is Ken Shoemake's arcball (a sphere), modified
  1287. to soften the abrupt edge (optionally).
  1288. See: Ken Shoemake, "ARCBALL: A user interface for specifying
  1289. three-dimensional rotation using a mouse." in
  1290. Proceedings of Graphics Interface '92, 1992, pp. 151-156,
  1291. https://doi.org/10.20380/GI1992.18
  1292. The smoothing of the edge is inspired by Gavin Bell's arcball
  1293. (a sphere combined with a hyperbola), but here, the sphere
  1294. is combined with a section of a cylinder, so it has finite support.
  1295. """
  1296. s = mpl.rcParams['axes3d.trackballsize'] / 2
  1297. b = mpl.rcParams['axes3d.trackballborder'] / s
  1298. x /= s
  1299. y /= s
  1300. r2 = x*x + y*y
  1301. r = np.sqrt(r2)
  1302. ra = 1 + b
  1303. a = b * (1 + b/2)
  1304. ri = 2/(ra + 1/ra)
  1305. if r < ri:
  1306. p = np.array([np.sqrt(1 - r2), x, y])
  1307. elif r < ra:
  1308. dr = ra - r
  1309. p = np.array([a - np.sqrt((a + dr) * (a - dr)), x, y])
  1310. p /= np.linalg.norm(p)
  1311. else:
  1312. p = np.array([0, x/r, y/r])
  1313. return p
  1314. def _on_move(self, event):
  1315. """
  1316. Mouse moving.
  1317. By default, button-1 rotates, button-2 pans, and button-3 zooms;
  1318. these buttons can be modified via `mouse_init`.
  1319. """
  1320. if not self.button_pressed:
  1321. return
  1322. if self.get_navigate_mode() is not None:
  1323. # we don't want to rotate if we are zooming/panning
  1324. # from the toolbar
  1325. return
  1326. if self.M is None:
  1327. return
  1328. x, y = event.xdata, event.ydata
  1329. # In case the mouse is out of bounds.
  1330. if x is None or event.inaxes != self:
  1331. return
  1332. dx, dy = x - self._sx, y - self._sy
  1333. w = self._pseudo_w
  1334. h = self._pseudo_h
  1335. # Rotation
  1336. if self.button_pressed in self._rotate_btn:
  1337. # rotate viewing point
  1338. # get the x and y pixel coords
  1339. if dx == 0 and dy == 0:
  1340. return
  1341. style = mpl.rcParams['axes3d.mouserotationstyle']
  1342. if style == 'azel':
  1343. roll = np.deg2rad(self.roll)
  1344. delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll)
  1345. dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
  1346. elev = self.elev + delev
  1347. azim = self.azim + dazim
  1348. roll = self.roll
  1349. else:
  1350. q = _Quaternion.from_cardan_angles(
  1351. *np.deg2rad((self.elev, self.azim, self.roll)))
  1352. if style == 'trackball':
  1353. k = np.array([0, -dy/h, dx/w])
  1354. nk = np.linalg.norm(k)
  1355. th = nk / mpl.rcParams['axes3d.trackballsize']
  1356. dq = _Quaternion(np.cos(th), k*np.sin(th)/nk)
  1357. else: # 'sphere', 'arcball'
  1358. current_vec = self._arcball(self._sx/w, self._sy/h)
  1359. new_vec = self._arcball(x/w, y/h)
  1360. if style == 'sphere':
  1361. dq = _Quaternion.rotate_from_to(current_vec, new_vec)
  1362. else: # 'arcball'
  1363. dq = _Quaternion(0, new_vec) * _Quaternion(0, -current_vec)
  1364. q = dq * q
  1365. elev, azim, roll = np.rad2deg(q.as_cardan_angles())
  1366. # update view
  1367. vertical_axis = self._axis_names[self._vertical_axis]
  1368. self.view_init(
  1369. elev=elev,
  1370. azim=azim,
  1371. roll=roll,
  1372. vertical_axis=vertical_axis,
  1373. share=True,
  1374. )
  1375. self.stale = True
  1376. # Pan
  1377. elif self.button_pressed in self._pan_btn:
  1378. # Start the pan event with pixel coordinates
  1379. px, py = self.transData.transform([self._sx, self._sy])
  1380. self.start_pan(px, py, 2)
  1381. # pan view (takes pixel coordinate input)
  1382. self.drag_pan(2, None, event.x, event.y)
  1383. self.end_pan()
  1384. # Zoom
  1385. elif self.button_pressed in self._zoom_btn:
  1386. # zoom view (dragging down zooms in)
  1387. scale = h/(h - dy)
  1388. self._scale_axis_limits(scale, scale, scale)
  1389. # Store the event coordinates for the next time through.
  1390. self._sx, self._sy = x, y
  1391. # Always request a draw update at the end of interaction
  1392. self.get_figure(root=True).canvas.draw_idle()
  1393. def drag_pan(self, button, key, x, y):
  1394. # docstring inherited
  1395. # Get the coordinates from the move event
  1396. p = self._pan_start
  1397. (xdata, ydata), (xdata_start, ydata_start) = p.trans_inverse.transform(
  1398. [(x, y), (p.x, p.y)])
  1399. self._sx, self._sy = xdata, ydata
  1400. # Calling start_pan() to set the x/y of this event as the starting
  1401. # move location for the next event
  1402. self.start_pan(x, y, button)
  1403. du, dv = xdata - xdata_start, ydata - ydata_start
  1404. dw = 0
  1405. if key == 'x':
  1406. dv = 0
  1407. elif key == 'y':
  1408. du = 0
  1409. if du == 0 and dv == 0:
  1410. return
  1411. # Transform the pan from the view axes to the data axes
  1412. R = np.array([self._view_u, self._view_v, self._view_w])
  1413. R = -R / self._box_aspect * self._dist
  1414. duvw_projected = R.T @ np.array([du, dv, dw])
  1415. # Calculate pan distance
  1416. minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
  1417. dx = (maxx - minx) * duvw_projected[0]
  1418. dy = (maxy - miny) * duvw_projected[1]
  1419. dz = (maxz - minz) * duvw_projected[2]
  1420. # Set the new axis limits
  1421. self.set_xlim3d(minx + dx, maxx + dx, auto=None)
  1422. self.set_ylim3d(miny + dy, maxy + dy, auto=None)
  1423. self.set_zlim3d(minz + dz, maxz + dz, auto=None)
  1424. def _calc_view_axes(self, eye):
  1425. """
  1426. Get the unit vectors for the viewing axes in data coordinates.
  1427. `u` is towards the right of the screen
  1428. `v` is towards the top of the screen
  1429. `w` is out of the screen
  1430. """
  1431. elev_rad = np.deg2rad(art3d._norm_angle(self.elev))
  1432. roll_rad = np.deg2rad(art3d._norm_angle(self.roll))
  1433. # Look into the middle of the world coordinates
  1434. R = 0.5 * self._roll_to_vertical(self._box_aspect)
  1435. # Define which axis should be vertical. A negative value
  1436. # indicates the plot is upside down and therefore the values
  1437. # have been reversed:
  1438. V = np.zeros(3)
  1439. V[self._vertical_axis] = -1 if abs(elev_rad) > np.pi/2 else 1
  1440. u, v, w = proj3d._view_axes(eye, R, V, roll_rad)
  1441. return u, v, w
  1442. def _set_view_from_bbox(self, bbox, direction='in',
  1443. mode=None, twinx=False, twiny=False):
  1444. """
  1445. Zoom in or out of the bounding box.
  1446. Will center the view in the center of the bounding box, and zoom by
  1447. the ratio of the size of the bounding box to the size of the Axes3D.
  1448. """
  1449. (start_x, start_y, stop_x, stop_y) = bbox
  1450. if mode == 'x':
  1451. start_y = self.bbox.min[1]
  1452. stop_y = self.bbox.max[1]
  1453. elif mode == 'y':
  1454. start_x = self.bbox.min[0]
  1455. stop_x = self.bbox.max[0]
  1456. # Clip to bounding box limits
  1457. start_x, stop_x = np.clip(sorted([start_x, stop_x]),
  1458. self.bbox.min[0], self.bbox.max[0])
  1459. start_y, stop_y = np.clip(sorted([start_y, stop_y]),
  1460. self.bbox.min[1], self.bbox.max[1])
  1461. # Move the center of the view to the center of the bbox
  1462. zoom_center_x = (start_x + stop_x)/2
  1463. zoom_center_y = (start_y + stop_y)/2
  1464. ax_center_x = (self.bbox.max[0] + self.bbox.min[0])/2
  1465. ax_center_y = (self.bbox.max[1] + self.bbox.min[1])/2
  1466. self.start_pan(zoom_center_x, zoom_center_y, 2)
  1467. self.drag_pan(2, None, ax_center_x, ax_center_y)
  1468. self.end_pan()
  1469. # Calculate zoom level
  1470. dx = abs(start_x - stop_x)
  1471. dy = abs(start_y - stop_y)
  1472. scale_u = dx / (self.bbox.max[0] - self.bbox.min[0])
  1473. scale_v = dy / (self.bbox.max[1] - self.bbox.min[1])
  1474. # Keep aspect ratios equal
  1475. scale = max(scale_u, scale_v)
  1476. # Zoom out
  1477. if direction == 'out':
  1478. scale = 1 / scale
  1479. self._zoom_data_limits(scale, scale, scale)
  1480. def _zoom_data_limits(self, scale_u, scale_v, scale_w):
  1481. """
  1482. Zoom in or out of a 3D plot.
  1483. Will scale the data limits by the scale factors. These will be
  1484. transformed to the x, y, z data axes based on the current view angles.
  1485. A scale factor > 1 zooms out and a scale factor < 1 zooms in.
  1486. For an Axes that has had its aspect ratio set to 'equal', 'equalxy',
  1487. 'equalyz', or 'equalxz', the relevant axes are constrained to zoom
  1488. equally.
  1489. Parameters
  1490. ----------
  1491. scale_u : float
  1492. Scale factor for the u view axis (view screen horizontal).
  1493. scale_v : float
  1494. Scale factor for the v view axis (view screen vertical).
  1495. scale_w : float
  1496. Scale factor for the w view axis (view screen depth).
  1497. """
  1498. scale = np.array([scale_u, scale_v, scale_w])
  1499. # Only perform frame conversion if unequal scale factors
  1500. if not np.allclose(scale, scale_u):
  1501. # Convert the scale factors from the view frame to the data frame
  1502. R = np.array([self._view_u, self._view_v, self._view_w])
  1503. S = scale * np.eye(3)
  1504. scale = np.linalg.norm(R.T @ S, axis=1)
  1505. # Set the constrained scale factors to the factor closest to 1
  1506. if self._aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'):
  1507. ax_idxs = self._equal_aspect_axis_indices(self._aspect)
  1508. min_ax_idxs = np.argmin(np.abs(scale[ax_idxs] - 1))
  1509. scale[ax_idxs] = scale[ax_idxs][min_ax_idxs]
  1510. self._scale_axis_limits(scale[0], scale[1], scale[2])
  1511. def _scale_axis_limits(self, scale_x, scale_y, scale_z):
  1512. """
  1513. Keeping the center of the x, y, and z data axes fixed, scale their
  1514. limits by scale factors. A scale factor > 1 zooms out and a scale
  1515. factor < 1 zooms in.
  1516. Parameters
  1517. ----------
  1518. scale_x : float
  1519. Scale factor for the x data axis.
  1520. scale_y : float
  1521. Scale factor for the y data axis.
  1522. scale_z : float
  1523. Scale factor for the z data axis.
  1524. """
  1525. # Get the axis centers and ranges
  1526. cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
  1527. # Set the scaled axis limits
  1528. self.set_xlim3d(cx - dx*scale_x/2, cx + dx*scale_x/2, auto=None)
  1529. self.set_ylim3d(cy - dy*scale_y/2, cy + dy*scale_y/2, auto=None)
  1530. self.set_zlim3d(cz - dz*scale_z/2, cz + dz*scale_z/2, auto=None)
  1531. def _get_w_centers_ranges(self):
  1532. """Get 3D world centers and axis ranges."""
  1533. # Calculate center of axis limits
  1534. minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
  1535. cx = (maxx + minx)/2
  1536. cy = (maxy + miny)/2
  1537. cz = (maxz + minz)/2
  1538. # Calculate range of axis limits
  1539. dx = (maxx - minx)
  1540. dy = (maxy - miny)
  1541. dz = (maxz - minz)
  1542. return cx, cy, cz, dx, dy, dz
  1543. def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
  1544. """
  1545. Set zlabel. See doc for `.set_ylabel` for description.
  1546. """
  1547. if labelpad is not None:
  1548. self.zaxis.labelpad = labelpad
  1549. return self.zaxis.set_label_text(zlabel, fontdict, **kwargs)
  1550. def get_zlabel(self):
  1551. """
  1552. Get the z-label text string.
  1553. """
  1554. label = self.zaxis.label
  1555. return label.get_text()
  1556. # Axes rectangle characteristics
  1557. # The frame_on methods are not available for 3D axes.
  1558. # Python will raise a TypeError if they are called.
  1559. get_frame_on = None
  1560. set_frame_on = None
  1561. def grid(self, visible=True, **kwargs):
  1562. """
  1563. Set / unset 3D grid.
  1564. .. note::
  1565. Currently, this function does not behave the same as
  1566. `.axes.Axes.grid`, but it is intended to eventually support that
  1567. behavior.
  1568. """
  1569. # TODO: Operate on each axes separately
  1570. if len(kwargs):
  1571. visible = True
  1572. self._draw_grid = visible
  1573. self.stale = True
  1574. def tick_params(self, axis='both', **kwargs):
  1575. """
  1576. Convenience method for changing the appearance of ticks and
  1577. tick labels.
  1578. See `.Axes.tick_params` for full documentation. Because this function
  1579. applies to 3D Axes, *axis* can also be set to 'z', and setting *axis*
  1580. to 'both' autoscales all three axes.
  1581. Also, because of how Axes3D objects are drawn very differently
  1582. from regular 2D Axes, some of these settings may have
  1583. ambiguous meaning. For simplicity, the 'z' axis will
  1584. accept settings as if it was like the 'y' axis.
  1585. .. note::
  1586. Axes3D currently ignores some of these settings.
  1587. """
  1588. _api.check_in_list(['x', 'y', 'z', 'both'], axis=axis)
  1589. if axis in ['x', 'y', 'both']:
  1590. super().tick_params(axis, **kwargs)
  1591. if axis in ['z', 'both']:
  1592. zkw = dict(kwargs)
  1593. zkw.pop('top', None)
  1594. zkw.pop('bottom', None)
  1595. zkw.pop('labeltop', None)
  1596. zkw.pop('labelbottom', None)
  1597. self.zaxis.set_tick_params(**zkw)
  1598. # data limits, ticks, tick labels, and formatting
  1599. def invert_zaxis(self):
  1600. """
  1601. Invert the z-axis.
  1602. See Also
  1603. --------
  1604. zaxis_inverted
  1605. get_zlim, set_zlim
  1606. get_zbound, set_zbound
  1607. """
  1608. bottom, top = self.get_zlim()
  1609. self.set_zlim(top, bottom, auto=None)
  1610. zaxis_inverted = _axis_method_wrapper("zaxis", "get_inverted")
  1611. def get_zbound(self):
  1612. """
  1613. Return the lower and upper z-axis bounds, in increasing order.
  1614. See Also
  1615. --------
  1616. set_zbound
  1617. get_zlim, set_zlim
  1618. invert_zaxis, zaxis_inverted
  1619. """
  1620. lower, upper = self.get_zlim()
  1621. if lower < upper:
  1622. return lower, upper
  1623. else:
  1624. return upper, lower
  1625. def text(self, x, y, z, s, zdir=None, *, axlim_clip=False, **kwargs):
  1626. """
  1627. Add the text *s* to the 3D Axes at location *x*, *y*, *z* in data coordinates.
  1628. Parameters
  1629. ----------
  1630. x, y, z : float
  1631. The position to place the text.
  1632. s : str
  1633. The text.
  1634. zdir : {'x', 'y', 'z', 3-tuple}, optional
  1635. The direction to be used as the z-direction. Default: 'z'.
  1636. See `.get_dir_vector` for a description of the values.
  1637. axlim_clip : bool, default: False
  1638. Whether to hide text that is outside the axes view limits.
  1639. .. versionadded:: 3.10
  1640. **kwargs
  1641. Other arguments are forwarded to `matplotlib.axes.Axes.text`.
  1642. Returns
  1643. -------
  1644. `.Text3D`
  1645. The created `.Text3D` instance.
  1646. """
  1647. text = super().text(x, y, s, **kwargs)
  1648. art3d.text_2d_to_3d(text, z, zdir, axlim_clip)
  1649. return text
  1650. text3D = text
  1651. text2D = Axes.text
  1652. def plot(self, xs, ys, *args, zdir='z', axlim_clip=False, **kwargs):
  1653. """
  1654. Plot 2D or 3D data.
  1655. Parameters
  1656. ----------
  1657. xs : 1D array-like
  1658. x coordinates of vertices.
  1659. ys : 1D array-like
  1660. y coordinates of vertices.
  1661. zs : float or 1D array-like
  1662. z coordinates of vertices; either one for all points or one for
  1663. each point.
  1664. zdir : {'x', 'y', 'z'}, default: 'z'
  1665. When plotting 2D data, the direction to use as z.
  1666. axlim_clip : bool, default: False
  1667. Whether to hide data that is outside the axes view limits.
  1668. .. versionadded:: 3.10
  1669. **kwargs
  1670. Other arguments are forwarded to `matplotlib.axes.Axes.plot`.
  1671. """
  1672. had_data = self.has_data()
  1673. # `zs` can be passed positionally or as keyword; checking whether
  1674. # args[0] is a string matches the behavior of 2D `plot` (via
  1675. # `_process_plot_var_args`).
  1676. if args and not isinstance(args[0], str):
  1677. zs, *args = args
  1678. if 'zs' in kwargs:
  1679. raise TypeError("plot() for multiple values for argument 'zs'")
  1680. else:
  1681. zs = kwargs.pop('zs', 0)
  1682. xs, ys, zs = cbook._broadcast_with_masks(xs, ys, zs)
  1683. lines = super().plot(xs, ys, *args, **kwargs)
  1684. for line in lines:
  1685. art3d.line_2d_to_3d(line, zs=zs, zdir=zdir, axlim_clip=axlim_clip)
  1686. xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir)
  1687. self.auto_scale_xyz(xs, ys, zs, had_data)
  1688. return lines
  1689. plot3D = plot
  1690. def fill_between(self, x1, y1, z1, x2, y2, z2, *,
  1691. where=None, mode='auto', facecolors=None, shade=None,
  1692. axlim_clip=False, **kwargs):
  1693. """
  1694. Fill the area between two 3D curves.
  1695. The curves are defined by the points (*x1*, *y1*, *z1*) and
  1696. (*x2*, *y2*, *z2*). This creates one or multiple quadrangle
  1697. polygons that are filled. All points must be the same length N, or a
  1698. single value to be used for all points.
  1699. Parameters
  1700. ----------
  1701. x1, y1, z1 : float or 1D array-like
  1702. x, y, and z coordinates of vertices for 1st line.
  1703. x2, y2, z2 : float or 1D array-like
  1704. x, y, and z coordinates of vertices for 2nd line.
  1705. where : array of bool (length N), optional
  1706. Define *where* to exclude some regions from being filled. The
  1707. filled regions are defined by the coordinates ``pts[where]``,
  1708. for all x, y, and z pts. More precisely, fill between ``pts[i]``
  1709. and ``pts[i+1]`` if ``where[i] and where[i+1]``. Note that this
  1710. definition implies that an isolated *True* value between two
  1711. *False* values in *where* will not result in filling. Both sides of
  1712. the *True* position remain unfilled due to the adjacent *False*
  1713. values.
  1714. mode : {'quad', 'polygon', 'auto'}, default: 'auto'
  1715. The fill mode. One of:
  1716. - 'quad': A separate quadrilateral polygon is created for each
  1717. pair of subsequent points in the two lines.
  1718. - 'polygon': The two lines are connected to form a single polygon.
  1719. This is faster and can render more cleanly for simple shapes
  1720. (e.g. for filling between two lines that lie within a plane).
  1721. - 'auto': If the points all lie on the same 3D plane, 'polygon' is
  1722. used. Otherwise, 'quad' is used.
  1723. facecolors : list of :mpltype:`color`, default: None
  1724. Colors of each individual patch, or a single color to be used for
  1725. all patches.
  1726. shade : bool, default: None
  1727. Whether to shade the facecolors. If *None*, then defaults to *True*
  1728. for 'quad' mode and *False* for 'polygon' mode.
  1729. axlim_clip : bool, default: False
  1730. Whether to hide data that is outside the axes view limits.
  1731. .. versionadded:: 3.10
  1732. **kwargs
  1733. All other keyword arguments are passed on to `.Poly3DCollection`.
  1734. Returns
  1735. -------
  1736. `.Poly3DCollection`
  1737. A `.Poly3DCollection` containing the plotted polygons.
  1738. """
  1739. _api.check_in_list(['auto', 'quad', 'polygon'], mode=mode)
  1740. had_data = self.has_data()
  1741. x1, y1, z1, x2, y2, z2 = cbook._broadcast_with_masks(x1, y1, z1, x2, y2, z2)
  1742. if facecolors is None:
  1743. facecolors = [self._get_patches_for_fill.get_next_color()]
  1744. facecolors = list(mcolors.to_rgba_array(facecolors))
  1745. if where is None:
  1746. where = True
  1747. else:
  1748. where = np.asarray(where, dtype=bool)
  1749. if where.size != x1.size:
  1750. raise ValueError(f"where size ({where.size}) does not match "
  1751. f"size ({x1.size})")
  1752. where = where & ~np.isnan(x1) # NaNs were broadcast in _broadcast_with_masks
  1753. if mode == 'auto':
  1754. if art3d._all_points_on_plane(np.concatenate((x1[where], x2[where])),
  1755. np.concatenate((y1[where], y2[where])),
  1756. np.concatenate((z1[where], z2[where])),
  1757. atol=1e-12):
  1758. mode = 'polygon'
  1759. else:
  1760. mode = 'quad'
  1761. if shade is None:
  1762. if mode == 'quad':
  1763. shade = True
  1764. else:
  1765. shade = False
  1766. polys = []
  1767. for idx0, idx1 in cbook.contiguous_regions(where):
  1768. x1i = x1[idx0:idx1]
  1769. y1i = y1[idx0:idx1]
  1770. z1i = z1[idx0:idx1]
  1771. x2i = x2[idx0:idx1]
  1772. y2i = y2[idx0:idx1]
  1773. z2i = z2[idx0:idx1]
  1774. if not len(x1i):
  1775. continue
  1776. if mode == 'quad':
  1777. # Preallocate the array for the region's vertices, and fill it in
  1778. n_polys_i = len(x1i) - 1
  1779. polys_i = np.empty((n_polys_i, 4, 3))
  1780. polys_i[:, 0, :] = np.column_stack((x1i[:-1], y1i[:-1], z1i[:-1]))
  1781. polys_i[:, 1, :] = np.column_stack((x1i[1:], y1i[1:], z1i[1:]))
  1782. polys_i[:, 2, :] = np.column_stack((x2i[1:], y2i[1:], z2i[1:]))
  1783. polys_i[:, 3, :] = np.column_stack((x2i[:-1], y2i[:-1], z2i[:-1]))
  1784. polys = polys + [*polys_i]
  1785. elif mode == 'polygon':
  1786. line1 = np.column_stack((x1i, y1i, z1i))
  1787. line2 = np.column_stack((x2i[::-1], y2i[::-1], z2i[::-1]))
  1788. poly = np.concatenate((line1, line2), axis=0)
  1789. polys.append(poly)
  1790. polyc = art3d.Poly3DCollection(polys, facecolors=facecolors, shade=shade,
  1791. axlim_clip=axlim_clip, **kwargs)
  1792. self.add_collection(polyc)
  1793. self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data)
  1794. return polyc
  1795. def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
  1796. vmax=None, lightsource=None, axlim_clip=False, **kwargs):
  1797. """
  1798. Create a surface plot.
  1799. By default, it will be colored in shades of a solid color, but it also
  1800. supports colormapping by supplying the *cmap* argument.
  1801. .. note::
  1802. The *rcount* and *ccount* kwargs, which both default to 50,
  1803. determine the maximum number of samples used in each direction. If
  1804. the input data is larger, it will be downsampled (by slicing) to
  1805. these numbers of points.
  1806. .. note::
  1807. To maximize rendering speed consider setting *rstride* and *cstride*
  1808. to divisors of the number of rows minus 1 and columns minus 1
  1809. respectively. For example, given 51 rows rstride can be any of the
  1810. divisors of 50.
  1811. Similarly, a setting of *rstride* and *cstride* equal to 1 (or
  1812. *rcount* and *ccount* equal the number of rows and columns) can use
  1813. the optimized path.
  1814. Parameters
  1815. ----------
  1816. X, Y, Z : 2D arrays
  1817. Data values.
  1818. rcount, ccount : int
  1819. Maximum number of samples used in each direction. If the input
  1820. data is larger, it will be downsampled (by slicing) to these
  1821. numbers of points. Defaults to 50.
  1822. rstride, cstride : int
  1823. Downsampling stride in each direction. These arguments are
  1824. mutually exclusive with *rcount* and *ccount*. If only one of
  1825. *rstride* or *cstride* is set, the other defaults to 10.
  1826. 'classic' mode uses a default of ``rstride = cstride = 10`` instead
  1827. of the new default of ``rcount = ccount = 50``.
  1828. color : :mpltype:`color`
  1829. Color of the surface patches.
  1830. cmap : Colormap, optional
  1831. Colormap of the surface patches.
  1832. facecolors : list of :mpltype:`color`
  1833. Colors of each individual patch.
  1834. norm : `~matplotlib.colors.Normalize`, optional
  1835. Normalization for the colormap.
  1836. vmin, vmax : float, optional
  1837. Bounds for the normalization.
  1838. shade : bool, default: True
  1839. Whether to shade the facecolors. Shading is always disabled when
  1840. *cmap* is specified.
  1841. lightsource : `~matplotlib.colors.LightSource`, optional
  1842. The lightsource to use when *shade* is True.
  1843. axlim_clip : bool, default: False
  1844. Whether to hide patches with a vertex outside the axes view limits.
  1845. .. versionadded:: 3.10
  1846. **kwargs
  1847. Other keyword arguments are forwarded to `.Poly3DCollection`.
  1848. """
  1849. had_data = self.has_data()
  1850. if Z.ndim != 2:
  1851. raise ValueError("Argument Z must be 2-dimensional.")
  1852. Z = cbook._to_unmasked_float_array(Z)
  1853. X, Y, Z = np.broadcast_arrays(X, Y, Z)
  1854. rows, cols = Z.shape
  1855. has_stride = 'rstride' in kwargs or 'cstride' in kwargs
  1856. has_count = 'rcount' in kwargs or 'ccount' in kwargs
  1857. if has_stride and has_count:
  1858. raise ValueError("Cannot specify both stride and count arguments")
  1859. rstride = kwargs.pop('rstride', 10)
  1860. cstride = kwargs.pop('cstride', 10)
  1861. rcount = kwargs.pop('rcount', 50)
  1862. ccount = kwargs.pop('ccount', 50)
  1863. if mpl.rcParams['_internal.classic_mode']:
  1864. # Strides have priority over counts in classic mode.
  1865. # So, only compute strides from counts
  1866. # if counts were explicitly given
  1867. compute_strides = has_count
  1868. else:
  1869. # If the strides are provided then it has priority.
  1870. # Otherwise, compute the strides from the counts.
  1871. compute_strides = not has_stride
  1872. if compute_strides:
  1873. rstride = int(max(np.ceil(rows / rcount), 1))
  1874. cstride = int(max(np.ceil(cols / ccount), 1))
  1875. fcolors = kwargs.pop('facecolors', None)
  1876. cmap = kwargs.get('cmap', None)
  1877. shade = kwargs.pop('shade', cmap is None)
  1878. if shade is None:
  1879. raise ValueError("shade cannot be None.")
  1880. colset = [] # the sampled facecolor
  1881. if (rows - 1) % rstride == 0 and \
  1882. (cols - 1) % cstride == 0 and \
  1883. fcolors is None:
  1884. polys = np.stack(
  1885. [cbook._array_patch_perimeters(a, rstride, cstride)
  1886. for a in (X, Y, Z)],
  1887. axis=-1)
  1888. else:
  1889. # evenly spaced, and including both endpoints
  1890. row_inds = list(range(0, rows-1, rstride)) + [rows-1]
  1891. col_inds = list(range(0, cols-1, cstride)) + [cols-1]
  1892. polys = []
  1893. for rs, rs_next in itertools.pairwise(row_inds):
  1894. for cs, cs_next in itertools.pairwise(col_inds):
  1895. ps = [
  1896. # +1 ensures we share edges between polygons
  1897. cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
  1898. for a in (X, Y, Z)
  1899. ]
  1900. # ps = np.stack(ps, axis=-1)
  1901. ps = np.array(ps).T
  1902. polys.append(ps)
  1903. if fcolors is not None:
  1904. colset.append(fcolors[rs][cs])
  1905. # In cases where there are non-finite values in the data (possibly NaNs from
  1906. # masked arrays), artifacts can be introduced. Here check whether such values
  1907. # are present and remove them.
  1908. if not isinstance(polys, np.ndarray) or not np.isfinite(polys).all():
  1909. new_polys = []
  1910. new_colset = []
  1911. # Depending on fcolors, colset is either an empty list or has as
  1912. # many elements as polys. In the former case new_colset results in
  1913. # a list with None entries, that is discarded later.
  1914. for p, col in itertools.zip_longest(polys, colset):
  1915. new_poly = np.array(p)[np.isfinite(p).all(axis=1)]
  1916. if len(new_poly):
  1917. new_polys.append(new_poly)
  1918. new_colset.append(col)
  1919. # Replace previous polys and, if fcolors is not None, colset
  1920. polys = new_polys
  1921. if fcolors is not None:
  1922. colset = new_colset
  1923. # note that the striding causes some polygons to have more coordinates
  1924. # than others
  1925. if fcolors is not None:
  1926. polyc = art3d.Poly3DCollection(
  1927. polys, edgecolors=colset, facecolors=colset, shade=shade,
  1928. lightsource=lightsource, axlim_clip=axlim_clip, **kwargs)
  1929. elif cmap:
  1930. polyc = art3d.Poly3DCollection(polys, axlim_clip=axlim_clip, **kwargs)
  1931. # can't always vectorize, because polys might be jagged
  1932. if isinstance(polys, np.ndarray):
  1933. avg_z = polys[..., 2].mean(axis=-1)
  1934. else:
  1935. avg_z = np.array([ps[:, 2].mean() for ps in polys])
  1936. polyc.set_array(avg_z)
  1937. if vmin is not None or vmax is not None:
  1938. polyc.set_clim(vmin, vmax)
  1939. if norm is not None:
  1940. polyc.set_norm(norm)
  1941. else:
  1942. color = kwargs.pop('color', None)
  1943. if color is None:
  1944. color = self._get_lines.get_next_color()
  1945. color = np.array(mcolors.to_rgba(color))
  1946. polyc = art3d.Poly3DCollection(
  1947. polys, facecolors=color, shade=shade, lightsource=lightsource,
  1948. axlim_clip=axlim_clip, **kwargs)
  1949. self.add_collection(polyc)
  1950. self.auto_scale_xyz(X, Y, Z, had_data)
  1951. return polyc
  1952. def plot_wireframe(self, X, Y, Z, *, axlim_clip=False, **kwargs):
  1953. """
  1954. Plot a 3D wireframe.
  1955. .. note::
  1956. The *rcount* and *ccount* kwargs, which both default to 50,
  1957. determine the maximum number of samples used in each direction. If
  1958. the input data is larger, it will be downsampled (by slicing) to
  1959. these numbers of points.
  1960. Parameters
  1961. ----------
  1962. X, Y, Z : 2D arrays
  1963. Data values.
  1964. axlim_clip : bool, default: False
  1965. Whether to hide lines and patches with vertices outside the axes
  1966. view limits.
  1967. .. versionadded:: 3.10
  1968. rcount, ccount : int
  1969. Maximum number of samples used in each direction. If the input
  1970. data is larger, it will be downsampled (by slicing) to these
  1971. numbers of points. Setting a count to zero causes the data to be
  1972. not sampled in the corresponding direction, producing a 3D line
  1973. plot rather than a wireframe plot. Defaults to 50.
  1974. rstride, cstride : int
  1975. Downsampling stride in each direction. These arguments are
  1976. mutually exclusive with *rcount* and *ccount*. If only one of
  1977. *rstride* or *cstride* is set, the other defaults to 1. Setting a
  1978. stride to zero causes the data to be not sampled in the
  1979. corresponding direction, producing a 3D line plot rather than a
  1980. wireframe plot.
  1981. 'classic' mode uses a default of ``rstride = cstride = 1`` instead
  1982. of the new default of ``rcount = ccount = 50``.
  1983. **kwargs
  1984. Other keyword arguments are forwarded to `.Line3DCollection`.
  1985. """
  1986. had_data = self.has_data()
  1987. if Z.ndim != 2:
  1988. raise ValueError("Argument Z must be 2-dimensional.")
  1989. # FIXME: Support masked arrays
  1990. X, Y, Z = np.broadcast_arrays(X, Y, Z)
  1991. rows, cols = Z.shape
  1992. has_stride = 'rstride' in kwargs or 'cstride' in kwargs
  1993. has_count = 'rcount' in kwargs or 'ccount' in kwargs
  1994. if has_stride and has_count:
  1995. raise ValueError("Cannot specify both stride and count arguments")
  1996. rstride = kwargs.pop('rstride', 1)
  1997. cstride = kwargs.pop('cstride', 1)
  1998. rcount = kwargs.pop('rcount', 50)
  1999. ccount = kwargs.pop('ccount', 50)
  2000. if mpl.rcParams['_internal.classic_mode']:
  2001. # Strides have priority over counts in classic mode.
  2002. # So, only compute strides from counts
  2003. # if counts were explicitly given
  2004. if has_count:
  2005. rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
  2006. cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
  2007. else:
  2008. # If the strides are provided then it has priority.
  2009. # Otherwise, compute the strides from the counts.
  2010. if not has_stride:
  2011. rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
  2012. cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
  2013. # We want two sets of lines, one running along the "rows" of
  2014. # Z and another set of lines running along the "columns" of Z.
  2015. # This transpose will make it easy to obtain the columns.
  2016. tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z)
  2017. if rstride:
  2018. rii = list(range(0, rows, rstride))
  2019. # Add the last index only if needed
  2020. if rows > 0 and rii[-1] != (rows - 1):
  2021. rii += [rows-1]
  2022. else:
  2023. rii = []
  2024. if cstride:
  2025. cii = list(range(0, cols, cstride))
  2026. # Add the last index only if needed
  2027. if cols > 0 and cii[-1] != (cols - 1):
  2028. cii += [cols-1]
  2029. else:
  2030. cii = []
  2031. if rstride == 0 and cstride == 0:
  2032. raise ValueError("Either rstride or cstride must be non zero")
  2033. # If the inputs were empty, then just
  2034. # reset everything.
  2035. if Z.size == 0:
  2036. rii = []
  2037. cii = []
  2038. xlines = [X[i] for i in rii]
  2039. ylines = [Y[i] for i in rii]
  2040. zlines = [Z[i] for i in rii]
  2041. txlines = [tX[i] for i in cii]
  2042. tylines = [tY[i] for i in cii]
  2043. tzlines = [tZ[i] for i in cii]
  2044. lines = ([list(zip(xl, yl, zl))
  2045. for xl, yl, zl in zip(xlines, ylines, zlines)]
  2046. + [list(zip(xl, yl, zl))
  2047. for xl, yl, zl in zip(txlines, tylines, tzlines)])
  2048. linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs)
  2049. self.add_collection(linec)
  2050. self.auto_scale_xyz(X, Y, Z, had_data)
  2051. return linec
  2052. def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
  2053. lightsource=None, axlim_clip=False, **kwargs):
  2054. """
  2055. Plot a triangulated surface.
  2056. The (optional) triangulation can be specified in one of two ways;
  2057. either::
  2058. plot_trisurf(triangulation, ...)
  2059. where triangulation is a `~matplotlib.tri.Triangulation` object, or::
  2060. plot_trisurf(X, Y, ...)
  2061. plot_trisurf(X, Y, triangles, ...)
  2062. plot_trisurf(X, Y, triangles=triangles, ...)
  2063. in which case a Triangulation object will be created. See
  2064. `.Triangulation` for an explanation of these possibilities.
  2065. The remaining arguments are::
  2066. plot_trisurf(..., Z)
  2067. where *Z* is the array of values to contour, one per point
  2068. in the triangulation.
  2069. Parameters
  2070. ----------
  2071. X, Y, Z : array-like
  2072. Data values as 1D arrays.
  2073. color
  2074. Color of the surface patches.
  2075. cmap
  2076. A colormap for the surface patches.
  2077. norm : `~matplotlib.colors.Normalize`, optional
  2078. An instance of Normalize to map values to colors.
  2079. vmin, vmax : float, optional
  2080. Minimum and maximum value to map.
  2081. shade : bool, default: True
  2082. Whether to shade the facecolors. Shading is always disabled when
  2083. *cmap* is specified.
  2084. lightsource : `~matplotlib.colors.LightSource`, optional
  2085. The lightsource to use when *shade* is True.
  2086. axlim_clip : bool, default: False
  2087. Whether to hide patches with a vertex outside the axes view limits.
  2088. .. versionadded:: 3.10
  2089. **kwargs
  2090. All other keyword arguments are passed on to
  2091. :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`
  2092. Examples
  2093. --------
  2094. .. plot:: gallery/mplot3d/trisurf3d.py
  2095. .. plot:: gallery/mplot3d/trisurf3d_2.py
  2096. """
  2097. had_data = self.has_data()
  2098. # TODO: Support custom face colours
  2099. if color is None:
  2100. color = self._get_lines.get_next_color()
  2101. color = np.array(mcolors.to_rgba(color))
  2102. cmap = kwargs.get('cmap', None)
  2103. shade = kwargs.pop('shade', cmap is None)
  2104. tri, args, kwargs = \
  2105. Triangulation.get_from_args_and_kwargs(*args, **kwargs)
  2106. try:
  2107. z = kwargs.pop('Z')
  2108. except KeyError:
  2109. # We do this so Z doesn't get passed as an arg to PolyCollection
  2110. z, *args = args
  2111. z = np.asarray(z)
  2112. triangles = tri.get_masked_triangles()
  2113. xt = tri.x[triangles]
  2114. yt = tri.y[triangles]
  2115. zt = z[triangles]
  2116. verts = np.stack((xt, yt, zt), axis=-1)
  2117. if cmap:
  2118. polyc = art3d.Poly3DCollection(verts, *args,
  2119. axlim_clip=axlim_clip, **kwargs)
  2120. # average over the three points of each triangle
  2121. avg_z = verts[:, :, 2].mean(axis=1)
  2122. polyc.set_array(avg_z)
  2123. if vmin is not None or vmax is not None:
  2124. polyc.set_clim(vmin, vmax)
  2125. if norm is not None:
  2126. polyc.set_norm(norm)
  2127. else:
  2128. polyc = art3d.Poly3DCollection(
  2129. verts, *args, shade=shade, lightsource=lightsource,
  2130. facecolors=color, axlim_clip=axlim_clip, **kwargs)
  2131. self.add_collection(polyc)
  2132. self.auto_scale_xyz(tri.x, tri.y, z, had_data)
  2133. return polyc
  2134. def _3d_extend_contour(self, cset, stride=5):
  2135. """
  2136. Extend a contour in 3D by creating
  2137. """
  2138. dz = (cset.levels[1] - cset.levels[0]) / 2
  2139. polyverts = []
  2140. colors = []
  2141. for idx, level in enumerate(cset.levels):
  2142. path = cset.get_paths()[idx]
  2143. subpaths = [*path._iter_connected_components()]
  2144. color = cset.get_edgecolor()[idx]
  2145. top = art3d._paths_to_3d_segments(subpaths, level - dz)
  2146. bot = art3d._paths_to_3d_segments(subpaths, level + dz)
  2147. if not len(top[0]):
  2148. continue
  2149. nsteps = max(round(len(top[0]) / stride), 2)
  2150. stepsize = (len(top[0]) - 1) / (nsteps - 1)
  2151. polyverts.extend([
  2152. (top[0][round(i * stepsize)], top[0][round((i + 1) * stepsize)],
  2153. bot[0][round((i + 1) * stepsize)], bot[0][round(i * stepsize)])
  2154. for i in range(round(nsteps) - 1)])
  2155. colors.extend([color] * (round(nsteps) - 1))
  2156. self.add_collection3d(art3d.Poly3DCollection(
  2157. np.array(polyverts), # All polygons have 4 vertices, so vectorize.
  2158. facecolors=colors, edgecolors=colors, shade=True))
  2159. cset.remove()
  2160. def add_contour_set(
  2161. self, cset, extend3d=False, stride=5, zdir='z', offset=None,
  2162. axlim_clip=False):
  2163. zdir = '-' + zdir
  2164. if extend3d:
  2165. self._3d_extend_contour(cset, stride)
  2166. else:
  2167. art3d.collection_2d_to_3d(
  2168. cset, zs=offset if offset is not None else cset.levels, zdir=zdir,
  2169. axlim_clip=axlim_clip)
  2170. def add_contourf_set(self, cset, zdir='z', offset=None, *, axlim_clip=False):
  2171. self._add_contourf_set(cset, zdir=zdir, offset=offset,
  2172. axlim_clip=axlim_clip)
  2173. def _add_contourf_set(self, cset, zdir='z', offset=None, axlim_clip=False):
  2174. """
  2175. Returns
  2176. -------
  2177. levels : `numpy.ndarray`
  2178. Levels at which the filled contours are added.
  2179. """
  2180. zdir = '-' + zdir
  2181. midpoints = cset.levels[:-1] + np.diff(cset.levels) / 2
  2182. # Linearly interpolate to get levels for any extensions
  2183. if cset._extend_min:
  2184. min_level = cset.levels[0] - np.diff(cset.levels[:2]) / 2
  2185. midpoints = np.insert(midpoints, 0, min_level)
  2186. if cset._extend_max:
  2187. max_level = cset.levels[-1] + np.diff(cset.levels[-2:]) / 2
  2188. midpoints = np.append(midpoints, max_level)
  2189. art3d.collection_2d_to_3d(
  2190. cset, zs=offset if offset is not None else midpoints, zdir=zdir,
  2191. axlim_clip=axlim_clip)
  2192. return midpoints
  2193. @_preprocess_data()
  2194. def contour(self, X, Y, Z, *args,
  2195. extend3d=False, stride=5, zdir='z', offset=None, axlim_clip=False,
  2196. **kwargs):
  2197. """
  2198. Create a 3D contour plot.
  2199. Parameters
  2200. ----------
  2201. X, Y, Z : array-like,
  2202. Input data. See `.Axes.contour` for supported data shapes.
  2203. extend3d : bool, default: False
  2204. Whether to extend contour in 3D.
  2205. stride : int, default: 5
  2206. Step size for extending contour.
  2207. zdir : {'x', 'y', 'z'}, default: 'z'
  2208. The direction to use.
  2209. offset : float, optional
  2210. If specified, plot a projection of the contour lines at this
  2211. position in a plane normal to *zdir*.
  2212. axlim_clip : bool, default: False
  2213. Whether to hide lines with a vertex outside the axes view limits.
  2214. .. versionadded:: 3.10
  2215. data : indexable object, optional
  2216. DATA_PARAMETER_PLACEHOLDER
  2217. *args, **kwargs
  2218. Other arguments are forwarded to `matplotlib.axes.Axes.contour`.
  2219. Returns
  2220. -------
  2221. matplotlib.contour.QuadContourSet
  2222. """
  2223. had_data = self.has_data()
  2224. jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
  2225. cset = super().contour(jX, jY, jZ, *args, **kwargs)
  2226. self.add_contour_set(cset, extend3d, stride, zdir, offset, axlim_clip)
  2227. self.auto_scale_xyz(X, Y, Z, had_data)
  2228. return cset
  2229. contour3D = contour
  2230. @_preprocess_data()
  2231. def tricontour(self, *args,
  2232. extend3d=False, stride=5, zdir='z', offset=None, axlim_clip=False,
  2233. **kwargs):
  2234. """
  2235. Create a 3D contour plot.
  2236. .. note::
  2237. This method currently produces incorrect output due to a
  2238. longstanding bug in 3D PolyCollection rendering.
  2239. Parameters
  2240. ----------
  2241. X, Y, Z : array-like
  2242. Input data. See `.Axes.tricontour` for supported data shapes.
  2243. extend3d : bool, default: False
  2244. Whether to extend contour in 3D.
  2245. stride : int, default: 5
  2246. Step size for extending contour.
  2247. zdir : {'x', 'y', 'z'}, default: 'z'
  2248. The direction to use.
  2249. offset : float, optional
  2250. If specified, plot a projection of the contour lines at this
  2251. position in a plane normal to *zdir*.
  2252. axlim_clip : bool, default: False
  2253. Whether to hide lines with a vertex outside the axes view limits.
  2254. .. versionadded:: 3.10
  2255. data : indexable object, optional
  2256. DATA_PARAMETER_PLACEHOLDER
  2257. *args, **kwargs
  2258. Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`.
  2259. Returns
  2260. -------
  2261. matplotlib.tri._tricontour.TriContourSet
  2262. """
  2263. had_data = self.has_data()
  2264. tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
  2265. *args, **kwargs)
  2266. X = tri.x
  2267. Y = tri.y
  2268. if 'Z' in kwargs:
  2269. Z = kwargs.pop('Z')
  2270. else:
  2271. # We do this so Z doesn't get passed as an arg to Axes.tricontour
  2272. Z, *args = args
  2273. jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
  2274. tri = Triangulation(jX, jY, tri.triangles, tri.mask)
  2275. cset = super().tricontour(tri, jZ, *args, **kwargs)
  2276. self.add_contour_set(cset, extend3d, stride, zdir, offset, axlim_clip)
  2277. self.auto_scale_xyz(X, Y, Z, had_data)
  2278. return cset
  2279. def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data):
  2280. # Autoscale in the zdir based on the levels added, which are
  2281. # different from data range if any contour extensions are present
  2282. dim_vals = {'x': X, 'y': Y, 'z': Z, zdir: levels}
  2283. # Input data and levels have different sizes, but auto_scale_xyz
  2284. # expected same-size input, so manually take min/max limits
  2285. limits = [(np.nanmin(dim_vals[dim]), np.nanmax(dim_vals[dim]))
  2286. for dim in ['x', 'y', 'z']]
  2287. self.auto_scale_xyz(*limits, had_data)
  2288. @_preprocess_data()
  2289. def contourf(self, X, Y, Z, *args,
  2290. zdir='z', offset=None, axlim_clip=False, **kwargs):
  2291. """
  2292. Create a 3D filled contour plot.
  2293. Parameters
  2294. ----------
  2295. X, Y, Z : array-like
  2296. Input data. See `.Axes.contourf` for supported data shapes.
  2297. zdir : {'x', 'y', 'z'}, default: 'z'
  2298. The direction to use.
  2299. offset : float, optional
  2300. If specified, plot a projection of the contour lines at this
  2301. position in a plane normal to *zdir*.
  2302. axlim_clip : bool, default: False
  2303. Whether to hide lines with a vertex outside the axes view limits.
  2304. .. versionadded:: 3.10
  2305. data : indexable object, optional
  2306. DATA_PARAMETER_PLACEHOLDER
  2307. *args, **kwargs
  2308. Other arguments are forwarded to `matplotlib.axes.Axes.contourf`.
  2309. Returns
  2310. -------
  2311. matplotlib.contour.QuadContourSet
  2312. """
  2313. had_data = self.has_data()
  2314. jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
  2315. cset = super().contourf(jX, jY, jZ, *args, **kwargs)
  2316. levels = self._add_contourf_set(cset, zdir, offset, axlim_clip)
  2317. self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
  2318. return cset
  2319. contourf3D = contourf
  2320. @_preprocess_data()
  2321. def tricontourf(self, *args, zdir='z', offset=None, axlim_clip=False, **kwargs):
  2322. """
  2323. Create a 3D filled contour plot.
  2324. .. note::
  2325. This method currently produces incorrect output due to a
  2326. longstanding bug in 3D PolyCollection rendering.
  2327. Parameters
  2328. ----------
  2329. X, Y, Z : array-like
  2330. Input data. See `.Axes.tricontourf` for supported data shapes.
  2331. zdir : {'x', 'y', 'z'}, default: 'z'
  2332. The direction to use.
  2333. offset : float, optional
  2334. If specified, plot a projection of the contour lines at this
  2335. position in a plane normal to zdir.
  2336. axlim_clip : bool, default: False
  2337. Whether to hide lines with a vertex outside the axes view limits.
  2338. .. versionadded:: 3.10
  2339. data : indexable object, optional
  2340. DATA_PARAMETER_PLACEHOLDER
  2341. *args, **kwargs
  2342. Other arguments are forwarded to
  2343. `matplotlib.axes.Axes.tricontourf`.
  2344. Returns
  2345. -------
  2346. matplotlib.tri._tricontour.TriContourSet
  2347. """
  2348. had_data = self.has_data()
  2349. tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
  2350. *args, **kwargs)
  2351. X = tri.x
  2352. Y = tri.y
  2353. if 'Z' in kwargs:
  2354. Z = kwargs.pop('Z')
  2355. else:
  2356. # We do this so Z doesn't get passed as an arg to Axes.tricontourf
  2357. Z, *args = args
  2358. jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
  2359. tri = Triangulation(jX, jY, tri.triangles, tri.mask)
  2360. cset = super().tricontourf(tri, jZ, *args, **kwargs)
  2361. levels = self._add_contourf_set(cset, zdir, offset, axlim_clip)
  2362. self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
  2363. return cset
  2364. def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *,
  2365. axlim_clip=False):
  2366. """
  2367. Add a 3D collection object to the plot.
  2368. 2D collection types are converted to a 3D version by
  2369. modifying the object and adding z coordinate information,
  2370. *zs* and *zdir*.
  2371. Supported 2D collection types are:
  2372. - `.PolyCollection`
  2373. - `.LineCollection`
  2374. - `.PatchCollection` (currently not supporting *autolim*)
  2375. Parameters
  2376. ----------
  2377. col : `.Collection`
  2378. A 2D collection object.
  2379. zs : float or array-like, default: 0
  2380. The z-positions to be used for the 2D objects.
  2381. zdir : {'x', 'y', 'z'}, default: 'z'
  2382. The direction to use for the z-positions.
  2383. autolim : bool, default: True
  2384. Whether to update the data limits.
  2385. axlim_clip : bool, default: False
  2386. Whether to hide the scatter points outside the axes view limits.
  2387. .. versionadded:: 3.10
  2388. """
  2389. had_data = self.has_data()
  2390. zvals = np.atleast_1d(zs)
  2391. zsortval = (np.min(zvals) if zvals.size
  2392. else 0) # FIXME: arbitrary default
  2393. # FIXME: use issubclass() (although, then a 3D collection
  2394. # object would also pass.) Maybe have a collection3d
  2395. # abstract class to test for and exclude?
  2396. if type(col) is mcoll.PolyCollection:
  2397. art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir,
  2398. axlim_clip=axlim_clip)
  2399. col.set_sort_zpos(zsortval)
  2400. elif type(col) is mcoll.LineCollection:
  2401. art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir,
  2402. axlim_clip=axlim_clip)
  2403. col.set_sort_zpos(zsortval)
  2404. elif type(col) is mcoll.PatchCollection:
  2405. art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir,
  2406. axlim_clip=axlim_clip)
  2407. col.set_sort_zpos(zsortval)
  2408. if autolim:
  2409. if isinstance(col, art3d.Line3DCollection):
  2410. self.auto_scale_xyz(*np.array(col._segments3d).transpose(),
  2411. had_data=had_data)
  2412. elif isinstance(col, art3d.Poly3DCollection):
  2413. self.auto_scale_xyz(*col._vec[:-1], had_data=had_data)
  2414. elif isinstance(col, art3d.Patch3DCollection):
  2415. pass
  2416. # FIXME: Implement auto-scaling function for Patch3DCollection
  2417. # Currently unable to do so due to issues with Patch3DCollection
  2418. # See https://github.com/matplotlib/matplotlib/issues/14298 for details
  2419. collection = super().add_collection(col)
  2420. return collection
  2421. @_preprocess_data(replace_names=["xs", "ys", "zs", "s",
  2422. "edgecolors", "c", "facecolor",
  2423. "facecolors", "color"])
  2424. def scatter(self, xs, ys,
  2425. zs=0, zdir='z', s=20, c=None, depthshade=True, *args,
  2426. axlim_clip=False, **kwargs):
  2427. """
  2428. Create a scatter plot.
  2429. Parameters
  2430. ----------
  2431. xs, ys : array-like
  2432. The data positions.
  2433. zs : float or array-like, default: 0
  2434. The z-positions. Either an array of the same length as *xs* and
  2435. *ys* or a single value to place all points in the same plane.
  2436. zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z'
  2437. The axis direction for the *zs*. This is useful when plotting 2D
  2438. data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting
  2439. *zdir* to 'y' then plots the data to the x-z-plane.
  2440. See also :doc:`/gallery/mplot3d/2dcollections3d`.
  2441. s : float or array-like, default: 20
  2442. The marker size in points**2. Either an array of the same length
  2443. as *xs* and *ys* or a single value to make all markers the same
  2444. size.
  2445. c : :mpltype:`color`, sequence, or sequence of colors, optional
  2446. The marker color. Possible values:
  2447. - A single color format string.
  2448. - A sequence of colors of length n.
  2449. - A sequence of n numbers to be mapped to colors using *cmap* and
  2450. *norm*.
  2451. - A 2D array in which the rows are RGB or RGBA.
  2452. For more details see the *c* argument of `~.axes.Axes.scatter`.
  2453. depthshade : bool, default: True
  2454. Whether to shade the scatter markers to give the appearance of
  2455. depth. Each call to ``scatter()`` will perform its depthshading
  2456. independently.
  2457. axlim_clip : bool, default: False
  2458. Whether to hide the scatter points outside the axes view limits.
  2459. .. versionadded:: 3.10
  2460. data : indexable object, optional
  2461. DATA_PARAMETER_PLACEHOLDER
  2462. **kwargs
  2463. All other keyword arguments are passed on to `~.axes.Axes.scatter`.
  2464. Returns
  2465. -------
  2466. paths : `~matplotlib.collections.PathCollection`
  2467. """
  2468. had_data = self.has_data()
  2469. zs_orig = zs
  2470. xs, ys, zs = cbook._broadcast_with_masks(xs, ys, zs)
  2471. s = np.ma.ravel(s) # This doesn't have to match x, y in size.
  2472. xs, ys, zs, s, c, color = cbook.delete_masked_points(
  2473. xs, ys, zs, s, c, kwargs.get('color', None)
  2474. )
  2475. if kwargs.get("color") is not None:
  2476. kwargs['color'] = color
  2477. # For xs and ys, 2D scatter() will do the copying.
  2478. if np.may_share_memory(zs_orig, zs): # Avoid unnecessary copies.
  2479. zs = zs.copy()
  2480. patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs)
  2481. art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir,
  2482. depthshade=depthshade,
  2483. axlim_clip=axlim_clip)
  2484. if self._zmargin < 0.05 and xs.size > 0:
  2485. self.set_zmargin(0.05)
  2486. self.auto_scale_xyz(xs, ys, zs, had_data)
  2487. return patches
  2488. scatter3D = scatter
  2489. @_preprocess_data()
  2490. def bar(self, left, height, zs=0, zdir='z', *args,
  2491. axlim_clip=False, **kwargs):
  2492. """
  2493. Add 2D bar(s).
  2494. Parameters
  2495. ----------
  2496. left : 1D array-like
  2497. The x coordinates of the left sides of the bars.
  2498. height : 1D array-like
  2499. The height of the bars.
  2500. zs : float or 1D array-like, default: 0
  2501. Z coordinate of bars; if a single value is specified, it will be
  2502. used for all bars.
  2503. zdir : {'x', 'y', 'z'}, default: 'z'
  2504. When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
  2505. axlim_clip : bool, default: False
  2506. Whether to hide bars with points outside the axes view limits.
  2507. .. versionadded:: 3.10
  2508. data : indexable object, optional
  2509. DATA_PARAMETER_PLACEHOLDER
  2510. **kwargs
  2511. Other keyword arguments are forwarded to
  2512. `matplotlib.axes.Axes.bar`.
  2513. Returns
  2514. -------
  2515. mpl_toolkits.mplot3d.art3d.Patch3DCollection
  2516. """
  2517. had_data = self.has_data()
  2518. patches = super().bar(left, height, *args, **kwargs)
  2519. zs = np.broadcast_to(zs, len(left), subok=True)
  2520. verts = []
  2521. verts_zs = []
  2522. for p, z in zip(patches, zs):
  2523. vs = art3d._get_patch_verts(p)
  2524. verts += vs.tolist()
  2525. verts_zs += [z] * len(vs)
  2526. art3d.patch_2d_to_3d(p, z, zdir, axlim_clip)
  2527. if 'alpha' in kwargs:
  2528. p.set_alpha(kwargs['alpha'])
  2529. if len(verts) > 0:
  2530. # the following has to be skipped if verts is empty
  2531. # NOTE: Bugs could still occur if len(verts) > 0,
  2532. # but the "2nd dimension" is empty.
  2533. xs, ys = zip(*verts)
  2534. else:
  2535. xs, ys = [], []
  2536. xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir)
  2537. self.auto_scale_xyz(xs, ys, verts_zs, had_data)
  2538. return patches
  2539. @_preprocess_data()
  2540. def bar3d(self, x, y, z, dx, dy, dz, color=None,
  2541. zsort='average', shade=True, lightsource=None, *args,
  2542. axlim_clip=False, **kwargs):
  2543. """
  2544. Generate a 3D barplot.
  2545. This method creates three-dimensional barplot where the width,
  2546. depth, height, and color of the bars can all be uniquely set.
  2547. Parameters
  2548. ----------
  2549. x, y, z : array-like
  2550. The coordinates of the anchor point of the bars.
  2551. dx, dy, dz : float or array-like
  2552. The width, depth, and height of the bars, respectively.
  2553. color : sequence of colors, optional
  2554. The color of the bars can be specified globally or
  2555. individually. This parameter can be:
  2556. - A single color, to color all bars the same color.
  2557. - An array of colors of length N bars, to color each bar
  2558. independently.
  2559. - An array of colors of length 6, to color the faces of the
  2560. bars similarly.
  2561. - An array of colors of length 6 * N bars, to color each face
  2562. independently.
  2563. When coloring the faces of the boxes specifically, this is
  2564. the order of the coloring:
  2565. 1. -Z (bottom of box)
  2566. 2. +Z (top of box)
  2567. 3. -Y
  2568. 4. +Y
  2569. 5. -X
  2570. 6. +X
  2571. zsort : {'average', 'min', 'max'}, default: 'average'
  2572. The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection`
  2573. shade : bool, default: True
  2574. When true, this shades the dark sides of the bars (relative
  2575. to the plot's source of light).
  2576. lightsource : `~matplotlib.colors.LightSource`, optional
  2577. The lightsource to use when *shade* is True.
  2578. axlim_clip : bool, default: False
  2579. Whether to hide the bars with points outside the axes view limits.
  2580. .. versionadded:: 3.10
  2581. data : indexable object, optional
  2582. DATA_PARAMETER_PLACEHOLDER
  2583. **kwargs
  2584. Any additional keyword arguments are passed onto
  2585. `~.art3d.Poly3DCollection`.
  2586. Returns
  2587. -------
  2588. collection : `~.art3d.Poly3DCollection`
  2589. A collection of three-dimensional polygons representing the bars.
  2590. """
  2591. had_data = self.has_data()
  2592. x, y, z, dx, dy, dz = np.broadcast_arrays(
  2593. np.atleast_1d(x), y, z, dx, dy, dz)
  2594. minx = np.min(x)
  2595. maxx = np.max(x + dx)
  2596. miny = np.min(y)
  2597. maxy = np.max(y + dy)
  2598. minz = np.min(z)
  2599. maxz = np.max(z + dz)
  2600. # shape (6, 4, 3)
  2601. # All faces are oriented facing outwards - when viewed from the
  2602. # outside, their vertices are in a counterclockwise ordering.
  2603. cuboid = np.array([
  2604. # -z
  2605. (
  2606. (0, 0, 0),
  2607. (0, 1, 0),
  2608. (1, 1, 0),
  2609. (1, 0, 0),
  2610. ),
  2611. # +z
  2612. (
  2613. (0, 0, 1),
  2614. (1, 0, 1),
  2615. (1, 1, 1),
  2616. (0, 1, 1),
  2617. ),
  2618. # -y
  2619. (
  2620. (0, 0, 0),
  2621. (1, 0, 0),
  2622. (1, 0, 1),
  2623. (0, 0, 1),
  2624. ),
  2625. # +y
  2626. (
  2627. (0, 1, 0),
  2628. (0, 1, 1),
  2629. (1, 1, 1),
  2630. (1, 1, 0),
  2631. ),
  2632. # -x
  2633. (
  2634. (0, 0, 0),
  2635. (0, 0, 1),
  2636. (0, 1, 1),
  2637. (0, 1, 0),
  2638. ),
  2639. # +x
  2640. (
  2641. (1, 0, 0),
  2642. (1, 1, 0),
  2643. (1, 1, 1),
  2644. (1, 0, 1),
  2645. ),
  2646. ])
  2647. # indexed by [bar, face, vertex, coord]
  2648. polys = np.empty(x.shape + cuboid.shape)
  2649. # handle each coordinate separately
  2650. for i, p, dp in [(0, x, dx), (1, y, dy), (2, z, dz)]:
  2651. p = p[..., np.newaxis, np.newaxis]
  2652. dp = dp[..., np.newaxis, np.newaxis]
  2653. polys[..., i] = p + dp * cuboid[..., i]
  2654. # collapse the first two axes
  2655. polys = polys.reshape((-1,) + polys.shape[2:])
  2656. facecolors = []
  2657. if color is None:
  2658. color = [self._get_patches_for_fill.get_next_color()]
  2659. color = list(mcolors.to_rgba_array(color))
  2660. if len(color) == len(x):
  2661. # bar colors specified, need to expand to number of faces
  2662. for c in color:
  2663. facecolors.extend([c] * 6)
  2664. else:
  2665. # a single color specified, or face colors specified explicitly
  2666. facecolors = color
  2667. if len(facecolors) < len(x):
  2668. facecolors *= (6 * len(x))
  2669. col = art3d.Poly3DCollection(polys,
  2670. zsort=zsort,
  2671. facecolors=facecolors,
  2672. shade=shade,
  2673. lightsource=lightsource,
  2674. axlim_clip=axlim_clip,
  2675. *args, **kwargs)
  2676. self.add_collection(col)
  2677. self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
  2678. return col
  2679. def set_title(self, label, fontdict=None, loc='center', **kwargs):
  2680. # docstring inherited
  2681. ret = super().set_title(label, fontdict=fontdict, loc=loc, **kwargs)
  2682. (x, y) = self.title.get_position()
  2683. self.title.set_y(0.92 * y)
  2684. return ret
  2685. @_preprocess_data()
  2686. def quiver(self, X, Y, Z, U, V, W, *,
  2687. length=1, arrow_length_ratio=.3, pivot='tail', normalize=False,
  2688. axlim_clip=False, **kwargs):
  2689. """
  2690. Plot a 3D field of arrows.
  2691. The arguments can be array-like or scalars, so long as they can be
  2692. broadcast together. The arguments can also be masked arrays. If an
  2693. element in any of argument is masked, then that corresponding quiver
  2694. element will not be plotted.
  2695. Parameters
  2696. ----------
  2697. X, Y, Z : array-like
  2698. The x, y and z coordinates of the arrow locations (default is
  2699. tail of arrow; see *pivot* kwarg).
  2700. U, V, W : array-like
  2701. The x, y and z components of the arrow vectors.
  2702. length : float, default: 1
  2703. The length of each quiver.
  2704. arrow_length_ratio : float, default: 0.3
  2705. The ratio of the arrow head with respect to the quiver.
  2706. pivot : {'tail', 'middle', 'tip'}, default: 'tail'
  2707. The part of the arrow that is at the grid point; the arrow
  2708. rotates about this point, hence the name *pivot*.
  2709. normalize : bool, default: False
  2710. Whether all arrows are normalized to have the same length, or keep
  2711. the lengths defined by *u*, *v*, and *w*.
  2712. axlim_clip : bool, default: False
  2713. Whether to hide arrows with points outside the axes view limits.
  2714. .. versionadded:: 3.10
  2715. data : indexable object, optional
  2716. DATA_PARAMETER_PLACEHOLDER
  2717. **kwargs
  2718. Any additional keyword arguments are delegated to
  2719. :class:`.Line3DCollection`
  2720. """
  2721. def calc_arrows(UVW):
  2722. # get unit direction vector perpendicular to (u, v, w)
  2723. x = UVW[:, 0]
  2724. y = UVW[:, 1]
  2725. norm = np.linalg.norm(UVW[:, :2], axis=1)
  2726. x_p = np.divide(y, norm, where=norm != 0, out=np.zeros_like(x))
  2727. y_p = np.divide(-x, norm, where=norm != 0, out=np.ones_like(x))
  2728. # compute the two arrowhead direction unit vectors
  2729. rangle = math.radians(15)
  2730. c = math.cos(rangle)
  2731. s = math.sin(rangle)
  2732. # construct the rotation matrices of shape (3, 3, n)
  2733. r13 = y_p * s
  2734. r32 = x_p * s
  2735. r12 = x_p * y_p * (1 - c)
  2736. Rpos = np.array(
  2737. [[c + (x_p ** 2) * (1 - c), r12, r13],
  2738. [r12, c + (y_p ** 2) * (1 - c), -r32],
  2739. [-r13, r32, np.full_like(x_p, c)]])
  2740. # opposite rotation negates all the sin terms
  2741. Rneg = Rpos.copy()
  2742. Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] *= -1
  2743. # Batch n (3, 3) x (3) matrix multiplications ((3, 3, n) x (n, 3)).
  2744. Rpos_vecs = np.einsum("ij...,...j->...i", Rpos, UVW)
  2745. Rneg_vecs = np.einsum("ij...,...j->...i", Rneg, UVW)
  2746. # Stack into (n, 2, 3) result.
  2747. return np.stack([Rpos_vecs, Rneg_vecs], axis=1)
  2748. had_data = self.has_data()
  2749. input_args = cbook._broadcast_with_masks(X, Y, Z, U, V, W,
  2750. compress=True)
  2751. if any(len(v) == 0 for v in input_args):
  2752. # No quivers, so just make an empty collection and return early
  2753. linec = art3d.Line3DCollection([], **kwargs)
  2754. self.add_collection(linec)
  2755. return linec
  2756. shaft_dt = np.array([0., length], dtype=float)
  2757. arrow_dt = shaft_dt * arrow_length_ratio
  2758. _api.check_in_list(['tail', 'middle', 'tip'], pivot=pivot)
  2759. if pivot == 'tail':
  2760. shaft_dt -= length
  2761. elif pivot == 'middle':
  2762. shaft_dt -= length / 2
  2763. XYZ = np.column_stack(input_args[:3])
  2764. UVW = np.column_stack(input_args[3:]).astype(float)
  2765. # Normalize rows of UVW
  2766. if normalize:
  2767. norm = np.linalg.norm(UVW, axis=1)
  2768. norm[norm == 0] = 1
  2769. UVW = UVW / norm.reshape((-1, 1))
  2770. if len(XYZ) > 0:
  2771. # compute the shaft lines all at once with an outer product
  2772. shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1)
  2773. # compute head direction vectors, n heads x 2 sides x 3 dimensions
  2774. head_dirs = calc_arrows(UVW)
  2775. # compute all head lines at once, starting from the shaft ends
  2776. heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs)
  2777. # stack left and right head lines together
  2778. heads = heads.reshape((len(arrow_dt), -1, 3))
  2779. # transpose to get a list of lines
  2780. heads = heads.swapaxes(0, 1)
  2781. lines = [*shafts, *heads[::2], *heads[1::2]]
  2782. else:
  2783. lines = []
  2784. linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs)
  2785. self.add_collection(linec)
  2786. self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data)
  2787. return linec
  2788. quiver3D = quiver
  2789. def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
  2790. lightsource=None, axlim_clip=False, **kwargs):
  2791. """
  2792. ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \
  2793. **kwargs)
  2794. Plot a set of filled voxels
  2795. All voxels are plotted as 1x1x1 cubes on the axis, with
  2796. ``filled[0, 0, 0]`` placed with its lower corner at the origin.
  2797. Occluded faces are not plotted.
  2798. Parameters
  2799. ----------
  2800. filled : 3D np.array of bool
  2801. A 3D array of values, with truthy values indicating which voxels
  2802. to fill
  2803. x, y, z : 3D np.array, optional
  2804. The coordinates of the corners of the voxels. This should broadcast
  2805. to a shape one larger in every dimension than the shape of
  2806. *filled*. These can be used to plot non-cubic voxels.
  2807. If not specified, defaults to increasing integers along each axis,
  2808. like those returned by :func:`~numpy.indices`.
  2809. As indicated by the ``/`` in the function signature, these
  2810. arguments can only be passed positionally.
  2811. facecolors, edgecolors : array-like, optional
  2812. The color to draw the faces and edges of the voxels. Can only be
  2813. passed as keyword arguments.
  2814. These parameters can be:
  2815. - A single color value, to color all voxels the same color. This
  2816. can be either a string, or a 1D RGB/RGBA array
  2817. - ``None``, the default, to use a single color for the faces, and
  2818. the style default for the edges.
  2819. - A 3D `~numpy.ndarray` of color names, with each item the color
  2820. for the corresponding voxel. The size must match the voxels.
  2821. - A 4D `~numpy.ndarray` of RGB/RGBA data, with the components
  2822. along the last axis.
  2823. shade : bool, default: True
  2824. Whether to shade the facecolors.
  2825. lightsource : `~matplotlib.colors.LightSource`, optional
  2826. The lightsource to use when *shade* is True.
  2827. axlim_clip : bool, default: False
  2828. Whether to hide voxels with points outside the axes view limits.
  2829. .. versionadded:: 3.10
  2830. **kwargs
  2831. Additional keyword arguments to pass onto
  2832. `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`.
  2833. Returns
  2834. -------
  2835. faces : dict
  2836. A dictionary indexed by coordinate, where ``faces[i, j, k]`` is a
  2837. `.Poly3DCollection` of the faces drawn for the voxel
  2838. ``filled[i, j, k]``. If no faces were drawn for a given voxel,
  2839. either because it was not asked to be drawn, or it is fully
  2840. occluded, then ``(i, j, k) not in faces``.
  2841. Examples
  2842. --------
  2843. .. plot:: gallery/mplot3d/voxels.py
  2844. .. plot:: gallery/mplot3d/voxels_rgb.py
  2845. .. plot:: gallery/mplot3d/voxels_torus.py
  2846. .. plot:: gallery/mplot3d/voxels_numpy_logo.py
  2847. """
  2848. # work out which signature we should be using, and use it to parse
  2849. # the arguments. Name must be voxels for the correct error message
  2850. if len(args) >= 3:
  2851. # underscores indicate position only
  2852. def voxels(__x, __y, __z, filled, **kwargs):
  2853. return (__x, __y, __z), filled, kwargs
  2854. else:
  2855. def voxels(filled, **kwargs):
  2856. return None, filled, kwargs
  2857. xyz, filled, kwargs = voxels(*args, **kwargs)
  2858. # check dimensions
  2859. if filled.ndim != 3:
  2860. raise ValueError("Argument filled must be 3-dimensional")
  2861. size = np.array(filled.shape, dtype=np.intp)
  2862. # check xyz coordinates, which are one larger than the filled shape
  2863. coord_shape = tuple(size + 1)
  2864. if xyz is None:
  2865. x, y, z = np.indices(coord_shape)
  2866. else:
  2867. x, y, z = (np.broadcast_to(c, coord_shape) for c in xyz)
  2868. def _broadcast_color_arg(color, name):
  2869. if np.ndim(color) in (0, 1):
  2870. # single color, like "red" or [1, 0, 0]
  2871. return np.broadcast_to(color, filled.shape + np.shape(color))
  2872. elif np.ndim(color) in (3, 4):
  2873. # 3D array of strings, or 4D array with last axis rgb
  2874. if np.shape(color)[:3] != filled.shape:
  2875. raise ValueError(
  2876. f"When multidimensional, {name} must match the shape "
  2877. "of filled")
  2878. return color
  2879. else:
  2880. raise ValueError(f"Invalid {name} argument")
  2881. # broadcast and default on facecolors
  2882. if facecolors is None:
  2883. facecolors = self._get_patches_for_fill.get_next_color()
  2884. facecolors = _broadcast_color_arg(facecolors, 'facecolors')
  2885. # broadcast but no default on edgecolors
  2886. edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')
  2887. # scale to the full array, even if the data is only in the center
  2888. self.auto_scale_xyz(x, y, z)
  2889. # points lying on corners of a square
  2890. square = np.array([
  2891. [0, 0, 0],
  2892. [1, 0, 0],
  2893. [1, 1, 0],
  2894. [0, 1, 0],
  2895. ], dtype=np.intp)
  2896. voxel_faces = defaultdict(list)
  2897. def permutation_matrices(n):
  2898. """Generate cyclic permutation matrices."""
  2899. mat = np.eye(n, dtype=np.intp)
  2900. for i in range(n):
  2901. yield mat
  2902. mat = np.roll(mat, 1, axis=0)
  2903. # iterate over each of the YZ, ZX, and XY orientations, finding faces
  2904. # to render
  2905. for permute in permutation_matrices(3):
  2906. # find the set of ranges to iterate over
  2907. pc, qc, rc = permute.T.dot(size)
  2908. pinds = np.arange(pc)
  2909. qinds = np.arange(qc)
  2910. rinds = np.arange(rc)
  2911. square_rot_pos = square.dot(permute.T)
  2912. square_rot_neg = square_rot_pos[::-1]
  2913. # iterate within the current plane
  2914. for p in pinds:
  2915. for q in qinds:
  2916. # iterate perpendicularly to the current plane, handling
  2917. # boundaries. We only draw faces between a voxel and an
  2918. # empty space, to avoid drawing internal faces.
  2919. # draw lower faces
  2920. p0 = permute.dot([p, q, 0])
  2921. i0 = tuple(p0)
  2922. if filled[i0]:
  2923. voxel_faces[i0].append(p0 + square_rot_neg)
  2924. # draw middle faces
  2925. for r1, r2 in itertools.pairwise(rinds):
  2926. p1 = permute.dot([p, q, r1])
  2927. p2 = permute.dot([p, q, r2])
  2928. i1 = tuple(p1)
  2929. i2 = tuple(p2)
  2930. if filled[i1] and not filled[i2]:
  2931. voxel_faces[i1].append(p2 + square_rot_pos)
  2932. elif not filled[i1] and filled[i2]:
  2933. voxel_faces[i2].append(p2 + square_rot_neg)
  2934. # draw upper faces
  2935. pk = permute.dot([p, q, rc-1])
  2936. pk2 = permute.dot([p, q, rc])
  2937. ik = tuple(pk)
  2938. if filled[ik]:
  2939. voxel_faces[ik].append(pk2 + square_rot_pos)
  2940. # iterate over the faces, and generate a Poly3DCollection for each
  2941. # voxel
  2942. polygons = {}
  2943. for coord, faces_inds in voxel_faces.items():
  2944. # convert indices into 3D positions
  2945. if xyz is None:
  2946. faces = faces_inds
  2947. else:
  2948. faces = []
  2949. for face_inds in faces_inds:
  2950. ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
  2951. face = np.empty(face_inds.shape)
  2952. face[:, 0] = x[ind]
  2953. face[:, 1] = y[ind]
  2954. face[:, 2] = z[ind]
  2955. faces.append(face)
  2956. # shade the faces
  2957. facecolor = facecolors[coord]
  2958. edgecolor = edgecolors[coord]
  2959. poly = art3d.Poly3DCollection(
  2960. faces, facecolors=facecolor, edgecolors=edgecolor,
  2961. shade=shade, lightsource=lightsource, axlim_clip=axlim_clip,
  2962. **kwargs)
  2963. self.add_collection3d(poly)
  2964. polygons[coord] = poly
  2965. return polygons
  2966. @_preprocess_data(replace_names=["x", "y", "z", "xerr", "yerr", "zerr"])
  2967. def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
  2968. barsabove=False, errorevery=1, ecolor=None, elinewidth=None,
  2969. capsize=None, capthick=None, xlolims=False, xuplims=False,
  2970. ylolims=False, yuplims=False, zlolims=False, zuplims=False,
  2971. axlim_clip=False,
  2972. **kwargs):
  2973. """
  2974. Plot lines and/or markers with errorbars around them.
  2975. *x*/*y*/*z* define the data locations, and *xerr*/*yerr*/*zerr* define
  2976. the errorbar sizes. By default, this draws the data markers/lines as
  2977. well the errorbars. Use fmt='none' to draw errorbars only.
  2978. Parameters
  2979. ----------
  2980. x, y, z : float or array-like
  2981. The data positions.
  2982. xerr, yerr, zerr : float or array-like, shape (N,) or (2, N), optional
  2983. The errorbar sizes:
  2984. - scalar: Symmetric +/- values for all data points.
  2985. - shape(N,): Symmetric +/-values for each data point.
  2986. - shape(2, N): Separate - and + values for each bar. First row
  2987. contains the lower errors, the second row contains the upper
  2988. errors.
  2989. - *None*: No errorbar.
  2990. Note that all error arrays should have *positive* values.
  2991. fmt : str, default: ''
  2992. The format for the data points / data lines. See `.plot` for
  2993. details.
  2994. Use 'none' (case-insensitive) to plot errorbars without any data
  2995. markers.
  2996. ecolor : :mpltype:`color`, default: None
  2997. The color of the errorbar lines. If None, use the color of the
  2998. line connecting the markers.
  2999. elinewidth : float, default: None
  3000. The linewidth of the errorbar lines. If None, the linewidth of
  3001. the current style is used.
  3002. capsize : float, default: :rc:`errorbar.capsize`
  3003. The length of the error bar caps in points.
  3004. capthick : float, default: None
  3005. An alias to the keyword argument *markeredgewidth* (a.k.a. *mew*).
  3006. This setting is a more sensible name for the property that
  3007. controls the thickness of the error bar cap in points. For
  3008. backwards compatibility, if *mew* or *markeredgewidth* are given,
  3009. then they will over-ride *capthick*. This may change in future
  3010. releases.
  3011. barsabove : bool, default: False
  3012. If True, will plot the errorbars above the plot
  3013. symbols. Default is below.
  3014. xlolims, ylolims, zlolims : bool, default: False
  3015. These arguments can be used to indicate that a value gives only
  3016. lower limits. In that case a caret symbol is used to indicate
  3017. this. *lims*-arguments may be scalars, or array-likes of the same
  3018. length as the errors. To use limits with inverted axes,
  3019. `~.set_xlim`, `~.set_ylim`, or `~.set_zlim` must be
  3020. called before `errorbar`. Note the tricky parameter names: setting
  3021. e.g. *ylolims* to True means that the y-value is a *lower* limit of
  3022. the True value, so, only an *upward*-pointing arrow will be drawn!
  3023. xuplims, yuplims, zuplims : bool, default: False
  3024. Same as above, but for controlling the upper limits.
  3025. errorevery : int or (int, int), default: 1
  3026. draws error bars on a subset of the data. *errorevery* =N draws
  3027. error bars on the points (x[::N], y[::N], z[::N]).
  3028. *errorevery* =(start, N) draws error bars on the points
  3029. (x[start::N], y[start::N], z[start::N]). e.g. *errorevery* =(6, 3)
  3030. adds error bars to the data at (x[6], x[9], x[12], x[15], ...).
  3031. Used to avoid overlapping error bars when two series share x-axis
  3032. values.
  3033. axlim_clip : bool, default: False
  3034. Whether to hide error bars that are outside the axes limits.
  3035. .. versionadded:: 3.10
  3036. Returns
  3037. -------
  3038. errlines : list
  3039. List of `~mpl_toolkits.mplot3d.art3d.Line3DCollection` instances
  3040. each containing an errorbar line.
  3041. caplines : list
  3042. List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each
  3043. containing a capline object.
  3044. limmarks : list
  3045. List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each
  3046. containing a marker with an upper or lower limit.
  3047. Other Parameters
  3048. ----------------
  3049. data : indexable object, optional
  3050. DATA_PARAMETER_PLACEHOLDER
  3051. **kwargs
  3052. All other keyword arguments for styling errorbar lines are passed
  3053. `~mpl_toolkits.mplot3d.art3d.Line3DCollection`.
  3054. Examples
  3055. --------
  3056. .. plot:: gallery/mplot3d/errorbar3d.py
  3057. """
  3058. had_data = self.has_data()
  3059. kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D)
  3060. # Drop anything that comes in as None to use the default instead.
  3061. kwargs = {k: v for k, v in kwargs.items() if v is not None}
  3062. kwargs.setdefault('zorder', 2)
  3063. self._process_unit_info([("x", x), ("y", y), ("z", z)], kwargs,
  3064. convert=False)
  3065. # make sure all the args are iterable; use lists not arrays to
  3066. # preserve units
  3067. x = x if np.iterable(x) else [x]
  3068. y = y if np.iterable(y) else [y]
  3069. z = z if np.iterable(z) else [z]
  3070. if not len(x) == len(y) == len(z):
  3071. raise ValueError("'x', 'y', and 'z' must have the same size")
  3072. everymask = self._errorevery_to_mask(x, errorevery)
  3073. label = kwargs.pop("label", None)
  3074. kwargs['label'] = '_nolegend_'
  3075. # Create the main line and determine overall kwargs for child artists.
  3076. # We avoid calling self.plot() directly, or self._get_lines(), because
  3077. # that would call self._process_unit_info again, and do other indirect
  3078. # data processing.
  3079. (data_line, base_style), = self._get_lines._plot_args(
  3080. self, (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True)
  3081. art3d.line_2d_to_3d(data_line, zs=z, axlim_clip=axlim_clip)
  3082. # Do this after creating `data_line` to avoid modifying `base_style`.
  3083. if barsabove:
  3084. data_line.set_zorder(kwargs['zorder'] - .1)
  3085. else:
  3086. data_line.set_zorder(kwargs['zorder'] + .1)
  3087. # Add line to plot, or throw it away and use it to determine kwargs.
  3088. if fmt.lower() != 'none':
  3089. self.add_line(data_line)
  3090. else:
  3091. data_line = None
  3092. # Remove alpha=0 color that _process_plot_format returns.
  3093. base_style.pop('color')
  3094. if 'color' not in base_style:
  3095. base_style['color'] = 'C0'
  3096. if ecolor is None:
  3097. ecolor = base_style['color']
  3098. # Eject any line-specific information from format string, as it's not
  3099. # needed for bars or caps.
  3100. for key in ['marker', 'markersize', 'markerfacecolor',
  3101. 'markeredgewidth', 'markeredgecolor', 'markevery',
  3102. 'linestyle', 'fillstyle', 'drawstyle', 'dash_capstyle',
  3103. 'dash_joinstyle', 'solid_capstyle', 'solid_joinstyle']:
  3104. base_style.pop(key, None)
  3105. # Make the style dict for the line collections (the bars).
  3106. eb_lines_style = {**base_style, 'color': ecolor}
  3107. if elinewidth:
  3108. eb_lines_style['linewidth'] = elinewidth
  3109. elif 'linewidth' in kwargs:
  3110. eb_lines_style['linewidth'] = kwargs['linewidth']
  3111. for key in ('transform', 'alpha', 'zorder', 'rasterized'):
  3112. if key in kwargs:
  3113. eb_lines_style[key] = kwargs[key]
  3114. # Make the style dict for caps (the "hats").
  3115. eb_cap_style = {**base_style, 'linestyle': 'None'}
  3116. if capsize is None:
  3117. capsize = mpl.rcParams["errorbar.capsize"]
  3118. if capsize > 0:
  3119. eb_cap_style['markersize'] = 2. * capsize
  3120. if capthick is not None:
  3121. eb_cap_style['markeredgewidth'] = capthick
  3122. eb_cap_style['color'] = ecolor
  3123. def _apply_mask(arrays, mask):
  3124. # Return, for each array in *arrays*, the elements for which *mask*
  3125. # is True, without using fancy indexing.
  3126. return [[*itertools.compress(array, mask)] for array in arrays]
  3127. def _extract_errs(err, data, lomask, himask):
  3128. # For separate +/- error values we need to unpack err
  3129. if len(err.shape) == 2:
  3130. low_err, high_err = err
  3131. else:
  3132. low_err, high_err = err, err
  3133. lows = np.where(lomask | ~everymask, data, data - low_err)
  3134. highs = np.where(himask | ~everymask, data, data + high_err)
  3135. return lows, highs
  3136. # collect drawn items while looping over the three coordinates
  3137. errlines, caplines, limmarks = [], [], []
  3138. # list of endpoint coordinates, used for auto-scaling
  3139. coorderrs = []
  3140. # define the markers used for errorbar caps and limits below
  3141. # the dictionary key is mapped by the `i_xyz` helper dictionary
  3142. capmarker = {0: '|', 1: '|', 2: '_'}
  3143. i_xyz = {'x': 0, 'y': 1, 'z': 2}
  3144. # Calculate marker size from points to quiver length. Because these are
  3145. # not markers, and 3D Axes do not use the normal transform stack, this
  3146. # is a bit involved. Since the quiver arrows will change size as the
  3147. # scene is rotated, they are given a standard size based on viewing
  3148. # them directly in planar form.
  3149. quiversize = eb_cap_style.get('markersize',
  3150. mpl.rcParams['lines.markersize']) ** 2
  3151. quiversize *= self.get_figure(root=True).dpi / 72
  3152. quiversize = self.transAxes.inverted().transform([
  3153. (0, 0), (quiversize, quiversize)])
  3154. quiversize = np.mean(np.diff(quiversize, axis=0))
  3155. # quiversize is now in Axes coordinates, and to convert back to data
  3156. # coordinates, we need to run it through the inverse 3D transform. For
  3157. # consistency, this uses a fixed elevation, azimuth, and roll.
  3158. with cbook._setattr_cm(self, elev=0, azim=0, roll=0):
  3159. invM = np.linalg.inv(self.get_proj())
  3160. # elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is
  3161. # 'y' in 3D, hence the 1 index.
  3162. quiversize = np.dot(invM, [quiversize, 0, 0, 0])[1]
  3163. # Quivers use a fixed 15-degree arrow head, so scale up the length so
  3164. # that the size corresponds to the base. In other words, this constant
  3165. # corresponds to the equation tan(15) = (base / 2) / (arrow length).
  3166. quiversize *= 1.8660254037844388
  3167. eb_quiver_style = {**eb_cap_style,
  3168. 'length': quiversize, 'arrow_length_ratio': 1}
  3169. eb_quiver_style.pop('markersize', None)
  3170. # loop over x-, y-, and z-direction and draw relevant elements
  3171. for zdir, data, err, lolims, uplims in zip(
  3172. ['x', 'y', 'z'], [x, y, z], [xerr, yerr, zerr],
  3173. [xlolims, ylolims, zlolims], [xuplims, yuplims, zuplims]):
  3174. dir_vector = art3d.get_dir_vector(zdir)
  3175. i_zdir = i_xyz[zdir]
  3176. if err is None:
  3177. continue
  3178. if not np.iterable(err):
  3179. err = [err] * len(data)
  3180. err = np.atleast_1d(err)
  3181. # arrays fine here, they are booleans and hence not units
  3182. lolims = np.broadcast_to(lolims, len(data)).astype(bool)
  3183. uplims = np.broadcast_to(uplims, len(data)).astype(bool)
  3184. # a nested list structure that expands to (xl,xh),(yl,yh),(zl,zh),
  3185. # where x/y/z and l/h correspond to dimensions and low/high
  3186. # positions of errorbars in a dimension we're looping over
  3187. coorderr = [
  3188. _extract_errs(err * dir_vector[i], coord, lolims, uplims)
  3189. for i, coord in enumerate([x, y, z])]
  3190. (xl, xh), (yl, yh), (zl, zh) = coorderr
  3191. # draws capmarkers - flat caps orthogonal to the error bars
  3192. nolims = ~(lolims | uplims)
  3193. if nolims.any() and capsize > 0:
  3194. lo_caps_xyz = _apply_mask([xl, yl, zl], nolims & everymask)
  3195. hi_caps_xyz = _apply_mask([xh, yh, zh], nolims & everymask)
  3196. # setting '_' for z-caps and '|' for x- and y-caps;
  3197. # these markers will rotate as the viewing angle changes
  3198. cap_lo = art3d.Line3D(*lo_caps_xyz, ls='',
  3199. marker=capmarker[i_zdir],
  3200. axlim_clip=axlim_clip,
  3201. **eb_cap_style)
  3202. cap_hi = art3d.Line3D(*hi_caps_xyz, ls='',
  3203. marker=capmarker[i_zdir],
  3204. axlim_clip=axlim_clip,
  3205. **eb_cap_style)
  3206. self.add_line(cap_lo)
  3207. self.add_line(cap_hi)
  3208. caplines.append(cap_lo)
  3209. caplines.append(cap_hi)
  3210. if lolims.any():
  3211. xh0, yh0, zh0 = _apply_mask([xh, yh, zh], lolims & everymask)
  3212. self.quiver(xh0, yh0, zh0, *dir_vector, **eb_quiver_style)
  3213. if uplims.any():
  3214. xl0, yl0, zl0 = _apply_mask([xl, yl, zl], uplims & everymask)
  3215. self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style)
  3216. errline = art3d.Line3DCollection(np.array(coorderr).T,
  3217. axlim_clip=axlim_clip,
  3218. **eb_lines_style)
  3219. self.add_collection(errline)
  3220. errlines.append(errline)
  3221. coorderrs.append(coorderr)
  3222. coorderrs = np.array(coorderrs)
  3223. def _digout_minmax(err_arr, coord_label):
  3224. return (np.nanmin(err_arr[:, i_xyz[coord_label], :, :]),
  3225. np.nanmax(err_arr[:, i_xyz[coord_label], :, :]))
  3226. minx, maxx = _digout_minmax(coorderrs, 'x')
  3227. miny, maxy = _digout_minmax(coorderrs, 'y')
  3228. minz, maxz = _digout_minmax(coorderrs, 'z')
  3229. self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
  3230. # Adapting errorbar containers for 3d case, assuming z-axis points "up"
  3231. errorbar_container = mcontainer.ErrorbarContainer(
  3232. (data_line, tuple(caplines), tuple(errlines)),
  3233. has_xerr=(xerr is not None or yerr is not None),
  3234. has_yerr=(zerr is not None),
  3235. label=label)
  3236. self.containers.append(errorbar_container)
  3237. return errlines, caplines, limmarks
  3238. def get_tightbbox(self, renderer=None, *, call_axes_locator=True,
  3239. bbox_extra_artists=None, for_layout_only=False):
  3240. ret = super().get_tightbbox(renderer,
  3241. call_axes_locator=call_axes_locator,
  3242. bbox_extra_artists=bbox_extra_artists,
  3243. for_layout_only=for_layout_only)
  3244. batch = [ret]
  3245. if self._axis3don:
  3246. for axis in self._axis_map.values():
  3247. if axis.get_visible():
  3248. axis_bb = martist._get_tightbbox_for_layout_only(
  3249. axis, renderer)
  3250. if axis_bb:
  3251. batch.append(axis_bb)
  3252. return mtransforms.Bbox.union(batch)
  3253. @_preprocess_data()
  3254. def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-',
  3255. bottom=0, label=None, orientation='z', axlim_clip=False):
  3256. """
  3257. Create a 3D stem plot.
  3258. A stem plot draws lines perpendicular to a baseline, and places markers
  3259. at the heads. By default, the baseline is defined by *x* and *y*, and
  3260. stems are drawn vertically from *bottom* to *z*.
  3261. Parameters
  3262. ----------
  3263. x, y, z : array-like
  3264. The positions of the heads of the stems. The stems are drawn along
  3265. the *orientation*-direction from the baseline at *bottom* (in the
  3266. *orientation*-coordinate) to the heads. By default, the *x* and *y*
  3267. positions are used for the baseline and *z* for the head position,
  3268. but this can be changed by *orientation*.
  3269. linefmt : str, default: 'C0-'
  3270. A string defining the properties of the vertical lines. Usually,
  3271. this will be a color or a color and a linestyle:
  3272. ========= =============
  3273. Character Line Style
  3274. ========= =============
  3275. ``'-'`` solid line
  3276. ``'--'`` dashed line
  3277. ``'-.'`` dash-dot line
  3278. ``':'`` dotted line
  3279. ========= =============
  3280. Note: While it is technically possible to specify valid formats
  3281. other than color or color and linestyle (e.g. 'rx' or '-.'), this
  3282. is beyond the intention of the method and will most likely not
  3283. result in a reasonable plot.
  3284. markerfmt : str, default: 'C0o'
  3285. A string defining the properties of the markers at the stem heads.
  3286. basefmt : str, default: 'C3-'
  3287. A format string defining the properties of the baseline.
  3288. bottom : float, default: 0
  3289. The position of the baseline, in *orientation*-coordinates.
  3290. label : str, optional
  3291. The label to use for the stems in legends.
  3292. orientation : {'x', 'y', 'z'}, default: 'z'
  3293. The direction along which stems are drawn.
  3294. axlim_clip : bool, default: False
  3295. Whether to hide stems that are outside the axes limits.
  3296. .. versionadded:: 3.10
  3297. data : indexable object, optional
  3298. DATA_PARAMETER_PLACEHOLDER
  3299. Returns
  3300. -------
  3301. `.StemContainer`
  3302. The container may be treated like a tuple
  3303. (*markerline*, *stemlines*, *baseline*)
  3304. Examples
  3305. --------
  3306. .. plot:: gallery/mplot3d/stem3d_demo.py
  3307. """
  3308. from matplotlib.container import StemContainer
  3309. had_data = self.has_data()
  3310. _api.check_in_list(['x', 'y', 'z'], orientation=orientation)
  3311. xlim = (np.min(x), np.max(x))
  3312. ylim = (np.min(y), np.max(y))
  3313. zlim = (np.min(z), np.max(z))
  3314. # Determine the appropriate plane for the baseline and the direction of
  3315. # stemlines based on the value of orientation.
  3316. if orientation == 'x':
  3317. basex, basexlim = y, ylim
  3318. basey, baseylim = z, zlim
  3319. lines = [[(bottom, thisy, thisz), (thisx, thisy, thisz)]
  3320. for thisx, thisy, thisz in zip(x, y, z)]
  3321. elif orientation == 'y':
  3322. basex, basexlim = x, xlim
  3323. basey, baseylim = z, zlim
  3324. lines = [[(thisx, bottom, thisz), (thisx, thisy, thisz)]
  3325. for thisx, thisy, thisz in zip(x, y, z)]
  3326. else:
  3327. basex, basexlim = x, xlim
  3328. basey, baseylim = y, ylim
  3329. lines = [[(thisx, thisy, bottom), (thisx, thisy, thisz)]
  3330. for thisx, thisy, thisz in zip(x, y, z)]
  3331. # Determine style for stem lines.
  3332. linestyle, linemarker, linecolor = _process_plot_format(linefmt)
  3333. if linestyle is None:
  3334. linestyle = mpl.rcParams['lines.linestyle']
  3335. # Plot everything in required order.
  3336. baseline, = self.plot(basex, basey, basefmt, zs=bottom,
  3337. zdir=orientation, label='_nolegend_')
  3338. stemlines = art3d.Line3DCollection(
  3339. lines, linestyles=linestyle, colors=linecolor, label='_nolegend_',
  3340. axlim_clip=axlim_clip)
  3341. self.add_collection(stemlines)
  3342. markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_')
  3343. stem_container = StemContainer((markerline, stemlines, baseline),
  3344. label=label)
  3345. self.add_container(stem_container)
  3346. jx, jy, jz = art3d.juggle_axes(basexlim, baseylim, [bottom, bottom],
  3347. orientation)
  3348. self.auto_scale_xyz([*jx, *xlim], [*jy, *ylim], [*jz, *zlim], had_data)
  3349. return stem_container
  3350. stem3D = stem
  3351. def get_test_data(delta=0.05):
  3352. """Return a tuple X, Y, Z with a test data set."""
  3353. x = y = np.arange(-3.0, 3.0, delta)
  3354. X, Y = np.meshgrid(x, y)
  3355. Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
  3356. Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
  3357. (2 * np.pi * 0.5 * 1.5))
  3358. Z = Z2 - Z1
  3359. X = X * 10
  3360. Y = Y * 10
  3361. Z = Z * 500
  3362. return X, Y, Z
  3363. class _Quaternion:
  3364. """
  3365. Quaternions
  3366. consisting of scalar, along 1, and vector, with components along i, j, k
  3367. """
  3368. def __init__(self, scalar, vector):
  3369. self.scalar = scalar
  3370. self.vector = np.array(vector)
  3371. def __neg__(self):
  3372. return self.__class__(-self.scalar, -self.vector)
  3373. def __mul__(self, other):
  3374. """
  3375. Product of two quaternions
  3376. i*i = j*j = k*k = i*j*k = -1
  3377. Quaternion multiplication can be expressed concisely
  3378. using scalar and vector parts,
  3379. see <https://en.wikipedia.org/wiki/Quaternion#Scalar_and_vector_parts>
  3380. """
  3381. return self.__class__(
  3382. self.scalar*other.scalar - np.dot(self.vector, other.vector),
  3383. self.scalar*other.vector + self.vector*other.scalar
  3384. + np.cross(self.vector, other.vector))
  3385. def conjugate(self):
  3386. """The conjugate quaternion -(1/2)*(q+i*q*i+j*q*j+k*q*k)"""
  3387. return self.__class__(self.scalar, -self.vector)
  3388. @property
  3389. def norm(self):
  3390. """The 2-norm, q*q', a scalar"""
  3391. return self.scalar*self.scalar + np.dot(self.vector, self.vector)
  3392. def normalize(self):
  3393. """Scaling such that norm equals 1"""
  3394. n = np.sqrt(self.norm)
  3395. return self.__class__(self.scalar/n, self.vector/n)
  3396. def reciprocal(self):
  3397. """The reciprocal, 1/q = q'/(q*q') = q' / norm(q)"""
  3398. n = self.norm
  3399. return self.__class__(self.scalar/n, -self.vector/n)
  3400. def __div__(self, other):
  3401. return self*other.reciprocal()
  3402. __truediv__ = __div__
  3403. def rotate(self, v):
  3404. # Rotate the vector v by the quaternion q, i.e.,
  3405. # calculate (the vector part of) q*v/q
  3406. v = self.__class__(0, v)
  3407. v = self*v/self
  3408. return v.vector
  3409. def __eq__(self, other):
  3410. return (self.scalar == other.scalar) and (self.vector == other.vector).all
  3411. def __repr__(self):
  3412. return "_Quaternion({}, {})".format(repr(self.scalar), repr(self.vector))
  3413. @classmethod
  3414. def rotate_from_to(cls, r1, r2):
  3415. """
  3416. The quaternion for the shortest rotation from vector r1 to vector r2
  3417. i.e., q = sqrt(r2*r1'), normalized.
  3418. If r1 and r2 are antiparallel, then the result is ambiguous;
  3419. a normal vector will be returned, and a warning will be issued.
  3420. """
  3421. k = np.cross(r1, r2)
  3422. nk = np.linalg.norm(k)
  3423. th = np.arctan2(nk, np.dot(r1, r2))
  3424. th /= 2
  3425. if nk == 0: # r1 and r2 are parallel or anti-parallel
  3426. if np.dot(r1, r2) < 0:
  3427. warnings.warn("Rotation defined by anti-parallel vectors is ambiguous")
  3428. k = np.zeros(3)
  3429. k[np.argmin(r1*r1)] = 1 # basis vector most perpendicular to r1-r2
  3430. k = np.cross(r1, k)
  3431. k = k / np.linalg.norm(k) # unit vector normal to r1-r2
  3432. q = cls(0, k)
  3433. else:
  3434. q = cls(1, [0, 0, 0]) # = 1, no rotation
  3435. else:
  3436. q = cls(np.cos(th), k*np.sin(th)/nk)
  3437. return q
  3438. @classmethod
  3439. def from_cardan_angles(cls, elev, azim, roll):
  3440. """
  3441. Converts the angles to a quaternion
  3442. q = exp((roll/2)*e_x)*exp((elev/2)*e_y)*exp((-azim/2)*e_z)
  3443. i.e., the angles are a kind of Tait-Bryan angles, -z,y',x".
  3444. The angles should be given in radians, not degrees.
  3445. """
  3446. ca, sa = np.cos(azim/2), np.sin(azim/2)
  3447. ce, se = np.cos(elev/2), np.sin(elev/2)
  3448. cr, sr = np.cos(roll/2), np.sin(roll/2)
  3449. qw = ca*ce*cr + sa*se*sr
  3450. qx = ca*ce*sr - sa*se*cr
  3451. qy = ca*se*cr + sa*ce*sr
  3452. qz = ca*se*sr - sa*ce*cr
  3453. return cls(qw, [qx, qy, qz])
  3454. def as_cardan_angles(self):
  3455. """
  3456. The inverse of `from_cardan_angles()`.
  3457. Note that the angles returned are in radians, not degrees.
  3458. The angles are not sensitive to the quaternion's norm().
  3459. """
  3460. qw = self.scalar
  3461. qx, qy, qz = self.vector[..., :]
  3462. azim = np.arctan2(2*(-qw*qz+qx*qy), qw*qw+qx*qx-qy*qy-qz*qz)
  3463. elev = np.arcsin(np.clip(2*(qw*qy+qz*qx)/(qw*qw+qx*qx+qy*qy+qz*qz), -1, 1))
  3464. roll = np.arctan2(2*(qw*qx-qy*qz), qw*qw-qx*qx-qy*qy+qz*qz)
  3465. return elev, azim, roll