Skip to content

Commit 7a82ea8

Browse files
committed
Added (experimental) support for large arcs
svn path=/branches/transforms/; revision=4700
1 parent 12cc2fc commit 7a82ea8

File tree

5 files changed

+191
-18
lines changed

5 files changed

+191
-18
lines changed

lib/matplotlib/patches.py

+175-2
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,7 @@ def __init__(self, xy, width, height, angle=0.0, **kwargs):
839839
self._width, self._height = width, height
840840
self._angle = angle
841841
self._recompute_transform()
842+
self._path = Path.unit_circle()
842843

843844
def _recompute_transform(self):
844845
self._patch_transform = transforms.Affine2D() \
@@ -850,7 +851,7 @@ def get_path(self):
850851
"""
851852
Return the vertices of the rectangle
852853
"""
853-
return Path.unit_circle()
854+
return self._path
854855

855856
def get_patch_transform(self):
856857
return self._patch_transform
@@ -881,7 +882,6 @@ def _set_angle(self, angle):
881882
self._recompute_transform()
882883
angle = property(_get_angle, _set_angle)
883884

884-
885885
class Circle(Ellipse):
886886
"""
887887
A circle patch
@@ -908,6 +908,179 @@ def __init__(self, xy, radius=5, **kwargs):
908908
Ellipse.__init__(self, xy, radius*2, radius*2, **kwargs)
909909
__init__.__doc__ = cbook.dedent(__init__.__doc__) % artist.kwdocd
910910

911+
class Arc(Ellipse):
912+
"""
913+
An elliptical arc. Because it performs various optimizations, it may not be
914+
filled.
915+
"""
916+
def __str__(self):
917+
return "Arc(%d,%d;%dx%d)"%(self.center[0],self.center[1],self.width,self.height)
918+
919+
def __init__(self, xy, width, height, angle=0.0, theta1=0.0, theta2=360.0, **kwargs):
920+
"""
921+
xy - center of ellipse
922+
width - length of horizontal axis
923+
height - length of vertical axis
924+
angle - rotation in degrees (anti-clockwise)
925+
theta1 - starting angle of the arc in degrees
926+
theta2 - ending angle of the arc in degrees
927+
928+
If theta1 and theta2 are not provided, the arc will form a
929+
complete ellipse.
930+
931+
Valid kwargs are:
932+
%(Patch)s
933+
"""
934+
fill = kwargs.pop('fill')
935+
if fill:
936+
raise ValueError("Arc objects can not be filled")
937+
kwargs['fill'] = False
938+
939+
Ellipse.__init__(self, xy, width, height, angle, **kwargs)
940+
941+
self._theta1 = theta1
942+
self._theta2 = theta2
943+
944+
def draw(self, renderer):
945+
"""
946+
Ellipses are normally drawn using an approximation that uses
947+
eight cubic bezier splines. The error of this approximation
948+
is 1.89818e-6, according to this unverified source:
949+
950+
Lancaster, Don. Approximating a Circle or an Ellipse Using
951+
Four Bezier Cubic Splines.
952+
953+
http://www.tinaja.com/glib/ellipse4.pdf
954+
955+
There is a use case where very large ellipses must be drawn
956+
with very high accuracy, and it is too expensive to render the
957+
entire ellipse with enough segments (either splines or line
958+
segments). Therefore, in the case where either radius of the
959+
ellipse is large enough that the error of the spline
960+
approximation will be visible (greater than one pixel offset
961+
from the ideal), a different technique is used.
962+
963+
In that case, only the visible parts of the ellipse are drawn,
964+
with each visible arc using a fixed number of spline segments
965+
(8). The algorithm proceeds as follows:
966+
967+
1. The points where the ellipse intersects the axes bounding
968+
box are located. (This is done be performing an inverse
969+
transformation on the axes bbox such that it is relative to
970+
the unit circle -- this makes the intersection calculation
971+
much easier than doing rotated ellipse intersection
972+
directly).
973+
974+
This uses the "line intersecting a circle" algorithm from:
975+
976+
Vince, John. Geometry for Computer Graphics: Formulae,
977+
Examples & Proofs. London: Springer-Verlag, 2005.
978+
979+
2. The angles of each of the intersection points are
980+
calculated.
981+
982+
3. Proceeding counterclockwise starting in the positive
983+
x-direction, each of the visible arc-segments between the
984+
pairs of vertices are drawn using the bezier arc
985+
approximation technique implemented in Path.arc().
986+
"""
987+
# Get the width and height in pixels
988+
width, height = self.get_transform().transform_point(
989+
(self._width, self._height))
990+
inv_error = (1.0 / 1.89818e-6)
991+
992+
if width < inv_error and height < inv_error and False:
993+
self._path = Path.arc(self._theta1, self._theta2)
994+
return Patch.draw(self, renderer)
995+
996+
# Transforms the axes box_path so that it is relative to the unit
997+
# circle in the same way that it is relative to the desired
998+
# ellipse.
999+
box_path = Path.unit_rectangle()
1000+
box_path_transform = transforms.BboxTransformTo(self.axes.bbox) + \
1001+
self.get_transform().inverted()
1002+
box_path = box_path.transformed(box_path_transform)
1003+
vertices = []
1004+
1005+
def iter_circle_intersect_on_line(x0, y0, x1, y1):
1006+
dx = x1 - x0
1007+
dy = y1 - y0
1008+
dr2 = dx*dx + dy*dy
1009+
dr = npy.sqrt(dr2)
1010+
D = x0*y1 - x1*y0
1011+
D2 = D*D
1012+
discrim = dr2 - D2
1013+
1014+
# Single (tangential) intersection
1015+
if discrim == 0.0:
1016+
x = (D*dy) / dr2
1017+
y = (-D*dx) / dr2
1018+
yield x, y
1019+
elif discrim > 0.0:
1020+
if dy < 0:
1021+
sign_dy = -1.0
1022+
else:
1023+
sign_dy = 1.0
1024+
sqrt_discrim = npy.sqrt(discrim)
1025+
for sign in (1., -1.):
1026+
x = (D*dy + sign * sign_dy * dx * sqrt_discrim) / dr2
1027+
y = (-D*dx + sign * npy.abs(dy) * sqrt_discrim) / dr2
1028+
yield x, y
1029+
1030+
def iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
1031+
epsilon = 1e-9
1032+
if x1 < x0:
1033+
x0e, x1e = x1, x0
1034+
else:
1035+
x0e, x1e = x0, x1
1036+
if y1 < y0:
1037+
y0e, y1e = y1, y0
1038+
else:
1039+
y0e, y1e = y0, y1
1040+
x0e -= epsilon
1041+
y0e -= epsilon
1042+
x1e += epsilon
1043+
y1e += epsilon
1044+
for x, y in iter_circle_intersect_on_line(x0, y0, x1, y1):
1045+
if x >= x0e and x <= x1e and y >= y0e and y <= y1e:
1046+
yield x, y
1047+
1048+
PI = npy.pi
1049+
TWOPI = PI * 2.0
1050+
RAD2DEG = 180.0 / PI
1051+
DEG2RAD = PI / 180.0
1052+
theta1 = self._theta1
1053+
theta2 = self._theta2
1054+
thetas = {}
1055+
# For each of the point pairs, there is a line segment
1056+
for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]):
1057+
x0, y0 = p0
1058+
x1, y1 = p1
1059+
for x, y in iter_circle_intersect_on_line_seg(x0, y0, x1, y1):
1060+
# Convert radians to angles
1061+
theta = npy.arccos(x)
1062+
if y < 0:
1063+
theta = TWOPI - theta
1064+
theta *= RAD2DEG
1065+
if theta > theta1 and theta < theta2:
1066+
thetas[theta] = None
1067+
1068+
thetas = thetas.keys()
1069+
thetas.sort()
1070+
thetas.append(theta2)
1071+
1072+
last_theta = theta1
1073+
theta1_rad = theta1 * DEG2RAD
1074+
inside = box_path.contains_point((npy.cos(theta1_rad), npy.sin(theta1_rad)))
1075+
1076+
for theta in thetas:
1077+
if inside:
1078+
self._path = Path.arc(last_theta, theta, 8)
1079+
Patch.draw(self, renderer)
1080+
inside = False
1081+
else:
1082+
inside = True
1083+
last_theta = theta
9111084

9121085
def bbox_artist(artist, renderer, props=None, fill=True):
9131086
"""

lib/matplotlib/path.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,7 @@ def unit_circle(cls):
408408
unit_circle = classmethod(unit_circle)
409409

410410
#@classmethod
411-
def arc(cls, theta1, theta2, is_wedge=False, n=None):
411+
def arc(cls, theta1, theta2, n=None, is_wedge=False):
412412
"""
413413
Returns an arc on the unit circle from angle theta1 to angle
414414
theta2 (in degrees).
@@ -486,12 +486,12 @@ def arc(cls, theta1, theta2, is_wedge=False, n=None):
486486
arc = classmethod(arc)
487487

488488
#@classmethod
489-
def wedge(cls, theta1, theta2):
489+
def wedge(cls, theta1, theta2, n=None):
490490
"""
491491
Returns a wedge of the unit circle from angle theta1 to angle
492492
theta2 (in degrees).
493493
"""
494-
return cls.arc(theta1, theta2, True)
494+
return cls.arc(theta1, theta2, True, n)
495495
wedge = classmethod(wedge)
496496

497497
_get_path_collection_extents = get_path_collection_extents

src/_path.cpp

+5-5
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class _path_module : public Py::ExtensionModule<_path_module>
109109
// Input 2D polygon _pgon_ with _numverts_ number of vertices and test point
110110
// _point_, returns 1 if inside, 0 if outside.
111111
template<class T>
112-
bool point_in_path_impl(double tx, double ty, T& path)
112+
bool point_in_path_impl(const double tx, const double ty, T& path)
113113
{
114114
int yflag0, yflag1, inside_flag;
115115
double vtx0, vty0, vtx1, vty1, sx, sy;
@@ -132,7 +132,7 @@ bool point_in_path_impl(double tx, double ty, T& path)
132132
yflag0 = (vty0 >= ty);
133133

134134
vtx1 = x;
135-
vty1 = x;
135+
vty1 = y;
136136

137137
inside_flag = 0;
138138
do
@@ -141,7 +141,7 @@ bool point_in_path_impl(double tx, double ty, T& path)
141141

142142
// The following cases denote the beginning on a new subpath
143143
if (code == agg::path_cmd_stop ||
144-
(code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly)
144+
(code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly)
145145
{
146146
x = sx;
147147
y = sy;
@@ -169,7 +169,7 @@ bool point_in_path_impl(double tx, double ty, T& path)
169169
// by Joseph Samosky's and Mark Haigh-Hutchinson's different
170170
// polygon inclusion tests.
171171
if ( ((vty1-ty) * (vtx0-vtx1) >=
172-
(vtx1-tx) * (vty0-vty1)) == yflag1 )
172+
(vtx1-tx) * (vty0-vty1)) == yflag1 )
173173
{
174174
inside_flag ^= 1;
175175
}
@@ -184,7 +184,7 @@ bool point_in_path_impl(double tx, double ty, T& path)
184184
vty1 = y;
185185
}
186186
while (code != agg::path_cmd_stop &&
187-
(code & agg::path_cmd_end_poly) != agg::path_cmd_end_poly);
187+
(code & agg::path_cmd_end_poly) != agg::path_cmd_end_poly);
188188

189189
yflag1 = (vty1 >= ty);
190190
if (yflag0 != yflag1)

unit/ellipse_compare.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Compare the ellipse generated with arcs versus a polygonal approximation
2+
Compare the ellipse generated with arcs versus a polygonal approximation
33
"""
44
import numpy as npy
55
from matplotlib import patches
@@ -29,7 +29,7 @@
2929
ax = fig.add_subplot(211, aspect='auto')
3030
ax.fill(x, y, alpha=0.2, facecolor='yellow', edgecolor='yellow', linewidth=1, zorder=1)
3131

32-
e1 = patches.Ellipse((xcenter, ycenter), width, height,
32+
e1 = patches.Arc((xcenter, ycenter), width, height,
3333
angle=angle, linewidth=2, fill=False, zorder=2)
3434

3535
ax.add_patch(e1)

unit/ellipse_large.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import math
88
from pylab import *
9-
from matplotlib.patches import Ellipse
9+
from matplotlib.patches import Arc
1010

1111
# given a point x, y
1212
x = 2692.440
@@ -54,22 +54,22 @@ def custom_ellipse( ax, x, y, major, minor, theta, numpoints = 750, **kwargs ):
5454

5555
# make the lower-bound ellipse
5656
diam = (r - delta) * 2.0
57-
lower_ellipse = Ellipse( (0.0, 0.0), diam, diam, 0.0, fill=False, edgecolor="darkgreen" )
57+
lower_ellipse = Arc( (0.0, 0.0), diam, diam, 0.0, fill=False, edgecolor="darkgreen" )
5858
ax.add_patch( lower_ellipse )
5959

6060
# make the target ellipse
6161
diam = r * 2.0
62-
target_ellipse = Ellipse( (0.0, 0.0), diam, diam, 0.0, fill=False, edgecolor="darkred" )
62+
target_ellipse = Arc( (0.0, 0.0), diam, diam, 0.0, fill=False, edgecolor="darkred" )
6363
ax.add_patch( target_ellipse )
6464

6565
# make the upper-bound ellipse
6666
diam = (r + delta) * 2.0
67-
upper_ellipse = Ellipse( (0.0, 0.0), diam, diam, 0.0, fill=False, edgecolor="darkblue" )
67+
upper_ellipse = Arc( (0.0, 0.0), diam, diam, 0.0, fill=False, edgecolor="darkblue" )
6868
ax.add_patch( upper_ellipse )
6969

7070
# make the target
7171
diam = delta * 2.0
72-
target = Ellipse( (x, y), diam, diam, 0.0, fill=False, edgecolor="#DD1208" )
72+
target = Arc( (x, y), diam, diam, 0.0, fill=False, edgecolor="#DD1208" )
7373
ax.add_patch( target )
7474

7575
# give it a big marker
@@ -104,4 +104,4 @@ def custom_ellipse( ax, x, y, major, minor, theta, numpoints = 750, **kwargs ):
104104
ax.set_ylim(6705, 6735)
105105
show()
106106

107-
savefig("ellipse")
107+
# savefig("ellipse")

0 commit comments

Comments
 (0)