Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5161141

Browse files
committed
Provide heavily-documented examples for adding new scales and
projections. Fix various bugs related to non-rectangular clipping. Remove MercatorLatitude scale from core and put it in an example. svn path=/branches/transforms/; revision=4803
1 parent b2b5397 commit 5161141

9 files changed

Lines changed: 728 additions & 264 deletions

File tree

doc/devel/add_new_projection.rst

Lines changed: 77 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -4,190 +4,99 @@ Adding new scales and projections to matplotlib
44

55
.. ::author Michael Droettboom
66
7-
Matplotlib supports the addition of new transformations that transform
8-
the data before it is displayed. In ``matplotlib`` nomenclature,
9-
separable transformations, working on a single dimension, are called
10-
"scales", and non-separable transformations, that take handle data in
11-
two or more dimensions at a time, are called "projections".
7+
Matplotlib supports the addition of custom procedures that transform
8+
the data before it is displayed.
9+
10+
There is an important distinction between two kinds of
11+
transformations. Separable transformations, working on a single
12+
dimension, are called "scales", and non-separable transformations,
13+
that handle data in two or more dimensions at a time, are called
14+
"projections".
1215

1316
From the user's perspective, the scale of a plot can be set with
14-
``set_xscale`` and ``set_yscale``. Choosing the projection
15-
currently has no *standardized* method. [MGDTODO]
17+
``set_xscale()`` and ``set_yscale()``. Projections can be chosen using
18+
the ``projection`` keyword argument to the ``plot()`` or ``subplot()``
19+
functions::
20+
21+
plot(x, y, projection="custom")
1622

1723
This document is intended for developers and advanced users who need
18-
to add more scales and projections to matplotlib.
24+
to create new scales and projections for matplotlib. The necessary
25+
code for scales and projections can be included anywhere: directly
26+
within a plot script, in third-party code, or in the matplotlib source
27+
tree itself.
1928

2029

2130
Creating a new scale
2231
====================
2332

24-
Adding a new scale consists of defining a subclass of ``ScaleBase``,
25-
that brings together the following elements:
33+
Adding a new scale consists of defining a subclass of ``ScaleBase``
34+
(in the ``matplotlib.scale`` module), that includes the following
35+
elements:
2636

27-
- A transformation from data space into plot space.
37+
- A transformation from data coordinates into display coordinates.
2838

29-
- An inverse of that transformation. For example, this is used to
30-
convert mouse positions back into data space.
39+
- An inverse of that transformation. This is used, for example, to
40+
convert mouse positions from screen space back into data space.
3141

32-
- A function to limit the range of the axis to acceptable values. A
33-
log scale, for instance, would prevent the range from including
34-
values less than or equal to zero.
42+
- A function to limit the range of the axis to acceptable values
43+
(``limit_range_for_scale()``). A log scale, for instance, would
44+
prevent the range from including values less than or equal to
45+
zero.
3546

3647
- Locators (major and minor) that determine where to place ticks in
3748
the plot, and optionally, how to adjust the limits of the plot to
38-
some "good" values.
49+
some "good" values. Unlike ``limit_range_for_scale()``, which is
50+
always enforced, the range setting here is only used when
51+
automatically setting the range of the plot.
3952

4053
- Formatters (major and minor) that specify how the tick labels
4154
should be drawn.
4255

43-
There are a number of ``Scale`` classes in ``scale.py`` that may be
44-
used as starting points for new scales. As an example, this document
45-
presents adding a new scale ``MercatorLatitudeScale`` which can be
46-
used to plot latitudes in a Mercator_ projection. For simplicity,
47-
this scale assumes that it has a fixed center at the equator. The
48-
code presented here is a simplification of actual code in
49-
``matplotlib``, with complications added only for the sake of
50-
optimization removed.
51-
52-
First define a new subclass of ``ScaleBase``::
53-
54-
class MercatorLatitudeScale(ScaleBase):
55-
"""
56-
Scales data in range -pi/2 to pi/2 (-90 to 90 degrees) using
57-
the system used to scale latitudes in a Mercator projection.
58-
59-
The scale function:
60-
ln(tan(y) + sec(y))
61-
62-
The inverse scale function:
63-
atan(sinh(y))
64-
65-
Since the Mercator scale tends to infinity at +/- 90 degrees,
66-
there is user-defined threshold, above and below which nothing
67-
will be plotted. This defaults to +/- 85 degrees.
68-
69-
source:
70-
http://en.wikipedia.org/wiki/Mercator_projection
71-
"""
72-
name = 'mercator_latitude'
73-
74-
This class must have a member ``name`` that defines the string used to
75-
select the scale. For example,
76-
``gca().set_yscale("mercator_latitude")`` would be used to select the
77-
Mercator latitude scale.
78-
79-
Next define two nested classes: one for the data transformation and
80-
one for its inverse. Both of these classes must be subclasses of
81-
``Transform`` (defined in ``transforms.py``).::
82-
83-
class MercatorLatitudeTransform(Transform):
84-
input_dims = 1
85-
output_dims = 1
86-
87-
There are two class-members that must be defined. ``input_dims`` and
88-
``output_dims`` specify number of input dimensions and output
89-
dimensions to the transformation. These are used by the
90-
transformation framework to do some error checking and prevent
91-
incompatible transformations from being connected together. When
92-
defining transforms for a scale, which are by definition separable and
93-
only have one dimension, these members should always be 1.
94-
95-
``MercatorLatitudeTransform`` has a simple constructor that takes and
96-
stores the *threshold* for the Mercator projection (to limit its range
97-
to prevent plotting to infinity).::
98-
99-
def __init__(self, thresh):
100-
Transform.__init__(self)
101-
self.thresh = thresh
102-
103-
The ``transform`` method is where the real work happens: It takes an N
104-
x 1 ``numpy`` array and returns a transformed copy. Since the range
105-
of the Mercator scale is limited by the user-specified threshold, the
106-
input array must be masked to contain only valid values.
107-
``matplotlib`` will handle masked arrays and remove the out-of-range
108-
data from the plot. Importantly, the transformation should return an
109-
array that is the same shape as the input array, since these values
110-
need to remain synchronized with values in the other dimension.::
111-
112-
def transform(self, a):
113-
masked = ma.masked_where((a < -self.thresh) | (a > self.thresh), a)
114-
return ma.log(ma.abs(ma.tan(masked) + 1.0 / ma.cos(masked)))
115-
116-
Lastly for the transformation class, define a method to get the
117-
inverse transformation::
118-
119-
def inverted(self):
120-
return MercatorLatitudeScale.InvertedMercatorLatitudeTransform(self.thresh)
121-
122-
The inverse transformation class follows the same pattern, but
123-
obviously the mathematical operation performed is different::
124-
125-
class InvertedMercatorLatitudeTransform(Transform):
126-
input_dims = 1
127-
output_dims = 1
128-
129-
def __init__(self, thresh):
130-
Transform.__init__(self)
131-
self.thresh = thresh
132-
133-
def transform(self, a):
134-
return npy.arctan(npy.sinh(a))
135-
136-
def inverted(self):
137-
return MercatorLatitudeScale.MercatorLatitudeTransform(self.thresh)
138-
139-
Now we're back to methods for the ``MercatorLatitudeScale`` class.
140-
Any keyword arguments passed to ``set_xscale`` and ``set_yscale`` will
141-
be passed along to the scale's constructor. In the case of
142-
``MercatorLatitudeScale``, the ``thresh`` keyword argument specifies
143-
the degree at which to crop the plot data. The constructor also
144-
creates a local instance of the ``Transform`` class defined above,
145-
which is made available through its ``get_transform`` method::
146-
147-
def __init__(self, axis, **kwargs):
148-
thresh = kwargs.pop("thresh", (85 / 180.0) * npy.pi)
149-
if thresh >= npy.pi / 2.0:
150-
raise ValueError("thresh must be less than pi/2")
151-
self.thresh = thresh
152-
self._transform = self.MercatorLatitudeTransform(thresh)
153-
154-
def get_transform(self):
155-
return self._transform
156-
157-
The ``limit_range_for_scale`` method must be provided to limit the
158-
bounds of the axis to the domain of the function. In the case of
159-
Mercator, the bounds should be limited to the threshold that was
160-
passed in. Unlike the autoscaling provided by the tick locators, this
161-
range limiting will always be adhered to, whether the axis range is set
162-
manually, determined automatically or changed through panning and
163-
zooming::
164-
165-
def limit_range_for_scale(self, vmin, vmax, minpos):
166-
return max(vmin, -self.thresh), min(vmax, self.thresh)
167-
168-
Lastly, the ``set_default_locators_and_formatters`` method sets up the
169-
locators and formatters to use with the scale. It may be that the new
170-
scale requires new locators and formatters. Doing so is outside the
171-
scope of this document, but there are many examples in ``ticker.py``.
172-
The Mercator example uses a fixed locator from -90 to 90 degrees and a
173-
custom formatter class to put convert the radians to degrees and put a
174-
degree symbol after the value::
175-
176-
def set_default_locators_and_formatters(self, axis):
177-
class DegreeFormatter(Formatter):
178-
def __call__(self, x, pos=None):
179-
# \u00b0 : degree symbol
180-
return u"%d\u00b0" % ((x / npy.pi) * 180.0)
181-
182-
deg2rad = npy.pi / 180.0
183-
axis.set_major_locator(FixedLocator(
184-
npy.arange(-90, 90, 10) * deg2rad))
185-
axis.set_major_formatter(DegreeFormatter())
186-
axis.set_minor_formatter(DegreeFormatter())
187-
188-
Now that the Scale class has been defined, it must be registered so
189-
that ``matplotlib`` can find it::
190-
191-
register_scale(MercatorLatitudeScale)
192-
193-
.. _Mercator: http://en.wikipedia.org/wiki/Mercator_projection
56+
Once the class is defined, it must be registered with ``matplotlib``
57+
so that the user can select it.
58+
59+
A full-fledged and heavily annotated example is in
60+
``examples/custom_scale_example.py``. There are also some ``Scale``
61+
classes in ``scale.py`` that may be used as starting points.
62+
63+
64+
Creating a new projection
65+
=========================
66+
67+
Adding a new projection consists of defining a subclass of ``Axes``
68+
(in the ``matplotlib.axes`` module), that includes the following
69+
elements:
70+
71+
- A transformation from data coordinates into display coordinates.
72+
73+
- An inverse of that transformation. This is used, for example, to
74+
convert mouse positions from screen space back into data space.
75+
76+
- Transformations for the gridlines, ticks and ticklabels. Custom
77+
projections will often need to place these elements in special
78+
locations, and ``matplotlib`` has a facility to help with doing so.
79+
80+
- Setting up default values (overriding ``cla()``), since the
81+
defaults for a rectilinear axes may not be appropriate.
82+
83+
- Defining the shape of the axes, for example, an elliptical axes,
84+
that will be used to draw the background of the plot and for
85+
clipping any data elements.
86+
87+
- Defining custom locators and formatters for the projection. For
88+
example, in a geographic projection, it may be more convenient to
89+
display the grid in degrees, even if the data is in radians.
90+
91+
- Set up interactive panning and zooming. This is left as an
92+
"advanced" feature left to the reader, but there is an example of
93+
this for polar plots in ``polar.py``.
94+
95+
- Any additional methods for additional convenience or features.
96+
97+
Once the class is defined, it must be registered with ``matplotlib``
98+
so that the user can select it.
99+
100+
A full-fledged and heavily annotated example is in
101+
``examples/custom_projection_example.py``. The polar plot
102+
functionality in ``polar.py`` may also be interest.

0 commit comments

Comments
 (0)