Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/mpl_toolkits/mplot3d/axis3d.py : 16%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# axis3d.py, original mplot3d version by John Porter
2# Created: 23 Sep 2005
3# Parts rewritten by Reinier Heeres <reinier@heeres.eu>
5import numpy as np
7from matplotlib import (
8 artist, cbook, lines as mlines, axis as maxis, patches as mpatches,
9 rcParams)
10from . import art3d, proj3d
13@cbook.deprecated("3.1")
14def get_flip_min_max(coord, index, mins, maxs):
15 if coord[index] == mins[index]:
16 return maxs[index]
17 else:
18 return mins[index]
21def move_from_center(coord, centers, deltas, axmask=(True, True, True)):
22 """
23 For each coordinate where *axmask* is True, move *coord* away from
24 *centers* by *deltas*.
25 """
26 coord = np.asarray(coord)
27 return coord + axmask * np.copysign(1, coord - centers) * deltas
30def tick_update_position(tick, tickxs, tickys, labelpos):
31 '''Update tick line and label position and style.'''
33 tick.label1.set_position(labelpos)
34 tick.label2.set_position(labelpos)
35 tick.tick1line.set_visible(True)
36 tick.tick2line.set_visible(False)
37 tick.tick1line.set_linestyle('-')
38 tick.tick1line.set_marker('')
39 tick.tick1line.set_data(tickxs, tickys)
40 tick.gridline.set_data(0, 0)
43class Axis(maxis.XAxis):
44 """An Axis class for the 3D plots."""
45 # These points from the unit cube make up the x, y and z-planes
46 _PLANES = (
47 (0, 3, 7, 4), (1, 2, 6, 5), # yz planes
48 (0, 1, 5, 4), (3, 2, 6, 7), # xz planes
49 (0, 1, 2, 3), (4, 5, 6, 7), # xy planes
50 )
52 # Some properties for the axes
53 _AXINFO = {
54 'x': {'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2),
55 'color': (0.95, 0.95, 0.95, 0.5)},
56 'y': {'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2),
57 'color': (0.90, 0.90, 0.90, 0.5)},
58 'z': {'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1),
59 'color': (0.925, 0.925, 0.925, 0.5)},
60 }
62 def __init__(self, adir, v_intervalx, d_intervalx, axes, *args,
63 rotate_label=None, **kwargs):
64 # adir identifies which axes this is
65 self.adir = adir
67 # This is a temporary member variable.
68 # Do not depend on this existing in future releases!
69 self._axinfo = self._AXINFO[adir].copy()
70 if rcParams['_internal.classic_mode']:
71 self._axinfo.update(
72 {'label': {'va': 'center',
73 'ha': 'center'},
74 'tick': {'inward_factor': 0.2,
75 'outward_factor': 0.1,
76 'linewidth': rcParams['lines.linewidth']},
77 'axisline': {'linewidth': 0.75,
78 'color': (0, 0, 0, 1)},
79 'grid': {'color': (0.9, 0.9, 0.9, 1),
80 'linewidth': 1.0,
81 'linestyle': '-'},
82 })
83 else:
84 self._axinfo.update(
85 {'label': {'va': 'center',
86 'ha': 'center'},
87 'tick': {'inward_factor': 0.2,
88 'outward_factor': 0.1,
89 'linewidth': rcParams.get(
90 adir + 'tick.major.width',
91 rcParams['xtick.major.width'])},
92 'axisline': {'linewidth': rcParams['axes.linewidth'],
93 'color': rcParams['axes.edgecolor']},
94 'grid': {'color': rcParams['grid.color'],
95 'linewidth': rcParams['grid.linewidth'],
96 'linestyle': rcParams['grid.linestyle']},
97 })
99 maxis.XAxis.__init__(self, axes, *args, **kwargs)
101 # data and viewing intervals for this direction
102 self.d_interval = d_intervalx
103 self.v_interval = v_intervalx
104 self.set_rotate_label(rotate_label)
106 def init3d(self):
107 self.line = mlines.Line2D(
108 xdata=(0, 0), ydata=(0, 0),
109 linewidth=self._axinfo['axisline']['linewidth'],
110 color=self._axinfo['axisline']['color'],
111 antialiased=True)
113 # Store dummy data in Polygon object
114 self.pane = mpatches.Polygon(
115 np.array([[0, 0], [0, 1], [1, 0], [0, 0]]),
116 closed=False, alpha=0.8, facecolor='k', edgecolor='k')
117 self.set_pane_color(self._axinfo['color'])
119 self.axes._set_artist_props(self.line)
120 self.axes._set_artist_props(self.pane)
121 self.gridlines = art3d.Line3DCollection([])
122 self.axes._set_artist_props(self.gridlines)
123 self.axes._set_artist_props(self.label)
124 self.axes._set_artist_props(self.offsetText)
125 # Need to be able to place the label at the correct location
126 self.label._transform = self.axes.transData
127 self.offsetText._transform = self.axes.transData
129 @cbook.deprecated("3.1")
130 def get_tick_positions(self):
131 majorLocs = self.major.locator()
132 majorLabels = self.major.formatter.format_ticks(majorLocs)
133 return majorLabels, majorLocs
135 def get_major_ticks(self, numticks=None):
136 ticks = maxis.XAxis.get_major_ticks(self, numticks)
137 for t in ticks:
138 t.tick1line.set_transform(self.axes.transData)
139 t.tick2line.set_transform(self.axes.transData)
140 t.gridline.set_transform(self.axes.transData)
141 t.label1.set_transform(self.axes.transData)
142 t.label2.set_transform(self.axes.transData)
143 return ticks
145 def set_pane_pos(self, xys):
146 xys = np.asarray(xys)
147 xys = xys[:, :2]
148 self.pane.xy = xys
149 self.stale = True
151 def set_pane_color(self, color):
152 '''Set pane color to a RGBA tuple.'''
153 self._axinfo['color'] = color
154 self.pane.set_edgecolor(color)
155 self.pane.set_facecolor(color)
156 self.pane.set_alpha(color[-1])
157 self.stale = True
159 def set_rotate_label(self, val):
160 '''
161 Whether to rotate the axis label: True, False or None.
162 If set to None the label will be rotated if longer than 4 chars.
163 '''
164 self._rotate_label = val
165 self.stale = True
167 def get_rotate_label(self, text):
168 if self._rotate_label is not None:
169 return self._rotate_label
170 else:
171 return len(text) > 4
173 def _get_coord_info(self, renderer):
174 mins, maxs = np.array([
175 self.axes.get_xbound(),
176 self.axes.get_ybound(),
177 self.axes.get_zbound(),
178 ]).T
179 centers = (maxs + mins) / 2.
180 deltas = (maxs - mins) / 12.
181 mins = mins - deltas / 4.
182 maxs = maxs + deltas / 4.
184 vals = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2]
185 tc = self.axes.tunit_cube(vals, renderer.M)
186 avgz = [tc[p1][2] + tc[p2][2] + tc[p3][2] + tc[p4][2]
187 for p1, p2, p3, p4 in self._PLANES]
188 highs = np.array([avgz[2*i] < avgz[2*i+1] for i in range(3)])
190 return mins, maxs, centers, deltas, tc, highs
192 def draw_pane(self, renderer):
193 renderer.open_group('pane3d', gid=self.get_gid())
195 mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer)
197 info = self._axinfo
198 index = info['i']
199 if not highs[index]:
200 plane = self._PLANES[2 * index]
201 else:
202 plane = self._PLANES[2 * index + 1]
203 xys = [tc[p] for p in plane]
204 self.set_pane_pos(xys)
205 self.pane.draw(renderer)
207 renderer.close_group('pane3d')
209 @artist.allow_rasterization
210 def draw(self, renderer):
211 self.label._transform = self.axes.transData
212 renderer.open_group('axis3d', gid=self.get_gid())
214 ticks = self._update_ticks()
216 info = self._axinfo
217 index = info['i']
219 mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer)
221 # Determine grid lines
222 minmax = np.where(highs, maxs, mins)
223 maxmin = np.where(highs, mins, maxs)
225 # Draw main axis line
226 juggled = info['juggled']
227 edgep1 = minmax.copy()
228 edgep1[juggled[0]] = maxmin[juggled[0]]
230 edgep2 = edgep1.copy()
231 edgep2[juggled[1]] = maxmin[juggled[1]]
232 pep = np.asarray(
233 proj3d.proj_trans_points([edgep1, edgep2], renderer.M))
234 centpt = proj3d.proj_transform(*centers, renderer.M)
235 self.line.set_data(pep[0], pep[1])
236 self.line.draw(renderer)
238 # Grid points where the planes meet
239 xyz0 = np.tile(minmax, (len(ticks), 1))
240 xyz0[:, index] = [tick.get_loc() for tick in ticks]
242 # Draw labels
243 # The transAxes transform is used because the Text object
244 # rotates the text relative to the display coordinate system.
245 # Therefore, if we want the labels to remain parallel to the
246 # axis regardless of the aspect ratio, we need to convert the
247 # edge points of the plane to display coordinates and calculate
248 # an angle from that.
249 # TODO: Maybe Text objects should handle this themselves?
250 dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) -
251 self.axes.transAxes.transform([pep[0:2, 0]]))[0]
253 lxyz = 0.5 * (edgep1 + edgep2)
255 # A rough estimate; points are ambiguous since 3D plots rotate
256 ax_scale = self.axes.bbox.size / self.figure.bbox.size
257 ax_inches = np.multiply(ax_scale, self.figure.get_size_inches())
258 ax_points_estimate = sum(72. * ax_inches)
259 deltas_per_point = 48 / ax_points_estimate
260 default_offset = 21.
261 labeldeltas = (
262 (self.labelpad + default_offset) * deltas_per_point * deltas)
263 axmask = [True, True, True]
264 axmask[index] = False
265 lxyz = move_from_center(lxyz, centers, labeldeltas, axmask)
266 tlx, tly, tlz = proj3d.proj_transform(*lxyz, renderer.M)
267 self.label.set_position((tlx, tly))
268 if self.get_rotate_label(self.label.get_text()):
269 angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx)))
270 self.label.set_rotation(angle)
271 self.label.set_va(info['label']['va'])
272 self.label.set_ha(info['label']['ha'])
273 self.label.draw(renderer)
275 # Draw Offset text
277 # Which of the two edge points do we want to
278 # use for locating the offset text?
279 if juggled[2] == 2:
280 outeredgep = edgep1
281 outerindex = 0
282 else:
283 outeredgep = edgep2
284 outerindex = 1
286 pos = move_from_center(outeredgep, centers, labeldeltas, axmask)
287 olx, oly, olz = proj3d.proj_transform(*pos, renderer.M)
288 self.offsetText.set_text(self.major.formatter.get_offset())
289 self.offsetText.set_position((olx, oly))
290 angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx)))
291 self.offsetText.set_rotation(angle)
292 # Must set rotation mode to "anchor" so that
293 # the alignment point is used as the "fulcrum" for rotation.
294 self.offsetText.set_rotation_mode('anchor')
296 #----------------------------------------------------------------------
297 # Note: the following statement for determining the proper alignment of
298 # the offset text. This was determined entirely by trial-and-error
299 # and should not be in any way considered as "the way". There are
300 # still some edge cases where alignment is not quite right, but this
301 # seems to be more of a geometry issue (in other words, I might be
302 # using the wrong reference points).
303 #
304 # (TT, FF, TF, FT) are the shorthand for the tuple of
305 # (centpt[info['tickdir']] <= pep[info['tickdir'], outerindex],
306 # centpt[index] <= pep[index, outerindex])
307 #
308 # Three-letters (e.g., TFT, FTT) are short-hand for the array of bools
309 # from the variable 'highs'.
310 # ---------------------------------------------------------------------
311 if centpt[info['tickdir']] > pep[info['tickdir'], outerindex]:
312 # if FT and if highs has an even number of Trues
313 if (centpt[index] <= pep[index, outerindex]
314 and np.count_nonzero(highs) % 2 == 0):
315 # Usually, this means align right, except for the FTT case,
316 # in which offset for axis 1 and 2 are aligned left.
317 if highs.tolist() == [False, True, True] and index in (1, 2):
318 align = 'left'
319 else:
320 align = 'right'
321 else:
322 # The FF case
323 align = 'left'
324 else:
325 # if TF and if highs has an even number of Trues
326 if (centpt[index] > pep[index, outerindex]
327 and np.count_nonzero(highs) % 2 == 0):
328 # Usually mean align left, except if it is axis 2
329 if index == 2:
330 align = 'right'
331 else:
332 align = 'left'
333 else:
334 # The TT case
335 align = 'right'
337 self.offsetText.set_va('center')
338 self.offsetText.set_ha(align)
339 self.offsetText.draw(renderer)
341 if self.axes._draw_grid and len(ticks):
342 # Grid lines go from the end of one plane through the plane
343 # intersection (at xyz0) to the end of the other plane. The first
344 # point (0) differs along dimension index-2 and the last (2) along
345 # dimension index-1.
346 lines = np.stack([xyz0, xyz0, xyz0], axis=1)
347 lines[:, 0, index - 2] = maxmin[index - 2]
348 lines[:, 2, index - 1] = maxmin[index - 1]
349 self.gridlines.set_segments(lines)
350 self.gridlines.set_color(info['grid']['color'])
351 self.gridlines.set_linewidth(info['grid']['linewidth'])
352 self.gridlines.set_linestyle(info['grid']['linestyle'])
353 self.gridlines.draw(renderer, project=True)
355 # Draw ticks
356 tickdir = info['tickdir']
357 tickdelta = deltas[tickdir]
358 if highs[tickdir]:
359 ticksign = 1
360 else:
361 ticksign = -1
363 for tick in ticks:
364 # Get tick line positions
365 pos = edgep1.copy()
366 pos[index] = tick.get_loc()
367 pos[tickdir] = (
368 edgep1[tickdir]
369 + info['tick']['outward_factor'] * ticksign * tickdelta)
370 x1, y1, z1 = proj3d.proj_transform(*pos, renderer.M)
371 pos[tickdir] = (
372 edgep1[tickdir]
373 - info['tick']['inward_factor'] * ticksign * tickdelta)
374 x2, y2, z2 = proj3d.proj_transform(*pos, renderer.M)
376 # Get position of label
377 default_offset = 8. # A rough estimate
378 labeldeltas = (
379 (tick.get_pad() + default_offset) * deltas_per_point * deltas)
381 axmask = [True, True, True]
382 axmask[index] = False
383 pos[tickdir] = edgep1[tickdir]
384 pos = move_from_center(pos, centers, labeldeltas, axmask)
385 lx, ly, lz = proj3d.proj_transform(*pos, renderer.M)
387 tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly))
388 tick.tick1line.set_linewidth(info['tick']['linewidth'])
389 tick.draw(renderer)
391 renderer.close_group('axis3d')
392 self.stale = False
394 # TODO: Get this to work properly when mplot3d supports
395 # the transforms framework.
396 def get_tightbbox(self, renderer):
397 # Currently returns None so that Axis.get_tightbbox
398 # doesn't return junk info.
399 return None
401 @property
402 def d_interval(self):
403 return self.get_data_interval()
405 @d_interval.setter
406 def d_interval(self, minmax):
407 return self.set_data_interval(*minmax)
409 @property
410 def v_interval(self):
411 return self.get_view_interval()
413 @v_interval.setter
414 def v_interval(self, minmax):
415 return self.set_view_interval(*minmax)
418# Use classes to look at different data limits
421class XAxis(Axis):
422 get_view_interval, set_view_interval = maxis._make_getset_interval(
423 "view", "xy_viewLim", "intervalx")
424 get_data_interval, set_data_interval = maxis._make_getset_interval(
425 "data", "xy_dataLim", "intervalx")
428class YAxis(Axis):
429 get_view_interval, set_view_interval = maxis._make_getset_interval(
430 "view", "xy_viewLim", "intervaly")
431 get_data_interval, set_data_interval = maxis._make_getset_interval(
432 "data", "xy_dataLim", "intervaly")
435class ZAxis(Axis):
436 get_view_interval, set_view_interval = maxis._make_getset_interval(
437 "view", "zz_viewLim", "intervalx")
438 get_data_interval, set_data_interval = maxis._make_getset_interval(
439 "data", "zz_dataLim", "intervalx")