You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1429 lines
47 KiB

4 years ago
  1. """
  2. This module contains all the 2D line class which can draw with a
  3. variety of line styles, markers and colors.
  4. """
  5. # TODO: expose cap and join style attrs
  6. from numbers import Integral, Number, Real
  7. import warnings
  8. import numpy as np
  9. from . import artist, cbook, colors as mcolors, docstring, rcParams
  10. from .artist import Artist, allow_rasterization
  11. from .cbook import (
  12. _to_unmasked_float_array, iterable, ls_mapper, ls_mapper_r,
  13. STEP_LOOKUP_MAP)
  14. from .markers import MarkerStyle
  15. from .path import Path
  16. from .transforms import Bbox, TransformedPath, IdentityTransform
  17. # Imported here for backward compatibility, even though they don't
  18. # really belong.
  19. from . import _path
  20. from .markers import (
  21. CARETLEFT, CARETRIGHT, CARETUP, CARETDOWN,
  22. CARETLEFTBASE, CARETRIGHTBASE, CARETUPBASE, CARETDOWNBASE,
  23. TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN)
  24. def _get_dash_pattern(style):
  25. """Convert linestyle -> dash pattern
  26. """
  27. # go from short hand -> full strings
  28. if isinstance(style, str):
  29. style = ls_mapper.get(style, style)
  30. # un-dashed styles
  31. if style in ['solid', 'None']:
  32. offset, dashes = None, None
  33. # dashed styles
  34. elif style in ['dashed', 'dashdot', 'dotted']:
  35. offset = 0
  36. dashes = tuple(rcParams['lines.{}_pattern'.format(style)])
  37. #
  38. elif isinstance(style, tuple):
  39. offset, dashes = style
  40. else:
  41. raise ValueError('Unrecognized linestyle: %s' % str(style))
  42. # normalize offset to be positive and shorter than the dash cycle
  43. if dashes is not None and offset is not None:
  44. dsum = sum(dashes)
  45. if dsum:
  46. offset %= dsum
  47. return offset, dashes
  48. def _scale_dashes(offset, dashes, lw):
  49. if not rcParams['lines.scale_dashes']:
  50. return offset, dashes
  51. scaled_offset = scaled_dashes = None
  52. if offset is not None:
  53. scaled_offset = offset * lw
  54. if dashes is not None:
  55. scaled_dashes = [x * lw if x is not None else None
  56. for x in dashes]
  57. return scaled_offset, scaled_dashes
  58. def segment_hits(cx, cy, x, y, radius):
  59. """
  60. Determine if any line segments are within radius of a
  61. point. Returns the list of line segments that are within that
  62. radius.
  63. """
  64. # Process single points specially
  65. if len(x) < 2:
  66. res, = np.nonzero((cx - x) ** 2 + (cy - y) ** 2 <= radius ** 2)
  67. return res
  68. # We need to lop the last element off a lot.
  69. xr, yr = x[:-1], y[:-1]
  70. # Only look at line segments whose nearest point to C on the line
  71. # lies within the segment.
  72. dx, dy = x[1:] - xr, y[1:] - yr
  73. Lnorm_sq = dx ** 2 + dy ** 2 # Possibly want to eliminate Lnorm==0
  74. u = ((cx - xr) * dx + (cy - yr) * dy) / Lnorm_sq
  75. candidates = (u >= 0) & (u <= 1)
  76. # Note that there is a little area near one side of each point
  77. # which will be near neither segment, and another which will
  78. # be near both, depending on the angle of the lines. The
  79. # following radius test eliminates these ambiguities.
  80. point_hits = (cx - x) ** 2 + (cy - y) ** 2 <= radius ** 2
  81. candidates = candidates & ~(point_hits[:-1] | point_hits[1:])
  82. # For those candidates which remain, determine how far they lie away
  83. # from the line.
  84. px, py = xr + u * dx, yr + u * dy
  85. line_hits = (cx - px) ** 2 + (cy - py) ** 2 <= radius ** 2
  86. line_hits = line_hits & candidates
  87. points, = point_hits.ravel().nonzero()
  88. lines, = line_hits.ravel().nonzero()
  89. return np.concatenate((points, lines))
  90. def _mark_every_path(markevery, tpath, affine, ax_transform):
  91. """
  92. Helper function that sorts out how to deal the input
  93. `markevery` and returns the points where markers should be drawn.
  94. Takes in the `markevery` value and the line path and returns the
  95. sub-sampled path.
  96. """
  97. # pull out the two bits of data we want from the path
  98. codes, verts = tpath.codes, tpath.vertices
  99. def _slice_or_none(in_v, slc):
  100. '''
  101. Helper function to cope with `codes` being an
  102. ndarray or `None`
  103. '''
  104. if in_v is None:
  105. return None
  106. return in_v[slc]
  107. # if just an int, assume starting at 0 and make a tuple
  108. if isinstance(markevery, Integral):
  109. markevery = (0, markevery)
  110. # if just a float, assume starting at 0.0 and make a tuple
  111. elif isinstance(markevery, Real):
  112. markevery = (0.0, markevery)
  113. if isinstance(markevery, tuple):
  114. if len(markevery) != 2:
  115. raise ValueError('`markevery` is a tuple but its len is not 2; '
  116. 'markevery={}'.format(markevery))
  117. start, step = markevery
  118. # if step is an int, old behavior
  119. if isinstance(step, Integral):
  120. # tuple of 2 int is for backwards compatibility,
  121. if not isinstance(start, Integral):
  122. raise ValueError(
  123. '`markevery` is a tuple with len 2 and second element is '
  124. 'an int, but the first element is not an int; markevery={}'
  125. .format(markevery))
  126. # just return, we are done here
  127. return Path(verts[slice(start, None, step)],
  128. _slice_or_none(codes, slice(start, None, step)))
  129. elif isinstance(step, Real):
  130. if not isinstance(start, Real):
  131. raise ValueError(
  132. '`markevery` is a tuple with len 2 and second element is '
  133. 'a float, but the first element is not a float or an int; '
  134. 'markevery={}'.format(markevery))
  135. # calc cumulative distance along path (in display coords):
  136. disp_coords = affine.transform(tpath.vertices)
  137. delta = np.empty((len(disp_coords), 2))
  138. delta[0, :] = 0
  139. delta[1:, :] = disp_coords[1:, :] - disp_coords[:-1, :]
  140. delta = np.sum(delta**2, axis=1)
  141. delta = np.sqrt(delta)
  142. delta = np.cumsum(delta)
  143. # calc distance between markers along path based on the axes
  144. # bounding box diagonal being a distance of unity:
  145. scale = ax_transform.transform(np.array([[0, 0], [1, 1]]))
  146. scale = np.diff(scale, axis=0)
  147. scale = np.sum(scale**2)
  148. scale = np.sqrt(scale)
  149. marker_delta = np.arange(start * scale, delta[-1], step * scale)
  150. # find closest actual data point that is closest to
  151. # the theoretical distance along the path:
  152. inds = np.abs(delta[np.newaxis, :] - marker_delta[:, np.newaxis])
  153. inds = inds.argmin(axis=1)
  154. inds = np.unique(inds)
  155. # return, we are done here
  156. return Path(verts[inds],
  157. _slice_or_none(codes, inds))
  158. else:
  159. raise ValueError(
  160. '`markevery` is a tuple with len 2, but its second element is '
  161. 'not an int or a float; markevery=%s' % (markevery,))
  162. elif isinstance(markevery, slice):
  163. # mazol tov, it's already a slice, just return
  164. return Path(verts[markevery], _slice_or_none(codes, markevery))
  165. elif iterable(markevery):
  166. #fancy indexing
  167. try:
  168. return Path(verts[markevery], _slice_or_none(codes, markevery))
  169. except (ValueError, IndexError):
  170. raise ValueError('`markevery` is iterable but '
  171. 'not a valid form of numpy fancy indexing; '
  172. 'markevery=%s' % (markevery,))
  173. else:
  174. raise ValueError('Value of `markevery` is not '
  175. 'recognized; '
  176. 'markevery=%s' % (markevery,))
  177. @cbook._define_aliases({
  178. "antialiased": ["aa"],
  179. "color": ["c"],
  180. "linestyle": ["ls"],
  181. "linewidth": ["lw"],
  182. "markeredgecolor": ["mec"],
  183. "markeredgewidth": ["mew"],
  184. "markerfacecolor": ["mfc"],
  185. "markerfacecoloralt": ["mfcalt"],
  186. "markersize": ["ms"],
  187. })
  188. class Line2D(Artist):
  189. """
  190. A line - the line can have both a solid linestyle connecting all
  191. the vertices, and a marker at each vertex. Additionally, the
  192. drawing of the solid line is influenced by the drawstyle, e.g., one
  193. can create "stepped" lines in various styles.
  194. """
  195. lineStyles = _lineStyles = { # hidden names deprecated
  196. '-': '_draw_solid',
  197. '--': '_draw_dashed',
  198. '-.': '_draw_dash_dot',
  199. ':': '_draw_dotted',
  200. 'None': '_draw_nothing',
  201. ' ': '_draw_nothing',
  202. '': '_draw_nothing',
  203. }
  204. _drawStyles_l = {
  205. 'default': '_draw_lines',
  206. 'steps-mid': '_draw_steps_mid',
  207. 'steps-pre': '_draw_steps_pre',
  208. 'steps-post': '_draw_steps_post',
  209. }
  210. _drawStyles_s = {
  211. 'steps': '_draw_steps_pre',
  212. }
  213. # drawStyles should now be deprecated.
  214. drawStyles = {**_drawStyles_l, **_drawStyles_s}
  215. # Need a list ordered with long names first:
  216. drawStyleKeys = [*_drawStyles_l, *_drawStyles_s]
  217. # Referenced here to maintain API. These are defined in
  218. # MarkerStyle
  219. markers = MarkerStyle.markers
  220. filled_markers = MarkerStyle.filled_markers
  221. fillStyles = MarkerStyle.fillstyles
  222. zorder = 2
  223. validCap = ('butt', 'round', 'projecting')
  224. validJoin = ('miter', 'round', 'bevel')
  225. def __str__(self):
  226. if self._label != "":
  227. return "Line2D(%s)" % (self._label)
  228. elif self._x is None:
  229. return "Line2D()"
  230. elif len(self._x) > 3:
  231. return "Line2D((%g,%g),(%g,%g),...,(%g,%g))"\
  232. % (self._x[0], self._y[0], self._x[0],
  233. self._y[0], self._x[-1], self._y[-1])
  234. else:
  235. return "Line2D(%s)"\
  236. % (",".join(["(%g,%g)" % (x, y) for x, y
  237. in zip(self._x, self._y)]))
  238. def __init__(self, xdata, ydata,
  239. linewidth=None, # all Nones default to rc
  240. linestyle=None,
  241. color=None,
  242. marker=None,
  243. markersize=None,
  244. markeredgewidth=None,
  245. markeredgecolor=None,
  246. markerfacecolor=None,
  247. markerfacecoloralt='none',
  248. fillstyle=None,
  249. antialiased=None,
  250. dash_capstyle=None,
  251. solid_capstyle=None,
  252. dash_joinstyle=None,
  253. solid_joinstyle=None,
  254. pickradius=5,
  255. drawstyle=None,
  256. markevery=None,
  257. **kwargs
  258. ):
  259. """
  260. Create a :class:`~matplotlib.lines.Line2D` instance with *x*
  261. and *y* data in sequences *xdata*, *ydata*.
  262. The kwargs are :class:`~matplotlib.lines.Line2D` properties:
  263. %(Line2D)s
  264. See :meth:`set_linestyle` for a description of the line styles,
  265. :meth:`set_marker` for a description of the markers, and
  266. :meth:`set_drawstyle` for a description of the draw styles.
  267. """
  268. Artist.__init__(self)
  269. #convert sequences to numpy arrays
  270. if not iterable(xdata):
  271. raise RuntimeError('xdata must be a sequence')
  272. if not iterable(ydata):
  273. raise RuntimeError('ydata must be a sequence')
  274. if linewidth is None:
  275. linewidth = rcParams['lines.linewidth']
  276. if linestyle is None:
  277. linestyle = rcParams['lines.linestyle']
  278. if marker is None:
  279. marker = rcParams['lines.marker']
  280. if markerfacecolor is None:
  281. markerfacecolor = rcParams['lines.markerfacecolor']
  282. if markeredgecolor is None:
  283. markeredgecolor = rcParams['lines.markeredgecolor']
  284. if color is None:
  285. color = rcParams['lines.color']
  286. if markersize is None:
  287. markersize = rcParams['lines.markersize']
  288. if antialiased is None:
  289. antialiased = rcParams['lines.antialiased']
  290. if dash_capstyle is None:
  291. dash_capstyle = rcParams['lines.dash_capstyle']
  292. if dash_joinstyle is None:
  293. dash_joinstyle = rcParams['lines.dash_joinstyle']
  294. if solid_capstyle is None:
  295. solid_capstyle = rcParams['lines.solid_capstyle']
  296. if solid_joinstyle is None:
  297. solid_joinstyle = rcParams['lines.solid_joinstyle']
  298. if isinstance(linestyle, str):
  299. ds, ls = self._split_drawstyle_linestyle(linestyle)
  300. if ds is not None and drawstyle is not None and ds != drawstyle:
  301. raise ValueError("Inconsistent drawstyle ({!r}) and linestyle "
  302. "({!r})".format(drawstyle, linestyle))
  303. linestyle = ls
  304. if ds is not None:
  305. drawstyle = ds
  306. if drawstyle is None:
  307. drawstyle = 'default'
  308. self._dashcapstyle = None
  309. self._dashjoinstyle = None
  310. self._solidjoinstyle = None
  311. self._solidcapstyle = None
  312. self.set_dash_capstyle(dash_capstyle)
  313. self.set_dash_joinstyle(dash_joinstyle)
  314. self.set_solid_capstyle(solid_capstyle)
  315. self.set_solid_joinstyle(solid_joinstyle)
  316. self._linestyles = None
  317. self._drawstyle = None
  318. self._linewidth = linewidth
  319. # scaled dash + offset
  320. self._dashSeq = None
  321. self._dashOffset = 0
  322. # unscaled dash + offset
  323. # this is needed scaling the dash pattern by linewidth
  324. self._us_dashSeq = None
  325. self._us_dashOffset = 0
  326. self.set_linewidth(linewidth)
  327. self.set_linestyle(linestyle)
  328. self.set_drawstyle(drawstyle)
  329. self._color = None
  330. self.set_color(color)
  331. self._marker = MarkerStyle(marker, fillstyle)
  332. self._markevery = None
  333. self._markersize = None
  334. self._antialiased = None
  335. self.set_markevery(markevery)
  336. self.set_antialiased(antialiased)
  337. self.set_markersize(markersize)
  338. self._markeredgecolor = None
  339. self._markeredgewidth = None
  340. self._markerfacecolor = None
  341. self._markerfacecoloralt = None
  342. self.set_markerfacecolor(markerfacecolor)
  343. self.set_markerfacecoloralt(markerfacecoloralt)
  344. self.set_markeredgecolor(markeredgecolor)
  345. self.set_markeredgewidth(markeredgewidth)
  346. self.verticalOffset = None
  347. # update kwargs before updating data to give the caller a
  348. # chance to init axes (and hence unit support)
  349. self.update(kwargs)
  350. self.pickradius = pickradius
  351. self.ind_offset = 0
  352. if isinstance(self._picker, Number):
  353. self.pickradius = self._picker
  354. self._xorig = np.asarray([])
  355. self._yorig = np.asarray([])
  356. self._invalidx = True
  357. self._invalidy = True
  358. self._x = None
  359. self._y = None
  360. self._xy = None
  361. self._path = None
  362. self._transformed_path = None
  363. self._subslice = False
  364. self._x_filled = None # used in subslicing; only x is needed
  365. self.set_data(xdata, ydata)
  366. def contains(self, mouseevent):
  367. """
  368. Test whether the mouse event occurred on the line. The pick
  369. radius determines the precision of the location test (usually
  370. within five points of the value). Use
  371. :meth:`~matplotlib.lines.Line2D.get_pickradius` or
  372. :meth:`~matplotlib.lines.Line2D.set_pickradius` to view or
  373. modify it.
  374. Returns *True* if any values are within the radius along with
  375. ``{'ind': pointlist}``, where *pointlist* is the set of points
  376. within the radius.
  377. TODO: sort returned indices by distance
  378. """
  379. if callable(self._contains):
  380. return self._contains(self, mouseevent)
  381. if not isinstance(self.pickradius, Number):
  382. raise ValueError("pick radius should be a distance")
  383. # Make sure we have data to plot
  384. if self._invalidy or self._invalidx:
  385. self.recache()
  386. if len(self._xy) == 0:
  387. return False, {}
  388. # Convert points to pixels
  389. transformed_path = self._get_transformed_path()
  390. path, affine = transformed_path.get_transformed_path_and_affine()
  391. path = affine.transform_path(path)
  392. xy = path.vertices
  393. xt = xy[:, 0]
  394. yt = xy[:, 1]
  395. # Convert pick radius from points to pixels
  396. if self.figure is None:
  397. warnings.warn('no figure set when check if mouse is on line')
  398. pixels = self.pickradius
  399. else:
  400. pixels = self.figure.dpi / 72. * self.pickradius
  401. # the math involved in checking for containment (here and inside of
  402. # segment_hits) assumes that it is OK to overflow. In case the
  403. # application has set the error flags such that an exception is raised
  404. # on overflow, we temporarily set the appropriate error flags here and
  405. # set them back when we are finished.
  406. with np.errstate(all='ignore'):
  407. # Check for collision
  408. if self._linestyle in ['None', None]:
  409. # If no line, return the nearby point(s)
  410. d = (xt - mouseevent.x) ** 2 + (yt - mouseevent.y) ** 2
  411. ind, = np.nonzero(np.less_equal(d, pixels ** 2))
  412. else:
  413. # If line, return the nearby segment(s)
  414. ind = segment_hits(mouseevent.x, mouseevent.y, xt, yt, pixels)
  415. if self._drawstyle.startswith("steps"):
  416. ind //= 2
  417. ind += self.ind_offset
  418. # Return the point(s) within radius
  419. return len(ind) > 0, dict(ind=ind)
  420. def get_pickradius(self):
  421. """return the pick radius used for containment tests"""
  422. return self.pickradius
  423. def set_pickradius(self, d):
  424. """Set the pick radius used for containment tests.
  425. Parameters
  426. ----------
  427. d : float
  428. Pick radius, in points.
  429. """
  430. self.pickradius = d
  431. def get_fillstyle(self):
  432. """
  433. return the marker fillstyle
  434. """
  435. return self._marker.get_fillstyle()
  436. def set_fillstyle(self, fs):
  437. """
  438. Set the marker fill style; 'full' means fill the whole marker.
  439. 'none' means no filling; other options are for half-filled markers.
  440. Parameters
  441. ----------
  442. fs : {'full', 'left', 'right', 'bottom', 'top', 'none'}
  443. """
  444. self._marker.set_fillstyle(fs)
  445. self.stale = True
  446. def set_markevery(self, every):
  447. """Set the markevery property to subsample the plot when using markers.
  448. e.g., if `every=5`, every 5-th marker will be plotted.
  449. Parameters
  450. ----------
  451. every: None or int or (int, int) or slice or List[int] or float or \
  452. (float, float)
  453. Which markers to plot.
  454. - every=None, every point will be plotted.
  455. - every=N, every N-th marker will be plotted starting with
  456. marker 0.
  457. - every=(start, N), every N-th marker, starting at point
  458. start, will be plotted.
  459. - every=slice(start, end, N), every N-th marker, starting at
  460. point start, up to but not including point end, will be plotted.
  461. - every=[i, j, m, n], only markers at points i, j, m, and n
  462. will be plotted.
  463. - every=0.1, (i.e. a float) then markers will be spaced at
  464. approximately equal distances along the line; the distance
  465. along the line between markers is determined by multiplying the
  466. display-coordinate distance of the axes bounding-box diagonal
  467. by the value of every.
  468. - every=(0.5, 0.1) (i.e. a length-2 tuple of float), the
  469. same functionality as every=0.1 is exhibited but the first
  470. marker will be 0.5 multiplied by the
  471. display-cordinate-diagonal-distance along the line.
  472. Notes
  473. -----
  474. Setting the markevery property will only show markers at actual data
  475. points. When using float arguments to set the markevery property
  476. on irregularly spaced data, the markers will likely not appear evenly
  477. spaced because the actual data points do not coincide with the
  478. theoretical spacing between markers.
  479. When using a start offset to specify the first marker, the offset will
  480. be from the first data point which may be different from the first
  481. the visible data point if the plot is zoomed in.
  482. If zooming in on a plot when using float arguments then the actual
  483. data points that have markers will change because the distance between
  484. markers is always determined from the display-coordinates
  485. axes-bounding-box-diagonal regardless of the actual axes data limits.
  486. """
  487. if self._markevery != every:
  488. self.stale = True
  489. self._markevery = every
  490. def get_markevery(self):
  491. """return the markevery setting"""
  492. return self._markevery
  493. def set_picker(self, p):
  494. """Sets the event picker details for the line.
  495. Parameters
  496. ----------
  497. p : float or callable[[Artist, Event], Tuple[bool, dict]]
  498. If a float, it is used as the pick radius in points.
  499. """
  500. if callable(p):
  501. self._contains = p
  502. else:
  503. self.pickradius = p
  504. self._picker = p
  505. def get_window_extent(self, renderer):
  506. bbox = Bbox([[0, 0], [0, 0]])
  507. trans_data_to_xy = self.get_transform().transform
  508. bbox.update_from_data_xy(trans_data_to_xy(self.get_xydata()),
  509. ignore=True)
  510. # correct for marker size, if any
  511. if self._marker:
  512. ms = (self._markersize / 72.0 * self.figure.dpi) * 0.5
  513. bbox = bbox.padded(ms)
  514. return bbox
  515. @Artist.axes.setter
  516. def axes(self, ax):
  517. # call the set method from the base-class property
  518. Artist.axes.fset(self, ax)
  519. if ax is not None:
  520. # connect unit-related callbacks
  521. if ax.xaxis is not None:
  522. self._xcid = ax.xaxis.callbacks.connect('units',
  523. self.recache_always)
  524. if ax.yaxis is not None:
  525. self._ycid = ax.yaxis.callbacks.connect('units',
  526. self.recache_always)
  527. def set_data(self, *args):
  528. """
  529. Set the x and y data
  530. ACCEPTS: 2D array (rows are x, y) or two 1D arrays
  531. """
  532. if len(args) == 1:
  533. x, y = args[0]
  534. else:
  535. x, y = args
  536. self.set_xdata(x)
  537. self.set_ydata(y)
  538. def recache_always(self):
  539. self.recache(always=True)
  540. def recache(self, always=False):
  541. if always or self._invalidx:
  542. xconv = self.convert_xunits(self._xorig)
  543. x = _to_unmasked_float_array(xconv).ravel()
  544. else:
  545. x = self._x
  546. if always or self._invalidy:
  547. yconv = self.convert_yunits(self._yorig)
  548. y = _to_unmasked_float_array(yconv).ravel()
  549. else:
  550. y = self._y
  551. self._xy = np.column_stack(np.broadcast_arrays(x, y)).astype(float)
  552. self._x, self._y = self._xy.T # views
  553. self._subslice = False
  554. if (self.axes and len(x) > 1000 and self._is_sorted(x) and
  555. self.axes.name == 'rectilinear' and
  556. self.axes.get_xscale() == 'linear' and
  557. self._markevery is None and
  558. self.get_clip_on() is True):
  559. self._subslice = True
  560. nanmask = np.isnan(x)
  561. if nanmask.any():
  562. self._x_filled = self._x.copy()
  563. indices = np.arange(len(x))
  564. self._x_filled[nanmask] = np.interp(indices[nanmask],
  565. indices[~nanmask], self._x[~nanmask])
  566. else:
  567. self._x_filled = self._x
  568. if self._path is not None:
  569. interpolation_steps = self._path._interpolation_steps
  570. else:
  571. interpolation_steps = 1
  572. xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy.T)
  573. self._path = Path(np.asarray(xy).T,
  574. _interpolation_steps=interpolation_steps)
  575. self._transformed_path = None
  576. self._invalidx = False
  577. self._invalidy = False
  578. def _transform_path(self, subslice=None):
  579. """
  580. Puts a TransformedPath instance at self._transformed_path;
  581. all invalidation of the transform is then handled by the
  582. TransformedPath instance.
  583. """
  584. # Masked arrays are now handled by the Path class itself
  585. if subslice is not None:
  586. xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy[subslice, :].T)
  587. _path = Path(np.asarray(xy).T,
  588. _interpolation_steps=self._path._interpolation_steps)
  589. else:
  590. _path = self._path
  591. self._transformed_path = TransformedPath(_path, self.get_transform())
  592. def _get_transformed_path(self):
  593. """
  594. Return the :class:`~matplotlib.transforms.TransformedPath` instance
  595. of this line.
  596. """
  597. if self._transformed_path is None:
  598. self._transform_path()
  599. return self._transformed_path
  600. def set_transform(self, t):
  601. """
  602. set the Transformation instance used by this artist
  603. Parameters
  604. ----------
  605. t : matplotlib.transforms.Transform
  606. """
  607. Artist.set_transform(self, t)
  608. self._invalidx = True
  609. self._invalidy = True
  610. self.stale = True
  611. def _is_sorted(self, x):
  612. """return True if x is sorted in ascending order"""
  613. # We don't handle the monotonically decreasing case.
  614. return _path.is_sorted(x)
  615. @allow_rasterization
  616. def draw(self, renderer):
  617. """draw the Line with `renderer` unless visibility is False"""
  618. if not self.get_visible():
  619. return
  620. if self._invalidy or self._invalidx:
  621. self.recache()
  622. self.ind_offset = 0 # Needed for contains() method.
  623. if self._subslice and self.axes:
  624. x0, x1 = self.axes.get_xbound()
  625. i0, = self._x_filled.searchsorted([x0], 'left')
  626. i1, = self._x_filled.searchsorted([x1], 'right')
  627. subslice = slice(max(i0 - 1, 0), i1 + 1)
  628. self.ind_offset = subslice.start
  629. self._transform_path(subslice)
  630. else:
  631. subslice = None
  632. if self.get_path_effects():
  633. from matplotlib.patheffects import PathEffectRenderer
  634. renderer = PathEffectRenderer(self.get_path_effects(), renderer)
  635. renderer.open_group('line2d', self.get_gid())
  636. if self._lineStyles[self._linestyle] != '_draw_nothing':
  637. tpath, affine = (self._get_transformed_path()
  638. .get_transformed_path_and_affine())
  639. if len(tpath.vertices):
  640. gc = renderer.new_gc()
  641. self._set_gc_clip(gc)
  642. lc_rgba = mcolors.to_rgba(self._color, self._alpha)
  643. gc.set_foreground(lc_rgba, isRGBA=True)
  644. gc.set_antialiased(self._antialiased)
  645. gc.set_linewidth(self._linewidth)
  646. if self.is_dashed():
  647. cap = self._dashcapstyle
  648. join = self._dashjoinstyle
  649. else:
  650. cap = self._solidcapstyle
  651. join = self._solidjoinstyle
  652. gc.set_joinstyle(join)
  653. gc.set_capstyle(cap)
  654. gc.set_snap(self.get_snap())
  655. if self.get_sketch_params() is not None:
  656. gc.set_sketch_params(*self.get_sketch_params())
  657. gc.set_dashes(self._dashOffset, self._dashSeq)
  658. renderer.draw_path(gc, tpath, affine.frozen())
  659. gc.restore()
  660. if self._marker and self._markersize > 0:
  661. gc = renderer.new_gc()
  662. self._set_gc_clip(gc)
  663. gc.set_linewidth(self._markeredgewidth)
  664. gc.set_antialiased(self._antialiased)
  665. ec_rgba = mcolors.to_rgba(
  666. self.get_markeredgecolor(), self._alpha)
  667. fc_rgba = mcolors.to_rgba(
  668. self._get_markerfacecolor(), self._alpha)
  669. fcalt_rgba = mcolors.to_rgba(
  670. self._get_markerfacecolor(alt=True), self._alpha)
  671. # If the edgecolor is "auto", it is set according to the *line*
  672. # color but inherits the alpha value of the *face* color, if any.
  673. if (cbook._str_equal(self._markeredgecolor, "auto")
  674. and not cbook._str_lower_equal(
  675. self.get_markerfacecolor(), "none")):
  676. ec_rgba = ec_rgba[:3] + (fc_rgba[3],)
  677. gc.set_foreground(ec_rgba, isRGBA=True)
  678. if self.get_sketch_params() is not None:
  679. scale, length, randomness = self.get_sketch_params()
  680. gc.set_sketch_params(scale/2, length/2, 2*randomness)
  681. marker = self._marker
  682. # Markers *must* be drawn ignoring the drawstyle (but don't pay the
  683. # recaching if drawstyle is already "default").
  684. if self.get_drawstyle() != "default":
  685. with cbook._setattr_cm(
  686. self, _drawstyle="default", _transformed_path=None):
  687. self.recache()
  688. self._transform_path(subslice)
  689. tpath, affine = (self._get_transformed_path()
  690. .get_transformed_path_and_affine())
  691. else:
  692. tpath, affine = (self._get_transformed_path()
  693. .get_transformed_path_and_affine())
  694. if len(tpath.vertices):
  695. # subsample the markers if markevery is not None
  696. markevery = self.get_markevery()
  697. if markevery is not None:
  698. subsampled = _mark_every_path(markevery, tpath,
  699. affine, self.axes.transAxes)
  700. else:
  701. subsampled = tpath
  702. snap = marker.get_snap_threshold()
  703. if isinstance(snap, Real):
  704. snap = renderer.points_to_pixels(self._markersize) >= snap
  705. gc.set_snap(snap)
  706. gc.set_joinstyle(marker.get_joinstyle())
  707. gc.set_capstyle(marker.get_capstyle())
  708. marker_path = marker.get_path()
  709. marker_trans = marker.get_transform()
  710. w = renderer.points_to_pixels(self._markersize)
  711. if cbook._str_equal(marker.get_marker(), ","):
  712. gc.set_linewidth(0)
  713. else:
  714. # Don't scale for pixels, and don't stroke them
  715. marker_trans = marker_trans.scale(w)
  716. renderer.draw_markers(gc, marker_path, marker_trans,
  717. subsampled, affine.frozen(),
  718. fc_rgba)
  719. alt_marker_path = marker.get_alt_path()
  720. if alt_marker_path:
  721. alt_marker_trans = marker.get_alt_transform()
  722. alt_marker_trans = alt_marker_trans.scale(w)
  723. renderer.draw_markers(
  724. gc, alt_marker_path, alt_marker_trans, subsampled,
  725. affine.frozen(), fcalt_rgba)
  726. gc.restore()
  727. renderer.close_group('line2d')
  728. self.stale = False
  729. def get_antialiased(self):
  730. return self._antialiased
  731. def get_color(self):
  732. return self._color
  733. def get_drawstyle(self):
  734. return self._drawstyle
  735. def get_linestyle(self):
  736. return self._linestyle
  737. def get_linewidth(self):
  738. return self._linewidth
  739. def get_marker(self):
  740. return self._marker.get_marker()
  741. def get_markeredgecolor(self):
  742. mec = self._markeredgecolor
  743. if cbook._str_equal(mec, 'auto'):
  744. if rcParams['_internal.classic_mode']:
  745. if self._marker.get_marker() in ('.', ','):
  746. return self._color
  747. if self._marker.is_filled() and self.get_fillstyle() != 'none':
  748. return 'k' # Bad hard-wired default...
  749. return self._color
  750. else:
  751. return mec
  752. def get_markeredgewidth(self):
  753. return self._markeredgewidth
  754. def _get_markerfacecolor(self, alt=False):
  755. fc = self._markerfacecoloralt if alt else self._markerfacecolor
  756. if cbook._str_lower_equal(fc, 'auto'):
  757. if self.get_fillstyle() == 'none':
  758. return 'none'
  759. else:
  760. return self._color
  761. else:
  762. return fc
  763. def get_markerfacecolor(self):
  764. return self._get_markerfacecolor(alt=False)
  765. def get_markerfacecoloralt(self):
  766. return self._get_markerfacecolor(alt=True)
  767. def get_markersize(self):
  768. return self._markersize
  769. def get_data(self, orig=True):
  770. """
  771. Return the xdata, ydata.
  772. If *orig* is *True*, return the original data.
  773. """
  774. return self.get_xdata(orig=orig), self.get_ydata(orig=orig)
  775. def get_xdata(self, orig=True):
  776. """
  777. Return the xdata.
  778. If *orig* is *True*, return the original data, else the
  779. processed data.
  780. """
  781. if orig:
  782. return self._xorig
  783. if self._invalidx:
  784. self.recache()
  785. return self._x
  786. def get_ydata(self, orig=True):
  787. """
  788. Return the ydata.
  789. If *orig* is *True*, return the original data, else the
  790. processed data.
  791. """
  792. if orig:
  793. return self._yorig
  794. if self._invalidy:
  795. self.recache()
  796. return self._y
  797. def get_path(self):
  798. """
  799. Return the :class:`~matplotlib.path.Path` object associated
  800. with this line.
  801. """
  802. if self._invalidy or self._invalidx:
  803. self.recache()
  804. return self._path
  805. def get_xydata(self):
  806. """
  807. Return the *xy* data as a Nx2 numpy array.
  808. """
  809. if self._invalidy or self._invalidx:
  810. self.recache()
  811. return self._xy
  812. def set_antialiased(self, b):
  813. """
  814. Set whether to use antialiased rendering.
  815. Parameters
  816. ----------
  817. b : bool
  818. """
  819. if self._antialiased != b:
  820. self.stale = True
  821. self._antialiased = b
  822. def set_color(self, color):
  823. """
  824. Set the color of the line
  825. Parameters
  826. ----------
  827. color : color
  828. """
  829. self._color = color
  830. self.stale = True
  831. def set_drawstyle(self, drawstyle):
  832. """
  833. Set the drawstyle of the plot
  834. 'default' connects the points with lines. The steps variants
  835. produce step-plots. 'steps' is equivalent to 'steps-pre' and
  836. is maintained for backward-compatibility.
  837. Parameters
  838. ----------
  839. drawstyle : {'default', 'steps', 'steps-pre', 'steps-mid', \
  840. 'steps-post'}
  841. """
  842. if drawstyle is None:
  843. drawstyle = 'default'
  844. if drawstyle not in self.drawStyles:
  845. raise ValueError('Unrecognized drawstyle {!r}'.format(drawstyle))
  846. if self._drawstyle != drawstyle:
  847. self.stale = True
  848. # invalidate to trigger a recache of the path
  849. self._invalidx = True
  850. self._drawstyle = drawstyle
  851. def set_linewidth(self, w):
  852. """
  853. Set the line width in points
  854. Parameters
  855. ----------
  856. w : float
  857. """
  858. w = float(w)
  859. if self._linewidth != w:
  860. self.stale = True
  861. self._linewidth = w
  862. # rescale the dashes + offset
  863. self._dashOffset, self._dashSeq = _scale_dashes(
  864. self._us_dashOffset, self._us_dashSeq, self._linewidth)
  865. def _split_drawstyle_linestyle(self, ls):
  866. '''Split drawstyle from linestyle string
  867. If `ls` is only a drawstyle default to returning a linestyle
  868. of '-'.
  869. Parameters
  870. ----------
  871. ls : str
  872. The linestyle to be processed
  873. Returns
  874. -------
  875. ret_ds : str or None
  876. If the linestyle string does not contain a drawstyle prefix
  877. return None, otherwise return it.
  878. ls : str
  879. The linestyle with the drawstyle (if any) stripped.
  880. '''
  881. for ds in self.drawStyleKeys: # long names are first in the list
  882. if ls.startswith(ds):
  883. return ds, ls[len(ds):] or '-'
  884. return None, ls
  885. def set_linestyle(self, ls):
  886. """
  887. Set the linestyle of the line (also accepts drawstyles,
  888. e.g., ``'steps--'``)
  889. =========================== =================
  890. linestyle description
  891. =========================== =================
  892. ``'-'`` or ``'solid'`` solid line
  893. ``'--'`` or ``'dashed'`` dashed line
  894. ``'-.'`` or ``'dashdot'`` dash-dotted line
  895. ``':'`` or ``'dotted'`` dotted line
  896. ``'None'`` draw nothing
  897. ``' '`` draw nothing
  898. ``''`` draw nothing
  899. =========================== =================
  900. 'steps' is equivalent to 'steps-pre' and is maintained for
  901. backward-compatibility.
  902. Alternatively a dash tuple of the following form can be provided::
  903. (offset, onoffseq),
  904. where ``onoffseq`` is an even length tuple of on and off ink in points.
  905. .. seealso::
  906. :meth:`set_drawstyle`
  907. To set the drawing style (stepping) of the plot.
  908. Parameters
  909. ----------
  910. ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
  911. The line style.
  912. """
  913. if isinstance(ls, str):
  914. ds, ls = self._split_drawstyle_linestyle(ls)
  915. if ds is not None:
  916. self.set_drawstyle(ds)
  917. if ls in [' ', '', 'none']:
  918. ls = 'None'
  919. if ls not in self._lineStyles:
  920. try:
  921. ls = ls_mapper_r[ls]
  922. except KeyError:
  923. raise ValueError("Invalid linestyle {!r}; see docs of "
  924. "Line2D.set_linestyle for valid values"
  925. .format(ls))
  926. self._linestyle = ls
  927. else:
  928. self._linestyle = '--'
  929. # get the unscaled dashes
  930. self._us_dashOffset, self._us_dashSeq = _get_dash_pattern(ls)
  931. # compute the linewidth scaled dashes
  932. self._dashOffset, self._dashSeq = _scale_dashes(
  933. self._us_dashOffset, self._us_dashSeq, self._linewidth)
  934. @docstring.dedent_interpd
  935. def set_marker(self, marker):
  936. """
  937. Set the line marker.
  938. Parameters
  939. ----------
  940. marker: marker style
  941. See `~matplotlib.markers` for full description of possible
  942. arguments.
  943. """
  944. self._marker.set_marker(marker)
  945. self.stale = True
  946. def set_markeredgecolor(self, ec):
  947. """
  948. Set the marker edge color.
  949. Parameters
  950. ----------
  951. ec : color
  952. """
  953. if ec is None:
  954. ec = 'auto'
  955. if (self._markeredgecolor is None
  956. or np.any(self._markeredgecolor != ec)):
  957. self.stale = True
  958. self._markeredgecolor = ec
  959. def set_markeredgewidth(self, ew):
  960. """
  961. Set the marker edge width in points.
  962. Parameters
  963. ----------
  964. ew : float
  965. """
  966. if ew is None:
  967. ew = rcParams['lines.markeredgewidth']
  968. if self._markeredgewidth != ew:
  969. self.stale = True
  970. self._markeredgewidth = ew
  971. def set_markerfacecolor(self, fc):
  972. """
  973. Set the marker face color.
  974. Parameters
  975. ----------
  976. fc : color
  977. """
  978. if fc is None:
  979. fc = 'auto'
  980. if np.any(self._markerfacecolor != fc):
  981. self.stale = True
  982. self._markerfacecolor = fc
  983. def set_markerfacecoloralt(self, fc):
  984. """
  985. Set the alternate marker face color.
  986. Parameters
  987. ----------
  988. fc : color
  989. """
  990. if fc is None:
  991. fc = 'auto'
  992. if np.any(self._markerfacecoloralt != fc):
  993. self.stale = True
  994. self._markerfacecoloralt = fc
  995. def set_markersize(self, sz):
  996. """
  997. Set the marker size in points.
  998. Parameters
  999. ----------
  1000. sz : float
  1001. """
  1002. sz = float(sz)
  1003. if self._markersize != sz:
  1004. self.stale = True
  1005. self._markersize = sz
  1006. def set_xdata(self, x):
  1007. """
  1008. Set the data array for x.
  1009. Parameters
  1010. ----------
  1011. x : 1D array
  1012. """
  1013. self._xorig = x
  1014. self._invalidx = True
  1015. self.stale = True
  1016. def set_ydata(self, y):
  1017. """
  1018. Set the data array for y.
  1019. Parameters
  1020. ----------
  1021. y : 1D array
  1022. """
  1023. self._yorig = y
  1024. self._invalidy = True
  1025. self.stale = True
  1026. def set_dashes(self, seq):
  1027. """
  1028. Set the dash sequence, sequence of dashes with on off ink in
  1029. points. If seq is empty or if seq = (None, None), the
  1030. linestyle will be set to solid.
  1031. Parameters
  1032. ----------
  1033. seq : sequence of floats (on/off ink in points) or (None, None)
  1034. """
  1035. if seq == (None, None) or len(seq) == 0:
  1036. self.set_linestyle('-')
  1037. else:
  1038. self.set_linestyle((0, seq))
  1039. def update_from(self, other):
  1040. """copy properties from other to self"""
  1041. Artist.update_from(self, other)
  1042. self._linestyle = other._linestyle
  1043. self._linewidth = other._linewidth
  1044. self._color = other._color
  1045. self._markersize = other._markersize
  1046. self._markerfacecolor = other._markerfacecolor
  1047. self._markerfacecoloralt = other._markerfacecoloralt
  1048. self._markeredgecolor = other._markeredgecolor
  1049. self._markeredgewidth = other._markeredgewidth
  1050. self._dashSeq = other._dashSeq
  1051. self._us_dashSeq = other._us_dashSeq
  1052. self._dashOffset = other._dashOffset
  1053. self._us_dashOffset = other._us_dashOffset
  1054. self._dashcapstyle = other._dashcapstyle
  1055. self._dashjoinstyle = other._dashjoinstyle
  1056. self._solidcapstyle = other._solidcapstyle
  1057. self._solidjoinstyle = other._solidjoinstyle
  1058. self._linestyle = other._linestyle
  1059. self._marker = MarkerStyle(other._marker.get_marker(),
  1060. other._marker.get_fillstyle())
  1061. self._drawstyle = other._drawstyle
  1062. def set_dash_joinstyle(self, s):
  1063. """
  1064. Set the join style for dashed linestyles.
  1065. Parameters
  1066. ----------
  1067. s : {'miter', 'round', 'bevel'}
  1068. """
  1069. s = s.lower()
  1070. if s not in self.validJoin:
  1071. raise ValueError('set_dash_joinstyle passed "%s";\n' % (s,)
  1072. + 'valid joinstyles are %s' % (self.validJoin,))
  1073. if self._dashjoinstyle != s:
  1074. self.stale = True
  1075. self._dashjoinstyle = s
  1076. def set_solid_joinstyle(self, s):
  1077. """
  1078. Set the join style for solid linestyles.
  1079. Parameters
  1080. ----------
  1081. s : {'miter', 'round', 'bevel'}
  1082. """
  1083. s = s.lower()
  1084. if s not in self.validJoin:
  1085. raise ValueError('set_solid_joinstyle passed "%s";\n' % (s,)
  1086. + 'valid joinstyles are %s' % (self.validJoin,))
  1087. if self._solidjoinstyle != s:
  1088. self.stale = True
  1089. self._solidjoinstyle = s
  1090. def get_dash_joinstyle(self):
  1091. """
  1092. Get the join style for dashed linestyles
  1093. """
  1094. return self._dashjoinstyle
  1095. def get_solid_joinstyle(self):
  1096. """
  1097. Get the join style for solid linestyles
  1098. """
  1099. return self._solidjoinstyle
  1100. def set_dash_capstyle(self, s):
  1101. """
  1102. Set the cap style for dashed linestyles.
  1103. Parameters
  1104. ----------
  1105. s : {'butt', 'round', 'projecting'}
  1106. """
  1107. s = s.lower()
  1108. if s not in self.validCap:
  1109. raise ValueError('set_dash_capstyle passed "%s";\n' % (s,)
  1110. + 'valid capstyles are %s' % (self.validCap,))
  1111. if self._dashcapstyle != s:
  1112. self.stale = True
  1113. self._dashcapstyle = s
  1114. def set_solid_capstyle(self, s):
  1115. """
  1116. Set the cap style for solid linestyles.
  1117. Parameters
  1118. ----------
  1119. s : {'butt', 'round', 'projecting'}
  1120. """
  1121. s = s.lower()
  1122. if s not in self.validCap:
  1123. raise ValueError('set_solid_capstyle passed "%s";\n' % (s,)
  1124. + 'valid capstyles are %s' % (self.validCap,))
  1125. if self._solidcapstyle != s:
  1126. self.stale = True
  1127. self._solidcapstyle = s
  1128. def get_dash_capstyle(self):
  1129. """
  1130. Get the cap style for dashed linestyles
  1131. """
  1132. return self._dashcapstyle
  1133. def get_solid_capstyle(self):
  1134. """
  1135. Get the cap style for solid linestyles
  1136. """
  1137. return self._solidcapstyle
  1138. def is_dashed(self):
  1139. 'return True if line is dashstyle'
  1140. return self._linestyle in ('--', '-.', ':')
  1141. class VertexSelector(object):
  1142. """
  1143. Manage the callbacks to maintain a list of selected vertices for
  1144. :class:`matplotlib.lines.Line2D`. Derived classes should override
  1145. :meth:`~matplotlib.lines.VertexSelector.process_selected` to do
  1146. something with the picks.
  1147. Here is an example which highlights the selected verts with red
  1148. circles::
  1149. import numpy as np
  1150. import matplotlib.pyplot as plt
  1151. import matplotlib.lines as lines
  1152. class HighlightSelected(lines.VertexSelector):
  1153. def __init__(self, line, fmt='ro', **kwargs):
  1154. lines.VertexSelector.__init__(self, line)
  1155. self.markers, = self.axes.plot([], [], fmt, **kwargs)
  1156. def process_selected(self, ind, xs, ys):
  1157. self.markers.set_data(xs, ys)
  1158. self.canvas.draw()
  1159. fig, ax = plt.subplots()
  1160. x, y = np.random.rand(2, 30)
  1161. line, = ax.plot(x, y, 'bs-', picker=5)
  1162. selector = HighlightSelected(line)
  1163. plt.show()
  1164. """
  1165. def __init__(self, line):
  1166. """
  1167. Initialize the class with a :class:`matplotlib.lines.Line2D`
  1168. instance. The line should already be added to some
  1169. :class:`matplotlib.axes.Axes` instance and should have the
  1170. picker property set.
  1171. """
  1172. if line.axes is None:
  1173. raise RuntimeError('You must first add the line to the Axes')
  1174. if line.get_picker() is None:
  1175. raise RuntimeError('You must first set the picker property '
  1176. 'of the line')
  1177. self.axes = line.axes
  1178. self.line = line
  1179. self.canvas = self.axes.figure.canvas
  1180. self.cid = self.canvas.mpl_connect('pick_event', self.onpick)
  1181. self.ind = set()
  1182. def process_selected(self, ind, xs, ys):
  1183. """
  1184. Default "do nothing" implementation of the
  1185. :meth:`process_selected` method.
  1186. *ind* are the indices of the selected vertices. *xs* and *ys*
  1187. are the coordinates of the selected vertices.
  1188. """
  1189. pass
  1190. def onpick(self, event):
  1191. """When the line is picked, update the set of selected indices."""
  1192. if event.artist is not self.line:
  1193. return
  1194. self.ind ^= set(event.ind)
  1195. ind = sorted(self.ind)
  1196. xdata, ydata = self.line.get_data()
  1197. self.process_selected(ind, xdata[ind], ydata[ind])
  1198. lineStyles = Line2D._lineStyles
  1199. lineMarkers = MarkerStyle.markers
  1200. drawStyles = Line2D.drawStyles
  1201. fillStyles = MarkerStyle.fillstyles
  1202. docstring.interpd.update(Line2D=artist.kwdoc(Line2D))
  1203. # You can not set the docstring of an instancemethod,
  1204. # but you can on the underlying function. Go figure.
  1205. docstring.dedent_interpd(Line2D.__init__)