Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/spines.py : 12%

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
1import numpy as np
3import matplotlib
4from matplotlib import cbook, docstring, rcParams
5from matplotlib.artist import allow_rasterization
6import matplotlib.cbook as cbook
7import matplotlib.transforms as mtransforms
8import matplotlib.patches as mpatches
9import matplotlib.path as mpath
12class Spine(mpatches.Patch):
13 """
14 An axis spine -- the line noting the data area boundaries
16 Spines are the lines connecting the axis tick marks and noting the
17 boundaries of the data area. They can be placed at arbitrary
18 positions. See function:`~matplotlib.spines.Spine.set_position`
19 for more information.
21 The default position is ``('outward',0)``.
23 Spines are subclasses of class:`~matplotlib.patches.Patch`, and
24 inherit much of their behavior.
26 Spines draw a line, a circle, or an arc depending if
27 function:`~matplotlib.spines.Spine.set_patch_line`,
28 function:`~matplotlib.spines.Spine.set_patch_circle`, or
29 function:`~matplotlib.spines.Spine.set_patch_arc` has been called.
30 Line-like is the default.
32 """
33 def __str__(self):
34 return "Spine"
36 @docstring.dedent_interpd
37 def __init__(self, axes, spine_type, path, **kwargs):
38 """
39 Parameters
40 ----------
41 axes : `~matplotlib.axes.Axes`
42 The `~.axes.Axes` instance containing the spine.
43 spine_type : str
44 The spine type.
45 path : `~matplotlib.path.Path`
46 The `.Path` instance used to draw the spine.
48 Other Parameters
49 ----------------
50 **kwargs
51 Valid keyword arguments are:
53 %(Patch)s
54 """
55 super().__init__(**kwargs)
56 self.axes = axes
57 self.set_figure(self.axes.figure)
58 self.spine_type = spine_type
59 self.set_facecolor('none')
60 self.set_edgecolor(rcParams['axes.edgecolor'])
61 self.set_linewidth(rcParams['axes.linewidth'])
62 self.set_capstyle('projecting')
63 self.axis = None
65 self.set_zorder(2.5)
66 self.set_transform(self.axes.transData) # default transform
68 self._bounds = None # default bounds
69 self._smart_bounds = False # deprecated in 3.2
71 # Defer initial position determination. (Not much support for
72 # non-rectangular axes is currently implemented, and this lets
73 # them pass through the spines machinery without errors.)
74 self._position = None
75 cbook._check_isinstance(matplotlib.path.Path, path=path)
76 self._path = path
78 # To support drawing both linear and circular spines, this
79 # class implements Patch behavior three ways. If
80 # self._patch_type == 'line', behave like a mpatches.PathPatch
81 # instance. If self._patch_type == 'circle', behave like a
82 # mpatches.Ellipse instance. If self._patch_type == 'arc', behave like
83 # a mpatches.Arc instance.
84 self._patch_type = 'line'
86 # Behavior copied from mpatches.Ellipse:
87 # Note: This cannot be calculated until this is added to an Axes
88 self._patch_transform = mtransforms.IdentityTransform()
90 @cbook.deprecated("3.2")
91 def set_smart_bounds(self, value):
92 """Set the spine and associated axis to have smart bounds."""
93 self._smart_bounds = value
95 # also set the axis if possible
96 if self.spine_type in ('left', 'right'):
97 self.axes.yaxis.set_smart_bounds(value)
98 elif self.spine_type in ('top', 'bottom'):
99 self.axes.xaxis.set_smart_bounds(value)
100 self.stale = True
102 @cbook.deprecated("3.2")
103 def get_smart_bounds(self):
104 """Return whether the spine has smart bounds."""
105 return self._smart_bounds
107 def set_patch_arc(self, center, radius, theta1, theta2):
108 """Set the spine to be arc-like."""
109 self._patch_type = 'arc'
110 self._center = center
111 self._width = radius * 2
112 self._height = radius * 2
113 self._theta1 = theta1
114 self._theta2 = theta2
115 self._path = mpath.Path.arc(theta1, theta2)
116 # arc drawn on axes transform
117 self.set_transform(self.axes.transAxes)
118 self.stale = True
120 def set_patch_circle(self, center, radius):
121 """Set the spine to be circular."""
122 self._patch_type = 'circle'
123 self._center = center
124 self._width = radius * 2
125 self._height = radius * 2
126 # circle drawn on axes transform
127 self.set_transform(self.axes.transAxes)
128 self.stale = True
130 def set_patch_line(self):
131 """Set the spine to be linear."""
132 self._patch_type = 'line'
133 self.stale = True
135 # Behavior copied from mpatches.Ellipse:
136 def _recompute_transform(self):
137 """
138 Notes
139 -----
140 This cannot be called until after this has been added to an Axes,
141 otherwise unit conversion will fail. This makes it very important to
142 call the accessor method and not directly access the transformation
143 member variable.
144 """
145 assert self._patch_type in ('arc', 'circle')
146 center = (self.convert_xunits(self._center[0]),
147 self.convert_yunits(self._center[1]))
148 width = self.convert_xunits(self._width)
149 height = self.convert_yunits(self._height)
150 self._patch_transform = mtransforms.Affine2D() \
151 .scale(width * 0.5, height * 0.5) \
152 .translate(*center)
154 def get_patch_transform(self):
155 if self._patch_type in ('arc', 'circle'):
156 self._recompute_transform()
157 return self._patch_transform
158 else:
159 return super().get_patch_transform()
161 def get_window_extent(self, renderer=None):
162 """
163 Return the window extent of the spines in display space, including
164 padding for ticks (but not their labels)
166 See Also
167 --------
168 matplotlib.axes.Axes.get_tightbbox
169 matplotlib.axes.Axes.get_window_extent
170 """
171 # make sure the location is updated so that transforms etc are correct:
172 self._adjust_location()
173 bb = super().get_window_extent(renderer=renderer)
174 if self.axis is None:
175 return bb
176 bboxes = [bb]
177 tickstocheck = [self.axis.majorTicks[0]]
178 if len(self.axis.minorTicks) > 1:
179 # only pad for minor ticks if there are more than one
180 # of them. There is always one...
181 tickstocheck.append(self.axis.minorTicks[1])
182 for tick in tickstocheck:
183 bb0 = bb.frozen()
184 tickl = tick._size
185 tickdir = tick._tickdir
186 if tickdir == 'out':
187 padout = 1
188 padin = 0
189 elif tickdir == 'in':
190 padout = 0
191 padin = 1
192 else:
193 padout = 0.5
194 padin = 0.5
195 padout = padout * tickl / 72 * self.figure.dpi
196 padin = padin * tickl / 72 * self.figure.dpi
198 if tick.tick1line.get_visible():
199 if self.spine_type == 'left':
200 bb0.x0 = bb0.x0 - padout
201 bb0.x1 = bb0.x1 + padin
202 elif self.spine_type == 'bottom':
203 bb0.y0 = bb0.y0 - padout
204 bb0.y1 = bb0.y1 + padin
206 if tick.tick2line.get_visible():
207 if self.spine_type == 'right':
208 bb0.x1 = bb0.x1 + padout
209 bb0.x0 = bb0.x0 - padin
210 elif self.spine_type == 'top':
211 bb0.y1 = bb0.y1 + padout
212 bb0.y0 = bb0.y0 - padout
213 bboxes.append(bb0)
215 return mtransforms.Bbox.union(bboxes)
217 def get_path(self):
218 return self._path
220 def _ensure_position_is_set(self):
221 if self._position is None:
222 # default position
223 self._position = ('outward', 0.0) # in points
224 self.set_position(self._position)
226 def register_axis(self, axis):
227 """Register an axis.
229 An axis should be registered with its corresponding spine from
230 the Axes instance. This allows the spine to clear any axis
231 properties when needed.
232 """
233 self.axis = axis
234 if self.axis is not None:
235 self.axis.cla()
236 self.stale = True
238 def cla(self):
239 """Clear the current spine."""
240 self._position = None # clear position
241 if self.axis is not None:
242 self.axis.cla()
244 @cbook.deprecated("3.1")
245 def is_frame_like(self):
246 """Return True if directly on axes frame.
248 This is useful for determining if a spine is the edge of an
249 old style MPL plot. If so, this function will return True.
250 """
251 self._ensure_position_is_set()
252 position = self._position
253 if isinstance(position, str):
254 if position == 'center':
255 position = ('axes', 0.5)
256 elif position == 'zero':
257 position = ('data', 0)
258 if len(position) != 2:
259 raise ValueError("position should be 2-tuple")
260 position_type, amount = position
261 if position_type == 'outward' and amount == 0:
262 return True
263 else:
264 return False
266 def _adjust_location(self):
267 """Automatically set spine bounds to the view interval."""
269 if self.spine_type == 'circle':
270 return
272 if self._bounds is None:
273 if self.spine_type in ('left', 'right'):
274 low, high = self.axes.viewLim.intervaly
275 elif self.spine_type in ('top', 'bottom'):
276 low, high = self.axes.viewLim.intervalx
277 else:
278 raise ValueError('unknown spine spine_type: %s' %
279 self.spine_type)
281 if self._smart_bounds: # deprecated in 3.2
282 # attempt to set bounds in sophisticated way
284 # handle inverted limits
285 viewlim_low, viewlim_high = sorted([low, high])
287 if self.spine_type in ('left', 'right'):
288 datalim_low, datalim_high = self.axes.dataLim.intervaly
289 ticks = self.axes.get_yticks()
290 elif self.spine_type in ('top', 'bottom'):
291 datalim_low, datalim_high = self.axes.dataLim.intervalx
292 ticks = self.axes.get_xticks()
293 # handle inverted limits
294 ticks = np.sort(ticks)
295 datalim_low, datalim_high = sorted([datalim_low, datalim_high])
297 if datalim_low < viewlim_low:
298 # Data extends past view. Clip line to view.
299 low = viewlim_low
300 else:
301 # Data ends before view ends.
302 cond = (ticks <= datalim_low) & (ticks >= viewlim_low)
303 tickvals = ticks[cond]
304 if len(tickvals):
305 # A tick is less than or equal to lowest data point.
306 low = tickvals[-1]
307 else:
308 # No tick is available
309 low = datalim_low
310 low = max(low, viewlim_low)
312 if datalim_high > viewlim_high:
313 # Data extends past view. Clip line to view.
314 high = viewlim_high
315 else:
316 # Data ends before view ends.
317 cond = (ticks >= datalim_high) & (ticks <= viewlim_high)
318 tickvals = ticks[cond]
319 if len(tickvals):
320 # A tick is greater than or equal to highest data
321 # point.
322 high = tickvals[0]
323 else:
324 # No tick is available
325 high = datalim_high
326 high = min(high, viewlim_high)
328 else:
329 low, high = self._bounds
331 if self._patch_type == 'arc':
332 if self.spine_type in ('bottom', 'top'):
333 try:
334 direction = self.axes.get_theta_direction()
335 except AttributeError:
336 direction = 1
337 try:
338 offset = self.axes.get_theta_offset()
339 except AttributeError:
340 offset = 0
341 low = low * direction + offset
342 high = high * direction + offset
343 if low > high:
344 low, high = high, low
346 self._path = mpath.Path.arc(np.rad2deg(low), np.rad2deg(high))
348 if self.spine_type == 'bottom':
349 rmin, rmax = self.axes.viewLim.intervaly
350 try:
351 rorigin = self.axes.get_rorigin()
352 except AttributeError:
353 rorigin = rmin
354 scaled_diameter = (rmin - rorigin) / (rmax - rorigin)
355 self._height = scaled_diameter
356 self._width = scaled_diameter
358 else:
359 raise ValueError('unable to set bounds for spine "%s"' %
360 self.spine_type)
361 else:
362 v1 = self._path.vertices
363 assert v1.shape == (2, 2), 'unexpected vertices shape'
364 if self.spine_type in ['left', 'right']:
365 v1[0, 1] = low
366 v1[1, 1] = high
367 elif self.spine_type in ['bottom', 'top']:
368 v1[0, 0] = low
369 v1[1, 0] = high
370 else:
371 raise ValueError('unable to set bounds for spine "%s"' %
372 self.spine_type)
374 @allow_rasterization
375 def draw(self, renderer):
376 self._adjust_location()
377 ret = super().draw(renderer)
378 self.stale = False
379 return ret
381 def set_position(self, position):
382 """Set the position of the spine.
384 Spine position is specified by a 2 tuple of (position type,
385 amount). The position types are:
387 * 'outward' : place the spine out from the data area by the
388 specified number of points. (Negative values specify placing the
389 spine inward.)
391 * 'axes' : place the spine at the specified Axes coordinate (from
392 0.0-1.0).
394 * 'data' : place the spine at the specified data coordinate.
396 Additionally, shorthand notations define a special positions:
398 * 'center' -> ('axes',0.5)
399 * 'zero' -> ('data', 0.0)
401 """
402 if position in ('center', 'zero'):
403 # special positions
404 pass
405 else:
406 if len(position) != 2:
407 raise ValueError("position should be 'center' or 2-tuple")
408 if position[0] not in ['outward', 'axes', 'data']:
409 raise ValueError("position[0] should be one of 'outward', "
410 "'axes', or 'data' ")
411 self._position = position
413 self.set_transform(self.get_spine_transform())
415 if self.axis is not None:
416 self.axis.reset_ticks()
417 self.stale = True
419 def get_position(self):
420 """Return the spine position."""
421 self._ensure_position_is_set()
422 return self._position
424 def get_spine_transform(self):
425 """Return the spine transform."""
426 self._ensure_position_is_set()
428 position = self._position
429 if isinstance(position, str):
430 if position == 'center':
431 position = ('axes', 0.5)
432 elif position == 'zero':
433 position = ('data', 0)
434 assert len(position) == 2, 'position should be 2-tuple'
435 position_type, amount = position
436 cbook._check_in_list(['axes', 'outward', 'data'],
437 position_type=position_type)
438 if self.spine_type in ['left', 'right']:
439 base_transform = self.axes.get_yaxis_transform(which='grid')
440 elif self.spine_type in ['top', 'bottom']:
441 base_transform = self.axes.get_xaxis_transform(which='grid')
442 else:
443 raise ValueError(f'unknown spine spine_type: {self.spine_type!r}')
445 if position_type == 'outward':
446 if amount == 0: # short circuit commonest case
447 return base_transform
448 else:
449 offset_vec = {'left': (-1, 0), 'right': (1, 0),
450 'bottom': (0, -1), 'top': (0, 1),
451 }[self.spine_type]
452 # calculate x and y offset in dots
453 offset_dots = amount * np.array(offset_vec) / 72
454 return (base_transform
455 + mtransforms.ScaledTranslation(
456 *offset_dots, self.figure.dpi_scale_trans))
457 elif position_type == 'axes':
458 if self.spine_type in ['left', 'right']:
459 # keep y unchanged, fix x at amount
460 return (mtransforms.Affine2D.from_values(0, 0, 0, 1, amount, 0)
461 + base_transform)
462 elif self.spine_type in ['bottom', 'top']:
463 # keep x unchanged, fix y at amount
464 return (mtransforms.Affine2D.from_values(1, 0, 0, 0, 0, amount)
465 + base_transform)
466 elif position_type == 'data':
467 if self.spine_type in ('right', 'top'):
468 # The right and top spines have a default position of 1 in
469 # axes coordinates. When specifying the position in data
470 # coordinates, we need to calculate the position relative to 0.
471 amount -= 1
472 if self.spine_type in ('left', 'right'):
473 return mtransforms.blended_transform_factory(
474 mtransforms.Affine2D().translate(amount, 0)
475 + self.axes.transData,
476 self.axes.transData)
477 elif self.spine_type in ('bottom', 'top'):
478 return mtransforms.blended_transform_factory(
479 self.axes.transData,
480 mtransforms.Affine2D().translate(0, amount)
481 + self.axes.transData)
483 def set_bounds(self, low=None, high=None):
484 """
485 Set the spine bounds.
487 Parameters
488 ----------
489 low : float or None, optional
490 The lower spine bound. Passing *None* leaves the limit unchanged.
492 The bounds may also be passed as the tuple (*low*, *high*) as the
493 first positional argument.
495 .. ACCEPTS: (low: float, high: float)
497 high : float or None, optional
498 The higher spine bound. Passing *None* leaves the limit unchanged.
499 """
500 if self.spine_type == 'circle':
501 raise ValueError(
502 'set_bounds() method incompatible with circular spines')
503 if high is None and np.iterable(low):
504 low, high = low
505 old_low, old_high = self.get_bounds() or (None, None)
506 if low is None:
507 low = old_low
508 if high is None:
509 high = old_high
510 self._bounds = (low, high)
511 self.stale = True
513 def get_bounds(self):
514 """Get the bounds of the spine."""
515 return self._bounds
517 @classmethod
518 def linear_spine(cls, axes, spine_type, **kwargs):
519 """
520 Returns a linear `Spine`.
521 """
522 # all values of 0.999 get replaced upon call to set_bounds()
523 if spine_type == 'left':
524 path = mpath.Path([(0.0, 0.999), (0.0, 0.999)])
525 elif spine_type == 'right':
526 path = mpath.Path([(1.0, 0.999), (1.0, 0.999)])
527 elif spine_type == 'bottom':
528 path = mpath.Path([(0.999, 0.0), (0.999, 0.0)])
529 elif spine_type == 'top':
530 path = mpath.Path([(0.999, 1.0), (0.999, 1.0)])
531 else:
532 raise ValueError('unable to make path for spine "%s"' % spine_type)
533 result = cls(axes, spine_type, path, **kwargs)
534 result.set_visible(rcParams['axes.spines.{0}'.format(spine_type)])
536 return result
538 @classmethod
539 def arc_spine(cls, axes, spine_type, center, radius, theta1, theta2,
540 **kwargs):
541 """
542 Returns an arc `Spine`.
543 """
544 path = mpath.Path.arc(theta1, theta2)
545 result = cls(axes, spine_type, path, **kwargs)
546 result.set_patch_arc(center, radius, theta1, theta2)
547 return result
549 @classmethod
550 def circular_spine(cls, axes, center, radius, **kwargs):
551 """
552 Returns a circular `Spine`.
553 """
554 path = mpath.Path.unit_circle()
555 spine_type = 'circle'
556 result = cls(axes, spine_type, path, **kwargs)
557 result.set_patch_circle(center, radius)
558 return result
560 def set_color(self, c):
561 """
562 Set the edgecolor.
564 Parameters
565 ----------
566 c : color
568 Notes
569 -----
570 This method does not modify the facecolor (which defaults to "none"),
571 unlike the `Patch.set_color` method defined in the parent class. Use
572 `Patch.set_facecolor` to set the facecolor.
573 """
574 self.set_edgecolor(c)
575 self.stale = True