Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/plot/arrow.py
8815 views
1
"""
2
Arrows
3
"""
4
#*****************************************************************************
5
# Copyright (C) 2006 Alex Clemesha <[email protected]>,
6
# William Stein <[email protected]>,
7
# 2008 Mike Hansen <[email protected]>,
8
# 2009 Emily Kirkman
9
#
10
# Distributed under the terms of the GNU General Public License (GPL)
11
#
12
# This code is distributed in the hope that it will be useful,
13
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
# General Public License for more details.
16
#
17
# The full text of the GPL is available at:
18
#
19
# http://www.gnu.org/licenses/
20
#*****************************************************************************
21
from sage.plot.primitive import GraphicPrimitive
22
from sage.misc.decorators import options, rename_keyword
23
from sage.plot.colors import to_mpl_color
24
25
class CurveArrow(GraphicPrimitive):
26
def __init__(self, path, options):
27
"""
28
Returns an arrow graphics primitive along the provided path (bezier curve).
29
30
EXAMPLES::
31
32
sage: from sage.plot.arrow import CurveArrow
33
sage: b = CurveArrow(path=[[(0,0),(.5,.5),(1,0)],[(.5,1),(0,0)]],options={})
34
sage: b
35
CurveArrow from (0, 0) to (0, 0)
36
"""
37
import numpy as np
38
self.path = path
39
codes = [1] + (len(self.path[0])-1)*[len(self.path[0])]
40
vertices = self.path[0]
41
for curve in self.path[1:]:
42
vertices += curve
43
codes += (len(curve))*[len(curve)+1]
44
self.codes = codes
45
self.vertices = np.array(vertices, np.float)
46
GraphicPrimitive.__init__(self, options)
47
48
def get_minmax_data(self):
49
"""
50
Returns a dictionary with the bounding box data.
51
52
EXAMPLES::
53
54
sage: from sage.plot.arrow import CurveArrow
55
sage: b = CurveArrow(path=[[(0,0),(.5,.5),(1,0)],[(.5,1),(0,0)]],options={})
56
sage: d = b.get_minmax_data()
57
sage: d['xmin']
58
0.0
59
sage: d['xmax']
60
1.0
61
"""
62
return {'xmin': self.vertices[:,0].min(),
63
'xmax': self.vertices[:,0].max(),
64
'ymin': self.vertices[:,1].min(),
65
'ymax': self.vertices[:,1].max()}
66
67
def _allowed_options(self):
68
"""
69
Return the dictionary of allowed options for the curve arrow graphics primitive.
70
71
EXAMPLES::
72
73
sage: from sage.plot.arrow import CurveArrow
74
sage: list(sorted(CurveArrow(path=[[(0,0),(2,3)]],options={})._allowed_options().iteritems()))
75
[('arrowsize', 'The size of the arrowhead'),
76
('arrowstyle', 'todo'),
77
('head', '2-d only: Which end of the path to draw the head (one of 0 (start), 1 (end) or 2 (both)'),
78
('hue', 'The color given as a hue.'),
79
('legend_color', 'The color of the legend text.'),
80
('legend_label', 'The label for this item in the legend.'),
81
('linestyle', "2d only: The style of the line, which is one of
82
'dashed', 'dotted', 'solid', 'dashdot', or '--', ':', '-', '-.',
83
respectively."),
84
('rgbcolor', 'The color as an RGB tuple.'),
85
('width', 'The width of the shaft of the arrow, in points.'),
86
('zorder', '2-d only: The layer level in which to draw')]
87
"""
88
return {'width':'The width of the shaft of the arrow, in points.',
89
'rgbcolor':'The color as an RGB tuple.',
90
'hue':'The color given as a hue.',
91
'legend_label':'The label for this item in the legend.',
92
'legend_color':'The color of the legend text.',
93
'arrowstyle': 'todo',
94
'arrowsize':'The size of the arrowhead',
95
'zorder':'2-d only: The layer level in which to draw',
96
'head':'2-d only: Which end of the path to draw the head (one of 0 (start), 1 (end) or 2 (both)',
97
'linestyle':"2d only: The style of the line, which is one of "
98
"'dashed', 'dotted', 'solid', 'dashdot', or '--', ':', '-', '-.', "
99
"respectively."}
100
101
def _repr_(self):
102
"""
103
Text representation of an arrow graphics primitive.
104
105
EXAMPLES::
106
107
sage: from sage.plot.arrow import CurveArrow
108
sage: CurveArrow(path=[[(0,0),(1,4),(2,3)]],options={})._repr_()
109
'CurveArrow from (0, 0) to (2, 3)'
110
"""
111
return "CurveArrow from %s to %s"%(self.path[0][0],self.path[-1][-1])
112
113
def _render_on_subplot(self, subplot):
114
"""
115
Render this arrow in a subplot. This is the key function that
116
defines how this arrow graphics primitive is rendered in
117
matplotlib's library.
118
119
EXAMPLES::
120
121
This function implicitly ends up rendering this arrow on a matplotlib subplot:
122
sage: arrow(path=[[(0,1), (2,-1), (4,5)]])
123
"""
124
from sage.plot.misc import get_matplotlib_linestyle
125
126
options = self.options()
127
width = float(options['width'])
128
head = options.pop('head')
129
if head == 0: style = '<|-'
130
elif head == 1: style = '-|>'
131
elif head == 2: style = '<|-|>'
132
else: raise KeyError('head parameter must be one of 0 (start), 1 (end) or 2 (both).')
133
arrowsize = float(options.get('arrowsize',5))
134
head_width=arrowsize
135
head_length=arrowsize*2.0
136
color = to_mpl_color(options['rgbcolor'])
137
from matplotlib.patches import FancyArrowPatch
138
from matplotlib.path import Path
139
bpath = Path(self.vertices, self.codes)
140
p = FancyArrowPatch(path=bpath,
141
lw=width, arrowstyle='%s,head_width=%s,head_length=%s'%(style,head_width, head_length),
142
fc=color, ec=color)
143
p.set_linestyle(get_matplotlib_linestyle(options['linestyle'],return_type='long'))
144
p.set_zorder(options['zorder'])
145
p.set_label(options['legend_label'])
146
subplot.add_patch(p)
147
return p
148
149
150
class Arrow(GraphicPrimitive):
151
"""
152
Primitive class that initializes the (line) arrow graphics type
153
154
EXAMPLES:
155
156
We create an arrow graphics object, then take the 0th entry
157
in it to get the actual Arrow graphics primitive::
158
159
sage: P = arrow((0,1), (2,3))[0]
160
sage: type(P)
161
<class 'sage.plot.arrow.Arrow'>
162
sage: P
163
Arrow from (0.0,1.0) to (2.0,3.0)
164
"""
165
def __init__(self, xtail, ytail, xhead, yhead, options):
166
"""
167
Create an arrow graphics primitive.
168
169
EXAMPLES::
170
171
sage: from sage.plot.arrow import Arrow
172
sage: Arrow(0,0,2,3,{})
173
Arrow from (0.0,0.0) to (2.0,3.0)
174
"""
175
self.xtail = float(xtail)
176
self.xhead = float(xhead)
177
self.ytail = float(ytail)
178
self.yhead = float(yhead)
179
GraphicPrimitive.__init__(self, options)
180
181
def get_minmax_data(self):
182
"""
183
Returns a bounding box for this arrow.
184
185
EXAMPLES::
186
187
sage: d = arrow((1,1), (5,5)).get_minmax_data()
188
sage: d['xmin']
189
1.0
190
sage: d['xmax']
191
5.0
192
"""
193
return {'xmin': min(self.xtail, self.xhead),
194
'xmax': max(self.xtail, self.xhead),
195
'ymin': min(self.ytail, self.yhead),
196
'ymax': max(self.ytail, self.yhead)}
197
198
199
def _allowed_options(self):
200
"""
201
Return the dictionary of allowed options for the line arrow graphics primitive.
202
203
EXAMPLES::
204
205
sage: from sage.plot.arrow import Arrow
206
sage: list(sorted(Arrow(0,0,2,3,{})._allowed_options().iteritems()))
207
[('arrowshorten', 'The length in points to shorten the arrow.'),
208
('arrowsize', 'The size of the arrowhead'),
209
('head',
210
'2-d only: Which end of the path to draw the head (one of 0 (start), 1 (end) or 2 (both)'),
211
('hue', 'The color given as a hue.'),
212
('legend_color', 'The color of the legend text.'),
213
('legend_label', 'The label for this item in the legend.'),
214
('linestyle',
215
"2d only: The style of the line, which is one of 'dashed',
216
'dotted', 'solid', 'dashdot', or '--', ':', '-', '-.',
217
respectively."),
218
('rgbcolor', 'The color as an RGB tuple.'),
219
('width', 'The width of the shaft of the arrow, in points.'),
220
('zorder', '2-d only: The layer level in which to draw')]
221
"""
222
return {'width':'The width of the shaft of the arrow, in points.',
223
'rgbcolor':'The color as an RGB tuple.',
224
'hue':'The color given as a hue.',
225
'arrowshorten':'The length in points to shorten the arrow.',
226
'arrowsize':'The size of the arrowhead',
227
'legend_label':'The label for this item in the legend.',
228
'legend_color':'The color of the legend text.',
229
'zorder':'2-d only: The layer level in which to draw',
230
'head':'2-d only: Which end of the path to draw the head (one of 0 (start), 1 (end) or 2 (both)',
231
'linestyle':"2d only: The style of the line, which is one of "
232
"'dashed', 'dotted', 'solid', 'dashdot', or '--', ':', '-', '-.', "
233
"respectively."}
234
235
def _plot3d_options(self, options=None):
236
"""
237
Translate 2D plot options into 3D plot options.
238
239
EXAMPLES::
240
241
sage: P = arrow((0,1), (2,3), width=5)
242
sage: p=P[0]; p
243
Arrow from (0.0,1.0) to (2.0,3.0)
244
sage: q=p.plot3d()
245
sage: q.thickness
246
5
247
"""
248
if options == None:
249
options = self.options()
250
options = dict(self.options())
251
options_3d = {}
252
if 'width' in options:
253
options_3d['thickness'] = options['width']
254
del options['width']
255
# ignore zorder and head in 3d plotting
256
if 'zorder' in options:
257
del options['zorder']
258
if 'head' in options:
259
del options['head']
260
if 'linestyle' in options:
261
del options['linestyle']
262
options_3d.update(GraphicPrimitive._plot3d_options(self, options))
263
return options_3d
264
265
def plot3d(self, ztail=0, zhead=0, **kwds):
266
"""
267
Takes 2D plot and places it in 3D.
268
269
EXAMPLES::
270
271
sage: A = arrow((0,0),(1,1))[0].plot3d()
272
sage: A.jmol_repr(A.testing_render_params())[0]
273
'draw line_1 diameter 2 arrow {0.0 0.0 0.0} {1.0 1.0 0.0} '
274
275
Note that we had to index the arrow to get the Arrow graphics
276
primitive. We can also change the height via the plot3d method
277
of Graphics, but only as a whole::
278
279
sage: A = arrow((0,0),(1,1)).plot3d(3)
280
sage: A.jmol_repr(A.testing_render_params())[0][0]
281
'draw line_1 diameter 2 arrow {0.0 0.0 3.0} {1.0 1.0 3.0} '
282
283
Optional arguments place both the head and tail outside the
284
`xy`-plane, but at different heights. This must be done on
285
the graphics primitive obtained by indexing::
286
287
sage: A=arrow((0,0),(1,1))[0].plot3d(3,4)
288
sage: A.jmol_repr(A.testing_render_params())[0]
289
'draw line_1 diameter 2 arrow {0.0 0.0 3.0} {1.0 1.0 4.0} '
290
"""
291
from sage.plot.plot3d.shapes2 import line3d
292
options = self._plot3d_options()
293
options.update(kwds)
294
return line3d([(self.xtail, self.ytail, ztail), (self.xhead, self.yhead, zhead)], arrow_head=True, **options)
295
296
def _repr_(self):
297
"""
298
Text representation of an arrow graphics primitive.
299
300
EXAMPLES::
301
302
sage: from sage.plot.arrow import Arrow
303
sage: Arrow(0,0,2,3,{})._repr_()
304
'Arrow from (0.0,0.0) to (2.0,3.0)'
305
"""
306
return "Arrow from (%s,%s) to (%s,%s)"%(self.xtail, self.ytail, self.xhead, self.yhead)
307
308
def _render_on_subplot(self, subplot):
309
r"""
310
Render this arrow in a subplot. This is the key function that
311
defines how this arrow graphics primitive is rendered in
312
matplotlib's library.
313
314
EXAMPLES:
315
316
This function implicitly ends up rendering this arrow on
317
a matplotlib subplot::
318
319
sage: arrow((0,1), (2,-1))
320
321
TESTS:
322
323
The length of the ends (shrinkA and shrinkB) should not depend
324
on the width of the arrow, because Matplotlib already takes
325
this into account. See :trac:`12836`::
326
327
sage: fig = Graphics().matplotlib()
328
sage: sp = fig.add_subplot(1,1,1)
329
sage: a = arrow((0,0), (1,1))
330
sage: b = arrow((0,0), (1,1), width=20)
331
sage: p1 = a[0]._render_on_subplot(sp)
332
sage: p2 = b[0]._render_on_subplot(sp)
333
sage: p1.shrinkA == p2.shrinkA
334
True
335
sage: p1.shrinkB == p2.shrinkB
336
True
337
338
Dashed arrows should have solid arrowheads,
339
:trac:`12852`. This test saves the plot of a dashed arrow to
340
an EPS file. Within the EPS file, ``stroke`` will be called
341
twice: once to draw the line, and again to draw the
342
arrowhead. We check that both calls do not occur while the
343
dashed line style is enabled::
344
345
sage: a = arrow((0,0), (1,1), linestyle='dashed')
346
sage: filename = tmp_filename(ext='.eps')
347
sage: a.save(filename=filename)
348
sage: with open(filename, 'r') as f:
349
....: contents = f.read().replace('\n', ' ')
350
sage: two_stroke_pattern = r'setdash.*stroke.*stroke.*setdash'
351
sage: import re
352
sage: two_stroke_re = re.compile(two_stroke_pattern)
353
sage: two_stroke_re.search(contents) is None
354
True
355
"""
356
from sage.plot.misc import get_matplotlib_linestyle
357
358
359
options = self.options()
360
head = options.pop('head')
361
if head == 0: style = '<|-'
362
elif head == 1: style = '-|>'
363
elif head == 2: style = '<|-|>'
364
else: raise KeyError('head parameter must be one of 0 (start), 1 (end) or 2 (both).')
365
width = float(options['width'])
366
arrowshorten_end = float(options.get('arrowshorten',0))/2.0
367
arrowsize = float(options.get('arrowsize',5))
368
head_width=arrowsize
369
head_length=arrowsize*2.0
370
color = to_mpl_color(options['rgbcolor'])
371
from matplotlib.patches import FancyArrowPatch
372
p = FancyArrowPatch((self.xtail, self.ytail), (self.xhead, self.yhead),
373
lw=width, arrowstyle='%s,head_width=%s,head_length=%s'%(style,head_width, head_length),
374
shrinkA=arrowshorten_end, shrinkB=arrowshorten_end,
375
fc=color, ec=color)
376
p.set_linestyle(get_matplotlib_linestyle(options['linestyle'],return_type='long'))
377
p.set_zorder(options['zorder'])
378
p.set_label(options['legend_label'])
379
380
if options['linestyle']!='solid':
381
# The next few lines work around a design issue in matplotlib. Currently, the specified
382
# linestyle is used to draw both the path and the arrowhead. If linestyle is 'dashed', this
383
# looks really odd. This code is from Jae-Joon Lee in response to a post to the matplotlib mailing
384
# list. See http://sourceforge.net/mailarchive/forum.php?thread_name=CAG%3DuJ%2Bnw2dE05P9TOXTz_zp-mGP3cY801vMH7yt6vgP9_WzU8w%40mail.gmail.com&forum_name=matplotlib-users
385
386
import matplotlib.patheffects as pe
387
class CheckNthSubPath(object):
388
def __init__(self, patch, n):
389
"""
390
creates an callable object that returns True if the provided
391
path is the n-th path from the patch.
392
"""
393
self._patch = patch
394
self._n = n
395
396
def get_paths(self, renderer):
397
self._patch.set_dpi_cor(renderer.points_to_pixels(1.))
398
paths, fillables = self._patch.get_path_in_displaycoord()
399
return paths
400
401
def __call__(self, renderer, gc, tpath, affine, rgbFace):
402
path = self.get_paths(renderer)[self._n]
403
vert1, code1 = path.vertices, path.codes
404
import numpy as np
405
406
if np.all(vert1 == tpath.vertices) and np.all(code1 == tpath.codes):
407
return True
408
else:
409
return False
410
411
412
class ConditionalStroke(pe._Base):
413
414
def __init__(self, condition_func, pe_list):
415
"""
416
path effect that is only applied when the condition_func
417
returns True.
418
"""
419
super(ConditionalStroke, self).__init__()
420
self._pe_list = pe_list
421
self._condition_func = condition_func
422
423
def draw_path(self, renderer, gc, tpath, affine, rgbFace):
424
425
if self._condition_func(renderer, gc, tpath, affine, rgbFace):
426
for pe1 in self._pe_list:
427
pe1.draw_path(renderer, gc, tpath, affine, rgbFace)
428
429
pe1 = ConditionalStroke(CheckNthSubPath(p, 0),[pe.Stroke()])
430
pe2 = ConditionalStroke(CheckNthSubPath(p, 1),[pe.Stroke(linestyle="solid")])
431
p.set_path_effects([pe1, pe2])
432
433
subplot.add_patch(p)
434
return p
435
436
def arrow(tailpoint=None, headpoint=None, **kwds):
437
"""
438
Returns either a 2-dimensional or 3-dimensional arrow depending
439
on value of points.
440
441
For information regarding additional arguments, see either arrow2d?
442
or arrow3d?.
443
444
EXAMPLES::
445
446
sage: arrow((0,0), (1,1))
447
sage: arrow((0,0,1), (1,1,1))
448
"""
449
try:
450
return arrow2d(tailpoint, headpoint, **kwds)
451
except ValueError:
452
from sage.plot.plot3d.shapes import arrow3d
453
return arrow3d(tailpoint, headpoint, **kwds)
454
455
@rename_keyword(color='rgbcolor')
456
@options(width=2, rgbcolor=(0,0,1),zorder=2, head = 1, linestyle='solid', legend_label=None)
457
def arrow2d(tailpoint=None, headpoint=None, path=None, **options):
458
"""
459
If tailpoint and headpoint are provided, returns an arrow from (xmin, ymin)
460
to (xmax, ymax). If tailpoint or headpoint is None and path is not None,
461
returns an arrow along the path. (See further info on paths in bezier_path).
462
463
INPUT:
464
465
- ``tailpoint`` - the starting point of the arrow
466
467
- ``headpoint`` - where the arrow is pointing to
468
469
- ``path`` - the list of points and control points (see bezier_path for detail) that
470
the arrow will follow from source to destination
471
472
- ``head`` - 0, 1 or 2, whether to draw the head at the start (0), end (1) or both (2)
473
of the path (using 0 will swap headpoint and tailpoint). This is ignored
474
in 3D plotting.
475
476
- ``linestyle`` - (default: ``'solid'``) The style of the line, which is one of
477
``'dashed'``, ``'dotted'``, ``'solid'``, ``'dashdot'``, or ``'--'``, ``':'``,
478
``'-'``, ``'-.'``, respectively.
479
480
- ``width`` - (default: 2) the width of the arrow shaft, in points
481
482
- ``color`` - (default: (0,0,1)) the color of the arrow (as an RGB tuple or a string)
483
484
- ``hue`` - the color of the arrow (as a number)
485
486
- ``arrowsize`` - the size of the arrowhead
487
488
- ``arrowshorten`` - the length in points to shorten the arrow (ignored if using path
489
parameter)
490
491
- ``legend_label`` - the label for this item in the legend
492
493
- ``legend_color`` - the color for the legend label
494
495
- ``zorder`` - the layer level to draw the arrow-- note that this is ignored in 3D
496
plotting.
497
498
EXAMPLES:
499
500
A straight, blue arrow::
501
502
sage: arrow2d((1, 1), (3, 3))
503
504
Make a red arrow::
505
506
sage: arrow2d((-1, -1), (2, 3), color=(1,0,0))
507
sage: arrow2d((-1, -1), (2, 3), color='red')
508
509
You can change the width of an arrow::
510
511
sage: arrow2d((1, 1), (3, 3), width=5, arrowsize=15)
512
513
Use a dashed line instead of a solid one for the arrow::
514
515
sage: arrow2d((1, 1), (3, 3), linestyle='dashed')
516
sage: arrow2d((1, 1), (3, 3), linestyle='--')
517
518
A pretty circle of arrows::
519
520
sage: sum([arrow2d((0,0), (cos(x),sin(x)), hue=x/(2*pi)) for x in [0..2*pi,step=0.1]])
521
522
If we want to draw the arrow between objects, for example, the
523
boundaries of two lines, we can use the arrowshorten option
524
to make the arrow shorter by a certain number of points::
525
526
sage: line([(0,0),(1,0)],thickness=10)+line([(0,1),(1,1)], thickness=10)+arrow2d((0.5,0),(0.5,1), arrowshorten=10,rgbcolor=(1,0,0))
527
528
If BOTH headpoint and tailpoint are None, then an empty plot is returned::
529
530
sage: arrow2d(headpoint=None, tailpoint=None)
531
532
We can also draw an arrow with a legend::
533
534
sage: arrow((0,0), (0,2), legend_label='up', legend_color='purple')
535
536
Extra options will get passed on to show(), as long as they are valid::
537
538
sage: arrow2d((-2, 2), (7,1), frame=True)
539
sage: arrow2d((-2, 2), (7,1)).show(frame=True)
540
"""
541
from sage.plot.all import Graphics
542
g = Graphics()
543
g._set_extra_kwds(Graphics._extract_kwds_for_show(options))
544
545
if headpoint is not None and tailpoint is not None:
546
xtail, ytail = tailpoint
547
xhead, yhead = headpoint
548
g.add_primitive(Arrow(xtail, ytail, xhead, yhead, options=options))
549
elif path is not None:
550
g.add_primitive(CurveArrow(path, options=options))
551
elif tailpoint is None and headpoint is None:
552
return g
553
else:
554
raise TypeError('Arrow requires either both headpoint and tailpoint or a path parameter.')
555
if options['legend_label']:
556
g.legend(True)
557
g._legend_colors = [options['legend_color']]
558
return g
559
560