123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955 |
- """
- axes3d.py, original mplot3d version by John Porter
- Created: 23 Sep 2005
- Parts fixed by Reinier Heeres <reinier@heeres.eu>
- Minor additions by Ben Axelrod <baxelrod@coroware.com>
- Significant updates and revisions by Ben Root <ben.v.root@gmail.com>
- Module containing Axes3D, an object which can plot 3D objects on a
- 2D matplotlib figure.
- """
- from collections import defaultdict
- from functools import reduce
- import math
- import textwrap
- import numpy as np
- from matplotlib import artist
- import matplotlib.axes as maxes
- import matplotlib.cbook as cbook
- import matplotlib.collections as mcoll
- import matplotlib.colors as mcolors
- import matplotlib.docstring as docstring
- import matplotlib.scale as mscale
- import matplotlib.transforms as mtransforms
- from matplotlib.axes import Axes, rcParams
- from matplotlib.axes._base import _axis_method_wrapper
- from matplotlib.transforms import Bbox
- from matplotlib.tri.triangulation import Triangulation
- from . import art3d
- from . import proj3d
- from . import axis3d
- @cbook.deprecated("3.2", alternative="Bbox.unit()")
- def unit_bbox():
- box = Bbox(np.array([[0, 0], [1, 1]]))
- return box
- @cbook._define_aliases({
- "xlim3d": ["xlim"], "ylim3d": ["ylim"], "zlim3d": ["zlim"]})
- class Axes3D(Axes):
- """
- 3D axes object.
- """
- name = '3d'
- _shared_z_axes = cbook.Grouper()
- def __init__(
- self, fig, rect=None, *args,
- azim=-60, elev=30, sharez=None, proj_type='persp',
- box_aspect=None,
- **kwargs):
- """
- Parameters
- ----------
- fig : Figure
- The parent figure.
- rect : (float, float, float, float)
- The ``(left, bottom, width, height)`` axes position.
- azim : float, default: -60
- Azimuthal viewing angle.
- elev : float, default: 30
- Elevation viewing angle.
- sharez : Axes3D, optional
- Other axes to share z-limits with.
- proj_type : {'persp', 'ortho'}
- The projection type, default 'persp'.
- **kwargs
- Other optional keyword arguments:
- %(Axes3D)s
- Notes
- -----
- .. versionadded:: 1.2.1
- The *sharez* parameter.
- """
- if rect is None:
- rect = [0.0, 0.0, 1.0, 1.0]
- self.initial_azim = azim
- self.initial_elev = elev
- self.set_proj_type(proj_type)
- self.xy_viewLim = Bbox.unit()
- self.zz_viewLim = Bbox.unit()
- self.xy_dataLim = Bbox.unit()
- self.zz_dataLim = Bbox.unit()
- # inhibit autoscale_view until the axes are defined
- # they can't be defined until Axes.__init__ has been called
- self.view_init(self.initial_elev, self.initial_azim)
- self._sharez = sharez
- if sharez is not None:
- self._shared_z_axes.join(self, sharez)
- self._adjustable = 'datalim'
- super().__init__(
- fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs
- )
- # Disable drawing of axes by base class
- super().set_axis_off()
- # Enable drawing of axes by Axes3D class
- self.set_axis_on()
- self.M = None
- # func used to format z -- fall back on major formatters
- self.fmt_zdata = None
- if self.zaxis is not None:
- self._zcid = self.zaxis.callbacks.connect(
- 'units finalize', lambda: self._on_units_changed(scalez=True))
- else:
- self._zcid = None
- self.mouse_init()
- self.figure.canvas.mpl_connect(
- 'motion_notify_event', self._on_move),
- self.figure.canvas.mpl_connect(
- 'button_press_event', self._button_press),
- self.figure.canvas.mpl_connect(
- 'button_release_event', self._button_release),
- self.set_top_view()
- self.patch.set_linewidth(0)
- # Calculate the pseudo-data width and height
- pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)])
- self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0]
- self.figure.add_axes(self)
- # mplot3d currently manages its own spines and needs these turned off
- # for bounding box calculations
- for k in self.spines.keys():
- self.spines[k].set_visible(False)
- def set_axis_off(self):
- self._axis3don = False
- self.stale = True
- def set_axis_on(self):
- self._axis3don = True
- self.stale = True
- def convert_zunits(self, z):
- """
- For artists in an axes, if the zaxis has units support,
- convert *z* using zaxis unit type
- .. versionadded:: 1.2.1
- """
- return self.zaxis.convert_units(z)
- def _process_unit_info(self, xdata=None, ydata=None, zdata=None,
- kwargs=None):
- """Update the axis instances based on unit *kwargs* if given."""
- super()._process_unit_info(xdata=xdata, ydata=ydata, kwargs=kwargs)
- if self.xaxis is None or self.yaxis is None or self.zaxis is None:
- return
- if zdata is not None:
- # we only need to update if there is nothing set yet.
- if not self.zaxis.have_units():
- self.zaxis.update_units(xdata)
- # process kwargs 2nd since these will override default units
- if kwargs is not None:
- zunits = kwargs.pop('zunits', self.zaxis.units)
- if zunits != self.zaxis.units:
- self.zaxis.set_units(zunits)
- # If the units being set imply a different converter,
- # we need to update.
- if zdata is not None:
- self.zaxis.update_units(zdata)
- def set_top_view(self):
- # this happens to be the right view for the viewing coordinates
- # moved up and to the left slightly to fit labels and axes
- xdwl = 0.95 / self.dist
- xdw = 0.9 / self.dist
- ydwl = 0.95 / self.dist
- ydw = 0.9 / self.dist
- # This is purposely using the 2D Axes's set_xlim and set_ylim,
- # because we are trying to place our viewing pane.
- super().set_xlim(-xdwl, xdw, auto=None)
- super().set_ylim(-ydwl, ydw, auto=None)
- def _init_axis(self):
- """Init 3D axes; overrides creation of regular X/Y axes."""
- self.xaxis = axis3d.XAxis('x', self.xy_viewLim.intervalx,
- self.xy_dataLim.intervalx, self)
- self.yaxis = axis3d.YAxis('y', self.xy_viewLim.intervaly,
- self.xy_dataLim.intervaly, self)
- self.zaxis = axis3d.ZAxis('z', self.zz_viewLim.intervalx,
- self.zz_dataLim.intervalx, self)
- for ax in self.xaxis, self.yaxis, self.zaxis:
- ax.init3d()
- def get_zaxis(self):
- """Return the ``ZAxis`` (`~.axis3d.Axis`) instance."""
- return self.zaxis
- get_zgridlines = _axis_method_wrapper("zaxis", "get_gridlines")
- get_zticklines = _axis_method_wrapper("zaxis", "get_ticklines")
- @cbook.deprecated("3.1", alternative="xaxis", pending=True)
- @property
- def w_xaxis(self):
- return self.xaxis
- @cbook.deprecated("3.1", alternative="yaxis", pending=True)
- @property
- def w_yaxis(self):
- return self.yaxis
- @cbook.deprecated("3.1", alternative="zaxis", pending=True)
- @property
- def w_zaxis(self):
- return self.zaxis
- def _get_axis_list(self):
- return super()._get_axis_list() + (self.zaxis, )
- def unit_cube(self, vals=None):
- minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims()
- return [(minx, miny, minz),
- (maxx, miny, minz),
- (maxx, maxy, minz),
- (minx, maxy, minz),
- (minx, miny, maxz),
- (maxx, miny, maxz),
- (maxx, maxy, maxz),
- (minx, maxy, maxz)]
- def tunit_cube(self, vals=None, M=None):
- if M is None:
- M = self.M
- xyzs = self.unit_cube(vals)
- tcube = proj3d.proj_points(xyzs, M)
- return tcube
- def tunit_edges(self, vals=None, M=None):
- tc = self.tunit_cube(vals, M)
- edges = [(tc[0], tc[1]),
- (tc[1], tc[2]),
- (tc[2], tc[3]),
- (tc[3], tc[0]),
- (tc[0], tc[4]),
- (tc[1], tc[5]),
- (tc[2], tc[6]),
- (tc[3], tc[7]),
- (tc[4], tc[5]),
- (tc[5], tc[6]),
- (tc[6], tc[7]),
- (tc[7], tc[4])]
- return edges
- def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
- """
- Set the aspect ratios.
- Axes 3D does not current support any aspect but 'auto' which fills
- the axes with the data limits.
- To simulate having equal aspect in data space, set the ratio
- of your data limits to match the value of `~.get_box_aspect`.
- To control box aspect ratios use `~.Axes3D.set_box_aspect`.
- Parameters
- ----------
- aspect : {'auto'}
- Possible values:
- ========= ==================================================
- value description
- ========= ==================================================
- 'auto' automatic; fill the position rectangle with data.
- ========= ==================================================
- adjustable : None
- Currently ignored by Axes3D
- If not *None*, this defines which parameter will be adjusted to
- meet the required aspect. See `.set_adjustable` for further
- details.
- anchor : None or str or 2-tuple of float, optional
- If not *None*, this defines where the Axes will be drawn if there
- is extra space due to aspect constraints. The most common way to
- to specify the anchor are abbreviations of cardinal directions:
- ===== =====================
- value description
- ===== =====================
- 'C' centered
- 'SW' lower left corner
- 'S' middle of bottom edge
- 'SE' lower right corner
- etc.
- ===== =====================
- See `.set_anchor` for further details.
- share : bool, default: False
- If ``True``, apply the settings to all shared Axes.
- See Also
- --------
- mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
- """
- if aspect != 'auto':
- raise NotImplementedError(
- "Axes3D currently only supports the aspect argument "
- f"'auto'. You passed in {aspect!r}."
- )
- if share:
- axes = {*self._shared_x_axes.get_siblings(self),
- *self._shared_y_axes.get_siblings(self),
- *self._shared_z_axes.get_siblings(self),
- }
- else:
- axes = {self}
- for ax in axes:
- ax._aspect = aspect
- ax.stale = True
- if anchor is not None:
- self.set_anchor(anchor, share=share)
- def set_anchor(self, anchor, share=False):
- # docstring inherited
- if not (anchor in mtransforms.Bbox.coefs or len(anchor) == 2):
- raise ValueError('anchor must be among %s' %
- ', '.join(mtransforms.Bbox.coefs))
- if share:
- axes = {*self._shared_x_axes.get_siblings(self),
- *self._shared_y_axes.get_siblings(self),
- *self._shared_z_axes.get_siblings(self),
- }
- else:
- axes = {self}
- for ax in axes:
- ax._anchor = anchor
- ax.stale = True
- def set_box_aspect(self, aspect, *, zoom=1):
- """
- Set the axes box aspect.
- The box aspect is the ratio of height to width in display
- units for each face of the box when viewed perpendicular to
- that face. This is not to be confused with the data aspect
- (which for Axes3D is always 'auto'). The default ratios are
- 4:4:3 (x:y:z).
- To simulate having equal aspect in data space, set the box
- aspect to match your data range in each dimension.
- *zoom* controls the overall size of the Axes3D in the figure.
- Parameters
- ----------
- aspect : 3-tuple of floats or None
- Changes the physical dimensions of the Axes3D, such that the ratio
- of the axis lengths in display units is x:y:z.
- If None, defaults to 4:4:3
- zoom : float
- Control overall size of the Axes3D in the figure.
- """
- if aspect is None:
- aspect = np.asarray((4, 4, 3), dtype=float)
- else:
- orig_aspect = aspect
- aspect = np.asarray(aspect, dtype=float)
- if aspect.shape != (3,):
- raise ValueError(
- "You must pass a 3-tuple that can be cast to floats. "
- f"You passed {orig_aspect!r}"
- )
- # default scale tuned to match the mpl32 appearance.
- aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect)
- self._box_aspect = aspect
- self.stale = True
- def apply_aspect(self, position=None):
- if position is None:
- position = self.get_position(original=True)
- # in the superclass, we would go through and actually deal with axis
- # scales and box/datalim. Those are all irrelevant - all we need to do
- # is make sure our coordinate system is square.
- figW, figH = self.get_figure().get_size_inches()
- fig_aspect = figH / figW
- box_aspect = 1
- pb = position.frozen()
- pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
- self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
- @artist.allow_rasterization
- def draw(self, renderer):
- # draw the background patch
- self.patch.draw(renderer)
- self._frameon = False
- # first, set the aspect
- # this is duplicated from `axes._base._AxesBase.draw`
- # but must be called before any of the artist are drawn as
- # it adjusts the view limits and the size of the bounding box
- # of the axes
- locator = self.get_axes_locator()
- if locator:
- pos = locator(self, renderer)
- self.apply_aspect(pos)
- else:
- self.apply_aspect()
- # add the projection matrix to the renderer
- self.M = self.get_proj()
- renderer.M = self.M
- renderer.vvec = self.vvec
- renderer.eye = self.eye
- renderer.get_axis_position = self.get_axis_position
- # Calculate projection of collections and patches and zorder them.
- # Make sure they are drawn above the grids.
- zorder_offset = max(axis.get_zorder()
- for axis in self._get_axis_list()) + 1
- for i, col in enumerate(
- sorted(self.collections,
- key=lambda col: col.do_3d_projection(renderer),
- reverse=True)):
- col.zorder = zorder_offset + i
- for i, patch in enumerate(
- sorted(self.patches,
- key=lambda patch: patch.do_3d_projection(renderer),
- reverse=True)):
- patch.zorder = zorder_offset + i
- if self._axis3don:
- # Draw panes first
- for axis in self._get_axis_list():
- axis.draw_pane(renderer)
- # Then axes
- for axis in self._get_axis_list():
- axis.draw(renderer)
- # Then rest
- super().draw(renderer)
- def get_axis_position(self):
- vals = self.get_w_lims()
- tc = self.tunit_cube(vals, self.M)
- xhigh = tc[1][2] > tc[2][2]
- yhigh = tc[3][2] > tc[2][2]
- zhigh = tc[0][2] > tc[2][2]
- return xhigh, yhigh, zhigh
- def _on_units_changed(self, scalex=False, scaley=False, scalez=False):
- """
- Callback for processing changes to axis units.
- Currently forces updates of data limits and view limits.
- """
- self.relim()
- self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez)
- def update_datalim(self, xys, **kwargs):
- pass
- def get_autoscale_on(self):
- """
- Get whether autoscaling is applied for all axes on plot commands
- .. versionadded:: 1.1.0
- This function was added, but not tested. Please report any bugs.
- """
- return super().get_autoscale_on() and self.get_autoscalez_on()
- def get_autoscalez_on(self):
- """
- Get whether autoscaling for the z-axis is applied on plot commands
- .. versionadded:: 1.1.0
- This function was added, but not tested. Please report any bugs.
- """
- return self._autoscaleZon
- def set_autoscale_on(self, b):
- """
- Set whether autoscaling is applied on plot commands
- .. versionadded:: 1.1.0
- This function was added, but not tested. Please report any bugs.
- Parameters
- ----------
- b : bool
- """
- super().set_autoscale_on(b)
- self.set_autoscalez_on(b)
- def set_autoscalez_on(self, b):
- """
- Set whether autoscaling for the z-axis is applied on plot commands
- .. versionadded:: 1.1.0
- Parameters
- ----------
- b : bool
- """
- self._autoscaleZon = b
- def set_zmargin(self, m):
- """
- Set padding of Z data limits prior to autoscaling.
- *m* times the data interval will be added to each
- end of that interval before it is used in autoscaling.
- accepts: float in range 0 to 1
- .. versionadded:: 1.1.0
- """
- if m < 0 or m > 1:
- raise ValueError("margin must be in range 0 to 1")
- self._zmargin = m
- self.stale = True
- def margins(self, *margins, x=None, y=None, z=None, tight=True):
- """
- Convenience method to set or retrieve autoscaling margins.
- Call signatures::
- margins()
- returns xmargin, ymargin, zmargin
- ::
- margins(margin)
- margins(xmargin, ymargin, zmargin)
- margins(x=xmargin, y=ymargin, z=zmargin)
- margins(..., tight=False)
- All forms above set the xmargin, ymargin and zmargin
- parameters. All keyword parameters are optional. A single
- positional argument specifies xmargin, ymargin and zmargin.
- Passing both positional and keyword arguments for xmargin,
- ymargin, and/or zmargin is invalid.
- The *tight* parameter
- is passed to :meth:`autoscale_view`, which is executed after
- a margin is changed; the default here is *True*, on the
- assumption that when margins are specified, no additional
- padding to match tick marks is usually desired. Setting
- *tight* to *None* will preserve the previous setting.
- Specifying any margin changes only the autoscaling; for example,
- if *xmargin* is not None, then *xmargin* times the X data
- interval will be added to each end of that interval before
- it is used in autoscaling.
- .. versionadded:: 1.1.0
- """
- if margins and x is not None and y is not None and z is not None:
- raise TypeError('Cannot pass both positional and keyword '
- 'arguments for x, y, and/or z.')
- elif len(margins) == 1:
- x = y = z = margins[0]
- elif len(margins) == 3:
- x, y, z = margins
- elif margins:
- raise TypeError('Must pass a single positional argument for all '
- 'margins, or one for each margin (x, y, z).')
- if x is None and y is None and z is None:
- if tight is not True:
- cbook._warn_external(f'ignoring tight={tight!r} in get mode')
- return self._xmargin, self._ymargin, self._zmargin
- if x is not None:
- self.set_xmargin(x)
- if y is not None:
- self.set_ymargin(y)
- if z is not None:
- self.set_zmargin(z)
- self.autoscale_view(
- tight=tight, scalex=(x is not None), scaley=(y is not None),
- scalez=(z is not None)
- )
- def autoscale(self, enable=True, axis='both', tight=None):
- """
- Convenience method for simple axis view autoscaling.
- See :meth:`matplotlib.axes.Axes.autoscale` for full explanation.
- Note that this function behaves the same, but for all
- three axes. Therefore, 'z' can be passed for *axis*,
- and 'both' applies to all three axes.
- .. versionadded:: 1.1.0
- """
- if enable is None:
- scalex = True
- scaley = True
- scalez = True
- else:
- if axis in ['x', 'both']:
- self._autoscaleXon = scalex = bool(enable)
- else:
- scalex = False
- if axis in ['y', 'both']:
- self._autoscaleYon = scaley = bool(enable)
- else:
- scaley = False
- if axis in ['z', 'both']:
- self._autoscaleZon = scalez = bool(enable)
- else:
- scalez = False
- self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
- scalez=scalez)
- def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
- # This updates the bounding boxes as to keep a record as to what the
- # minimum sized rectangular volume holds the data.
- X = np.reshape(X, -1)
- Y = np.reshape(Y, -1)
- self.xy_dataLim.update_from_data_xy(
- np.column_stack([X, Y]), not had_data)
- if Z is not None:
- Z = np.reshape(Z, -1)
- self.zz_dataLim.update_from_data_xy(
- np.column_stack([Z, Z]), not had_data)
- # Let autoscale_view figure out how to use this data.
- self.autoscale_view()
- def autoscale_view(self, tight=None, scalex=True, scaley=True,
- scalez=True):
- """
- Autoscale the view limits using the data limits.
- See :meth:`matplotlib.axes.Axes.autoscale_view` for documentation.
- Note that this function applies to the 3D axes, and as such
- adds the *scalez* to the function arguments.
- .. versionchanged:: 1.1.0
- Function signature was changed to better match the 2D version.
- *tight* is now explicitly a kwarg and placed first.
- .. versionchanged:: 1.2.1
- This is now fully functional.
- """
- # This method looks at the rectangular volume (see above)
- # of data and decides how to scale the view portal to fit it.
- if tight is None:
- # if image data only just use the datalim
- _tight = self._tight or (
- len(self.images) > 0
- and len(self.lines) == len(self.patches) == 0)
- else:
- _tight = self._tight = bool(tight)
- if scalex and self._autoscaleXon:
- self._shared_x_axes.clean()
- x0, x1 = self.xy_dataLim.intervalx
- xlocator = self.xaxis.get_major_locator()
- x0, x1 = xlocator.nonsingular(x0, x1)
- if self._xmargin > 0:
- delta = (x1 - x0) * self._xmargin
- x0 -= delta
- x1 += delta
- if not _tight:
- x0, x1 = xlocator.view_limits(x0, x1)
- self.set_xbound(x0, x1)
- if scaley and self._autoscaleYon:
- self._shared_y_axes.clean()
- y0, y1 = self.xy_dataLim.intervaly
- ylocator = self.yaxis.get_major_locator()
- y0, y1 = ylocator.nonsingular(y0, y1)
- if self._ymargin > 0:
- delta = (y1 - y0) * self._ymargin
- y0 -= delta
- y1 += delta
- if not _tight:
- y0, y1 = ylocator.view_limits(y0, y1)
- self.set_ybound(y0, y1)
- if scalez and self._autoscaleZon:
- self._shared_z_axes.clean()
- z0, z1 = self.zz_dataLim.intervalx
- zlocator = self.zaxis.get_major_locator()
- z0, z1 = zlocator.nonsingular(z0, z1)
- if self._zmargin > 0:
- delta = (z1 - z0) * self._zmargin
- z0 -= delta
- z1 += delta
- if not _tight:
- z0, z1 = zlocator.view_limits(z0, z1)
- self.set_zbound(z0, z1)
- def get_w_lims(self):
- """Get 3D world limits."""
- minx, maxx = self.get_xlim3d()
- miny, maxy = self.get_ylim3d()
- minz, maxz = self.get_zlim3d()
- return minx, maxx, miny, maxy, minz, maxz
- def set_xlim3d(self, left=None, right=None, emit=True, auto=False,
- *, xmin=None, xmax=None):
- """
- Set 3D x limits.
- See :meth:`matplotlib.axes.Axes.set_xlim` for full documentation.
- """
- if right is None and np.iterable(left):
- left, right = left
- if xmin is not None:
- if left is not None:
- raise TypeError('Cannot pass both `xmin` and `left`')
- left = xmin
- if xmax is not None:
- if right is not None:
- raise TypeError('Cannot pass both `xmax` and `right`')
- right = xmax
- self._process_unit_info(xdata=(left, right))
- left = self._validate_converted_limits(left, self.convert_xunits)
- right = self._validate_converted_limits(right, self.convert_xunits)
- old_left, old_right = self.get_xlim()
- if left is None:
- left = old_left
- if right is None:
- right = old_right
- if left == right:
- cbook._warn_external(
- f"Attempting to set identical left == right == {left} results "
- f"in singular transformations; automatically expanding.")
- reverse = left > right
- left, right = self.xaxis.get_major_locator().nonsingular(left, right)
- left, right = self.xaxis.limit_range_for_scale(left, right)
- # cast to bool to avoid bad interaction between python 3.8 and np.bool_
- left, right = sorted([left, right], reverse=bool(reverse))
- self.xy_viewLim.intervalx = (left, right)
- if auto is not None:
- self._autoscaleXon = bool(auto)
- if emit:
- self.callbacks.process('xlim_changed', self)
- # Call all of the other x-axes that are shared with this one
- for other in self._shared_x_axes.get_siblings(self):
- if other is not self:
- other.set_xlim(self.xy_viewLim.intervalx,
- emit=False, auto=auto)
- if other.figure != self.figure:
- other.figure.canvas.draw_idle()
- self.stale = True
- return left, right
- def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False,
- *, ymin=None, ymax=None):
- """
- Set 3D y limits.
- See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation.
- """
- if top is None and np.iterable(bottom):
- bottom, top = bottom
- if ymin is not None:
- if bottom is not None:
- raise TypeError('Cannot pass both `ymin` and `bottom`')
- bottom = ymin
- if ymax is not None:
- if top is not None:
- raise TypeError('Cannot pass both `ymax` and `top`')
- top = ymax
- self._process_unit_info(ydata=(bottom, top))
- bottom = self._validate_converted_limits(bottom, self.convert_yunits)
- top = self._validate_converted_limits(top, self.convert_yunits)
- old_bottom, old_top = self.get_ylim()
- if bottom is None:
- bottom = old_bottom
- if top is None:
- top = old_top
- if bottom == top:
- cbook._warn_external(
- f"Attempting to set identical bottom == top == {bottom} "
- f"results in singular transformations; automatically "
- f"expanding.")
- swapped = bottom > top
- bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top)
- bottom, top = self.yaxis.limit_range_for_scale(bottom, top)
- if swapped:
- bottom, top = top, bottom
- self.xy_viewLim.intervaly = (bottom, top)
- if auto is not None:
- self._autoscaleYon = bool(auto)
- if emit:
- self.callbacks.process('ylim_changed', self)
- # Call all of the other y-axes that are shared with this one
- for other in self._shared_y_axes.get_siblings(self):
- if other is not self:
- other.set_ylim(self.xy_viewLim.intervaly,
- emit=False, auto=auto)
- if other.figure != self.figure:
- other.figure.canvas.draw_idle()
- self.stale = True
- return bottom, top
- def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False,
- *, zmin=None, zmax=None):
- """
- Set 3D z limits.
- See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation
- """
- if top is None and np.iterable(bottom):
- bottom, top = bottom
- if zmin is not None:
- if bottom is not None:
- raise TypeError('Cannot pass both `zmin` and `bottom`')
- bottom = zmin
- if zmax is not None:
- if top is not None:
- raise TypeError('Cannot pass both `zmax` and `top`')
- top = zmax
- self._process_unit_info(zdata=(bottom, top))
- bottom = self._validate_converted_limits(bottom, self.convert_zunits)
- top = self._validate_converted_limits(top, self.convert_zunits)
- old_bottom, old_top = self.get_zlim()
- if bottom is None:
- bottom = old_bottom
- if top is None:
- top = old_top
- if bottom == top:
- cbook._warn_external(
- f"Attempting to set identical bottom == top == {bottom} "
- f"results in singular transformations; automatically "
- f"expanding.")
- swapped = bottom > top
- bottom, top = self.zaxis.get_major_locator().nonsingular(bottom, top)
- bottom, top = self.zaxis.limit_range_for_scale(bottom, top)
- if swapped:
- bottom, top = top, bottom
- self.zz_viewLim.intervalx = (bottom, top)
- if auto is not None:
- self._autoscaleZon = bool(auto)
- if emit:
- self.callbacks.process('zlim_changed', self)
- # Call all of the other y-axes that are shared with this one
- for other in self._shared_z_axes.get_siblings(self):
- if other is not self:
- other.set_zlim(self.zz_viewLim.intervalx,
- emit=False, auto=auto)
- if other.figure != self.figure:
- other.figure.canvas.draw_idle()
- self.stale = True
- return bottom, top
- def get_xlim3d(self):
- return tuple(self.xy_viewLim.intervalx)
- get_xlim3d.__doc__ = maxes.Axes.get_xlim.__doc__
- if get_xlim3d.__doc__ is not None:
- get_xlim3d.__doc__ += """
- .. versionchanged:: 1.1.0
- This function now correctly refers to the 3D x-limits
- """
- def get_ylim3d(self):
- return tuple(self.xy_viewLim.intervaly)
- get_ylim3d.__doc__ = maxes.Axes.get_ylim.__doc__
- if get_ylim3d.__doc__ is not None:
- get_ylim3d.__doc__ += """
- .. versionchanged:: 1.1.0
- This function now correctly refers to the 3D y-limits.
- """
- def get_zlim3d(self):
- """Get 3D z limits."""
- return tuple(self.zz_viewLim.intervalx)
- def get_zscale(self):
- """
- Return the zaxis scale string %s
- """ % (", ".join(mscale.get_scale_names()))
- return self.zaxis.get_scale()
- # We need to slightly redefine these to pass scalez=False
- # to their calls of autoscale_view.
- def set_xscale(self, value, **kwargs):
- self.xaxis._set_scale(value, **kwargs)
- self.autoscale_view(scaley=False, scalez=False)
- self._update_transScale()
- self.stale = True
- def set_yscale(self, value, **kwargs):
- self.yaxis._set_scale(value, **kwargs)
- self.autoscale_view(scalex=False, scalez=False)
- self._update_transScale()
- self.stale = True
- def set_zscale(self, value, **kwargs):
- self.zaxis._set_scale(value, **kwargs)
- self.autoscale_view(scalex=False, scaley=False)
- self._update_transScale()
- self.stale = True
- set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map(
- """
- Set the {}-axis scale.
- Parameters
- ----------
- value : {{"linear"}}
- The axis scale type to apply. 3D axes currently only support
- linear scales; other scales yield nonsensical results.
- **kwargs
- Keyword arguments are nominally forwarded to the scale class, but
- none of them is applicable for linear scales.
- """.format,
- ["x", "y", "z"])
- get_zticks = _axis_method_wrapper("zaxis", "get_ticklocs")
- set_zticks = _axis_method_wrapper("zaxis", "set_ticks")
- get_zmajorticklabels = _axis_method_wrapper("zaxis", "get_majorticklabels")
- get_zminorticklabels = _axis_method_wrapper("zaxis", "get_minorticklabels")
- get_zticklabels = _axis_method_wrapper("zaxis", "get_ticklabels")
- set_zticklabels = _axis_method_wrapper(
- "zaxis", "_set_ticklabels",
- doc_sub={"Axis.set_ticks": "Axes3D.set_zticks"})
- zaxis_date = _axis_method_wrapper("zaxis", "axis_date")
- if zaxis_date.__doc__:
- zaxis_date.__doc__ += textwrap.dedent("""
- Notes
- -----
- This function is merely provided for completeness, but 3d axes do not
- support dates for ticks, and so this may not work as expected.
- """)
- def clabel(self, *args, **kwargs):
- """Currently not implemented for 3D axes, and returns *None*."""
- return None
- def view_init(self, elev=None, azim=None):
- """
- Set the elevation and azimuth of the axes in degrees (not radians).
- This can be used to rotate the axes programmatically.
- 'elev' stores the elevation angle in the z plane (in degrees).
- 'azim' stores the azimuth angle in the (x, y) plane (in degrees).
- if 'elev' or 'azim' are None (default), then the initial value
- is used which was specified in the :class:`Axes3D` constructor.
- """
- self.dist = 10
- if elev is None:
- self.elev = self.initial_elev
- else:
- self.elev = elev
- if azim is None:
- self.azim = self.initial_azim
- else:
- self.azim = azim
- def set_proj_type(self, proj_type):
- """
- Set the projection type.
- Parameters
- ----------
- proj_type : {'persp', 'ortho'}
- """
- self._projection = cbook._check_getitem({
- 'persp': proj3d.persp_transformation,
- 'ortho': proj3d.ortho_transformation,
- }, proj_type=proj_type)
- def get_proj(self):
- """Create the projection matrix from the current viewing position."""
- # elev stores the elevation angle in the z plane
- # azim stores the azimuth angle in the x,y plane
- #
- # dist is the distance of the eye viewing point from the object
- # point.
- relev, razim = np.pi * self.elev/180, np.pi * self.azim/180
- xmin, xmax = self.get_xlim3d()
- ymin, ymax = self.get_ylim3d()
- zmin, zmax = self.get_zlim3d()
- # transform to uniform world coordinates 0-1, 0-1, 0-1
- worldM = proj3d.world_transformation(xmin, xmax,
- ymin, ymax,
- zmin, zmax,
- pb_aspect=self._box_aspect)
- # look into the middle of the new coordinates
- R = self._box_aspect / 2
- xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
- yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist
- zp = R[2] + np.sin(relev) * self.dist
- E = np.array((xp, yp, zp))
- self.eye = E
- self.vvec = R - E
- self.vvec = self.vvec / np.linalg.norm(self.vvec)
- if abs(relev) > np.pi/2:
- # upside down
- V = np.array((0, 0, -1))
- else:
- V = np.array((0, 0, 1))
- zfront, zback = -self.dist, self.dist
- viewM = proj3d.view_transformation(E, R, V)
- projM = self._projection(zfront, zback)
- M0 = np.dot(viewM, worldM)
- M = np.dot(projM, M0)
- return M
- def mouse_init(self, rotate_btn=1, zoom_btn=3):
- """
- Set the mouse buttons for 3D rotation and zooming.
- Parameters
- ----------
- rotate_btn : int or list of int, default: 1
- The mouse button or buttons to use for 3D rotation of the axes.
- zoom_btn : int or list of int, default: 3
- The mouse button or buttons to use to zoom the 3D axes.
- """
- self.button_pressed = None
- # coerce scalars into array-like, then convert into
- # a regular list to avoid comparisons against None
- # which breaks in recent versions of numpy.
- self._rotate_btn = np.atleast_1d(rotate_btn).tolist()
- self._zoom_btn = np.atleast_1d(zoom_btn).tolist()
- def disable_mouse_rotation(self):
- """Disable mouse buttons for 3D rotation and zooming."""
- self.mouse_init(rotate_btn=[], zoom_btn=[])
- def can_zoom(self):
- """
- Return *True* if this axes supports the zoom box button functionality.
- 3D axes objects do not use the zoom box button.
- """
- return False
- def can_pan(self):
- """
- Return *True* if this axes supports the pan/zoom button functionality.
- 3D axes objects do not use the pan/zoom button.
- """
- return False
- def cla(self):
- # docstring inherited.
- super().cla()
- self.zaxis.cla()
- if self._sharez is not None:
- self.zaxis.major = self._sharez.zaxis.major
- self.zaxis.minor = self._sharez.zaxis.minor
- z0, z1 = self._sharez.get_zlim()
- self.set_zlim(z0, z1, emit=False, auto=None)
- self.zaxis._set_scale(self._sharez.zaxis.get_scale())
- else:
- self.zaxis._set_scale('linear')
- try:
- self.set_zlim(0, 1)
- except TypeError:
- pass
- self._autoscaleZon = True
- self._zmargin = 0
- self.grid(rcParams['axes3d.grid'])
- def _button_press(self, event):
- if event.inaxes == self:
- self.button_pressed = event.button
- self.sx, self.sy = event.xdata, event.ydata
- toolbar = getattr(self.figure.canvas, "toolbar")
- if toolbar and toolbar._nav_stack() is None:
- self.figure.canvas.toolbar.push_current()
- def _button_release(self, event):
- self.button_pressed = None
- toolbar = getattr(self.figure.canvas, "toolbar")
- if toolbar:
- self.figure.canvas.toolbar.push_current()
- def _get_view(self):
- # docstring inherited
- return (self.get_xlim(), self.get_ylim(), self.get_zlim(),
- self.elev, self.azim)
- def _set_view(self, view):
- # docstring inherited
- xlim, ylim, zlim, elev, azim = view
- self.set(xlim=xlim, ylim=ylim, zlim=zlim)
- self.elev = elev
- self.azim = azim
- def format_zdata(self, z):
- """
- Return *z* string formatted. This function will use the
- :attr:`fmt_zdata` attribute if it is callable, else will fall
- back on the zaxis major formatter
- """
- try:
- return self.fmt_zdata(z)
- except (AttributeError, TypeError):
- func = self.zaxis.get_major_formatter().format_data_short
- val = func(z)
- return val
- def format_coord(self, xd, yd):
- """
- Given the 2D view coordinates attempt to guess a 3D coordinate.
- Looks for the nearest edge to the point and then assumes that
- the point is at the same z location as the nearest point on the edge.
- """
- if self.M is None:
- return ''
- if self.button_pressed in self._rotate_btn:
- return 'azimuth={:.0f} deg, elevation={:.0f} deg '.format(
- self.azim, self.elev)
- # ignore xd and yd and display angles instead
- # nearest edge
- p0, p1 = min(self.tunit_edges(),
- key=lambda edge: proj3d._line2d_seg_dist(
- edge[0], edge[1], (xd, yd)))
- # scale the z value to match
- x0, y0, z0 = p0
- x1, y1, z1 = p1
- d0 = np.hypot(x0-xd, y0-yd)
- d1 = np.hypot(x1-xd, y1-yd)
- dt = d0+d1
- z = d1/dt * z0 + d0/dt * z1
- x, y, z = proj3d.inv_transform(xd, yd, z, self.M)
- xs = self.format_xdata(x)
- ys = self.format_ydata(y)
- zs = self.format_zdata(z)
- return 'x=%s, y=%s, z=%s' % (xs, ys, zs)
- def _on_move(self, event):
- """
- Mouse moving.
- By default, button-1 rotates and button-3 zooms; these buttons can be
- modified via `mouse_init`.
- """
- if not self.button_pressed:
- return
- if self.M is None:
- return
- x, y = event.xdata, event.ydata
- # In case the mouse is out of bounds.
- if x is None:
- return
- dx, dy = x - self.sx, y - self.sy
- w = self._pseudo_w
- h = self._pseudo_h
- self.sx, self.sy = x, y
- # Rotation
- if self.button_pressed in self._rotate_btn:
- # rotate viewing point
- # get the x and y pixel coords
- if dx == 0 and dy == 0:
- return
- self.elev = art3d._norm_angle(self.elev - (dy/h)*180)
- self.azim = art3d._norm_angle(self.azim - (dx/w)*180)
- self.get_proj()
- self.stale = True
- self.figure.canvas.draw_idle()
- # elif self.button_pressed == 2:
- # pan view
- # project xv, yv, zv -> xw, yw, zw
- # pan
- # pass
- # Zoom
- elif self.button_pressed in self._zoom_btn:
- # zoom view
- # hmmm..this needs some help from clipping....
- minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
- df = 1-((h - dy)/h)
- dx = (maxx-minx)*df
- dy = (maxy-miny)*df
- dz = (maxz-minz)*df
- self.set_xlim3d(minx - dx, maxx + dx)
- self.set_ylim3d(miny - dy, maxy + dy)
- self.set_zlim3d(minz - dz, maxz + dz)
- self.get_proj()
- self.figure.canvas.draw_idle()
- def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
- """
- Set zlabel. See doc for `.set_ylabel` for description.
- """
- if labelpad is not None:
- self.zaxis.labelpad = labelpad
- return self.zaxis.set_label_text(zlabel, fontdict, **kwargs)
- def get_zlabel(self):
- """
- Get the z-label text string.
- .. versionadded:: 1.1.0
- This function was added, but not tested. Please report any bugs.
- """
- label = self.zaxis.get_label()
- return label.get_text()
- # Axes rectangle characteristics
- def get_frame_on(self):
- """Get whether the 3D axes panels are drawn."""
- return self._frameon
- def set_frame_on(self, b):
- """
- Set whether the 3D axes panels are drawn.
- Parameters
- ----------
- b : bool
- """
- self._frameon = bool(b)
- self.stale = True
- def grid(self, b=True, **kwargs):
- """
- Set / unset 3D grid.
- .. note::
- Currently, this function does not behave the same as
- :meth:`matplotlib.axes.Axes.grid`, but it is intended to
- eventually support that behavior.
- .. versionadded:: 1.1.0
- """
- # TODO: Operate on each axes separately
- if len(kwargs):
- b = True
- self._draw_grid = b
- self.stale = True
- def locator_params(self, axis='both', tight=None, **kwargs):
- """
- Convenience method for controlling tick locators.
- See :meth:`matplotlib.axes.Axes.locator_params` for full
- documentation. Note that this is for Axes3D objects,
- therefore, setting *axis* to 'both' will result in the
- parameters being set for all three axes. Also, *axis*
- can also take a value of 'z' to apply parameters to the
- z axis.
- .. versionadded:: 1.1.0
- This function was added, but not tested. Please report any bugs.
- """
- _x = axis in ['x', 'both']
- _y = axis in ['y', 'both']
- _z = axis in ['z', 'both']
- if _x:
- self.xaxis.get_major_locator().set_params(**kwargs)
- if _y:
- self.yaxis.get_major_locator().set_params(**kwargs)
- if _z:
- self.zaxis.get_major_locator().set_params(**kwargs)
- self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z)
- def tick_params(self, axis='both', **kwargs):
- """
- Convenience method for changing the appearance of ticks and
- tick labels.
- See :meth:`matplotlib.axes.Axes.tick_params` for more complete
- documentation.
- The only difference is that setting *axis* to 'both' will
- mean that the settings are applied to all three axes. Also,
- the *axis* parameter also accepts a value of 'z', which
- would mean to apply to only the z-axis.
- Also, because of how Axes3D objects are drawn very differently
- from regular 2D axes, some of these settings may have
- ambiguous meaning. For simplicity, the 'z' axis will
- accept settings as if it was like the 'y' axis.
- .. note::
- Axes3D currently ignores some of these settings.
- .. versionadded:: 1.1.0
- """
- cbook._check_in_list(['x', 'y', 'z', 'both'], axis=axis)
- if axis in ['x', 'y', 'both']:
- super().tick_params(axis, **kwargs)
- if axis in ['z', 'both']:
- zkw = dict(kwargs)
- zkw.pop('top', None)
- zkw.pop('bottom', None)
- zkw.pop('labeltop', None)
- zkw.pop('labelbottom', None)
- self.zaxis.set_tick_params(**zkw)
- # data limits, ticks, tick labels, and formatting
- def invert_zaxis(self):
- """
- Invert the z-axis.
- .. versionadded:: 1.1.0
- This function was added, but not tested. Please report any bugs.
- """
- bottom, top = self.get_zlim()
- self.set_zlim(top, bottom, auto=None)
- def zaxis_inverted(self):
- """
- Returns True if the z-axis is inverted.
- .. versionadded:: 1.1.0
- """
- bottom, top = self.get_zlim()
- return top < bottom
- def get_zbound(self):
- """
- Return the lower and upper z-axis bounds, in increasing order.
- .. versionadded:: 1.1.0
- """
- bottom, top = self.get_zlim()
- if bottom < top:
- return bottom, top
- else:
- return top, bottom
- def set_zbound(self, lower=None, upper=None):
- """
- Set the lower and upper numerical bounds of the z-axis.
- This method will honor axes inversion regardless of parameter order.
- It will not change the autoscaling setting (`.get_autoscalez_on()`).
- .. versionadded:: 1.1.0
- """
- if upper is None and np.iterable(lower):
- lower, upper = lower
- old_lower, old_upper = self.get_zbound()
- if lower is None:
- lower = old_lower
- if upper is None:
- upper = old_upper
- self.set_zlim(sorted((lower, upper),
- reverse=bool(self.zaxis_inverted())),
- auto=None)
- def text(self, x, y, z, s, zdir=None, **kwargs):
- """
- Add text to the plot. kwargs will be passed on to Axes.text,
- except for the *zdir* keyword, which sets the direction to be
- used as the z direction.
- """
- text = super().text(x, y, s, **kwargs)
- art3d.text_2d_to_3d(text, z, zdir)
- return text
- text3D = text
- text2D = Axes.text
- def plot(self, xs, ys, *args, zdir='z', **kwargs):
- """
- Plot 2D or 3D data.
- Parameters
- ----------
- xs : 1D array-like
- x coordinates of vertices.
- ys : 1D array-like
- y coordinates of vertices.
- zs : float or 1D array-like
- z coordinates of vertices; either one for all points or one for
- each point.
- zdir : {'x', 'y', 'z'}, default: 'z'
- When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
- **kwargs
- Other arguments are forwarded to `matplotlib.axes.Axes.plot`.
- """
- had_data = self.has_data()
- # `zs` can be passed positionally or as keyword; checking whether
- # args[0] is a string matches the behavior of 2D `plot` (via
- # `_process_plot_var_args`).
- if args and not isinstance(args[0], str):
- zs, *args = args
- if 'zs' in kwargs:
- raise TypeError("plot() for multiple values for argument 'z'")
- else:
- zs = kwargs.pop('zs', 0)
- # Match length
- zs = np.broadcast_to(zs, np.shape(xs))
- lines = super().plot(xs, ys, *args, **kwargs)
- for line in lines:
- art3d.line_2d_to_3d(line, zs=zs, zdir=zdir)
- xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir)
- self.auto_scale_xyz(xs, ys, zs, had_data)
- return lines
- plot3D = plot
- def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
- vmax=None, lightsource=None, **kwargs):
- """
- Create a surface plot.
- By default it will be colored in shades of a solid color, but it also
- supports color mapping by supplying the *cmap* argument.
- .. note::
- The *rcount* and *ccount* kwargs, which both default to 50,
- determine the maximum number of samples used in each direction. If
- the input data is larger, it will be downsampled (by slicing) to
- these numbers of points.
- .. note::
- To maximize rendering speed consider setting *rstride* and *cstride*
- to divisors of the number of rows minus 1 and columns minus 1
- respectively. For example, given 51 rows rstride can be any of the
- divisors of 50.
- Similarly, a setting of *rstride* and *cstride* equal to 1 (or
- *rcount* and *ccount* equal the number of rows and columns) can use
- the optimized path.
- Parameters
- ----------
- X, Y, Z : 2d arrays
- Data values.
- rcount, ccount : int
- Maximum number of samples used in each direction. If the input
- data is larger, it will be downsampled (by slicing) to these
- numbers of points. Defaults to 50.
- .. versionadded:: 2.0
- rstride, cstride : int
- Downsampling stride in each direction. These arguments are
- mutually exclusive with *rcount* and *ccount*. If only one of
- *rstride* or *cstride* is set, the other defaults to 10.
- 'classic' mode uses a default of ``rstride = cstride = 10`` instead
- of the new default of ``rcount = ccount = 50``.
- color : color-like
- Color of the surface patches.
- cmap : Colormap
- Colormap of the surface patches.
- facecolors : array-like of colors.
- Colors of each individual patch.
- norm : Normalize
- Normalization for the colormap.
- vmin, vmax : float
- Bounds for the normalization.
- shade : bool, default: True
- Whether to shade the facecolors. Shading is always disabled when
- *cmap* is specified.
- lightsource : `~matplotlib.colors.LightSource`
- The lightsource to use when *shade* is True.
- **kwargs
- Other arguments are forwarded to `.Poly3DCollection`.
- """
- had_data = self.has_data()
- if Z.ndim != 2:
- raise ValueError("Argument Z must be 2-dimensional.")
- if np.any(np.isnan(Z)):
- cbook._warn_external(
- "Z contains NaN values. This may result in rendering "
- "artifacts.")
- # TODO: Support masked arrays
- X, Y, Z = np.broadcast_arrays(X, Y, Z)
- rows, cols = Z.shape
- has_stride = 'rstride' in kwargs or 'cstride' in kwargs
- has_count = 'rcount' in kwargs or 'ccount' in kwargs
- if has_stride and has_count:
- raise ValueError("Cannot specify both stride and count arguments")
- rstride = kwargs.pop('rstride', 10)
- cstride = kwargs.pop('cstride', 10)
- rcount = kwargs.pop('rcount', 50)
- ccount = kwargs.pop('ccount', 50)
- if rcParams['_internal.classic_mode']:
- # Strides have priority over counts in classic mode.
- # So, only compute strides from counts
- # if counts were explicitly given
- compute_strides = has_count
- else:
- # If the strides are provided then it has priority.
- # Otherwise, compute the strides from the counts.
- compute_strides = not has_stride
- if compute_strides:
- rstride = int(max(np.ceil(rows / rcount), 1))
- cstride = int(max(np.ceil(cols / ccount), 1))
- if 'facecolors' in kwargs:
- fcolors = kwargs.pop('facecolors')
- else:
- color = kwargs.pop('color', None)
- if color is None:
- color = self._get_lines.get_next_color()
- color = np.array(mcolors.to_rgba(color))
- fcolors = None
- cmap = kwargs.get('cmap', None)
- shade = kwargs.pop('shade', cmap is None)
- if shade is None:
- cbook.warn_deprecated(
- "3.1",
- message="Passing shade=None to Axes3D.plot_surface() is "
- "deprecated since matplotlib 3.1 and will change its "
- "semantic or raise an error in matplotlib 3.3. "
- "Please use shade=False instead.")
- colset = [] # the sampled facecolor
- if (rows - 1) % rstride == 0 and \
- (cols - 1) % cstride == 0 and \
- fcolors is None:
- polys = np.stack(
- [cbook._array_patch_perimeters(a, rstride, cstride)
- for a in (X, Y, Z)],
- axis=-1)
- else:
- # evenly spaced, and including both endpoints
- row_inds = list(range(0, rows-1, rstride)) + [rows-1]
- col_inds = list(range(0, cols-1, cstride)) + [cols-1]
- polys = []
- for rs, rs_next in zip(row_inds[:-1], row_inds[1:]):
- for cs, cs_next in zip(col_inds[:-1], col_inds[1:]):
- ps = [
- # +1 ensures we share edges between polygons
- cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
- for a in (X, Y, Z)
- ]
- # ps = np.stack(ps, axis=-1)
- ps = np.array(ps).T
- polys.append(ps)
- if fcolors is not None:
- colset.append(fcolors[rs][cs])
- # note that the striding causes some polygons to have more coordinates
- # than others
- polyc = art3d.Poly3DCollection(polys, *args, **kwargs)
- if fcolors is not None:
- if shade:
- colset = self._shade_colors(
- colset, self._generate_normals(polys), lightsource)
- polyc.set_facecolors(colset)
- polyc.set_edgecolors(colset)
- elif cmap:
- # can't always vectorize, because polys might be jagged
- if isinstance(polys, np.ndarray):
- avg_z = polys[..., 2].mean(axis=-1)
- else:
- avg_z = np.array([ps[:, 2].mean() for ps in polys])
- polyc.set_array(avg_z)
- if vmin is not None or vmax is not None:
- polyc.set_clim(vmin, vmax)
- if norm is not None:
- polyc.set_norm(norm)
- else:
- if shade:
- colset = self._shade_colors(
- color, self._generate_normals(polys), lightsource)
- else:
- colset = color
- polyc.set_facecolors(colset)
- self.add_collection(polyc)
- self.auto_scale_xyz(X, Y, Z, had_data)
- return polyc
- def _generate_normals(self, polygons):
- """
- Compute the normals of a list of polygons.
- Normals point towards the viewer for a face with its vertices in
- counterclockwise order, following the right hand rule.
- Uses three points equally spaced around the polygon.
- This normal of course might not make sense for polygons with more than
- three points not lying in a plane, but it's a plausible and fast
- approximation.
- Parameters
- ----------
- polygons: list of (M_i, 3) array-like, or (..., M, 3) array-like
- A sequence of polygons to compute normals for, which can have
- varying numbers of vertices. If the polygons all have the same
- number of vertices and array is passed, then the operation will
- be vectorized.
- Returns
- -------
- normals: (..., 3) array-like
- A normal vector estimated for the polygon.
- """
- if isinstance(polygons, np.ndarray):
- # optimization: polygons all have the same number of points, so can
- # vectorize
- n = polygons.shape[-2]
- i1, i2, i3 = 0, n//3, 2*n//3
- v1 = polygons[..., i1, :] - polygons[..., i2, :]
- v2 = polygons[..., i2, :] - polygons[..., i3, :]
- else:
- # The subtraction doesn't vectorize because polygons is jagged.
- v1 = np.empty((len(polygons), 3))
- v2 = np.empty((len(polygons), 3))
- for poly_i, ps in enumerate(polygons):
- n = len(ps)
- i1, i2, i3 = 0, n//3, 2*n//3
- v1[poly_i, :] = ps[i1, :] - ps[i2, :]
- v2[poly_i, :] = ps[i2, :] - ps[i3, :]
- return np.cross(v1, v2)
- def _shade_colors(self, color, normals, lightsource=None):
- """
- Shade *color* using normal vectors given by *normals*.
- *color* can also be an array of the same length as *normals*.
- """
- if lightsource is None:
- # chosen for backwards-compatibility
- lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
- with np.errstate(invalid="ignore"):
- shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
- @ lightsource.direction)
- mask = ~np.isnan(shade)
- if mask.any():
- # convert dot product to allowed shading fractions
- in_norm = mcolors.Normalize(-1, 1)
- out_norm = mcolors.Normalize(0.3, 1).inverse
- def norm(x):
- return out_norm(in_norm(x))
- shade[~mask] = 0
- color = mcolors.to_rgba_array(color)
- # shape of color should be (M, 4) (where M is number of faces)
- # shape of shade should be (M,)
- # colors should have final shape of (M, 4)
- alpha = color[:, 3]
- colors = norm(shade)[:, np.newaxis] * color
- colors[:, 3] = alpha
- else:
- colors = np.asanyarray(color).copy()
- return colors
- def plot_wireframe(self, X, Y, Z, *args, **kwargs):
- """
- Plot a 3D wireframe.
- .. note::
- The *rcount* and *ccount* kwargs, which both default to 50,
- determine the maximum number of samples used in each direction. If
- the input data is larger, it will be downsampled (by slicing) to
- these numbers of points.
- Parameters
- ----------
- X, Y, Z : 2d arrays
- Data values.
- rcount, ccount : int
- Maximum number of samples used in each direction. If the input
- data is larger, it will be downsampled (by slicing) to these
- numbers of points. Setting a count to zero causes the data to be
- not sampled in the corresponding direction, producing a 3D line
- plot rather than a wireframe plot. Defaults to 50.
- .. versionadded:: 2.0
- rstride, cstride : int
- Downsampling stride in each direction. These arguments are
- mutually exclusive with *rcount* and *ccount*. If only one of
- *rstride* or *cstride* is set, the other defaults to 1. Setting a
- stride to zero causes the data to be not sampled in the
- corresponding direction, producing a 3D line plot rather than a
- wireframe plot.
- 'classic' mode uses a default of ``rstride = cstride = 1`` instead
- of the new default of ``rcount = ccount = 50``.
- **kwargs
- Other arguments are forwarded to `.Line3DCollection`.
- """
- had_data = self.has_data()
- if Z.ndim != 2:
- raise ValueError("Argument Z must be 2-dimensional.")
- # FIXME: Support masked arrays
- X, Y, Z = np.broadcast_arrays(X, Y, Z)
- rows, cols = Z.shape
- has_stride = 'rstride' in kwargs or 'cstride' in kwargs
- has_count = 'rcount' in kwargs or 'ccount' in kwargs
- if has_stride and has_count:
- raise ValueError("Cannot specify both stride and count arguments")
- rstride = kwargs.pop('rstride', 1)
- cstride = kwargs.pop('cstride', 1)
- rcount = kwargs.pop('rcount', 50)
- ccount = kwargs.pop('ccount', 50)
- if rcParams['_internal.classic_mode']:
- # Strides have priority over counts in classic mode.
- # So, only compute strides from counts
- # if counts were explicitly given
- if has_count:
- rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
- cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
- else:
- # If the strides are provided then it has priority.
- # Otherwise, compute the strides from the counts.
- if not has_stride:
- rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
- cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
- # We want two sets of lines, one running along the "rows" of
- # Z and another set of lines running along the "columns" of Z.
- # This transpose will make it easy to obtain the columns.
- tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z)
- if rstride:
- rii = list(range(0, rows, rstride))
- # Add the last index only if needed
- if rows > 0 and rii[-1] != (rows - 1):
- rii += [rows-1]
- else:
- rii = []
- if cstride:
- cii = list(range(0, cols, cstride))
- # Add the last index only if needed
- if cols > 0 and cii[-1] != (cols - 1):
- cii += [cols-1]
- else:
- cii = []
- if rstride == 0 and cstride == 0:
- raise ValueError("Either rstride or cstride must be non zero")
- # If the inputs were empty, then just
- # reset everything.
- if Z.size == 0:
- rii = []
- cii = []
- xlines = [X[i] for i in rii]
- ylines = [Y[i] for i in rii]
- zlines = [Z[i] for i in rii]
- txlines = [tX[i] for i in cii]
- tylines = [tY[i] for i in cii]
- tzlines = [tZ[i] for i in cii]
- lines = ([list(zip(xl, yl, zl))
- for xl, yl, zl in zip(xlines, ylines, zlines)]
- + [list(zip(xl, yl, zl))
- for xl, yl, zl in zip(txlines, tylines, tzlines)])
- linec = art3d.Line3DCollection(lines, *args, **kwargs)
- self.add_collection(linec)
- self.auto_scale_xyz(X, Y, Z, had_data)
- return linec
- def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
- lightsource=None, **kwargs):
- """
- Plot a triangulated surface.
- The (optional) triangulation can be specified in one of two ways;
- either::
- plot_trisurf(triangulation, ...)
- where triangulation is a `~matplotlib.tri.Triangulation` object, or::
- plot_trisurf(X, Y, ...)
- plot_trisurf(X, Y, triangles, ...)
- plot_trisurf(X, Y, triangles=triangles, ...)
- in which case a Triangulation object will be created. See
- `.Triangulation` for a explanation of these possibilities.
- The remaining arguments are::
- plot_trisurf(..., Z)
- where *Z* is the array of values to contour, one per point
- in the triangulation.
- Parameters
- ----------
- X, Y, Z : array-like
- Data values as 1D arrays.
- color
- Color of the surface patches.
- cmap
- A colormap for the surface patches.
- norm : Normalize
- An instance of Normalize to map values to colors.
- vmin, vmax : float, default: None
- Minimum and maximum value to map.
- shade : bool, default: True
- Whether to shade the facecolors. Shading is always disabled when
- *cmap* is specified.
- lightsource : `~matplotlib.colors.LightSource`
- The lightsource to use when *shade* is True.
- **kwargs
- All other arguments are passed on to
- :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`
- Examples
- --------
- .. plot:: gallery/mplot3d/trisurf3d.py
- .. plot:: gallery/mplot3d/trisurf3d_2.py
- .. versionadded:: 1.2.0
- """
- had_data = self.has_data()
- # TODO: Support custom face colours
- if color is None:
- color = self._get_lines.get_next_color()
- color = np.array(mcolors.to_rgba(color))
- cmap = kwargs.get('cmap', None)
- shade = kwargs.pop('shade', cmap is None)
- tri, args, kwargs = \
- Triangulation.get_from_args_and_kwargs(*args, **kwargs)
- try:
- z = kwargs.pop('Z')
- except KeyError:
- # We do this so Z doesn't get passed as an arg to PolyCollection
- z, *args = args
- z = np.asarray(z)
- triangles = tri.get_masked_triangles()
- xt = tri.x[triangles]
- yt = tri.y[triangles]
- zt = z[triangles]
- verts = np.stack((xt, yt, zt), axis=-1)
- polyc = art3d.Poly3DCollection(verts, *args, **kwargs)
- if cmap:
- # average over the three points of each triangle
- avg_z = verts[:, :, 2].mean(axis=1)
- polyc.set_array(avg_z)
- if vmin is not None or vmax is not None:
- polyc.set_clim(vmin, vmax)
- if norm is not None:
- polyc.set_norm(norm)
- else:
- if shade:
- normals = self._generate_normals(verts)
- colset = self._shade_colors(color, normals, lightsource)
- else:
- colset = color
- polyc.set_facecolors(colset)
- self.add_collection(polyc)
- self.auto_scale_xyz(tri.x, tri.y, z, had_data)
- return polyc
- def _3d_extend_contour(self, cset, stride=5):
- """
- Extend a contour in 3D by creating
- """
- levels = cset.levels
- colls = cset.collections
- dz = (levels[1] - levels[0]) / 2
- for z, linec in zip(levels, colls):
- paths = linec.get_paths()
- if not paths:
- continue
- topverts = art3d._paths_to_3d_segments(paths, z - dz)
- botverts = art3d._paths_to_3d_segments(paths, z + dz)
- color = linec.get_color()[0]
- polyverts = []
- normals = []
- nsteps = round(len(topverts[0]) / stride)
- if nsteps <= 1:
- if len(topverts[0]) > 1:
- nsteps = 2
- else:
- continue
- stepsize = (len(topverts[0]) - 1) / (nsteps - 1)
- for i in range(int(round(nsteps)) - 1):
- i1 = int(round(i * stepsize))
- i2 = int(round((i + 1) * stepsize))
- polyverts.append([topverts[0][i1],
- topverts[0][i2],
- botverts[0][i2],
- botverts[0][i1]])
- # all polygons have 4 vertices, so vectorize
- polyverts = np.array(polyverts)
- normals = self._generate_normals(polyverts)
- colors = self._shade_colors(color, normals)
- colors2 = self._shade_colors(color, normals)
- polycol = art3d.Poly3DCollection(polyverts,
- facecolors=colors,
- edgecolors=colors2)
- polycol.set_sort_zpos(z)
- self.add_collection3d(polycol)
- for col in colls:
- self.collections.remove(col)
- def add_contour_set(
- self, cset, extend3d=False, stride=5, zdir='z', offset=None):
- zdir = '-' + zdir
- if extend3d:
- self._3d_extend_contour(cset, stride)
- else:
- for z, linec in zip(cset.levels, cset.collections):
- if offset is not None:
- z = offset
- art3d.line_collection_2d_to_3d(linec, z, zdir=zdir)
- def add_contourf_set(self, cset, zdir='z', offset=None):
- zdir = '-' + zdir
- for z, linec in zip(cset.levels, cset.collections):
- if offset is not None:
- z = offset
- art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir)
- linec.set_sort_zpos(z)
- def contour(self, X, Y, Z, *args,
- extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
- """
- Create a 3D contour plot.
- Parameters
- ----------
- X, Y, Z : array-like
- Input data.
- extend3d : bool, default: False
- Whether to extend contour in 3D.
- stride : int
- Step size for extending contour.
- zdir : {'x', 'y', 'z'}, default: 'z'
- The direction to use.
- offset : float, optional
- If specified, plot a projection of the contour lines at this
- position in a plane normal to zdir.
- *args, **kwargs
- Other arguments are forwarded to `matplotlib.axes.Axes.contour`.
- Returns
- -------
- matplotlib.contour.QuadContourSet
- """
- had_data = self.has_data()
- jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
- cset = super().contour(jX, jY, jZ, *args, **kwargs)
- self.add_contour_set(cset, extend3d, stride, zdir, offset)
- self.auto_scale_xyz(X, Y, Z, had_data)
- return cset
- contour3D = contour
- def tricontour(self, *args,
- extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
- """
- Create a 3D contour plot.
- .. versionchanged:: 1.3.0
- Added support for custom triangulations
- .. note::
- This method currently produces incorrect output due to a
- longstanding bug in 3D PolyCollection rendering.
- Parameters
- ----------
- X, Y, Z : array-like
- Input data.
- extend3d : bool, default: False
- Whether to extend contour in 3D.
- stride : int
- Step size for extending contour.
- zdir : {'x', 'y', 'z'}, default: 'z'
- The direction to use.
- offset : float, optional
- If specified, plot a projection of the contour lines at this
- position in a plane normal to zdir.
- *args, **kwargs
- Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`.
- Returns
- -------
- matplotlib.tri.tricontour.TriContourSet
- """
- had_data = self.has_data()
- tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
- *args, **kwargs)
- X = tri.x
- Y = tri.y
- if 'Z' in kwargs:
- Z = kwargs.pop('Z')
- else:
- # We do this so Z doesn't get passed as an arg to Axes.tricontour
- Z, *args = args
- jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
- tri = Triangulation(jX, jY, tri.triangles, tri.mask)
- cset = super().tricontour(tri, jZ, *args, **kwargs)
- self.add_contour_set(cset, extend3d, stride, zdir, offset)
- self.auto_scale_xyz(X, Y, Z, had_data)
- return cset
- def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs):
- """
- Create a 3D filled contour plot.
- Parameters
- ----------
- X, Y, Z : array-like
- Input data.
- zdir : {'x', 'y', 'z'}, default: 'z'
- The direction to use.
- offset : float, optional
- If specified, plot a projection of the contour lines at this
- position in a plane normal to zdir.
- *args, **kwargs
- Other arguments are forwarded to `matplotlib.axes.Axes.contourf`.
- Returns
- -------
- matplotlib.contour.QuadContourSet
- Notes
- -----
- .. versionadded:: 1.1.0
- The *zdir* and *offset* parameters.
- """
- had_data = self.has_data()
- jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
- cset = super().contourf(jX, jY, jZ, *args, **kwargs)
- self.add_contourf_set(cset, zdir, offset)
- self.auto_scale_xyz(X, Y, Z, had_data)
- return cset
- contourf3D = contourf
- def tricontourf(self, *args, zdir='z', offset=None, **kwargs):
- """
- Create a 3D filled contour plot.
- .. note::
- This method currently produces incorrect output due to a
- longstanding bug in 3D PolyCollection rendering.
- Parameters
- ----------
- X, Y, Z : array-like
- Input data.
- zdir : {'x', 'y', 'z'}, default: 'z'
- The direction to use.
- offset : float, optional
- If specified, plot a projection of the contour lines at this
- position in a plane normal to zdir.
- *args, **kwargs
- Other arguments are forwarded to
- `matplotlib.axes.Axes.tricontourf`.
- Returns
- -------
- matplotlib.tri.tricontour.TriContourSet
- Notes
- -----
- .. versionadded:: 1.1.0
- The *zdir* and *offset* parameters.
- .. versionchanged:: 1.3.0
- Added support for custom triangulations
- """
- had_data = self.has_data()
- tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
- *args, **kwargs)
- X = tri.x
- Y = tri.y
- if 'Z' in kwargs:
- Z = kwargs.pop('Z')
- else:
- # We do this so Z doesn't get passed as an arg to Axes.tricontourf
- Z, *args = args
- jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
- tri = Triangulation(jX, jY, tri.triangles, tri.mask)
- cset = super().tricontourf(tri, jZ, *args, **kwargs)
- self.add_contourf_set(cset, zdir, offset)
- self.auto_scale_xyz(X, Y, Z, had_data)
- return cset
- def add_collection3d(self, col, zs=0, zdir='z'):
- """
- Add a 3D collection object to the plot.
- 2D collection types are converted to a 3D version by
- modifying the object and adding z coordinate information.
- Supported are:
- - PolyCollection
- - LineCollection
- - PatchCollection
- """
- zvals = np.atleast_1d(zs)
- zsortval = (np.min(zvals) if zvals.size
- else 0) # FIXME: arbitrary default
- # FIXME: use issubclass() (although, then a 3D collection
- # object would also pass.) Maybe have a collection3d
- # abstract class to test for and exclude?
- if type(col) is mcoll.PolyCollection:
- art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir)
- col.set_sort_zpos(zsortval)
- elif type(col) is mcoll.LineCollection:
- art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir)
- col.set_sort_zpos(zsortval)
- elif type(col) is mcoll.PatchCollection:
- art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir)
- col.set_sort_zpos(zsortval)
- super().add_collection(col)
- def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True,
- *args, **kwargs):
- """
- Create a scatter plot.
- Parameters
- ----------
- xs, ys : array-like
- The data positions.
- zs : float or array-like, default: 0
- The z-positions. Either an array of the same length as *xs* and
- *ys* or a single value to place all points in the same plane.
- zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z'
- The axis direction for the *zs*. This is useful when plotting 2D
- data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting
- *zdir* to 'y' then plots the data to the x-z-plane.
- See also :doc:`/gallery/mplot3d/2dcollections3d`.
- s : float or array-like, default: 20
- The marker size in points**2. Either an array of the same length
- as *xs* and *ys* or a single value to make all markers the same
- size.
- c : color, sequence, or sequence of colors, optional
- The marker color. Possible values:
- - A single color format string.
- - A sequence of colors of length n.
- - A sequence of n numbers to be mapped to colors using *cmap* and
- *norm*.
- - A 2-D array in which the rows are RGB or RGBA.
- For more details see the *c* argument of `~.axes.Axes.scatter`.
- depthshade : bool, default: True
- Whether to shade the scatter markers to give the appearance of
- depth. Each call to ``scatter()`` will perform its depthshading
- independently.
- **kwargs
- All other arguments are passed on to `~.axes.Axes.scatter`.
- Returns
- -------
- paths : `~matplotlib.collections.PathCollection`
- """
- had_data = self.has_data()
- zs_orig = zs
- xs, ys, zs = np.broadcast_arrays(
- *[np.ravel(np.ma.filled(t, np.nan)) for t in [xs, ys, zs]])
- s = np.ma.ravel(s) # This doesn't have to match x, y in size.
- xs, ys, zs, s, c = cbook.delete_masked_points(xs, ys, zs, s, c)
- # For xs and ys, 2D scatter() will do the copying.
- if np.may_share_memory(zs_orig, zs): # Avoid unnecessary copies.
- zs = zs.copy()
- patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs)
- art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir,
- depthshade=depthshade)
- if self._zmargin < 0.05 and xs.size > 0:
- self.set_zmargin(0.05)
- self.auto_scale_xyz(xs, ys, zs, had_data)
- return patches
- scatter3D = scatter
- def bar(self, left, height, zs=0, zdir='z', *args, **kwargs):
- """
- Add 2D bar(s).
- Parameters
- ----------
- left : 1D array-like
- The x coordinates of the left sides of the bars.
- height : 1D array-like
- The height of the bars.
- zs : float or 1D array-like
- Z coordinate of bars; if a single value is specified, it will be
- used for all bars.
- zdir : {'x', 'y', 'z'}, default: 'z'
- When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
- **kwargs
- Other arguments are forwarded to `matplotlib.axes.Axes.bar`.
- Returns
- -------
- mpl_toolkits.mplot3d.art3d.Patch3DCollection
- """
- had_data = self.has_data()
- patches = super().bar(left, height, *args, **kwargs)
- zs = np.broadcast_to(zs, len(left))
- verts = []
- verts_zs = []
- for p, z in zip(patches, zs):
- vs = art3d._get_patch_verts(p)
- verts += vs.tolist()
- verts_zs += [z] * len(vs)
- art3d.patch_2d_to_3d(p, z, zdir)
- if 'alpha' in kwargs:
- p.set_alpha(kwargs['alpha'])
- if len(verts) > 0:
- # the following has to be skipped if verts is empty
- # NOTE: Bugs could still occur if len(verts) > 0,
- # but the "2nd dimension" is empty.
- xs, ys = zip(*verts)
- else:
- xs, ys = [], []
- xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir)
- self.auto_scale_xyz(xs, ys, verts_zs, had_data)
- return patches
- def bar3d(self, x, y, z, dx, dy, dz, color=None,
- zsort='average', shade=True, lightsource=None, *args, **kwargs):
- """
- Generate a 3D barplot.
- This method creates three dimensional barplot where the width,
- depth, height, and color of the bars can all be uniquely set.
- Parameters
- ----------
- x, y, z : array-like
- The coordinates of the anchor point of the bars.
- dx, dy, dz : float or array-like
- The width, depth, and height of the bars, respectively.
- color : sequence of colors, optional
- The color of the bars can be specified globally or
- individually. This parameter can be:
- - A single color, to color all bars the same color.
- - An array of colors of length N bars, to color each bar
- independently.
- - An array of colors of length 6, to color the faces of the
- bars similarly.
- - An array of colors of length 6 * N bars, to color each face
- independently.
- When coloring the faces of the boxes specifically, this is
- the order of the coloring:
- 1. -Z (bottom of box)
- 2. +Z (top of box)
- 3. -Y
- 4. +Y
- 5. -X
- 6. +X
- zsort : str, optional
- The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection`
- shade : bool, default: True
- When true, this shades the dark sides of the bars (relative
- to the plot's source of light).
- lightsource : `~matplotlib.colors.LightSource`
- The lightsource to use when *shade* is True.
- **kwargs
- Any additional keyword arguments are passed onto
- `~.art3d.Poly3DCollection`.
- Returns
- -------
- collection : `~.art3d.Poly3DCollection`
- A collection of three dimensional polygons representing
- the bars.
- """
- had_data = self.has_data()
- x, y, z, dx, dy, dz = np.broadcast_arrays(
- np.atleast_1d(x), y, z, dx, dy, dz)
- minx = np.min(x)
- maxx = np.max(x + dx)
- miny = np.min(y)
- maxy = np.max(y + dy)
- minz = np.min(z)
- maxz = np.max(z + dz)
- # shape (6, 4, 3)
- # All faces are oriented facing outwards - when viewed from the
- # outside, their vertices are in a counterclockwise ordering.
- cuboid = np.array([
- # -z
- (
- (0, 0, 0),
- (0, 1, 0),
- (1, 1, 0),
- (1, 0, 0),
- ),
- # +z
- (
- (0, 0, 1),
- (1, 0, 1),
- (1, 1, 1),
- (0, 1, 1),
- ),
- # -y
- (
- (0, 0, 0),
- (1, 0, 0),
- (1, 0, 1),
- (0, 0, 1),
- ),
- # +y
- (
- (0, 1, 0),
- (0, 1, 1),
- (1, 1, 1),
- (1, 1, 0),
- ),
- # -x
- (
- (0, 0, 0),
- (0, 0, 1),
- (0, 1, 1),
- (0, 1, 0),
- ),
- # +x
- (
- (1, 0, 0),
- (1, 1, 0),
- (1, 1, 1),
- (1, 0, 1),
- ),
- ])
- # indexed by [bar, face, vertex, coord]
- polys = np.empty(x.shape + cuboid.shape)
- # handle each coordinate separately
- for i, p, dp in [(0, x, dx), (1, y, dy), (2, z, dz)]:
- p = p[..., np.newaxis, np.newaxis]
- dp = dp[..., np.newaxis, np.newaxis]
- polys[..., i] = p + dp * cuboid[..., i]
- # collapse the first two axes
- polys = polys.reshape((-1,) + polys.shape[2:])
- facecolors = []
- if color is None:
- color = [self._get_patches_for_fill.get_next_color()]
- color = list(mcolors.to_rgba_array(color))
- if len(color) == len(x):
- # bar colors specified, need to expand to number of faces
- for c in color:
- facecolors.extend([c] * 6)
- else:
- # a single color specified, or face colors specified explicitly
- facecolors = color
- if len(facecolors) < len(x):
- facecolors *= (6 * len(x))
- if shade:
- normals = self._generate_normals(polys)
- sfacecolors = self._shade_colors(facecolors, normals, lightsource)
- else:
- sfacecolors = facecolors
- col = art3d.Poly3DCollection(polys,
- zsort=zsort,
- facecolor=sfacecolors,
- *args, **kwargs)
- self.add_collection(col)
- self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
- return col
- def set_title(self, label, fontdict=None, loc='center', **kwargs):
- # docstring inherited
- ret = super().set_title(label, fontdict=fontdict, loc=loc, **kwargs)
- (x, y) = self.title.get_position()
- self.title.set_y(0.92 * y)
- return ret
- def quiver(self, *args,
- length=1, arrow_length_ratio=.3, pivot='tail', normalize=False,
- **kwargs):
- """
- ax.quiver(X, Y, Z, U, V, W, /, length=1, arrow_length_ratio=.3, \
- pivot='tail', normalize=False, **kwargs)
- Plot a 3D field of arrows.
- The arguments could be array-like or scalars, so long as they
- they can be broadcast together. The arguments can also be
- masked arrays. If an element in any of argument is masked, then
- that corresponding quiver element will not be plotted.
- Parameters
- ----------
- X, Y, Z : array-like
- The x, y and z coordinates of the arrow locations (default is
- tail of arrow; see *pivot* kwarg).
- U, V, W : array-like
- The x, y and z components of the arrow vectors.
- length : float, default: 1
- The length of each quiver.
- arrow_length_ratio : float, default: 0.3
- The ratio of the arrow head with respect to the quiver.
- pivot : {'tail', 'middle', 'tip'}, default: 'tail'
- The part of the arrow that is at the grid point; the arrow
- rotates about this point, hence the name *pivot*.
- normalize : bool, default: False
- Whether all arrows are normalized to have the same length, or keep
- the lengths defined by *u*, *v*, and *w*.
- **kwargs
- Any additional keyword arguments are delegated to
- :class:`~matplotlib.collections.LineCollection`
- """
- def calc_arrows(UVW, angle=15):
- # get unit direction vector perpendicular to (u, v, w)
- x = UVW[:, 0]
- y = UVW[:, 1]
- norm = np.linalg.norm(UVW[:, :2], axis=1)
- x_p = np.divide(y, norm, where=norm != 0, out=np.zeros_like(x))
- y_p = np.divide(-x, norm, where=norm != 0, out=np.ones_like(x))
- # compute the two arrowhead direction unit vectors
- ra = math.radians(angle)
- c = math.cos(ra)
- s = math.sin(ra)
- # construct the rotation matrices of shape (3, 3, n)
- Rpos = np.array(
- [[c + (x_p ** 2) * (1 - c), x_p * y_p * (1 - c), y_p * s],
- [y_p * x_p * (1 - c), c + (y_p ** 2) * (1 - c), -x_p * s],
- [-y_p * s, x_p * s, np.full_like(x_p, c)]])
- # opposite rotation negates all the sin terms
- Rneg = Rpos.copy()
- Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] *= -1
- # Batch n (3, 3) x (3) matrix multiplications ((3, 3, n) x (n, 3)).
- Rpos_vecs = np.einsum("ij...,...j->...i", Rpos, UVW)
- Rneg_vecs = np.einsum("ij...,...j->...i", Rneg, UVW)
- # Stack into (n, 2, 3) result.
- head_dirs = np.stack([Rpos_vecs, Rneg_vecs], axis=1)
- return head_dirs
- had_data = self.has_data()
- # handle args
- argi = 6
- if len(args) < argi:
- raise ValueError('Wrong number of arguments. Expected %d got %d' %
- (argi, len(args)))
- # first 6 arguments are X, Y, Z, U, V, W
- input_args = args[:argi]
- # extract the masks, if any
- masks = [k.mask for k in input_args
- if isinstance(k, np.ma.MaskedArray)]
- # broadcast to match the shape
- bcast = np.broadcast_arrays(*input_args, *masks)
- input_args = bcast[:argi]
- masks = bcast[argi:]
- if masks:
- # combine the masks into one
- mask = reduce(np.logical_or, masks)
- # put mask on and compress
- input_args = [np.ma.array(k, mask=mask).compressed()
- for k in input_args]
- else:
- input_args = [np.ravel(k) for k in input_args]
- if any(len(v) == 0 for v in input_args):
- # No quivers, so just make an empty collection and return early
- linec = art3d.Line3DCollection([], *args[argi:], **kwargs)
- self.add_collection(linec)
- return linec
- shaft_dt = np.array([0., length], dtype=float)
- arrow_dt = shaft_dt * arrow_length_ratio
- cbook._check_in_list(['tail', 'middle', 'tip'], pivot=pivot)
- if pivot == 'tail':
- shaft_dt -= length
- elif pivot == 'middle':
- shaft_dt -= length / 2
- XYZ = np.column_stack(input_args[:3])
- UVW = np.column_stack(input_args[3:argi]).astype(float)
- # Normalize rows of UVW
- norm = np.linalg.norm(UVW, axis=1)
- # If any row of UVW is all zeros, don't make a quiver for it
- mask = norm > 0
- XYZ = XYZ[mask]
- if normalize:
- UVW = UVW[mask] / norm[mask].reshape((-1, 1))
- else:
- UVW = UVW[mask]
- if len(XYZ) > 0:
- # compute the shaft lines all at once with an outer product
- shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1)
- # compute head direction vectors, n heads x 2 sides x 3 dimensions
- head_dirs = calc_arrows(UVW)
- # compute all head lines at once, starting from the shaft ends
- heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs)
- # stack left and right head lines together
- heads = heads.reshape((len(arrow_dt), -1, 3))
- # transpose to get a list of lines
- heads = heads.swapaxes(0, 1)
- lines = [*shafts, *heads]
- else:
- lines = []
- linec = art3d.Line3DCollection(lines, *args[argi:], **kwargs)
- self.add_collection(linec)
- self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data)
- return linec
- quiver3D = quiver
- def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
- lightsource=None, **kwargs):
- """
- ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \
- **kwargs)
- Plot a set of filled voxels
- All voxels are plotted as 1x1x1 cubes on the axis, with
- ``filled[0, 0, 0]`` placed with its lower corner at the origin.
- Occluded faces are not plotted.
- .. versionadded:: 2.1
- Parameters
- ----------
- filled : 3D np.array of bool
- A 3d array of values, with truthy values indicating which voxels
- to fill
- x, y, z : 3D np.array, optional
- The coordinates of the corners of the voxels. This should broadcast
- to a shape one larger in every dimension than the shape of
- *filled*. These can be used to plot non-cubic voxels.
- If not specified, defaults to increasing integers along each axis,
- like those returned by :func:`~numpy.indices`.
- As indicated by the ``/`` in the function signature, these
- arguments can only be passed positionally.
- facecolors, edgecolors : array-like, optional
- The color to draw the faces and edges of the voxels. Can only be
- passed as keyword arguments.
- This parameter can be:
- - A single color value, to color all voxels the same color. This
- can be either a string, or a 1D rgb/rgba array
- - ``None``, the default, to use a single color for the faces, and
- the style default for the edges.
- - A 3D ndarray of color names, with each item the color for the
- corresponding voxel. The size must match the voxels.
- - A 4D ndarray of rgb/rgba data, with the components along the
- last axis.
- shade : bool, default: True
- Whether to shade the facecolors. Shading is always disabled when
- *cmap* is specified.
- .. versionadded:: 3.1
- lightsource : `~matplotlib.colors.LightSource`
- The lightsource to use when *shade* is True.
- .. versionadded:: 3.1
- **kwargs
- Additional keyword arguments to pass onto
- `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`.
- Returns
- -------
- faces : dict
- A dictionary indexed by coordinate, where ``faces[i, j, k]`` is a
- `.Poly3DCollection` of the faces drawn for the voxel
- ``filled[i, j, k]``. If no faces were drawn for a given voxel,
- either because it was not asked to be drawn, or it is fully
- occluded, then ``(i, j, k) not in faces``.
- Examples
- --------
- .. plot:: gallery/mplot3d/voxels.py
- .. plot:: gallery/mplot3d/voxels_rgb.py
- .. plot:: gallery/mplot3d/voxels_torus.py
- .. plot:: gallery/mplot3d/voxels_numpy_logo.py
- """
- # work out which signature we should be using, and use it to parse
- # the arguments. Name must be voxels for the correct error message
- if len(args) >= 3:
- # underscores indicate position only
- def voxels(__x, __y, __z, filled, **kwargs):
- return (__x, __y, __z), filled, kwargs
- else:
- def voxels(filled, **kwargs):
- return None, filled, kwargs
- xyz, filled, kwargs = voxels(*args, **kwargs)
- # check dimensions
- if filled.ndim != 3:
- raise ValueError("Argument filled must be 3-dimensional")
- size = np.array(filled.shape, dtype=np.intp)
- # check xyz coordinates, which are one larger than the filled shape
- coord_shape = tuple(size + 1)
- if xyz is None:
- x, y, z = np.indices(coord_shape)
- else:
- x, y, z = (np.broadcast_to(c, coord_shape) for c in xyz)
- def _broadcast_color_arg(color, name):
- if np.ndim(color) in (0, 1):
- # single color, like "red" or [1, 0, 0]
- return np.broadcast_to(color, filled.shape + np.shape(color))
- elif np.ndim(color) in (3, 4):
- # 3D array of strings, or 4D array with last axis rgb
- if np.shape(color)[:3] != filled.shape:
- raise ValueError(
- "When multidimensional, {} must match the shape of "
- "filled".format(name))
- return color
- else:
- raise ValueError("Invalid {} argument".format(name))
- # broadcast and default on facecolors
- if facecolors is None:
- facecolors = self._get_patches_for_fill.get_next_color()
- facecolors = _broadcast_color_arg(facecolors, 'facecolors')
- # broadcast but no default on edgecolors
- edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')
- # scale to the full array, even if the data is only in the center
- self.auto_scale_xyz(x, y, z)
- # points lying on corners of a square
- square = np.array([
- [0, 0, 0],
- [1, 0, 0],
- [1, 1, 0],
- [0, 1, 0],
- ], dtype=np.intp)
- voxel_faces = defaultdict(list)
- def permutation_matrices(n):
- """Generate cyclic permutation matrices."""
- mat = np.eye(n, dtype=np.intp)
- for i in range(n):
- yield mat
- mat = np.roll(mat, 1, axis=0)
- # iterate over each of the YZ, ZX, and XY orientations, finding faces
- # to render
- for permute in permutation_matrices(3):
- # find the set of ranges to iterate over
- pc, qc, rc = permute.T.dot(size)
- pinds = np.arange(pc)
- qinds = np.arange(qc)
- rinds = np.arange(rc)
- square_rot_pos = square.dot(permute.T)
- square_rot_neg = square_rot_pos[::-1]
- # iterate within the current plane
- for p in pinds:
- for q in qinds:
- # iterate perpendicularly to the current plane, handling
- # boundaries. We only draw faces between a voxel and an
- # empty space, to avoid drawing internal faces.
- # draw lower faces
- p0 = permute.dot([p, q, 0])
- i0 = tuple(p0)
- if filled[i0]:
- voxel_faces[i0].append(p0 + square_rot_neg)
- # draw middle faces
- for r1, r2 in zip(rinds[:-1], rinds[1:]):
- p1 = permute.dot([p, q, r1])
- p2 = permute.dot([p, q, r2])
- i1 = tuple(p1)
- i2 = tuple(p2)
- if filled[i1] and not filled[i2]:
- voxel_faces[i1].append(p2 + square_rot_pos)
- elif not filled[i1] and filled[i2]:
- voxel_faces[i2].append(p2 + square_rot_neg)
- # draw upper faces
- pk = permute.dot([p, q, rc-1])
- pk2 = permute.dot([p, q, rc])
- ik = tuple(pk)
- if filled[ik]:
- voxel_faces[ik].append(pk2 + square_rot_pos)
- # iterate over the faces, and generate a Poly3DCollection for each
- # voxel
- polygons = {}
- for coord, faces_inds in voxel_faces.items():
- # convert indices into 3D positions
- if xyz is None:
- faces = faces_inds
- else:
- faces = []
- for face_inds in faces_inds:
- ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
- face = np.empty(face_inds.shape)
- face[:, 0] = x[ind]
- face[:, 1] = y[ind]
- face[:, 2] = z[ind]
- faces.append(face)
- # shade the faces
- facecolor = facecolors[coord]
- edgecolor = edgecolors[coord]
- if shade:
- normals = self._generate_normals(faces)
- facecolor = self._shade_colors(facecolor, normals, lightsource)
- if edgecolor is not None:
- edgecolor = self._shade_colors(
- edgecolor, normals, lightsource
- )
- poly = art3d.Poly3DCollection(
- faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs)
- self.add_collection3d(poly)
- polygons[coord] = poly
- return polygons
- def get_tightbbox(self, renderer, call_axes_locator=True,
- bbox_extra_artists=None, *, for_layout_only=False):
- ret = super().get_tightbbox(renderer,
- call_axes_locator=call_axes_locator,
- bbox_extra_artists=bbox_extra_artists,
- for_layout_only=for_layout_only)
- batch = [ret]
- if self._axis3don:
- for axis in self._get_axis_list():
- if axis.get_visible():
- try:
- axis_bb = axis.get_tightbbox(
- renderer,
- for_layout_only=for_layout_only
- )
- except TypeError:
- # in case downstream library has redefined axis:
- axis_bb = axis.get_tightbbox(renderer)
- if axis_bb:
- batch.append(axis_bb)
- return mtransforms.Bbox.union(batch)
- docstring.interpd.update(Axes3D=artist.kwdoc(Axes3D))
- docstring.dedent_interpd(Axes3D.__init__)
- def get_test_data(delta=0.05):
- """Return a tuple X, Y, Z with a test data set."""
- x = y = np.arange(-3.0, 3.0, delta)
- X, Y = np.meshgrid(x, y)
- Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
- Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
- (2 * np.pi * 0.5 * 1.5))
- Z = Z2 - Z1
- X = X * 10
- Y = Y * 10
- Z = Z * 500
- return X, Y, Z
|