|
10 | 10 | from matplotlib import colors as mcolors
|
11 | 11 | from matplotlib import patches as mpatches
|
12 | 12 | from matplotlib import transforms as mtransforms
|
| 13 | +from matplotlib.path import Path |
| 14 | +import numpy as np |
13 | 15 |
|
14 | 16 |
|
15 | 17 | class AbstractPathEffect:
|
@@ -371,3 +373,148 @@ def draw_path(self, renderer, gc, tpath, affine, rgbFace):
|
371 | 373 | if clip_path:
|
372 | 374 | self.patch.set_clip_path(*clip_path)
|
373 | 375 | self.patch.draw(renderer)
|
| 376 | + |
| 377 | + |
| 378 | +class TickedStroke(AbstractPathEffect): |
| 379 | + """ |
| 380 | + A line-based PathEffect which draws a path with a ticked style. |
| 381 | +
|
| 382 | + This line style is frequently used to represent constraints in |
| 383 | + optimization. The ticks may be used to indicate that one side |
| 384 | + of the line is invalid or to represent a closed boundary of a |
| 385 | + domain (i.e. a wall or the edge of a pipe). |
| 386 | +
|
| 387 | + The spacing, length, and angle of ticks can be controlled. |
| 388 | +
|
| 389 | + This line style is sometimes referred to as a hatched line. |
| 390 | +
|
| 391 | + See also the :doc:`contour demo example |
| 392 | + </gallery/lines_bars_and_markers/lines_with_ticks_demo>`. |
| 393 | +
|
| 394 | + See also the :doc:`contours in optimization example |
| 395 | + </gallery/images_contours_and_fields/contours_in_optimization_demo>`. |
| 396 | + """ |
| 397 | + |
| 398 | + def __init__(self, offset=(0, 0), |
| 399 | + spacing=10.0, angle=45.0, length=np.sqrt(2), |
| 400 | + **kwargs): |
| 401 | + """ |
| 402 | + Parameters |
| 403 | + ---------- |
| 404 | + offset : pair of floats, default: (0, 0) |
| 405 | + The offset to apply to the path, in points. |
| 406 | + spacing : float, default: 10.0 |
| 407 | + The spacing between ticks in points. |
| 408 | + angle : float, default: 45.0 |
| 409 | + The angle between the path and the tick in degrees. The angle |
| 410 | + is measured as if you were an ant walking along the curve, with |
| 411 | + zero degrees pointing directly ahead, 90 to your left, -90 |
| 412 | + to your right, and 180 behind you. |
| 413 | + length : float, default: 1.414 |
| 414 | + The length of the tick relative to spacing. |
| 415 | + Recommended length = 1.414 (sqrt(2)) when angle=45, length=1.0 |
| 416 | + when angle=90 and length=2.0 when angle=60. |
| 417 | + **kwargs |
| 418 | + Extra keywords are stored and passed through to |
| 419 | + :meth:`AbstractPathEffect._update_gc`. |
| 420 | +
|
| 421 | + Examples |
| 422 | + -------- |
| 423 | + See :doc:`/gallery/misc/tickedstroke_demo`. |
| 424 | + """ |
| 425 | + super().__init__(offset) |
| 426 | + |
| 427 | + self._spacing = spacing |
| 428 | + self._angle = angle |
| 429 | + self._length = length |
| 430 | + self._gc = kwargs |
| 431 | + |
| 432 | + def draw_path(self, renderer, gc, tpath, affine, rgbFace): |
| 433 | + """ |
| 434 | + Draw the path with updated gc. |
| 435 | + """ |
| 436 | + # Do not modify the input! Use copy instead. |
| 437 | + gc0 = renderer.new_gc() |
| 438 | + gc0.copy_properties(gc) |
| 439 | + |
| 440 | + gc0 = self._update_gc(gc0, self._gc) |
| 441 | + trans = affine + self._offset_transform(renderer) |
| 442 | + |
| 443 | + theta = -np.radians(self._angle) |
| 444 | + trans_matrix = np.array([[np.cos(theta), -np.sin(theta)], |
| 445 | + [np.sin(theta), np.cos(theta)]]) |
| 446 | + |
| 447 | + # Convert spacing parameter to pixels. |
| 448 | + spcpx = renderer.points_to_pixels(self._spacing) |
| 449 | + |
| 450 | + # Transform before evaluation because to_polygons works at resolution |
| 451 | + # of one -- assuming it is working in pixel space. |
| 452 | + transpath = affine.transform_path(tpath) |
| 453 | + |
| 454 | + # Evaluate path to straight line segments that can be used to |
| 455 | + # construct line ticks. |
| 456 | + polys = transpath.to_polygons(closed_only=False) |
| 457 | + |
| 458 | + for p in polys: |
| 459 | + x = p[:, 0] |
| 460 | + y = p[:, 1] |
| 461 | + |
| 462 | + # Can not interpolate points or draw line if only one point in |
| 463 | + # polyline. |
| 464 | + if x.size < 2: |
| 465 | + continue |
| 466 | + |
| 467 | + # Find distance between points on the line |
| 468 | + ds = np.hypot(x[1:] - x[:-1], y[1:] - y[:-1]) |
| 469 | + |
| 470 | + # Build parametric coordinate along curve |
| 471 | + s = np.concatenate(([0.0], np.cumsum(ds))) |
| 472 | + stot = s[-1] |
| 473 | + |
| 474 | + num = int(np.ceil(stot / spcpx))-1 |
| 475 | + # Pick parameter values for ticks. |
| 476 | + s_tick = np.linspace(spcpx/2, stot-spcpx/2, num) |
| 477 | + |
| 478 | + # Find points along the parameterized curve |
| 479 | + x_tick = np.interp(s_tick, s, x) |
| 480 | + y_tick = np.interp(s_tick, s, y) |
| 481 | + |
| 482 | + # Find unit vectors in local direction of curve |
| 483 | + delta_s = self._spacing * .001 |
| 484 | + u = (np.interp(s_tick + delta_s, s, x) - x_tick) / delta_s |
| 485 | + v = (np.interp(s_tick + delta_s, s, y) - y_tick) / delta_s |
| 486 | + |
| 487 | + # Normalize slope into unit slope vector. |
| 488 | + n = np.hypot(u, v) |
| 489 | + mask = n == 0 |
| 490 | + n[mask] = 1.0 |
| 491 | + |
| 492 | + uv = np.array([u / n, v / n]).T |
| 493 | + uv[mask] = np.array([0, 0]).T |
| 494 | + |
| 495 | + # Rotate and scale unit vector into tick vector |
| 496 | + dxy = np.dot(uv, trans_matrix) * self._length * spcpx |
| 497 | + |
| 498 | + # Build tick endpoints |
| 499 | + x_end = x_tick + dxy[:, 0] |
| 500 | + y_end = y_tick + dxy[:, 1] |
| 501 | + |
| 502 | + # Interleave ticks to form Path vertices |
| 503 | + xyt = np.empty((2 * num, 2), dtype=x_tick.dtype) |
| 504 | + xyt[0::2, 0] = x_tick |
| 505 | + xyt[1::2, 0] = x_end |
| 506 | + xyt[0::2, 1] = y_tick |
| 507 | + xyt[1::2, 1] = y_end |
| 508 | + |
| 509 | + # Build up vector of Path codes |
| 510 | + codes = np.tile([Path.MOVETO, Path.LINETO], num) |
| 511 | + |
| 512 | + # Construct and draw resulting path |
| 513 | + h = Path(xyt, codes) |
| 514 | +
795E
# Transform back to data space during render |
| 515 | + renderer.draw_path(gc0, h, affine.inverted() + trans, rgbFace) |
| 516 | + |
| 517 | + gc0.restore() |
| 518 | + |
| 519 | + |
| 520 | +withTickedStroke = _subclass_with_normal(effect_class=TickedStroke) |
0 commit comments