8000 Merge pull request #15458 from ramcdona/TickedStroke · matplotlib/matplotlib@d38e443 · GitHub
[go: up one dir, main page]

Skip to content

Commit d38e443

Browse files
authored
Merge pull request #15458 from ramcdona/TickedStroke
TickedStroke, a stroke style with ticks useful for depicting constraints
2 parents 52761de + b6a3a25 commit d38e443

File tree

6 files changed

+395
-0
lines changed

6 files changed

+395
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
==============================================
3+
Contouring the solution space of optimizations
4+
==============================================
5+
6+
Contour plotting is particularly handy when illustrating the solution
7+
space of optimization problems. Not only can `.axes.Axes.contour` be
8+
used to represent the topography of the objective function, it can be
9+
used to generate boundary curves of the constraint functions. The
10+
constraint lines can be drawn with
11+
`~matplotlib.patheffects.TickedStroke` to distinguish the valid and
12+
invalid sides of the constraint boundaries.
13+
14+
`.axes.Axes.contour` generates curves with larger values to the left
15+
of the contour. The angle parameter is measured zero ahead with
16+
increasing values to the left. Consequently, when using
17+
`~matplotlib.patheffects.TickedStroke` to illustrate a constraint in
18+
a typical optimization problem, the angle should be set between
19+
zero and 180 degrees.
20+
21+
"""
22+
23+
import numpy as np
24+
import matplotlib.pyplot as plt
25+
import matplotlib.patheffects as patheffects
26+
27+
fig, ax = plt.subplots(figsize=(6, 6))
28+
29+
nx = 101
30+
ny = 105
31+
32+
# Set up survey vectors
33+
xvec = np.linspace(0.001, 4.0, nx)
34+
yvec = np.linspace(0.001, 4.0, ny)
35+
36+
# Set up survey matrices. Design disk loading and gear ratio.
37+
x1, x2 = np.meshgrid(xvec, yvec)
38+
39+
# Evaluate some stuff to plot
40+
obj = x1**2 + x2**2 - 2*x1 - 2*x2 + 2
41+
g1 = -(3*x1 + x2 - 5.5)
42+
g2 = -(x1 + 2*x2 - 4)
43+
g3 = 0.8 + x1**-3 - x2
44+
45+
cntr = ax.contour(x1, x2, obj, [0.01, 0.1, 0.5, 1, 2, 4, 8, 16],
46+
colors=('k',))
47+
ax.clabel(cntr, fmt="%2.1f", use_clabeltext=True)
48+
49+
cg1 = ax.contour(x1, x2, g1, [0], colors=('k',))
50+
plt.setp(cg1.collections,
51+
path_effects=[patheffects.withTickedStroke(angle=135)])
52+
53+
cg2 = ax.contour(x1, x2, g2, [0], colors=('r',))
54+
plt.setp(cg2.collections,
55+
path_effects=[patheffects.withTickedStroke(angle=60, length=2)])
56+
57+
cg3 = ax.contour(x1, x2, g3, [0], colors=('b',))
58+
plt.setp(cg3.collections,
59+
path_effects=[patheffects.withTickedStroke(spacing=7)])
60+
61+
ax.set_xlim(0, 4)
62+
ax.set_ylim(0, 4)
63+
64+
plt.show()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
==============================
3+
Lines with a ticked patheffect
4+
==============================
5+
6+
Ticks can be added along a line to mark one side as a barrier using
7+
`~matplotlib.patheffects.TickedStroke`. You can control the angle,
8+
spacing, and length of the ticks.
9+
10+
The ticks will also appear appropriately in the legend.
11+
12+
"""
13+
14+
import numpy as np
15+
import matplotlib.pyplot as plt
16+
from matplotlib import patheffects
17+
18+
fig, ax = plt.subplots(figsize=(6, 6))
19+
ax.plot([0, 1], [0, 1], label="Line",
20+
path_effects=[patheffects.withTickedStroke(spacing=7, angle=135)])
21+
22+
nx = 101
23+
x = np.linspace(0.0, 1.0, nx)
24+
y = 0.3*np.sin(x*8) + 0.4
25+
ax.plot(x, y, label="Curve", path_effects=[patheffects.withTickedStroke()])
26+
27+
ax.legend()
28+
29+
plt.show()

examples/misc/tickedstroke_demo.py

+99
10000
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""
2+
=======================
3+
TickedStroke patheffect
4+
=======================
5+
6+
Matplotlib's :mod:`.patheffects` can be used to alter the way paths
7+
are drawn at a low enough level that they can affect almost anything.
8+
9+
The :doc:`patheffects guide</tutorials/advanced/patheffects_guide>`
10+
details the use of patheffects.
11+
12+
The `~matplotlib.patheffects.TickedStroke` patheffect illustrated here
13+
draws a path with a ticked style. The spacing, length, and angle of
14+
ticks can be controlled.
15+
16+
See also the :doc:`contour demo example
17+
</gallery/lines_bars_and_markers/lines_with_ticks_demo>`.
18+
19+
See also the :doc:`contours in optimization example
20+
</gallery/images_contours_and_fields/contours_in_optimization_demo>`.
21+
"""
22+
23+
import matplotlib.patches as patches
24+
from matplotlib.path import Path
25+
import numpy as np
26+
import matplotlib.pyplot as plt
27+
import matplotlib.patheffects as patheffects
28+
29+
# Direct to path
30+
fig, ax = plt.subplots(figsize=(6, 6))
31+
path = Path.unit_circle()
32+
patch = patches.PathPatch(path, facecolor='none', lw=2, path_effects=[
33+
patheffects.withTickedStroke(angle=-90, spacing=10, length=1)])
34+
35+
ax.add_patch(patch)
36+
ax.axis('equal')
37+
ax.set_xlim(-2, 2)
38+
ax.set_ylim(-2, 2)
39+
40+
plt.show()
41+
42+
###############################################################################
43+
# Lines and curves with plot and legend
44+
fig, ax = plt.subplots(figsize=(6, 6))
45+
ax.plot([0, 1], [0, 1], label="Line",
46+
path_effects=[patheffects.withTickedStroke(spacing=7, angle=135)])
47+
48+
nx = 101
49+
x = np.linspace(0.0, 1.0, nx)
50+
y = 0.3*np.sin(x*8) + 0.4
51+
ax.plot(x, y, label="Curve", path_effects=[patheffects.withTickedStroke()])
52+
53+
ax.legend()
54+
55+
plt.show()
56+
57+
###############################################################################
58+
# Contour plot with objective and constraints.
59+
# Curves generated by contour to represent a typical constraint in an
60+
# optimization problem should be plotted with angles between zero and
61+
# 180 degrees.
62+
fig, ax = plt.subplots(figsize=(6, 6))
63+
64+
nx = 101
65+
ny = 105
66+
67+
# Set up survey vectors
68+
xvec = np.linspace(0.001, 4.0, nx)
69+
yvec = np.linspace(0.001, 4.0, ny)
70+
71+
# Set up survey matrices. Design disk loading and gear ratio.
72+
x1, x2 = np.meshgrid(xvec, yvec)
73+
74+
# Evaluate some stuff to plot
75+
obj = x1**2 + x2**2 - 2*x1 - 2*x2 + 2
76+
g1 = -(3*x1 + x2 - 5.5)
77+
g2 = -(x1 + 2*x2 - 4)
78+
g3 = 0.8 + x1**-3 - x2
79+
80+
cntr = ax.contour(x1, x2, obj, [0.01, 0.1, 0.5, 1, 2, 4, 8, 16],
81+
colors=('k',))
82+
ax.clabel(cntr, fmt="%2.1f", use_clabeltext=True)
83+
84+
cg1 = ax.contour(x1, x2, g1, [0], colors='black')
85+
plt.setp(cg1.collections,
86+
path_effects=[patheffects.withTickedStroke(angle=135)])
87+
88+
cg2 = ax.contour(x1, x2, g2, [0], colors='red')
89+
plt.setp(cg2.collections,
90+
path_effects=[patheffects.withTickedStroke(angle=60, length=2)])
91+
92+
cg3 = ax.contour(x1, x2, g3, [0], colors='blue')
93+
plt.setp(cg3.collections,
94+
path_effects=[patheffects.withTickedStroke(spacing=7)])
95+
96+
ax.set_xlim(0, 4)
97+
ax.set_ylim(0, 4)
98+
99+
plt.show()

lib/matplotlib/patheffects.py

+147
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from matplotlib import colors as mcolors
1111
from matplotlib import patches as mpatches
1212
from matplotlib import transforms as mtransforms
13+
from matplotlib.path import Path
14+
import numpy as np
1315

1416

1517
class AbstractPathEffect:
@@ -371,3 +373,148 @@ def draw_path(self, renderer, gc, tpath, affine, rgbFace):
371373
if clip_path:
372374
self.patch.set_clip_path(*clip_path)
373375
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)
Loading

0 commit comments

Comments
 (0)
0