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

Lines changed: 175 additions & 2 deletions
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

Lines changed: 3 additions & 3 deletions
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

Lines changed: 5 additions & 5 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 6 additions & 6 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy