axis3d.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750
  1. # axis3d.py, original mplot3d version by John Porter
  2. # Created: 23 Sep 2005
  3. # Parts rewritten by Reinier Heeres <reinier@heeres.eu>
  4. import inspect
  5. import numpy as np
  6. import matplotlib as mpl
  7. from matplotlib import (
  8. _api, artist, lines as mlines, axis as maxis, patches as mpatches,
  9. transforms as mtransforms, colors as mcolors)
  10. from . import art3d, proj3d
  11. def _move_from_center(coord, centers, deltas, axmask=(True, True, True)):
  12. """
  13. For each coordinate where *axmask* is True, move *coord* away from
  14. *centers* by *deltas*.
  15. """
  16. coord = np.asarray(coord)
  17. return coord + axmask * np.copysign(1, coord - centers) * deltas
  18. def _tick_update_position(tick, tickxs, tickys, labelpos):
  19. """Update tick line and label position and style."""
  20. tick.label1.set_position(labelpos)
  21. tick.label2.set_position(labelpos)
  22. tick.tick1line.set_visible(True)
  23. tick.tick2line.set_visible(False)
  24. tick.tick1line.set_linestyle('-')
  25. tick.tick1line.set_marker('')
  26. tick.tick1line.set_data(tickxs, tickys)
  27. tick.gridline.set_data([0], [0])
  28. class Axis(maxis.XAxis):
  29. """An Axis class for the 3D plots."""
  30. # These points from the unit cube make up the x, y and z-planes
  31. _PLANES = (
  32. (0, 3, 7, 4), (1, 2, 6, 5), # yz planes
  33. (0, 1, 5, 4), (3, 2, 6, 7), # xz planes
  34. (0, 1, 2, 3), (4, 5, 6, 7), # xy planes
  35. )
  36. # Some properties for the axes
  37. _AXINFO = {
  38. 'x': {'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2)},
  39. 'y': {'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2)},
  40. 'z': {'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1)},
  41. }
  42. def _old_init(self, adir, v_intervalx, d_intervalx, axes, *args,
  43. rotate_label=None, **kwargs):
  44. return locals()
  45. def _new_init(self, axes, *, rotate_label=None, **kwargs):
  46. return locals()
  47. def __init__(self, *args, **kwargs):
  48. params = _api.select_matching_signature(
  49. [self._old_init, self._new_init], *args, **kwargs)
  50. if "adir" in params:
  51. _api.warn_deprecated(
  52. "3.6", message=f"The signature of 3D Axis constructors has "
  53. f"changed in %(since)s; the new signature is "
  54. f"{inspect.signature(type(self).__init__)}", pending=True)
  55. if params["adir"] != self.axis_name:
  56. raise ValueError(f"Cannot instantiate {type(self).__name__} "
  57. f"with adir={params['adir']!r}")
  58. axes = params["axes"]
  59. rotate_label = params["rotate_label"]
  60. args = params.get("args", ())
  61. kwargs = params["kwargs"]
  62. name = self.axis_name
  63. self._label_position = 'default'
  64. self._tick_position = 'default'
  65. # This is a temporary member variable.
  66. # Do not depend on this existing in future releases!
  67. self._axinfo = self._AXINFO[name].copy()
  68. # Common parts
  69. self._axinfo.update({
  70. 'label': {'va': 'center', 'ha': 'center',
  71. 'rotation_mode': 'anchor'},
  72. 'color': mpl.rcParams[f'axes3d.{name}axis.panecolor'],
  73. 'tick': {
  74. 'inward_factor': 0.2,
  75. 'outward_factor': 0.1,
  76. },
  77. })
  78. if mpl.rcParams['_internal.classic_mode']:
  79. self._axinfo.update({
  80. 'axisline': {'linewidth': 0.75, 'color': (0, 0, 0, 1)},
  81. 'grid': {
  82. 'color': (0.9, 0.9, 0.9, 1),
  83. 'linewidth': 1.0,
  84. 'linestyle': '-',
  85. },
  86. })
  87. self._axinfo['tick'].update({
  88. 'linewidth': {
  89. True: mpl.rcParams['lines.linewidth'], # major
  90. False: mpl.rcParams['lines.linewidth'], # minor
  91. }
  92. })
  93. else:
  94. self._axinfo.update({
  95. 'axisline': {
  96. 'linewidth': mpl.rcParams['axes.linewidth'],
  97. 'color': mpl.rcParams['axes.edgecolor'],
  98. },
  99. 'grid': {
  100. 'color': mpl.rcParams['grid.color'],
  101. 'linewidth': mpl.rcParams['grid.linewidth'],
  102. 'linestyle': mpl.rcParams['grid.linestyle'],
  103. },
  104. })
  105. self._axinfo['tick'].update({
  106. 'linewidth': {
  107. True: ( # major
  108. mpl.rcParams['xtick.major.width'] if name in 'xz'
  109. else mpl.rcParams['ytick.major.width']),
  110. False: ( # minor
  111. mpl.rcParams['xtick.minor.width'] if name in 'xz'
  112. else mpl.rcParams['ytick.minor.width']),
  113. }
  114. })
  115. super().__init__(axes, *args, **kwargs)
  116. # data and viewing intervals for this direction
  117. if "d_intervalx" in params:
  118. self.set_data_interval(*params["d_intervalx"])
  119. if "v_intervalx" in params:
  120. self.set_view_interval(*params["v_intervalx"])
  121. self.set_rotate_label(rotate_label)
  122. self._init3d() # Inline after init3d deprecation elapses.
  123. __init__.__signature__ = inspect.signature(_new_init)
  124. adir = _api.deprecated("3.6", pending=True)(
  125. property(lambda self: self.axis_name))
  126. def _init3d(self):
  127. self.line = mlines.Line2D(
  128. xdata=(0, 0), ydata=(0, 0),
  129. linewidth=self._axinfo['axisline']['linewidth'],
  130. color=self._axinfo['axisline']['color'],
  131. antialiased=True)
  132. # Store dummy data in Polygon object
  133. self.pane = mpatches.Polygon([[0, 0], [0, 1]], closed=False)
  134. self.set_pane_color(self._axinfo['color'])
  135. self.axes._set_artist_props(self.line)
  136. self.axes._set_artist_props(self.pane)
  137. self.gridlines = art3d.Line3DCollection([])
  138. self.axes._set_artist_props(self.gridlines)
  139. self.axes._set_artist_props(self.label)
  140. self.axes._set_artist_props(self.offsetText)
  141. # Need to be able to place the label at the correct location
  142. self.label._transform = self.axes.transData
  143. self.offsetText._transform = self.axes.transData
  144. @_api.deprecated("3.6", pending=True)
  145. def init3d(self): # After deprecation elapses, inline _init3d to __init__.
  146. self._init3d()
  147. def get_major_ticks(self, numticks=None):
  148. ticks = super().get_major_ticks(numticks)
  149. for t in ticks:
  150. for obj in [
  151. t.tick1line, t.tick2line, t.gridline, t.label1, t.label2]:
  152. obj.set_transform(self.axes.transData)
  153. return ticks
  154. def get_minor_ticks(self, numticks=None):
  155. ticks = super().get_minor_ticks(numticks)
  156. for t in ticks:
  157. for obj in [
  158. t.tick1line, t.tick2line, t.gridline, t.label1, t.label2]:
  159. obj.set_transform(self.axes.transData)
  160. return ticks
  161. def set_ticks_position(self, position):
  162. """
  163. Set the ticks position.
  164. Parameters
  165. ----------
  166. position : {'lower', 'upper', 'both', 'default', 'none'}
  167. The position of the bolded axis lines, ticks, and tick labels.
  168. """
  169. _api.check_in_list(['lower', 'upper', 'both', 'default', 'none'],
  170. position=position)
  171. self._tick_position = position
  172. def get_ticks_position(self):
  173. """
  174. Get the ticks position.
  175. Returns
  176. -------
  177. str : {'lower', 'upper', 'both', 'default', 'none'}
  178. The position of the bolded axis lines, ticks, and tick labels.
  179. """
  180. return self._tick_position
  181. def set_label_position(self, position):
  182. """
  183. Set the label position.
  184. Parameters
  185. ----------
  186. position : {'lower', 'upper', 'both', 'default', 'none'}
  187. The position of the axis label.
  188. """
  189. _api.check_in_list(['lower', 'upper', 'both', 'default', 'none'],
  190. position=position)
  191. self._label_position = position
  192. def get_label_position(self):
  193. """
  194. Get the label position.
  195. Returns
  196. -------
  197. str : {'lower', 'upper', 'both', 'default', 'none'}
  198. The position of the axis label.
  199. """
  200. return self._label_position
  201. def set_pane_color(self, color, alpha=None):
  202. """
  203. Set pane color.
  204. Parameters
  205. ----------
  206. color : :mpltype:`color`
  207. Color for axis pane.
  208. alpha : float, optional
  209. Alpha value for axis pane. If None, base it on *color*.
  210. """
  211. color = mcolors.to_rgba(color, alpha)
  212. self._axinfo['color'] = color
  213. self.pane.set_edgecolor(color)
  214. self.pane.set_facecolor(color)
  215. self.pane.set_alpha(color[-1])
  216. self.stale = True
  217. def set_rotate_label(self, val):
  218. """
  219. Whether to rotate the axis label: True, False or None.
  220. If set to None the label will be rotated if longer than 4 chars.
  221. """
  222. self._rotate_label = val
  223. self.stale = True
  224. def get_rotate_label(self, text):
  225. if self._rotate_label is not None:
  226. return self._rotate_label
  227. else:
  228. return len(text) > 4
  229. def _get_coord_info(self):
  230. mins, maxs = np.array([
  231. self.axes.get_xbound(),
  232. self.axes.get_ybound(),
  233. self.axes.get_zbound(),
  234. ]).T
  235. # Project the bounds along the current position of the cube:
  236. bounds = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2]
  237. bounds_proj = self.axes._transformed_cube(bounds)
  238. # Determine which one of the parallel planes are higher up:
  239. means_z0 = np.zeros(3)
  240. means_z1 = np.zeros(3)
  241. for i in range(3):
  242. means_z0[i] = np.mean(bounds_proj[self._PLANES[2 * i], 2])
  243. means_z1[i] = np.mean(bounds_proj[self._PLANES[2 * i + 1], 2])
  244. highs = means_z0 < means_z1
  245. # Special handling for edge-on views
  246. equals = np.abs(means_z0 - means_z1) <= np.finfo(float).eps
  247. if np.sum(equals) == 2:
  248. vertical = np.where(~equals)[0][0]
  249. if vertical == 2: # looking at XY plane
  250. highs = np.array([True, True, highs[2]])
  251. elif vertical == 1: # looking at XZ plane
  252. highs = np.array([True, highs[1], False])
  253. elif vertical == 0: # looking at YZ plane
  254. highs = np.array([highs[0], False, False])
  255. return mins, maxs, bounds_proj, highs
  256. def _calc_centers_deltas(self, maxs, mins):
  257. centers = 0.5 * (maxs + mins)
  258. # In mpl3.8, the scale factor was 1/12. mpl3.9 changes this to
  259. # 1/12 * 24/25 = 0.08 to compensate for the change in automargin
  260. # behavior and keep appearance the same. The 24/25 factor is from the
  261. # 1/48 padding added to each side of the axis in mpl3.8.
  262. scale = 0.08
  263. deltas = (maxs - mins) * scale
  264. return centers, deltas
  265. def _get_axis_line_edge_points(self, minmax, maxmin, position=None):
  266. """Get the edge points for the black bolded axis line."""
  267. # When changing vertical axis some of the axes has to be
  268. # moved to the other plane so it looks the same as if the z-axis
  269. # was the vertical axis.
  270. mb = [minmax, maxmin] # line from origin to nearest corner to camera
  271. mb_rev = mb[::-1]
  272. mm = [[mb, mb_rev, mb_rev], [mb_rev, mb_rev, mb], [mb, mb, mb]]
  273. mm = mm[self.axes._vertical_axis][self._axinfo["i"]]
  274. juggled = self._axinfo["juggled"]
  275. edge_point_0 = mm[0].copy() # origin point
  276. if ((position == 'lower' and mm[1][juggled[-1]] < mm[0][juggled[-1]]) or
  277. (position == 'upper' and mm[1][juggled[-1]] > mm[0][juggled[-1]])):
  278. edge_point_0[juggled[-1]] = mm[1][juggled[-1]]
  279. else:
  280. edge_point_0[juggled[0]] = mm[1][juggled[0]]
  281. edge_point_1 = edge_point_0.copy()
  282. edge_point_1[juggled[1]] = mm[1][juggled[1]]
  283. return edge_point_0, edge_point_1
  284. def _get_all_axis_line_edge_points(self, minmax, maxmin, axis_position=None):
  285. # Determine edge points for the axis lines
  286. edgep1s = []
  287. edgep2s = []
  288. position = []
  289. if axis_position in (None, 'default'):
  290. edgep1, edgep2 = self._get_axis_line_edge_points(minmax, maxmin)
  291. edgep1s = [edgep1]
  292. edgep2s = [edgep2]
  293. position = ['default']
  294. else:
  295. edgep1_l, edgep2_l = self._get_axis_line_edge_points(minmax, maxmin,
  296. position='lower')
  297. edgep1_u, edgep2_u = self._get_axis_line_edge_points(minmax, maxmin,
  298. position='upper')
  299. if axis_position in ('lower', 'both'):
  300. edgep1s.append(edgep1_l)
  301. edgep2s.append(edgep2_l)
  302. position.append('lower')
  303. if axis_position in ('upper', 'both'):
  304. edgep1s.append(edgep1_u)
  305. edgep2s.append(edgep2_u)
  306. position.append('upper')
  307. return edgep1s, edgep2s, position
  308. def _get_tickdir(self, position):
  309. """
  310. Get the direction of the tick.
  311. Parameters
  312. ----------
  313. position : str, optional : {'upper', 'lower', 'default'}
  314. The position of the axis.
  315. Returns
  316. -------
  317. tickdir : int
  318. Index which indicates which coordinate the tick line will
  319. align with.
  320. """
  321. _api.check_in_list(('upper', 'lower', 'default'), position=position)
  322. # TODO: Move somewhere else where it's triggered less:
  323. tickdirs_base = [v["tickdir"] for v in self._AXINFO.values()] # default
  324. elev_mod = np.mod(self.axes.elev + 180, 360) - 180
  325. azim_mod = np.mod(self.axes.azim, 360)
  326. if position == 'upper':
  327. if elev_mod >= 0:
  328. tickdirs_base = [2, 2, 0]
  329. else:
  330. tickdirs_base = [1, 0, 0]
  331. if 0 <= azim_mod < 180:
  332. tickdirs_base[2] = 1
  333. elif position == 'lower':
  334. if elev_mod >= 0:
  335. tickdirs_base = [1, 0, 1]
  336. else:
  337. tickdirs_base = [2, 2, 1]
  338. if 0 <= azim_mod < 180:
  339. tickdirs_base[2] = 0
  340. info_i = [v["i"] for v in self._AXINFO.values()]
  341. i = self._axinfo["i"]
  342. vert_ax = self.axes._vertical_axis
  343. j = vert_ax - 2
  344. # default: tickdir = [[1, 2, 1], [2, 2, 0], [1, 0, 0]][vert_ax][i]
  345. tickdir = np.roll(info_i, -j)[np.roll(tickdirs_base, j)][i]
  346. return tickdir
  347. def active_pane(self):
  348. mins, maxs, tc, highs = self._get_coord_info()
  349. info = self._axinfo
  350. index = info['i']
  351. if not highs[index]:
  352. loc = mins[index]
  353. plane = self._PLANES[2 * index]
  354. else:
  355. loc = maxs[index]
  356. plane = self._PLANES[2 * index + 1]
  357. xys = np.array([tc[p] for p in plane])
  358. return xys, loc
  359. def draw_pane(self, renderer):
  360. """
  361. Draw pane.
  362. Parameters
  363. ----------
  364. renderer : `~matplotlib.backend_bases.RendererBase` subclass
  365. """
  366. renderer.open_group('pane3d', gid=self.get_gid())
  367. xys, loc = self.active_pane()
  368. self.pane.xy = xys[:, :2]
  369. self.pane.draw(renderer)
  370. renderer.close_group('pane3d')
  371. def _axmask(self):
  372. axmask = [True, True, True]
  373. axmask[self._axinfo["i"]] = False
  374. return axmask
  375. def _draw_ticks(self, renderer, edgep1, centers, deltas, highs,
  376. deltas_per_point, pos):
  377. ticks = self._update_ticks()
  378. info = self._axinfo
  379. index = info["i"]
  380. juggled = info["juggled"]
  381. mins, maxs, tc, highs = self._get_coord_info()
  382. centers, deltas = self._calc_centers_deltas(maxs, mins)
  383. # Draw ticks:
  384. tickdir = self._get_tickdir(pos)
  385. tickdelta = deltas[tickdir] if highs[tickdir] else -deltas[tickdir]
  386. tick_info = info['tick']
  387. tick_out = tick_info['outward_factor'] * tickdelta
  388. tick_in = tick_info['inward_factor'] * tickdelta
  389. tick_lw = tick_info['linewidth']
  390. edgep1_tickdir = edgep1[tickdir]
  391. out_tickdir = edgep1_tickdir + tick_out
  392. in_tickdir = edgep1_tickdir - tick_in
  393. default_label_offset = 8. # A rough estimate
  394. points = deltas_per_point * deltas
  395. for tick in ticks:
  396. # Get tick line positions
  397. pos = edgep1.copy()
  398. pos[index] = tick.get_loc()
  399. pos[tickdir] = out_tickdir
  400. x1, y1, z1 = proj3d.proj_transform(*pos, self.axes.M)
  401. pos[tickdir] = in_tickdir
  402. x2, y2, z2 = proj3d.proj_transform(*pos, self.axes.M)
  403. # Get position of label
  404. labeldeltas = (tick.get_pad() + default_label_offset) * points
  405. pos[tickdir] = edgep1_tickdir
  406. pos = _move_from_center(pos, centers, labeldeltas, self._axmask())
  407. lx, ly, lz = proj3d.proj_transform(*pos, self.axes.M)
  408. _tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly))
  409. tick.tick1line.set_linewidth(tick_lw[tick._major])
  410. tick.draw(renderer)
  411. def _draw_offset_text(self, renderer, edgep1, edgep2, labeldeltas, centers,
  412. highs, pep, dx, dy):
  413. # Get general axis information:
  414. info = self._axinfo
  415. index = info["i"]
  416. juggled = info["juggled"]
  417. tickdir = info["tickdir"]
  418. # Which of the two edge points do we want to
  419. # use for locating the offset text?
  420. if juggled[2] == 2:
  421. outeredgep = edgep1
  422. outerindex = 0
  423. else:
  424. outeredgep = edgep2
  425. outerindex = 1
  426. pos = _move_from_center(outeredgep, centers, labeldeltas,
  427. self._axmask())
  428. olx, oly, olz = proj3d.proj_transform(*pos, self.axes.M)
  429. self.offsetText.set_text(self.major.formatter.get_offset())
  430. self.offsetText.set_position((olx, oly))
  431. angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx)))
  432. self.offsetText.set_rotation(angle)
  433. # Must set rotation mode to "anchor" so that
  434. # the alignment point is used as the "fulcrum" for rotation.
  435. self.offsetText.set_rotation_mode('anchor')
  436. # ----------------------------------------------------------------------
  437. # Note: the following statement for determining the proper alignment of
  438. # the offset text. This was determined entirely by trial-and-error
  439. # and should not be in any way considered as "the way". There are
  440. # still some edge cases where alignment is not quite right, but this
  441. # seems to be more of a geometry issue (in other words, I might be
  442. # using the wrong reference points).
  443. #
  444. # (TT, FF, TF, FT) are the shorthand for the tuple of
  445. # (centpt[tickdir] <= pep[tickdir, outerindex],
  446. # centpt[index] <= pep[index, outerindex])
  447. #
  448. # Three-letters (e.g., TFT, FTT) are short-hand for the array of bools
  449. # from the variable 'highs'.
  450. # ---------------------------------------------------------------------
  451. centpt = proj3d.proj_transform(*centers, self.axes.M)
  452. if centpt[tickdir] > pep[tickdir, outerindex]:
  453. # if FT and if highs has an even number of Trues
  454. if (centpt[index] <= pep[index, outerindex]
  455. and np.count_nonzero(highs) % 2 == 0):
  456. # Usually, this means align right, except for the FTT case,
  457. # in which offset for axis 1 and 2 are aligned left.
  458. if highs.tolist() == [False, True, True] and index in (1, 2):
  459. align = 'left'
  460. else:
  461. align = 'right'
  462. else:
  463. # The FF case
  464. align = 'left'
  465. else:
  466. # if TF and if highs has an even number of Trues
  467. if (centpt[index] > pep[index, outerindex]
  468. and np.count_nonzero(highs) % 2 == 0):
  469. # Usually mean align left, except if it is axis 2
  470. align = 'right' if index == 2 else 'left'
  471. else:
  472. # The TT case
  473. align = 'right'
  474. self.offsetText.set_va('center')
  475. self.offsetText.set_ha(align)
  476. self.offsetText.draw(renderer)
  477. def _draw_labels(self, renderer, edgep1, edgep2, labeldeltas, centers, dx, dy):
  478. label = self._axinfo["label"]
  479. # Draw labels
  480. lxyz = 0.5 * (edgep1 + edgep2)
  481. lxyz = _move_from_center(lxyz, centers, labeldeltas, self._axmask())
  482. tlx, tly, tlz = proj3d.proj_transform(*lxyz, self.axes.M)
  483. self.label.set_position((tlx, tly))
  484. if self.get_rotate_label(self.label.get_text()):
  485. angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx)))
  486. self.label.set_rotation(angle)
  487. self.label.set_va(label['va'])
  488. self.label.set_ha(label['ha'])
  489. self.label.set_rotation_mode(label['rotation_mode'])
  490. self.label.draw(renderer)
  491. @artist.allow_rasterization
  492. def draw(self, renderer):
  493. self.label._transform = self.axes.transData
  494. self.offsetText._transform = self.axes.transData
  495. renderer.open_group("axis3d", gid=self.get_gid())
  496. # Get general axis information:
  497. mins, maxs, tc, highs = self._get_coord_info()
  498. centers, deltas = self._calc_centers_deltas(maxs, mins)
  499. # Calculate offset distances
  500. # A rough estimate; points are ambiguous since 3D plots rotate
  501. reltoinches = self.get_figure(root=False).dpi_scale_trans.inverted()
  502. ax_inches = reltoinches.transform(self.axes.bbox.size)
  503. ax_points_estimate = sum(72. * ax_inches)
  504. deltas_per_point = 48 / ax_points_estimate
  505. default_offset = 21.
  506. labeldeltas = (self.labelpad + default_offset) * deltas_per_point * deltas
  507. # Determine edge points for the axis lines
  508. minmax = np.where(highs, maxs, mins) # "origin" point
  509. maxmin = np.where(~highs, maxs, mins) # "opposite" corner near camera
  510. for edgep1, edgep2, pos in zip(*self._get_all_axis_line_edge_points(
  511. minmax, maxmin, self._tick_position)):
  512. # Project the edge points along the current position
  513. pep = proj3d._proj_trans_points([edgep1, edgep2], self.axes.M)
  514. pep = np.asarray(pep)
  515. # The transAxes transform is used because the Text object
  516. # rotates the text relative to the display coordinate system.
  517. # Therefore, if we want the labels to remain parallel to the
  518. # axis regardless of the aspect ratio, we need to convert the
  519. # edge points of the plane to display coordinates and calculate
  520. # an angle from that.
  521. # TODO: Maybe Text objects should handle this themselves?
  522. dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) -
  523. self.axes.transAxes.transform([pep[0:2, 0]]))[0]
  524. # Draw the lines
  525. self.line.set_data(pep[0], pep[1])
  526. self.line.draw(renderer)
  527. # Draw ticks
  528. self._draw_ticks(renderer, edgep1, centers, deltas, highs,
  529. deltas_per_point, pos)
  530. # Draw Offset text
  531. self._draw_offset_text(renderer, edgep1, edgep2, labeldeltas,
  532. centers, highs, pep, dx, dy)
  533. for edgep1, edgep2, pos in zip(*self._get_all_axis_line_edge_points(
  534. minmax, maxmin, self._label_position)):
  535. # See comments above
  536. pep = proj3d._proj_trans_points([edgep1, edgep2], self.axes.M)
  537. pep = np.asarray(pep)
  538. dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) -
  539. self.axes.transAxes.transform([pep[0:2, 0]]))[0]
  540. # Draw labels
  541. self._draw_labels(renderer, edgep1, edgep2, labeldeltas, centers, dx, dy)
  542. renderer.close_group('axis3d')
  543. self.stale = False
  544. @artist.allow_rasterization
  545. def draw_grid(self, renderer):
  546. if not self.axes._draw_grid:
  547. return
  548. renderer.open_group("grid3d", gid=self.get_gid())
  549. ticks = self._update_ticks()
  550. if len(ticks):
  551. # Get general axis information:
  552. info = self._axinfo
  553. index = info["i"]
  554. mins, maxs, tc, highs = self._get_coord_info()
  555. minmax = np.where(highs, maxs, mins)
  556. maxmin = np.where(~highs, maxs, mins)
  557. # Grid points where the planes meet
  558. xyz0 = np.tile(minmax, (len(ticks), 1))
  559. xyz0[:, index] = [tick.get_loc() for tick in ticks]
  560. # Grid lines go from the end of one plane through the plane
  561. # intersection (at xyz0) to the end of the other plane. The first
  562. # point (0) differs along dimension index-2 and the last (2) along
  563. # dimension index-1.
  564. lines = np.stack([xyz0, xyz0, xyz0], axis=1)
  565. lines[:, 0, index - 2] = maxmin[index - 2]
  566. lines[:, 2, index - 1] = maxmin[index - 1]
  567. self.gridlines.set_segments(lines)
  568. gridinfo = info['grid']
  569. self.gridlines.set_color(gridinfo['color'])
  570. self.gridlines.set_linewidth(gridinfo['linewidth'])
  571. self.gridlines.set_linestyle(gridinfo['linestyle'])
  572. self.gridlines.do_3d_projection()
  573. self.gridlines.draw(renderer)
  574. renderer.close_group('grid3d')
  575. # TODO: Get this to work (more) properly when mplot3d supports the
  576. # transforms framework.
  577. def get_tightbbox(self, renderer=None, *, for_layout_only=False):
  578. # docstring inherited
  579. if not self.get_visible():
  580. return
  581. # We have to directly access the internal data structures
  582. # (and hope they are up to date) because at draw time we
  583. # shift the ticks and their labels around in (x, y) space
  584. # based on the projection, the current view port, and their
  585. # position in 3D space. If we extend the transforms framework
  586. # into 3D we would not need to do this different book keeping
  587. # than we do in the normal axis
  588. major_locs = self.get_majorticklocs()
  589. minor_locs = self.get_minorticklocs()
  590. ticks = [*self.get_minor_ticks(len(minor_locs)),
  591. *self.get_major_ticks(len(major_locs))]
  592. view_low, view_high = self.get_view_interval()
  593. if view_low > view_high:
  594. view_low, view_high = view_high, view_low
  595. interval_t = self.get_transform().transform([view_low, view_high])
  596. ticks_to_draw = []
  597. for tick in ticks:
  598. try:
  599. loc_t = self.get_transform().transform(tick.get_loc())
  600. except AssertionError:
  601. # Transform.transform doesn't allow masked values but
  602. # some scales might make them, so we need this try/except.
  603. pass
  604. else:
  605. if mtransforms._interval_contains_close(interval_t, loc_t):
  606. ticks_to_draw.append(tick)
  607. ticks = ticks_to_draw
  608. bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer)
  609. other = []
  610. if self.line.get_visible():
  611. other.append(self.line.get_window_extent(renderer))
  612. if (self.label.get_visible() and not for_layout_only and
  613. self.label.get_text()):
  614. other.append(self.label.get_window_extent(renderer))
  615. return mtransforms.Bbox.union([*bb_1, *bb_2, *other])
  616. d_interval = _api.deprecated(
  617. "3.6", alternative="get_data_interval", pending=True)(
  618. property(lambda self: self.get_data_interval(),
  619. lambda self, minmax: self.set_data_interval(*minmax)))
  620. v_interval = _api.deprecated(
  621. "3.6", alternative="get_view_interval", pending=True)(
  622. property(lambda self: self.get_view_interval(),
  623. lambda self, minmax: self.set_view_interval(*minmax)))
  624. class XAxis(Axis):
  625. axis_name = "x"
  626. get_view_interval, set_view_interval = maxis._make_getset_interval(
  627. "view", "xy_viewLim", "intervalx")
  628. get_data_interval, set_data_interval = maxis._make_getset_interval(
  629. "data", "xy_dataLim", "intervalx")
  630. class YAxis(Axis):
  631. axis_name = "y"
  632. get_view_interval, set_view_interval = maxis._make_getset_interval(
  633. "view", "xy_viewLim", "intervaly")
  634. get_data_interval, set_data_interval = maxis._make_getset_interval(
  635. "data", "xy_dataLim", "intervaly")
  636. class ZAxis(Axis):
  637. axis_name = "z"
  638. get_view_interval, set_view_interval = maxis._make_getset_interval(
  639. "view", "zz_viewLim", "intervalx")
  640. get_data_interval, set_data_interval = maxis._make_getset_interval(
  641. "data", "zz_dataLim", "intervalx")