From: <md...@us...> - 2008-05-23 17:41:45
|
Revision: 5230 http://matplotlib.svn.sourceforge.net/matplotlib/?rev=5230&view=rev Author: mdboom Date: 2008-05-23 10:41:36 -0700 (Fri, 23 May 2008) Log Message: ----------- Move examples from pylab to api. Added Paths: ----------- trunk/matplotlib/examples/api/custom_projection_example.py trunk/matplotlib/examples/api/custom_scale_example.py Removed Paths: ------------- trunk/matplotlib/examples/pylab/custom_projection_example.py trunk/matplotlib/examples/pylab/custom_scale_example.py Copied: trunk/matplotlib/examples/api/custom_projection_example.py (from rev 5226, trunk/matplotlib/examples/pylab/custom_projection_example.py) =================================================================== --- trunk/matplotlib/examples/api/custom_projection_example.py (rev 0) +++ trunk/matplotlib/examples/api/custom_projection_example.py 2008-05-23 17:41:36 UTC (rev 5230) @@ -0,0 +1,475 @@ +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 +from matplotlib.projections import register_projection + +import numpy as np + +# This example projection class is rather long, but it is designed to +# illustrate many features, not all of which will be used every time. +# It is also common to factor out a lot of these methods into common +# code used by a number of projections with similar characteristics +# (see geo.py). + +class HammerAxes(Axes): + """ + A custom class for the Aitoff-Hammer projection, an equal-area map + projection. + + http://en.wikipedia.org/wiki/Hammer_projection + """ + # The projection must specify a name. This will be used be the + # user to select the projection, i.e. ``subplot(111, + # projection='hammer')``. + name = 'hammer' + + # The number of interpolation steps when converting from straight + # lines to curves. (See ``transform_path``). + RESOLUTION = 75 + + def __init__(self, *args, **kwargs): + Axes.__init__(self, *args, **kwargs) + self.set_aspect(0.5, adjustable='box', anchor='C') + self.cla() + + def cla(self): + """ + Override to set up some reasonable defaults. + """ + # Don't forget to call the base class + Axes.cla(self) + + # Set up a default grid spacing + self.set_longitude_grid(30) + self.set_latitude_grid(15) + self.set_longitude_grid_ends(75) + + # Turn off minor ticking altogether + self.xaxis.set_minor_locator(NullLocator()) + self.yaxis.set_minor_locator(NullLocator()) + + # Do not display ticks -- we only want gridlines and text + self.xaxis.set_ticks_position('none') + self.yaxis.set_ticks_position('none') + + # The limits on this projection are fixed -- they are not to + # be changed by the user. This makes the math in the + # transformation itself easier, and since this is a toy + # example, the easier, the better. + Axes.set_xlim(self, -np.pi, np.pi) + Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0) + + def cla(self): + """ + Initialize the Axes object to reasonable defaults. + """ + 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, -np.pi, np.pi) + Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0) + + def _set_lim_and_transforms(self): + """ + This is called once when the plot is created to set up all the + transforms for the data, text and grids. + """ + # There are three important coordinate spaces going on here: + # + # 1. Data space: The space of the data itself + # + # 2. Axes space: The unit rectangle (0, 0) to (1, 1) + # covering the entire plot area. + # + # 3. Display space: The coordinates of the resulting image, + # often in pixels or dpi/inch. + + # This function makes heavy use of the Transform classes in + # ``lib/matplotlib/transforms.py.`` For more information, see + # the inline documentation there. + + # The goal of the first two transformations is to get from the + # data space (in this case longitude and latitude) to axes + # space. It is separated into a non-affine and affine part so + # that the non-affine part does not have to be recomputed when + # a simple affine change to the figure has been made (such as + # resizing the window or changing the dpi). + + # 1) The core transformation from data space into + # rectilinear space defined in the HammerTransform class. + self.transProjection = self.HammerTransform(self.RESOLUTION) + + # 2) The above has an output range that is not in the unit + # rectangle, so scale and translate it so it fits correctly + # within the axes. The peculiar calculations of xscale and + # yscale are specific to a Aitoff-Hammer projection, so don't + # worry about them too much. + xscale = 2.0 * np.sqrt(2.0) * np.sin(0.5 * np.pi) + yscale = np.sqrt(2.0) * np.sin(0.5 * np.pi) + self.transAffine = Affine2D() \ + .scale(0.5 / xscale, 0.5 / yscale) \ + .translate(0.5, 0.5) + + # 3) This is the transformation from axes space to display + # space. + self.transAxes = BboxTransformTo(self.bbox) + + # Now put these 3 transforms together -- from data all the way + # to display coordinates. Using the '+' operator, these + # transforms will be applied "in order". The transforms are + # automatically simplified, if possible, by the underlying + # transformation framework. + self.transData = \ + self.transProjection + \ + self.transAffine + \ + self.transAxes + + # The main data transformation is set up. Now deal with + # gridlines and tick labels. + + # Longitude gridlines and ticklabels. The input to these + # transforms are in display space in x and axes space in y. + # Therefore, the input values will be in range (-xmin, 0), + # (xmax, 1). The goal of these transforms is to go from that + # space to display space. The tick labels will be offset 4 + # pixels from the equator. + self._xaxis_pretransform = \ + Affine2D() \ + .scale(1.0, np.pi) \ + .translate(0.0, -np.pi) + 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) + + # Now set up the transforms for the latitude ticks. The input to + # these transforms are in axes space in x and display space in + # y. Therefore, the input values will be in range (0, -ymin), + # (1, ymax). The goal of these transforms is to go from that + # space to display space. The tick labels will be offset 4 + # pixels from the edge of the axes ellipse. + yaxis_stretch = Affine2D().scale(np.pi * 2.0, 1.0).translate(-np.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_xaxis_transform(self): + """ + Override this method to provide a transformation for the + x-axis grid and ticks. + """ + return self._xaxis_transform + + def get_xaxis_text1_transform(self, pixelPad): + """ + Override this method to provide a transformation for the + x-axis tick labels. + + Returns a tuple of the form (transform, valign, halign) + """ + return self._xaxis_text1_transform, 'bottom', 'center' + + def get_xaxis_text2_transform(self, pixelPad): + """ + Override this method to provide a transformation for the + secondary x-axis tick labels. + + Returns a tuple of the form (transform, valign, halign) + """ + return self._xaxis_text2_transform, 'top', 'center' + + def get_yaxis_transform(self): + """ + Override this method to provide a transformation for the + y-axis grid and ticks. + """ + return self._yaxis_transform + + def get_yaxis_text1_transform(self, pixelPad): + """ + Override this method to provide a transformation for the + y-axis tick labels. + + Returns a tuple of the form (transform, valign, halign) + """ + return self._yaxis_text1_transform, 'center', 'right' + + def get_yaxis_text2_transform(self, pixelPad): + """ + Override this method to provide a transformation for the + secondary y-axis tick labels. + + Returns a tuple of the form (transform, valign, halign) + """ + return self._yaxis_text2_transform, 'center', 'left' + + def get_axes_patch(self): + """ + Override this method to define the shape that is used for the + background of the plot. It should be a subclass of Patch. + + In this case, it is a Circle (that may be warped by the axes + transform into an ellipse). Any data and gridlines will be + clipped to this shape. + """ + return Circle((0.5, 0.5), 0.5) + + # Prevent the user from applying scales to one or both of the + # axes. In this particular case, scaling the axes wouldn't make + # sense, so we don't allow it. + def set_xscale(self, *args, **kwargs): + if args[0] != 'linear': + raise NotImplementedError + Axes.set_xscale(self, *args, **kwargs) + + def set_yscale(self, *args, **kwargs): + if args[0] != 'linear': + raise NotImplementedError + Axes.set_yscale(self, *args, **kwargs) + + # Prevent the user from changing the axes limits. In our case, we + # want to display the whole sphere all the time, so we override + # set_xlim and set_ylim to ignore any input. This also applies to + # interactive panning and zooming in the GUI interfaces. + def set_xlim(self, *args, **kwargs): + Axes.set_xlim(self, -np.pi, np.pi) + Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0) + set_ylim = set_xlim + + def format_coord(self, long, lat): + """ + Override this method to change how the values are displayed in + the status bar. + + In this case, we want them to be displayed in degrees N/S/E/W. + """ + long = long * (180.0 / np.pi) + lat = lat * (180.0 / np.pi) + if lat >= 0.0: + ns = 'N' + else: + ns = 'S' + if long >= 0.0: + ew = 'E' + else: + ew = 'W' + # \u00b0 : degree symbol + return u'%f\u00b0%s, %f\u00b0%s' % (abs(lat), ns, abs(long), ew) + + class DegreeFormatter(Formatter): + """ + This is a custom formatter that converts the native unit of + radians into (truncated) 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 / np.pi) * 180.0 + degrees = round(degrees / self._round_to) * self._round_to + # \u00b0 : degree symbol + return u"%d\u00b0" % degrees + + def set_longitude_grid(self, degrees): + """ + Set the number of degrees between each longitude grid. + + This is an example method that is specific to this projection + class -- it provides a more convenient interface to set the + ticking than set_xticks would. + """ + # Set up a FixedLocator at each of the points, evenly spaced + # by degrees. + number = (360.0 / degrees) + 1 + self.xaxis.set_major_locator( + FixedLocator( + np.linspace(-np.pi, np.pi, number, True)[1:-1])) + # Set the formatter to display the tick labels in degrees, + # rather than radians. + self.xaxis.set_major_formatter(self.DegreeFormatter(degrees)) + + def set_latitude_grid(self, degrees): + """ + Set the number of degrees between each longitude grid. + + This is an example method that is specific to this projection + class -- it provides a more convenient interface than + set_yticks would. + """ + # Set up a FixedLocator at each of the points, evenly spaced + # by degrees. + number = (180.0 / degrees) + 1 + self.yaxis.set_major_locator( + FixedLocator( + np.linspace(-np.pi / 2.0, np.pi / 2.0, number, True)[1:-1])) + # Set the formatter to display the tick labels in degrees, + # rather than radians. + self.yaxis.set_major_formatter(self.DegreeFormatter(degrees)) + + def set_longitude_grid_ends(self, degrees): + """ + Set the latitude(s) at which to stop drawing the longitude grids. + + Often, in geographic projections, you wouldn't want to draw + longitude gridlines near the poles. This allows the user to + specify the degree at which to stop drawing longitude grids. + + This is an example method that is specific to this projection + class -- it provides an interface to something that has no + analogy in the base Axes class. + """ + longitude_cap = degrees * (np.pi / 180.0) + # Change the xaxis gridlines transform so that it draws from + # -degrees to degrees, rather than -pi to pi. + self._xaxis_pretransform \ + .clear() \ + .scale(1.0, longitude_cap * 2.0) \ + .translate(0.0, -longitude_cap) + + def get_data_ratio(self): + """ + Return the aspect ratio of the data itself. + + This method should be overridden by any Axes that have a + fixed data ratio. + """ + return 1.0 + + # Interactive panning and zooming is not supported with this projection, + # so we override all of the following methods to disable it. + 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 + + # Now, the transforms themselves. + + 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): + """ + Override the transform method to implement the custom transform. + + The input and output are Nx2 numpy arrays. + """ + longitude = ll[:, 0:1] + latitude = ll[:, 1:2] + + # Pre-compute some values + half_long = longitude / 2.0 + cos_latitude = np.cos(latitude) + sqrt2 = np.sqrt(2.0) + + alpha = 1.0 + cos_latitude * np.cos(half_long) + x = (2.0 * sqrt2) * (cos_latitude * np.sin(half_long)) / alpha + y = (sqrt2 * np.sin(latitude)) / alpha + return np.concatenate((x, y), 1) + + # This is where things get interesting. With this projection, + # straight lines in data space become curves in display space. + # This is done by interpolating new values between the input + # values of the data. Since ``transform`` must not return a + # differently-sized array, any transform that requires + # changing the length of the data array must happen within + # ``transform_path``. + def transform_path(self, path): + vertices = path.vertices + ipath = path.interpolated(self._resolution) + return Path(self.transform(ipath.vertices), ipath.codes) + + 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 = np.sqrt(1.0 - quarter_x*quarter_x - half_y*half_y) + longitude = 2 * np.arctan((z*x) / (2.0 * (2.0*z*z - 1.0))) + latitude = np.arcsin(y*z) + return np.concatenate((longitude, latitude), 1) + transform.__doc__ = Transform.transform.__doc__ + + def inverted(self): + # The inverse of the inverse is the original transform... ;) + return HammerAxes.HammerTransform(self._resolution) + inverted.__doc__ = Transform.inverted.__doc__ + +# Now register the projection with matplotlib so the user can select +# it. +register_projection(HammerAxes) + +# Now make a simple example using the custom projection. +from pylab import * + +subplot(111, projection="hammer") +grid(True) + +show() Copied: trunk/matplotlib/examples/api/custom_scale_example.py (from rev 5226, trunk/matplotlib/examples/pylab/custom_scale_example.py) =================================================================== --- trunk/matplotlib/examples/api/custom_scale_example.py (rev 0) +++ trunk/matplotlib/examples/api/custom_scale_example.py 2008-05-23 17:41:36 UTC (rev 5230) @@ -0,0 +1,165 @@ +from matplotlib import scale as mscale +from matplotlib import transforms as mtransforms + +class MercatorLatitudeScale(mscale.ScaleBase): + """ + Scales data in range -pi/2 to pi/2 (-90 to 90 degrees) using + the system used to scale latitudes in a Mercator projection. + + The scale function: + ln(tan(y) + sec(y)) + + The inverse scale function: + atan(sinh(y)) + + Since the Mercator scale tends to infinity at +/- 90 degrees, + there is user-defined threshold, above and below which nothing + will be plotted. This defaults to +/- 85 degrees. + + source: + http://en.wikipedia.org/wiki/Mercator_projection + """ + + # The scale class must have a member ``name`` that defines the + # string used to select the scale. For example, + # ``gca().set_yscale("mercator")`` would be used to select this + # scale. + name = 'mercator' + + + def __init__(self, axis, **kwargs): + """ + Any keyword arguments passed to ``set_xscale`` and + ``set_yscale`` will be passed along to the scale's + constructor. + + thresh: The degree above which to crop the data. + """ + mscale.ScaleBase.__init__(self) + thresh = kwargs.pop("thresh", (85 / 180.0) * np.pi) + if thresh >= np.pi / 2.0: + raise ValueError("thresh must be less than pi/2") + self.thresh = thresh + + def get_transform(self): + """ + Override this method to return a new instance that does the + actual transformation of the data. + + The MercatorLatitudeTransform class is defined below as a + nested class of this one. + """ + return self.MercatorLatitudeTransform(self.thresh) + + def set_default_locators_and_formatters(self, axis): + """ + Override to set up the locators and formatters to use with the + scale. This is only required if the scale requires custom + locators and formatters. Writing custom locators and + formatters is rather outside the scope of this example, but + there are many helpful examples in ``ticker.py``. + + In our case, the Mercator example uses a fixed locator from + -90 to 90 degrees and a custom formatter class to put convert + the radians to degrees and put a degree symbol after the + value:: + """ + class DegreeFormatter(Formatter): + def __call__(self, x, pos=None): + # \u00b0 : degree symbol + return u"%d\u00b0" % ((x / np.pi) * 180.0) + + deg2rad = np.pi / 180.0 + axis.set_major_locator(FixedLocator( + np.arange(-90, 90, 10) * deg2rad)) + axis.set_major_formatter(DegreeFormatter()) + axis.set_minor_formatter(DegreeFormatter()) + + def limit_range_for_scale(self, vmin, vmax, minpos): + """ + Override to limit the bounds of the axis to the domain of the + transform. In the case of Mercator, the bounds should be + limited to the threshold that was passed in. Unlike the + autoscaling provided by the tick locators, this range limiting + will always be adhered to, whether the axis range is set + manually, determined automatically or changed through panning + and zooming. + """ + return max(vmin, -self.thresh), min(vmax, self.thresh) + + class MercatorLatitudeTransform(mtransforms.Transform): + # There are two value members that must be defined. + # ``input_dims`` and ``output_dims`` specify number of input + # dimensions and output dimensions to the transformation. + # These are used by the transformation framework to do some + # error checking and prevent incompatible transformations from + # being connected together. When defining transforms for a + # scale, which are, by definition, separable and have only one + # dimension, these members should always be set to 1. + input_dims = 1 + output_dims = 1 + is_separable = True + + def __init__(self, thresh): + mtransforms.Transform.__init__(self) + self.thresh = thresh + + def transform(self, a): + """ + This transform takes an Nx1 ``numpy`` array and returns a + transformed copy. Since the range of the Mercator scale + is limited by the user-specified threshold, the input + array must be masked to contain only valid values. + ``matplotlib`` will handle masked arrays and remove the + out-of-range data from the plot. Importantly, the + ``transform`` method *must* return an array that is the + same shape as the input array, since these values need to + remain synchronized with values in the other dimension. + """ + masked = ma.masked_where((a < -self.thresh) | (a > self.thresh), a) + if masked.mask.any(): + return ma.log(np.abs(ma.tan(masked) + 1.0 / ma.cos(masked))) + else: + return np.log(np.abs(np.tan(a) + 1.0 / np.cos(a))) + + def inverted(self): + """ + Override this method so matplotlib knows how to get the + inverse transform for this transform. + """ + return MercatorLatitudeScale.InvertedMercatorLatitudeTransform(self.thresh) + + class InvertedMercatorLatitudeTransform(mtransforms.Transform): + input_dims = 1 + output_dims = 1 + is_separable = True + + def __init__(self, thresh): + mtransforms.Transform.__init__(self) + self.thresh = thresh + + def transform(self, a): + return np.arctan(np.sinh(a)) + + def inverted(self): + return MercatorLatitudeScale.MercatorLatitudeTransform(self.thresh) + +# Now that the Scale class has been defined, it must be registered so +# that ``matplotlib`` can find it. +mscale.register_scale(MercatorLatitudeScale) + +from pylab import * +import numpy as np + +t = arange(-180.0, 180.0, 0.1) +s = t / 360.0 * np.pi + +plot(t, s, '-', lw=2) +gca().set_yscale('mercator') + +xlabel('Longitude') +ylabel('Latitude') +title('Mercator: Projection of the Oppressor') +grid(True) + +show() Deleted: trunk/matplotlib/examples/pylab/custom_projection_example.py =================================================================== --- trunk/matplotlib/examples/pylab/custom_projection_example.py 2008-05-23 17:41:07 UTC (rev 5229) +++ trunk/matplotlib/examples/pylab/custom_projection_example.py 2008-05-23 17:41:36 UTC (rev 5230) @@ -1,475 +0,0 @@ -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 -from matplotlib.projections import register_projection - -import numpy as np - -# This example projection class is rather long, but it is designed to -# illustrate many features, not all of which will be used every time. -# It is also common to factor out a lot of these methods into common -# code used by a number of projections with similar characteristics -# (see geo.py). - -class HammerAxes(Axes): - """ - A custom class for the Aitoff-Hammer projection, an equal-area map - projection. - - http://en.wikipedia.org/wiki/Hammer_projection - """ - # The projection must specify a name. This will be used be the - # user to select the projection, i.e. ``subplot(111, - # projection='hammer')``. - name = 'hammer' - - # The number of interpolation steps when converting from straight - # lines to curves. (See ``transform_path``). - RESOLUTION = 75 - - def __init__(self, *args, **kwargs): - Axes.__init__(self, *args, **kwargs) - self.set_aspect(0.5, adjustable='box', anchor='C') - self.cla() - - def cla(self): - """ - Override to set up some reasonable defaults. - """ - # Don't forget to call the base class - Axes.cla(self) - - # Set up a default grid spacing - self.set_longitude_grid(30) - self.set_latitude_grid(15) - self.set_longitude_grid_ends(75) - - # Turn off minor ticking altogether - self.xaxis.set_minor_locator(NullLocator()) - self.yaxis.set_minor_locator(NullLocator()) - - # Do not display ticks -- we only want gridlines and text - self.xaxis.set_ticks_position('none') - self.yaxis.set_ticks_position('none') - - # The limits on this projection are fixed -- they are not to - # be changed by the user. This makes the math in the - # transformation itself easier, and since this is a toy - # example, the easier, the better. - Axes.set_xlim(self, -np.pi, np.pi) - Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0) - - def cla(self): - """ - Initialize the Axes object to reasonable defaults. - """ - 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, -np.pi, np.pi) - Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0) - - def _set_lim_and_transforms(self): - """ - This is called once when the plot is created to set up all the - transforms for the data, text and grids. - """ - # There are three important coordinate spaces going on here: - # - # 1. Data space: The space of the data itself - # - # 2. Axes space: The unit rectangle (0, 0) to (1, 1) - # covering the entire plot area. - # - # 3. Display space: The coordinates of the resulting image, - # often in pixels or dpi/inch. - - # This function makes heavy use of the Transform classes in - # ``lib/matplotlib/transforms.py.`` For more information, see - # the inline documentation there. - - # The goal of the first two transformations is to get from the - # data space (in this case longitude and latitude) to axes - # space. It is separated into a non-affine and affine part so - # that the non-affine part does not have to be recomputed when - # a simple affine change to the figure has been made (such as - # resizing the window or changing the dpi). - - # 1) The core transformation from data space into - # rectilinear space defined in the HammerTransform class. - self.transProjection = self.HammerTransform(self.RESOLUTION) - - # 2) The above has an output range that is not in the unit - # rectangle, so scale and translate it so it fits correctly - # within the axes. The peculiar calculations of xscale and - # yscale are specific to a Aitoff-Hammer projection, so don't - # worry about them too much. - xscale = 2.0 * np.sqrt(2.0) * np.sin(0.5 * np.pi) - yscale = np.sqrt(2.0) * np.sin(0.5 * np.pi) - self.transAffine = Affine2D() \ - .scale(0.5 / xscale, 0.5 / yscale) \ - .translate(0.5, 0.5) - - # 3) This is the transformation from axes space to display - # space. - self.transAxes = BboxTransformTo(self.bbox) - - # Now put these 3 transforms together -- from data all the way - # to display coordinates. Using the '+' operator, these - # transforms will be applied "in order". The transforms are - # automatically simplified, if possible, by the underlying - # transformation framework. - self.transData = \ - self.transProjection + \ - self.transAffine + \ - self.transAxes - - # The main data transformation is set up. Now deal with - # gridlines and tick labels. - - # Longitude gridlines and ticklabels. The input to these - # transforms are in display space in x and axes space in y. - # Therefore, the input values will be in range (-xmin, 0), - # (xmax, 1). The goal of these transforms is to go from that - # space to display space. The tick labels will be offset 4 - # pixels from the equator. - self._xaxis_pretransform = \ - Affine2D() \ - .scale(1.0, np.pi) \ - .translate(0.0, -np.pi) - 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) - - # Now set up the transforms for the latitude ticks. The input to - # these transforms are in axes space in x and display space in - # y. Therefore, the input values will be in range (0, -ymin), - # (1, ymax). The goal of these transforms is to go from that - # space to display space. The tick labels will be offset 4 - # pixels from the edge of the axes ellipse. - yaxis_stretch = Affine2D().scale(np.pi * 2.0, 1.0).translate(-np.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_xaxis_transform(self): - """ - Override this method to provide a transformation for the - x-axis grid and ticks. - """ - return self._xaxis_transform - - def get_xaxis_text1_transform(self, pixelPad): - """ - Override this method to provide a transformation for the - x-axis tick labels. - - Returns a tuple of the form (transform, valign, halign) - """ - return self._xaxis_text1_transform, 'bottom', 'center' - - def get_xaxis_text2_transform(self, pixelPad): - """ - Override this method to provide a transformation for the - secondary x-axis tick labels. - - Returns a tuple of the form (transform, valign, halign) - """ - return self._xaxis_text2_transform, 'top', 'center' - - def get_yaxis_transform(self): - """ - Override this method to provide a transformation for the - y-axis grid and ticks. - """ - return self._yaxis_transform - - def get_yaxis_text1_transform(self, pixelPad): - """ - Override this method to provide a transformation for the - y-axis tick labels. - - Returns a tuple of the form (transform, valign, halign) - """ - return self._yaxis_text1_transform, 'center', 'right' - - def get_yaxis_text2_transform(self, pixelPad): - """ - Override this method to provide a transformation for the - secondary y-axis tick labels. - - Returns a tuple of the form (transform, valign, halign) - """ - return self._yaxis_text2_transform, 'center', 'left' - - def get_axes_patch(self): - """ - Override this method to define the shape that is used for the - background of the plot. It should be a subclass of Patch. - - In this case, it is a Circle (that may be warped by the axes - transform into an ellipse). Any data and gridlines will be - clipped to this shape. - """ - return Circle((0.5, 0.5), 0.5) - - # Prevent the user from applying scales to one or both of the - # axes. In this particular case, scaling the axes wouldn't make - # sense, so we don't allow it. - def set_xscale(self, *args, **kwargs): - if args[0] != 'linear': - raise NotImplementedError - Axes.set_xscale(self, *args, **kwargs) - - def set_yscale(self, *args, **kwargs): - if args[0] != 'linear': - raise NotImplementedError - Axes.set_yscale(self, *args, **kwargs) - - # Prevent the user from changing the axes limits. In our case, we - # want to display the whole sphere all the time, so we override - # set_xlim and set_ylim to ignore any input. This also applies to - # interactive panning and zooming in the GUI interfaces. - def set_xlim(self, *args, **kwargs): - Axes.set_xlim(self, -np.pi, np.pi) - Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0) - set_ylim = set_xlim - - def format_coord(self, long, lat): - """ - Override this method to change how the values are displayed in - the status bar. - - In this case, we want them to be displayed in degrees N/S/E/W. - """ - long = long * (180.0 / np.pi) - lat = lat * (180.0 / np.pi) - if lat >= 0.0: - ns = 'N' - else: - ns = 'S' - if long >= 0.0: - ew = 'E' - else: - ew = 'W' - # \u00b0 : degree symbol - return u'%f\u00b0%s, %f\u00b0%s' % (abs(lat), ns, abs(long), ew) - - class DegreeFormatter(Formatter): - """ - This is a custom formatter that converts the native unit of - radians into (truncated) 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 / np.pi) * 180.0 - degrees = round(degrees / self._round_to) * self._round_to - # \u00b0 : degree symbol - return u"%d\u00b0" % degrees - - def set_longitude_grid(self, degrees): - """ - Set the number of degrees between each longitude grid. - - This is an example method that is specific to this projection - class -- it provides a more convenient interface to set the - ticking than set_xticks would. - """ - # Set up a FixedLocator at each of the points, evenly spaced - # by degrees. - number = (360.0 / degrees) + 1 - self.xaxis.set_major_locator( - FixedLocator( - np.linspace(-np.pi, np.pi, number, True)[1:-1])) - # Set the formatter to display the tick labels in degrees, - # rather than radians. - self.xaxis.set_major_formatter(self.DegreeFormatter(degrees)) - - def set_latitude_grid(self, degrees): - """ - Set the number of degrees between each longitude grid. - - This is an example method that is specific to this projection - class -- it provides a more convenient interface than - set_yticks would. - """ - # Set up a FixedLocator at each of the points, evenly spaced - # by degrees. - number = (180.0 / degrees) + 1 - self.yaxis.set_major_locator( - FixedLocator( - np.linspace(-np.pi / 2.0, np.pi / 2.0, number, True)[1:-1])) - # Set the formatter to display the tick labels in degrees, - # rather than radians. - self.yaxis.set_major_formatter(self.DegreeFormatter(degrees)) - - def set_longitude_grid_ends(self, degrees): - """ - Set the latitude(s) at which to stop drawing the longitude grids. - - Often, in geographic projections, you wouldn't want to draw - longitude gridlines near the poles. This allows the user to - specify the degree at which to stop drawing longitude grids. - - This is an example method that is specific to this projection - class -- it provides an interface to something that has no - analogy in the base Axes class. - """ - longitude_cap = degrees * (np.pi / 180.0) - # Change the xaxis gridlines transform so that it draws from - # -degrees to degrees, rather than -pi to pi. - self._xaxis_pretransform \ - .clear() \ - .scale(1.0, longitude_cap * 2.0) \ - .translate(0.0, -longitude_cap) - - def get_data_ratio(self): - """ - Return the aspect ratio of the data itself. - - This method should be overridden by any Axes that have a - fixed data ratio. - """ - return 1.0 - - # Interactive panning and zooming is not supported with this projection, - # so we override all of the following methods to disable it. - 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 - - # Now, the transforms themselves. - - 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): - """ - Override the transform method to implement the custom transform. - - The input and output are Nx2 numpy arrays. - """ - longitude = ll[:, 0:1] - latitude = ll[:, 1:2] - - # Pre-compute some values - half_long = longitude / 2.0 - cos_latitude = np.cos(latitude) - sqrt2 = np.sqrt(2.0) - - alpha = 1.0 + cos_latitude * np.cos(half_long) - x = (2.0 * sqrt2) * (cos_latitude * np.sin(half_long)) / alpha - y = (sqrt2 * np.sin(latitude)) / alpha - return np.concatenate((x, y), 1) - - # This is where things get interesting. With this projection, - # straight lines in data space become curves in display space. - # This is done by interpolating new values between the input - # values of the data. Since ``transform`` must not return a - # differently-sized array, any transform that requires - # changing the length of the data array must happen within - # ``transform_path``. - def transform_path(self, path): - vertices = path.vertices - ipath = path.interpolated(self._resolution) - return Path(self.transform(ipath.vertices), ipath.codes) - - 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 = np.sqrt(1.0 - quarter_x*quarter_x - half_y*half_y) - longitude = 2 * np.arctan((z*x) / (2.0 * (2.0*z*z - 1.0))) - latitude = np.arcsin(y*z) - return np.concatenate((longitude, latitude), 1) - transform.__doc__ = Transform.transform.__doc__ - - def inverted(self): - # The inverse of the inverse is the original transform... ;) - return HammerAxes.HammerTransform(self._resolution) - inverted.__doc__ = Transform.inverted.__doc__ - -# Now register the projection with matplotlib so the user can select -# it. -register_projection(HammerAxes) - -# Now make a simple example using the custom projection. -from pylab import * - -subplot(111, projection="hammer") -grid(True) - -show() Deleted: trunk/matplotlib/examples/pylab/custom_scale_example.py =================================================================== --- trunk/matplotlib/examples/pylab/custom_scale_example.py 2008-05-23 17:41:07 UTC (rev 5229) +++ trunk/matplotlib/examples/pylab/custom_scale_example.py 2008-05-23 17:41:36 UTC (rev 5230) @@ -1,165 +0,0 @@ -from matplotlib import scale as mscale -from matplotlib import transforms as mtransforms - -class MercatorLatitudeScale(mscale.ScaleBase): - """ - Scales data in range -pi/2 to pi/2 (-90 to 90 degrees) using - the system used to scale latitudes in a Mercator projection. - - The scale function: - ln(tan(y) + sec(y)) - - The inverse scale function: - atan(sinh(y)) - - Since the Mercator scale tends to infinity at +/- 90 degrees, - there is user-defined threshold, above and below which nothing - will be plotted. This defaults to +/- 85 degrees. - - source: - http://en.wikipedia.org/wiki/Mercator_projection - """ - - # The scale class must have a member ``name`` that defines the - # string used to select the scale. For example, - # ``gca().set_yscale("mercator")`` would be used to select this - # scale. - name = 'mercator' - - - def __init__(self, axis, **kwargs): - """ - Any keyword arguments passed to ``set_xscale`` and - ``set_yscale`` will be passed along to the scale's - constructor. - - thresh: The degree above which to crop the data. - """ - mscale.ScaleBase.__init__(self) - thresh = kwargs.pop("thresh", (85 / 180.0) * np.pi) - if thresh >= np.pi / 2.0: - raise ValueError("thresh must be less than pi/2") - self.thresh = thresh - - def get_transform(self): - """ - Override this method to return a new instance that does the - actual transformation of the data. - - The MercatorLatitudeTransform class is defined below as a - nested class of this one. - """ - return self.MercatorLatitudeTransform(self.thresh) - - def set_default_locators_and_formatters(self, axis): - """ - Override to set up the locators and formatters to use with the - scale. This is only required if the scale requires custom - locators and formatters. Writing custom locators and - formatters is rather outside the scope of this example, but - there are many helpful examples in ``ticker.py``. - - In our case, the Mercator example uses a fixed locator from - -90 to 90 degrees and a custom formatter class to put convert - the radians to degrees and put a degree symbol after the - value:: - """ - class DegreeFormatter(Formatter): - def __call__(self, x, pos=None): - # \u00b0 : degree symbol - return u"%d\u00b0" % ((x / np.pi) * 180.0) - - deg2rad = np.pi / 180.0 - axis.set_major_locator(FixedLocator( - np.arange(-90, 90, 10) * deg2rad)) - axis.set_major_formatter(DegreeFormatter()) - axis.set_minor_formatter(DegreeFormatter()) - - def limit_range_for_scale(self, vmin, vmax, minpos): - """ - Override to limit the bounds of the axis to the domain of the - transform. In the case of Mercator, the bounds should be - limited to the threshold that was passed in. Unlike the - autoscaling provided by the tick locators, this range limiting - will always be adhered to, whether the axis range is set - manually, determined automatically or changed through panning - and zooming. - """ - return max(vmin, -self.thresh), min(vmax, self.thresh) - - class MercatorLatitudeTransform(mtransforms.Transform): - # There are two value members that must be defined. - # ``input_dims`` and ``output_dims`` specify number of input - # dimensions and output dimensions to the transformation. - # These are used by the transformation framework to do some - # error checking and prevent incompatible transformations from - # being connected together. When defining transforms for a - # scale, which are, by definition, separable and have only one - # dimension, these members should always be set to 1. - input_dims = 1 - output_dims = 1 - is_separable = True - - def __init__(self, thresh): - mtransforms.Transform.__init__(self) - self.thresh = thresh - - def transform(self, a): - """ - This transform takes an Nx1 ``numpy`` array and returns a - transformed copy. Since the range of the Mercator scale - is limited by the user-specified threshold, the input - array must be masked to contain only valid values. - ``matplotlib`` will handle masked arrays and remove the - out-of-range data from the plot. Importantly, the - ``transform`` method *must* return an array that is the - same shape as the input array, since these values need to - remain synchronized with values in the other dimension. - """ - masked = ma.masked_where((a < -self.thresh) | (a > self.thresh), a) - if masked.mask.any(): - return ma.log(np.abs(ma.tan(masked) + 1.0 / ma.cos(masked))) - else: - return np.log(np.abs(np.tan(a) + 1.0 / np.cos(a))) - - def inverted(self): - """ - Override this method so matplotlib knows how to get the - inverse transform for this transform. - """ - return MercatorLatitudeScale.InvertedMercatorLatitudeTransform(self.thresh) - - class InvertedMercatorLatitudeTransform(mtransforms.Transform): - input_dims = 1 - output_dims = 1 - is_separable = True - - def __init__(self, thresh): - mtransforms.Transform.__init__(self) - self.thresh = thresh - - def transform(self, a): - return np.arctan(np.sinh(a)) - - def inverted(self): - return MercatorLatitudeScale.MercatorLatitudeTransform(self.thresh) - -# Now that the Scale class has been defined, it must be registered so -# that ``matplotlib`` can find it. -mscale.register_scale(MercatorLatitudeScale) - -from pylab import * -import numpy as np - -t = arange(-180.0, 180.0, 0.1) -s = t / 360.0 * np.pi - -plot(t, s, '-', lw=2) -gca().set_yscale('mercator') - -xlabel('Longitude') -ylabel('Latitude') -title('Mercator: Projection of the Oppressor') -grid(True) - -show() This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |