From: <md...@us...> - 2008-01-08 21:55:39
|
Revision: 4820 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=4820&view=rev Author: mdboom Date: 2008-01-08 13:55:36 -0800 (Tue, 08 Jan 2008) Log Message: ----------- Adding projections directory Added Paths: ----------- trunk/matplotlib/lib/matplotlib/projections/ trunk/matplotlib/lib/matplotlib/projections/__init__.py trunk/matplotlib/lib/matplotlib/projections/geo.py trunk/matplotlib/lib/matplotlib/projections/polar.py Added: trunk/matplotlib/lib/matplotlib/projections/__init__.py =================================================================== --- trunk/matplotlib/lib/matplotlib/projections/__init__.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/projections/__init__.py 2008-01-08 21:55:36 UTC (rev 4820) @@ -0,0 +1,47 @@ +from geo import AitoffAxes, HammerAxes, LambertAxes +from polar import PolarAxes +from matplotlib import axes + +class ProjectionRegistry(object): + def __init__(self): + self._all_projection_types = {} + + def register(self, *projections): + for projection in projections: + name = projection.name + self._all_projection_types[name] = projection + + def get_projection_class(self, name): + return self._all_projection_types[name] + + def get_projection_names(self): + names = self._all_projection_types.keys() + names.sort() + return names +projection_registry = ProjectionRegistry() + +projection_registry.register( + axes.Axes, + PolarAxes, + AitoffAxes, + HammerAxes, + LambertAxes) + + +def register_projection(cls): + projection_registry.register(cls) + +def get_projection_class(projection): + if projection is None: + projection = 'rectilinear' + + try: + return projection_registry.get_projection_class(projection) + except KeyError: + raise ValueError("Unknown projection '%s'" % projection) + +def projection_factory(projection, figure, rect, **kwargs): + return get_projection_class(projection)(figure, rect, **kwargs) + +def get_projection_names(): + return projection_registry.get_projection_names() Added: trunk/matplotlib/lib/matplotlib/projections/geo.py =================================================================== --- trunk/matplotlib/lib/matplotlib/projections/geo.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/projections/geo.py 2008-01-08 21:55:36 UTC (rev 4820) @@ -0,0 +1,591 @@ +import math + +import numpy as npy +from matplotlib.numerix import npyma as ma + +import matplotlib +rcParams = matplotlib.rcParams +from matplotlib.artist import kwdocd +from matplotlib.axes import Axes +from matplotlib import cbook +from matplotlib.patches import Circle +from matplotlib.path import Path +from matplotlib.ticker import Formatter, Locator, NullLocator, FixedLocator, NullFormatter +from matplotlib.transforms import Affine2D, Affine2DBase, Bbox, \ + BboxTransformTo, IdentityTransform, Transform, TransformWrapper + +class GeoAxes(Axes): + """ + An abstract base class for geographic projections + """ + class ThetaFormatter(Formatter): + """ + Used to format the theta tick labels. Converts the native + unit of radians into degrees and adds a degree symbol. + """ + def __init__(self, round_to=1.0): + self._round_to = round_to + + def __call__(self, x, pos=None): + degrees = (x / npy.pi) * 180.0 + degrees = round(degrees / self._round_to) * self._round_to + # \u00b0 : degree symbol + return u"%d\u00b0" % degrees + + RESOLUTION = 75 + + def cla(self): + Axes.cla(self) + + self.set_longitude_grid(30) + self.set_latitude_grid(15) + self.set_longitude_grid_ends(75) + self.xaxis.set_minor_locator(NullLocator()) + self.yaxis.set_minor_locator(NullLocator()) + self.xaxis.set_ticks_position('none') + self.yaxis.set_ticks_position('none') + + self.grid(rcParams['axes.grid']) + + Axes.set_xlim(self, -npy.pi, npy.pi) + Axes.set_ylim(self, -npy.pi / 2.0, npy.pi / 2.0) + + def _set_lim_and_transforms(self): + # A (possibly non-linear) projection on the (already scaled) data + self.transProjection = self._get_core_transform(self.RESOLUTION) + + self.transAffine = self._get_affine_transform() + + self.transAxes = BboxTransformTo(self.bbox) + + # The complete data transformation stack -- from data all the + # way to display coordinates + self.transData = \ + self.transProjection + \ + self.transAffine + \ + self.transAxes + + # This is the transform for longitude ticks. + self._xaxis_pretransform = \ + Affine2D() \ + .scale(1.0, self._longitude_cap * 2.0) \ + .translate(0.0, -self._longitude_cap) + self._xaxis_transform = \ + self._xaxis_pretransform + \ + self.transData + self._xaxis_text1_transform = \ + Affine2D().scale(1.0, 0.0) + \ + self.transData + \ + Affine2D().translate(0.0, 4.0) + self._xaxis_text2_transform = \ + Affine2D().scale(1.0, 0.0) + \ + self.transData + \ + Affine2D().translate(0.0, -4.0) + + # This is the transform for latitude ticks. + yaxis_stretch = Affine2D().scale(npy.pi * 2.0, 1.0).translate(-npy.pi, 0.0) + yaxis_space = Affine2D().scale(1.0, 1.1) + self._yaxis_transform = \ + yaxis_stretch + \ + self.transData + yaxis_text_base = \ + yaxis_stretch + \ + self.transProjection + \ + (yaxis_space + \ + self.transAffine + \ + self.transAxes) + self._yaxis_text1_transform = \ + yaxis_text_base + \ + Affine2D().translate(-8.0, 0.0) + self._yaxis_text2_transform = \ + yaxis_text_base + \ + Affine2D().translate(8.0, 0.0) + + def _get_affine_transform(self): + transform = self._get_core_transform(1) + xscale, _ = transform.transform_point((npy.pi, 0)) + _, yscale = transform.transform_point((0, npy.pi / 2.0)) + return Affine2D() \ + .scale(0.5 / xscale, 0.5 / yscale) \ + .translate(0.5, 0.5) + + def update_layout(self, renderer): + t_text, b_text = self.xaxis.get_text_heights(renderer) + l_text, r_text = self.yaxis.get_text_widths(renderer) + originalPosition = self.get_position(True) + title_offset = (b_text - originalPosition.transformed( + self.figure.transFigure).height) / 2.0 + self.titleOffsetTrans.clear().translate(0, title_offset) + + def get_xaxis_transform(self): + return self._xaxis_transform + + def get_xaxis_text1_transform(self, pixelPad): + return self._xaxis_text1_transform, 'bottom', 'center' + + def get_xaxis_text2_transform(self, pixelPad): + return self._xaxis_text2_transform, 'top', 'center' + + def get_yaxis_transform(self): + return self._yaxis_transform + + def get_yaxis_text1_transform(self, pixelPad): + return self._yaxis_text1_transform, 'center', 'right' + + def get_yaxis_text2_transform(self, pixelPad): + return self._yaxis_text2_transform, 'center', 'left' + + def get_axes_patch(self): + return Circle((0.5, 0.5), 0.5) + + def set_yscale(self, *args, **kwargs): + if args[0] != 'linear': + raise NotImplementedError + + set_xscale = set_yscale + + def set_xlim(self, *args, **kwargs): + Axes.set_xlim(self, -npy.pi, npy.pi) + Axes.set_ylim(self, -npy.pi / 2.0, npy.pi / 2.0) + + set_ylim = set_xlim + + def format_coord(self, long, lat): + 'return a format string formatting the coordinate' + long = long * (180.0 / npy.pi) + lat = lat * (180.0 / npy.pi) + if lat >= 0.0: + ns = 'N' + else: + ns = 'S' + if long >= 0.0: + ew = 'E' + else: + ew = 'W' + return u'%f\u00b0%s, %f\u00b0%s' % (abs(lat), ns, abs(long), ew) + + def set_longitude_grid(self, degrees): + """ + Set the number of degrees between each longitude grid. + """ + number = (360.0 / degrees) + 1 + self.xaxis.set_major_locator( + FixedLocator( + npy.linspace(-npy.pi, npy.pi, number, True)[1:-1])) + self._logitude_degrees = degrees + self.xaxis.set_major_formatter(self.ThetaFormatter(degrees)) + + def set_latitude_grid(self, degrees): + """ + Set the number of degrees between each longitude grid. + """ + number = (180.0 / degrees) + 1 + self.yaxis.set_major_locator( + FixedLocator( + npy.linspace(-npy.pi / 2.0, npy.pi / 2.0, number, True)[1:-1])) + self._latitude_degrees = degrees + self.yaxis.set_major_formatter(self.ThetaFormatter(degrees)) + + def set_longitude_grid_ends(self, degrees): + """ + Set the latitude(s) at which to stop drawing the longitude grids. + """ + self._longitude_cap = degrees * (npy.pi / 180.0) + self._xaxis_pretransform \ + .clear() \ + .scale(1.0, self._longitude_cap * 2.0) \ + .translate(0.0, -self._longitude_cap) + + def get_data_ratio(self): + ''' + Return the aspect ratio of the data itself. + ''' + return 1.0 + + ### Interactive panning + + def can_zoom(self): + """ + Return True if this axes support the zoom box + """ + return False + + def start_pan(self, x, y, button): + pass + + def end_pan(self): + pass + + def drag_pan(self, button, key, x, y): + pass + + +class AitoffAxes(GeoAxes): + name = 'aitoff' + + class AitoffTransform(Transform): + """ + The base Aitoff transform. + """ + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, resolution): + """ + Create a new Aitoff transform. Resolution is the number of steps + to interpolate between each input line segment to approximate its + path in curved Aitoff space. + """ + Transform.__init__(self) + self._resolution = resolution + + def transform(self, ll): + longitude = ll[:, 0:1] + latitude = ll[:, 1:2] + + # Pre-compute some values + half_long = longitude / 2.0 + cos_latitude = npy.cos(latitude) + + alpha = npy.arccos(cos_latitude * npy.cos(half_long)) + # Mask this array, or we'll get divide-by-zero errors + alpha = ma.masked_where(alpha == 0.0, alpha) + # We want unnormalized sinc. numpy.sinc gives us normalized + sinc_alpha = ma.sin(alpha) / alpha + + x = (cos_latitude * npy.sin(half_long)) / sinc_alpha + y = (npy.sin(latitude) / sinc_alpha) + x.set_fill_value(0.0) + y.set_fill_value(0.0) + return npy.concatenate((x.filled(), y.filled()), 1) + transform.__doc__ = Transform.transform.__doc__ + + transform_non_affine = transform + transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ + + def transform_path(self, path): + vertices = path.vertices + ipath = path.interpolated(self._resolution) + return Path(self.transform(ipath.vertices), ipath.codes) + transform_path.__doc__ = Transform.transform_path.__doc__ + + transform_path_non_affine = transform_path + transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ + + def inverted(self): + return AitoffAxes.InvertedAitoffTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + class InvertedAitoffTransform(Transform): + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, resolution): + Transform.__init__(self) + self._resolution = resolution + + def transform(self, xy): + # MGDTODO: Math is hard ;( + return xy + transform.__doc__ = Transform.transform.__doc__ + + def inverted(self): + return AitoffAxes.AitoffTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + def __init__(self, *args, **kwargs): + self._longitude_cap = npy.pi / 2.0 + GeoAxes.__init__(self, *args, **kwargs) + self.set_aspect(0.5, adjustable='box', anchor='C') + self.cla() + + def _get_core_transform(self, resolution): + return self.AitoffTransform(resolution) + + +class HammerAxes(GeoAxes): + name = 'hammer' + + class HammerTransform(Transform): + """ + The base Hammer transform. + """ + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, resolution): + """ + Create a new Hammer transform. Resolution is the number of steps + to interpolate between each input line segment to approximate its + path in curved Hammer space. + """ + Transform.__init__(self) + self._resolution = resolution + + def transform(self, ll): + longitude = ll[:, 0:1] + latitude = ll[:, 1:2] + + # Pre-compute some values + half_long = longitude / 2.0 + cos_latitude = npy.cos(latitude) + sqrt2 = npy.sqrt(2.0) + + alpha = 1.0 + cos_latitude * npy.cos(half_long) + x = (2.0 * sqrt2) * (cos_latitude * npy.sin(half_long)) / alpha + y = (sqrt2 * npy.sin(latitude)) / alpha + return npy.concatenate((x, y), 1) + transform.__doc__ = Transform.transform.__doc__ + + transform_non_affine = transform + transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ + + def transform_path(self, path): + vertices = path.vertices + ipath = path.interpolated(self._resolution) + return Path(self.transform(ipath.vertices), ipath.codes) + transform_path.__doc__ = Transform.transform_path.__doc__ + + transform_path_non_affine = transform_path + transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ + + def inverted(self): + return HammerAxes.InvertedHammerTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + class InvertedHammerTransform(Transform): + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, resolution): + Transform.__init__(self) + self._resolution = resolution + + def transform(self, xy): + x = xy[:, 0:1] + y = xy[:, 1:2] + + quarter_x = 0.25 * x + half_y = 0.5 * y + z = npy.sqrt(1.0 - quarter_x*quarter_x - half_y*half_y) + longitude = 2 * npy.arctan((z*x) / (2.0 * (2.0*z*z - 1.0))) + latitude = npy.arcsin(y*z) + return npy.concatenate((longitude, latitude), 1) + transform.__doc__ = Transform.transform.__doc__ + + def inverted(self): + return HammerAxes.HammerTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + def __init__(self, *args, **kwargs): + self._longitude_cap = npy.pi / 2.0 + GeoAxes.__init__(self, *args, **kwargs) + self.set_aspect(0.5, adjustable='box', anchor='C') + self.cla() + + def _get_core_transform(self, resolution): + return self.HammerTransform(resolution) + + +class MollweideAxes(GeoAxes): + name = 'mollweide' + + class MollweideTransform(Transform): + """ + The base Mollweide transform. + """ + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, resolution): + """ + Create a new Mollweide transform. Resolution is the number of steps + to interpolate between each input line segment to approximate its + path in curved Mollweide space. + """ + Transform.__init__(self) + self._resolution = resolution + + def transform(self, ll): + longitude = ll[:, 0:1] + latitude = ll[:, 1:2] + + aux = 2.0 * npy.arcsin((2.0 * latitude) / npy.pi) + x = (2.0 * npy.sqrt(2.0) * longitude * npy.cos(aux)) / npy.pi + y = (npy.sqrt(2.0) * npy.sin(aux)) + + return npy.concatenate((x, y), 1) + transform.__doc__ = Transform.transform.__doc__ + + transform_non_affine = transform + transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ + + def transform_path(self, path): + vertices = path.vertices + ipath = path.interpolated(self._resolution) + return Path(self.transform(ipath.vertices), ipath.codes) + transform_path.__doc__ = Transform.transform_path.__doc__ + + transform_path_non_affine = transform_path + transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ + + def inverted(self): + return MollweideAxes.InvertedMollweideTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + class InvertedMollweideTransform(Transform): + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, resolution): + Transform.__init__(self) + self._resolution = resolution + + def transform(self, xy): + # MGDTODO: Math is hard ;( + return xy + transform.__doc__ = Transform.transform.__doc__ + + def inverted(self): + return MollweideAxes.MollweideTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + def __init__(self, *args, **kwargs): + self._longitude_cap = npy.pi / 2.0 + GeoAxes.__init__(self, *args, **kwargs) + self.set_aspect(0.5, adjustable='box', anchor='C') + self.cla() + + def _get_core_transform(self, resolution): + return self.MollweideTransform(resolution) + + +class LambertAxes(GeoAxes): + name = 'lambert' + + class LambertTransform(Transform): + """ + The base Lambert transform. + """ + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, center_longitude, center_latitude, resolution): + """ + Create a new Lambert transform. Resolution is the number of steps + to interpolate between each input line segment to approximate its + path in curved Lambert space. + """ + Transform.__init__(self) + self._resolution = resolution + self._center_longitude = center_longitude + self._center_latitude = center_latitude + + def transform(self, ll): + longitude = ll[:, 0:1] + latitude = ll[:, 1:2] + clong = self._center_longitude + clat = self._center_latitude + cos_lat = npy.cos(latitude) + sin_lat = npy.sin(latitude) + diff_long = longitude - clong + cos_diff_long = npy.cos(diff_long) + + inner_k = (1.0 + + npy.sin(clat)*sin_lat + + npy.cos(clat)*cos_lat*cos_diff_long) + # Prevent divide-by-zero problems + inner_k = npy.where(inner_k == 0.0, 1e-15, inner_k) + k = npy.sqrt(2.0 / inner_k) + x = k*cos_lat*npy.sin(diff_long) + y = k*(npy.cos(clat)*sin_lat - + npy.sin(clat)*cos_lat*cos_diff_long) + + return npy.concatenate((x, y), 1) + transform.__doc__ = Transform.transform.__doc__ + + transform_non_affine = transform + transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ + + def transform_path(self, path): + vertices = path.vertices + ipath = path.interpolated(self._resolution) + return Path(self.transform(ipath.vertices), ipath.codes) + transform_path.__doc__ = Transform.transform_path.__doc__ + + transform_path_non_affine = transform_path + transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ + + def inverted(self): + return LambertAxes.InvertedLambertTransform( + self._center_longitude, + self._center_latitude, + self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + class InvertedLambertTransform(Transform): + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, center_longitude, center_latitude, resolution): + Transform.__init__(self) + self._resolution = resolution + self._center_longitude = center_longitude + self._center_latitude = center_latitude + + def transform(self, xy): + x = xy[:, 0:1] + y = xy[:, 1:2] + clong = self._center_longitude + clat = self._center_latitude + p = npy.sqrt(x*x + y*y) + p = npy.where(p == 0.0, 1e-9, p) + c = 2.0 * npy.arcsin(0.5 * p) + sin_c = npy.sin(c) + cos_c = npy.cos(c) + + lat = npy.arcsin(cos_c*npy.sin(clat) + + ((y*sin_c*npy.cos(clat)) / p)) + long = clong + npy.arctan( + (x*sin_c) / (p*npy.cos(clat)*cos_c - y*npy.sin(clat)*sin_c)) + + return npy.concatenate((long, lat), 1) + transform.__doc__ = Transform.transform.__doc__ + + def inverted(self): + return LambertAxes.LambertTransform( + self._center_longitude, + self._center_latitude, + self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + def __init__(self, *args, **kwargs): + self._longitude_cap = npy.pi / 2.0 + self._center_longitude = kwargs.pop("center_longitude", 0.0) + self._center_latitude = kwargs.pop("center_latitude", 0.0) + GeoAxes.__init__(self, *args, **kwargs) + self.set_aspect('equal', adjustable='box', anchor='C') + self.cla() + + def cla(self): + GeoAxes.cla(self) + self.yaxis.set_major_formatter(NullFormatter()) + + def _get_core_transform(self, resolution): + return self.LambertTransform( + self._center_longitude, + self._center_latitude, + resolution) + + def _get_affine_transform(self): + return Affine2D() \ + .scale(0.25) \ + .translate(0.5, 0.5) Added: trunk/matplotlib/lib/matplotlib/projections/polar.py =================================================================== --- trunk/matplotlib/lib/matplotlib/projections/polar.py (rev 0) +++ trunk/matplotlib/lib/matplotlib/projections/polar.py 2008-01-08 21:55:36 UTC (rev 4820) @@ -0,0 +1,569 @@ +import math + +import numpy as npy + +import matplotlib +rcParams = matplotlib.rcParams +from matplotlib.artist import kwdocd +from matplotlib.axes import Axes +from matplotlib import cbook +from matplotlib.patches import Circle +from matplotlib.path import Path +from matplotlib.ticker import Formatter, Locator +from matplotlib.transforms import Affine2D, Affine2DBase, Bbox, \ + BboxTransformTo, IdentityTransform, Transform, TransformWrapper + +class PolarAxes(Axes): + """ + A polar graph projection, where the input dimensions are theta, r. + + Theta starts pointing east and goes anti-clockwise. + """ + name = 'polar' + + class PolarTransform(Transform): + """ + The base polar transform. This handles projection theta and r into + Cartesian coordinate space, but does not perform the ultimate affine + transformation into the correct position. + """ + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, resolution): + """ + Create a new polar transform. Resolution is the number of steps + to interpolate between each input line segment to approximate its + path in curved polar space. + """ + Transform.__init__(self) + self._resolution = resolution + + def transform(self, tr): + xy = npy.zeros(tr.shape, npy.float_) + t = tr[:, 0:1] + r = tr[:, 1:2] + x = xy[:, 0:1] + y = xy[:, 1:2] + x[:] = r * npy.cos(t) + y[:] = r * npy.sin(t) + return xy + transform.__doc__ = Transform.transform.__doc__ + + transform_non_affine = transform + transform_non_affine.__doc__ = Transform.transform_non_affine.__doc__ + + def transform_path(self, path): + vertices = path.vertices + if len(vertices) == 2 and vertices[0, 0] == vertices[1, 0]: + return Path(self.transform(vertices), path.codes) + ipath = path.interpolated(self._resolution) + return Path(self.transform(ipath.vertices), ipath.codes) + transform_path.__doc__ = Transform.transform_path.__doc__ + + transform_path_non_affine = transform_path + transform_path_non_affine.__doc__ = Transform.transform_path_non_affine.__doc__ + + def inverted(self): + return PolarAxes.InvertedPolarTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + class PolarAffine(Affine2DBase): + """ + The affine part of the polar projection. Scales the output so + that maximum radius rests on the edge of the axes circle. + """ + def __init__(self, scale_transform, limits): + """ + limits is the view limit of the data. The only part of + its bounds that is used is ymax (for the radius maximum). + """ + Affine2DBase.__init__(self) + self._scale_transform = scale_transform + self._limits = limits + self.set_children(scale_transform, limits) + self._mtx = None + + def get_matrix(self): + if self._invalid: + limits_scaled = self._limits.transformed(self._scale_transform) + ymax = limits_scaled.ymax + affine = Affine2D() \ + .scale(0.5 / ymax) \ + .translate(0.5, 0.5) + self._mtx = affine.get_matrix() + self._inverted = None + self._invalid = 0 + return self._mtx + get_matrix.__doc__ = Affine2DBase.get_matrix.__doc__ + + class InvertedPolarTransform(Transform): + """ + The inverse of the polar transform, mapping Cartesian + coordinate space back to t and r. + """ + input_dims = 2 + output_dims = 2 + is_separable = False + + def __init__(self, resolution): + Transform.__init__(self) + self._resolution = resolution + + def transform(self, xy): + x = xy[:, 0:1] + y = xy[:, 1:] + r = npy.sqrt(x*x + y*y) + theta = npy.arccos(x / r) + theta = npy.where(y < 0, 2 * npy.pi - theta, theta) + return npy.concatenate((theta, r), 1) + transform.__doc__ = Transform.transform.__doc__ + + def inverted(self): + return PolarAxes.PolarTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + + class ThetaFormatter(Formatter): + """ + Used to format the theta tick labels. Converts the native + unit of radians into degrees and adds a degree symbol. + """ + def __call__(self, x, pos=None): + # \u00b0 : degree symbol + return u"%d\u00b0" % ((x / npy.pi) * 180.0) + + class RadialLocator(Locator): + """ + Used to locate radius ticks. + + Ensures that all ticks are strictly positive. For all other + tasks, it delegates to the base Locator (which may be + different depending on the scale of the r-axis. + """ + def __init__(self, base): + self.base = base + + def __call__(self): + ticks = self.base() + return [x for x in ticks if x > 0] + + def autoscale(self): + return self.base.autoscale() + + def pan(self, numsteps): + return self.base.pan(numsteps) + + def zoom(self, direction): + return self.base.zoom(direction) + + def refresh(self): + return self.base.refresh() + + RESOLUTION = 75 + + def __init__(self, *args, **kwargs): + """ + Create a new Polar Axes for a polar plot. + """ + + self._rpad = 0.05 + Axes.__init__(self, *args, **kwargs) + self.set_aspect('equal', adjustable='box', anchor='C') + self.cla() + __init__.__doc__ = Axes.__init__.__doc__ + + def cla(self): + Axes.cla(self) + + self.xaxis.set_major_formatter(self.ThetaFormatter()) + angles = npy.arange(0.0, 360.0, 45.0) + self.set_thetagrids(angles) + self.yaxis.set_major_locator(self.RadialLocator(self.yaxis.get_major_locator())) + + self.grid(rcParams['polaraxes.grid']) + self.xaxis.set_ticks_position('none') + self.yaxis.set_ticks_position('none') + + def _set_lim_and_transforms(self): + self.transAxes = BboxTransformTo(self.bbox) + + # Transforms the x and y axis separately by a scale factor + # It is assumed that this part will have non-linear components + self.transScale = TransformWrapper(IdentityTransform()) + + # A (possibly non-linear) projection on the (already scaled) data + self.transProjection = self.PolarTransform(self.RESOLUTION) + + # An affine transformation on the data, generally to limit the + # range of the axes + self.transProjectionAffine = self.PolarAffine(self.transScale, self.viewLim) + + # The complete data transformation stack -- from data all the + # way to display coordinates + self.transData = self.transScale + self.transProjection + \ + (self.transProjectionAffine + self.transAxes) + + # This is the transform for theta-axis ticks. It is + # equivalent to transData, except it always puts r == 1.0 at + # the edge of the axis circle. + self._xaxis_transform = ( + self.transProjection + + self.PolarAffine(IdentityTransform(), Bbox.unit()) + + self.transAxes) + # The theta labels are moved from radius == 0.0 to radius == 1.1 + self._theta_label1_position = Affine2D().translate(0.0, 1.1) + self._xaxis_text1_transform = ( + self._theta_label1_position + + self._xaxis_transform) + self._theta_label2_position = Affine2D().translate(0.0, 1.0 / 1.1) + self._xaxis_text2_transform = ( + self._theta_label2_position + + self._xaxis_transform) + + # This is the transform for r-axis ticks. It scales the theta + # axis so the gridlines from 0.0 to 1.0, now go from 0.0 to + # 2pi. + self._yaxis_transform = ( + Affine2D().scale(npy.pi * 2.0, 1.0) + + self.transData) + # The r-axis labels are put at an angle and padded in the r-direction + self._r_label1_position = Affine2D().translate(22.5, self._rpad) + self._yaxis_text1_transform = ( + self._r_label1_position + + Affine2D().scale(1.0 / 360.0, 1.0) + + self._yaxis_transform + ) + self._r_label2_position = Affine2D().translate(22.5, self._rpad) + self._yaxis_text2_transform = ( + self._r_label2_position + + Affine2D().scale(1.0 / 360.0, 1.0) + + self._yaxis_transform + ) + + def update_layout(self, renderer): + t_text, b_text = self.xaxis.get_text_heights(renderer) + l_text, r_text = self.yaxis.get_text_widths(renderer) + originalPosition = self.get_position(True) + title_offset = (b_text - originalPosition.transformed( + self.figure.transFigure).height) / 2.0 + self.titleOffsetTrans.clear().translate(0, title_offset) + + def get_xaxis_transform(self): + return self._xaxis_transform + + def get_xaxis_text1_transform(self, pixelPad): + return self._xaxis_text1_transform, 'center', 'center' + + def get_xaxis_text2_transform(self, pixelPad): + return self._xaxis_text2_transform, 'center', 'center' + + def get_yaxis_transform(self): + return self._yaxis_transform + + def get_yaxis_text1_transform(self, pixelPad): + return self._yaxis_text1_transform, 'center', 'center' + + def get_yaxis_text2_transform(self, pixelPad): + return self._yaxis_text2_transform, 'center', 'center' + + def get_axes_patch(self): + return Circle((0.5, 0.5), 0.5) + + def set_rmax(self, rmax): + self.viewLim.y1 = rmax + angle = self._r_label1_position.to_values()[4] + self._r_label1_position.clear().translate( + angle, rmax * self._rpad) + self._r_label2_position.clear().translate( + angle, -rmax * self._rpad) + + def get_rmax(self): + return self.viewLim.ymax + + def set_yscale(self, *args, **kwargs): + Axes.set_yscale(self, *args, **kwargs) + self.yaxis.set_major_locator( + self.RadialLocator(self.yaxis.get_major_locator())) + + set_rscale = Axes.set_yscale + set_rticks = Axes.set_yticks + + def set_thetagrids(self, angles, labels=None, frac=None, + **kwargs): + """ + Set the angles at which to place the theta grids (these + gridlines are equal along the theta dimension). angles is in + degrees + + labels, if not None, is a len(angles) list of strings of the + labels to use at each angle. + + if labels is None, the labels with be fmt%%angle + + frac is the fraction of the polar axes radius at which to + place the label (1 is the edge).Eg 1.05 isd outside the axes + and 0.95 is inside the axes + + Return value is a list of lines, labels where the lines are + lines.Line2D instances and the labels are Text + instances: + + kwargs are optional text properties for the labels + %(Text)s + ACCEPTS: sequence of floats + """ + angles = npy.asarray(angles, npy.float_) + self.set_xticks(angles * (npy.pi / 180.0)) + if labels is not None: + self.set_xticklabels(labels) + if frac is not None: + self._theta_label1_position.clear().translate(0.0, frac) + self._theta_label2_position.clear().translate(0.0, 1.0 / frac) + for t in self.xaxis.get_ticklabels(): + t.update(kwargs) + set_thetagrids.__doc__ = cbook.dedent(set_thetagrids.__doc__) % kwdocd + + def set_rgrids(self, radii, labels=None, angle=None, rpad=None, **kwargs): + """ + set the radial locations and labels of the r grids + + The labels will appear at radial distances radii at angle + + labels, if not None, is a len(radii) list of strings of the + labels to use at each angle. + + if labels is None, the self.rformatter will be used + + rpad is a fraction of the max of radii which will pad each of + the radial labels in the radial direction. + + Return value is a list of lines, labels where the lines are + lines.Line2D instances and the labels are text.Text + instances + + kwargs control the rgrid Text label properties: + %(Text)s + + ACCEPTS: sequence of floats + """ + radii = npy.asarray(radii) + rmin = radii.min() + if rmin <= 0: + raise ValueError('radial grids must be strictly positive') + + self.set_yticks(radii) + if labels is not None: + self.set_yticklabels(labels) + if angle is None: + angle = self._r_label1_position.to_values()[4] + if rpad is not None: + self._rpad = rpad + rmax = self.get_rmax() + self._r_label1_position.clear().translate(angle, self._rpad * rmax) + self._r_label2_position.clear().translate(angle, -self._rpad * rmax) + for t in self.yaxis.get_ticklabels(): + t.update(kwargs) + + set_rgrids.__doc__ = cbook.dedent(set_rgrids.__doc__) % kwdocd + + def set_xscale(self, scale, *args, **kwargs): + if scale != 'linear': + raise NotImplementedError("You can not set the xscale on a polar plot.") + + def set_xlim(self, *args, **kargs): + # The xlim is fixed, no matter what you do + self.viewLim.intervalx = (0.0, npy.pi * 2.0) + + def format_coord(self, theta, r): + 'return a format string formatting the coordinate' + theta /= math.pi + # \u03b8: lower-case theta + # \u03c0: lower-case pi + # \u00b0: degree symbol + return u'\u03b8=%0.3f\u03c0 (%0.3f\u00b0), r=%0.3f' % (theta, theta * 180.0, r) + + def get_data_ratio(self): + ''' + Return the aspect ratio of the data itself. For a polar plot, + this should always be 1.0 + ''' + return 1.0 + + ### Interactive panning + + def can_zoom(self): + """ + Return True if this axes support the zoom box + """ + return False + + def start_pan(self, x, y, button): + angle = self._r_label1_position.to_values()[4] / 180.0 * npy.pi + mode = '' + if button == 1: + epsilon = npy.pi / 45.0 + t, r = self.transData.inverted().transform_point((x, y)) + if t >= angle - epsilon and t <= angle + epsilon: + mode = 'drag_r_labels' + elif button == 3: + mode = 'zoom' + + self._pan_start = cbook.Bunch( + rmax = self.get_rmax(), + trans = self.transData.frozen(), + trans_inverse = self.transData.inverted().frozen(), + r_label_angle = self._r_label1_position.to_values()[4], + x = x, + y = y, + mode = mode + ) + + def end_pan(self): + del self._pan_start + + def drag_pan(self, button, key, x, y): + p = self._pan_start + + if p.mode == 'drag_r_labels': + startt, startr = p.trans_inverse.transform_point((p.x, p.y)) + t, r = p.trans_inverse.transform_point((x, y)) + + # Deal with theta + dt0 = t - startt + dt1 = startt - t + if abs(dt1) < abs(dt0): + dt = abs(dt1) * sign(dt0) * -1.0 + else: + dt = dt0 * -1.0 + dt = (dt / npy.pi) * 180.0 + + rpad = self._r_label1_position.to_values()[5] + self._r_label1_position.clear().translate( + p.r_label_angle - dt, rpad) + self._r_label2_position.clear().translate( + p.r_label_angle - dt, -rpad) + + elif p.mode == 'zoom': + startt, startr = p.trans_inverse.transform_point((p.x, p.y)) + t, r = p.trans_inverse.transform_point((x, y)) + + dr = r - startr + + # Deal with r + scale = r / startr + self.set_rmax(p.rmax / scale) + +# These are a couple of aborted attempts to project a polar plot using +# cubic bezier curves. + +# def transform_path(self, path): +# twopi = 2.0 * npy.pi +# halfpi = 0.5 * npy.pi + +# vertices = path.vertices +# t0 = vertices[0:-1, 0] +# t1 = vertices[1: , 0] +# td = npy.where(t1 > t0, t1 - t0, twopi - (t0 - t1)) +# maxtd = td.max() +# interpolate = npy.ceil(maxtd / halfpi) +# if interpolate > 1.0: +# vertices = self.interpolate(vertices, interpolate) + +# vertices = self.transform(vertices) + +# result = npy.zeros((len(vertices) * 3 - 2, 2), npy.float_) +# codes = mpath.Path.CURVE4 * npy.ones((len(vertices) * 3 - 2, ), mpath.Path.code_type) +# result[0] = vertices[0] +# codes[0] = mpath.Path.MOVETO + +# kappa = 4.0 * ((npy.sqrt(2.0) - 1.0) / 3.0) +# kappa = 0.5 + +# p0 = vertices[0:-1] +# p1 = vertices[1: ] + +# x0 = p0[:, 0:1] +# y0 = p0[:, 1: ] +# b0 = ((y0 - x0) - y0) / ((x0 + y0) - x0) +# a0 = y0 - b0*x0 + +# x1 = p1[:, 0:1] +# y1 = p1[:, 1: ] +# b1 = ((y1 - x1) - y1) / ((x1 + y1) - x1) +# a1 = y1 - b1*x1 + +# x = -(a0-a1) / (b0-b1) +# y = a0 + b0*x + +# xk = (x - x0) * kappa + x0 +# yk = (y - y0) * kappa + y0 + +# result[1::3, 0:1] = xk +# result[1::3, 1: ] = yk + +# xk = (x - x1) * kappa + x1 +# yk = (y - y1) * kappa + y1 + +# result[2::3, 0:1] = xk +# result[2::3, 1: ] = yk + +# result[3::3] = p1 + +# print vertices[-2:] +# print result[-2:] + +# return mpath.Path(result, codes) + +# twopi = 2.0 * npy.pi +# halfpi = 0.5 * npy.pi + +# vertices = path.vertices +# t0 = vertices[0:-1, 0] +# t1 = vertices[1: , 0] +# td = npy.where(t1 > t0, t1 - t0, twopi - (t0 - t1)) +# maxtd = td.max() +# interpolate = npy.ceil(maxtd / halfpi) + +# print "interpolate", interpolate +# if interpolate > 1.0: +# vertices = self.interpolate(vertices, interpolate) + +# result = npy.zeros((len(vertices) * 3 - 2, 2), npy.float_) +# codes = mpath.Path.CURVE4 * npy.ones((len(vertices) * 3 - 2, ), mpath.Path.code_type) +# result[0] = vertices[0] +# codes[0] = mpath.Path.MOVETO + +# kappa = 4.0 * ((npy.sqrt(2.0) - 1.0) / 3.0) +# tkappa = npy.arctan(kappa) +# hyp_kappa = npy.sqrt(kappa*kappa + 1.0) + +# t0 = vertices[0:-1, 0] +# t1 = vertices[1: , 0] +# r0 = vertices[0:-1, 1] +# r1 = vertices[1: , 1] + +# td = npy.where(t1 > t0, t1 - t0, twopi - (t0 - t1)) +# td_scaled = td / (npy.pi * 0.5) +# rd = r1 - r0 +# r0kappa = r0 * kappa * td_scaled +# r1kappa = r1 * kappa * td_scaled +# ravg_kappa = ((r1 + r0) / 2.0) * kappa * td_scaled + +# result[1::3, 0] = t0 + (tkappa * td_scaled) +# result[1::3, 1] = r0*hyp_kappa +# # result[1::3, 1] = r0 / npy.cos(tkappa * td_scaled) # npy.sqrt(r0*r0 + ravg_kappa*ravg_kappa) + +# result[2::3, 0] = t1 - (tkappa * td_scaled) +# result[2::3, 1] = r1*hyp_kappa +# # result[2::3, 1] = r1 / npy.cos(tkappa * td_scaled) # npy.sqrt(r1*r1 + ravg_kappa*ravg_kappa) + +# result[3::3, 0] = t1 +# result[3::3, 1] = r1 + +# print vertices[:6], result[:6], t0[:6], t1[:6], td[:6], td_scaled[:6], tkappa +# result = self.transform(result) +# return mpath.Path(result, codes) +# transform_path_non_affine = transform_path + + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |