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

Skip to content

Commit 325b6d6

Browse files
committed
Preliminary version of "adding new scales and projections" document.
(In reST, since that appears to be where mpl documentation is heading.) svn path=/branches/transforms/; revision=4767
1 parent 04b4381 commit 325b6d6

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

doc/devel/add_new_projection.rst

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
===============================================
2+
Adding new scales and projections to matplotlib
3+
===============================================
4+
5+
.. ::author Michael Droettboom
6+
7+
Matplotlib supports the addition of new transformations that transform
8+
the data before it is displayed. Separable transformations, that work
9+
on a single dimension are called "scales", and non-separable
10+
transformations, that take data in two or more dimensions as input are
11+
called "projections".
12+
13+
This document is intended for developers and advanced users who need
14+
to add more scales and projections to matplotlib.
15+
16+
From the user's perspective, the scale of a plot can be set with
17+
``set_xscale`` and ``set_yscale``. Choosing the projection
18+
currently has no *standardized* method. [MGDTODO]
19+
20+
Creating a new scale
21+
====================
22+
23+
Adding a new scale consists of defining a subclass of ``ScaleBase``,
24+
that brings together the following elements:
25+
26+
- A transformation from data space into plot space.
27+
28+
- An inverse of that transformation. For example, this is used to
29+
convert mouse positions back into data space.
30+
31+
- A function to limit the range of the axis to acceptable values. A
32+
log scale, for instance, would prevent the range from including
33+
values less than or equal to zero.
34+
35+
- Locators (major and minor) that determine where to place ticks in
36+
the plot, and optionally, how to adjust the limits of the plot to
37+
some "good" values.
38+
39+
- Formatters (major and minor) that specify how the tick labels
40+
should be drawn.
41+
42+
There are a number of ``Scale`` classes in ``scale.py`` that may be
43+
used as starting points for new scales. As an example, this document
44+
presents adding a new scale ``MercatorLatitudeScale`` which can be
45+
used to plot latitudes in a Mercator_ projection. For simplicity,
46+
this scale assumes that it has a fixed center at the equator. The
47+
code presented here is a simplification of actual code in
48+
``matplotlib``, with complications added only for the sake of
49+
optimization removed.
50+
51+
First define a new subclass of ``ScaleBase``::
52+
53+
class MercatorLatitudeScale(ScaleBase):
54+
"""
55+
Scales data in range -pi/2 to pi/2 (-90 to 90 degrees) using
56+
the system used to scale latitudes in a Mercator projection.
57+
58+
The scale function:
59+
ln(tan(y) + sec(y))
60+
61+
The inverse scale function:
62+
atan(sinh(y))
63+
64+
Since the Mercator scale tends to infinity at +/- 90 degrees,
65+
there is user-defined threshold, above and below which nothing
66+
will be plotted. This defaults to +/- 85 degrees.
67+
68+
source:
69+
http://en.wikipedia.org/wiki/Mercator_projection
70+
"""
71+
name = 'mercator_latitude'
72+
73+
This class must have a member ``name`` that defines the string used to
74+
select the scale. For example,
75+
``gca().set_yscale("mercator_latitude")`` would be used to select the
76+
Mercator latitude scale.
77+
78+
Next define two nested classes: one for the data transformation and
79+
one for its inverse. Both of these classes must be subclasses of
80+
``Transform`` (defined in ``transforms.py``).::
81+
82+
class MercatorLatitudeTransform(Transform):
83+
input_dims = 1
84+
output_dims = 1
85+
86+
There are two class-members that must be defined. ``input_dims`` and
87+
``output_dims`` specify number of input dimensions and output
88+
dimensions to the transformation. These are used by the
89+
transformation framework to do some error checking and prevent
90+
incompatible transformations from being connected together. When
91+
defining transforms for a scale, which are by definition separable and
92+
only have one dimension, these members should always be 1.
93+
94+
``MercatorLatitudeTransform`` has a simple constructor that takes and
95+
stores the *threshold* for the Mercator projection (to limit its range
96+
to prevent plotting to infinity).::
97+
98+
def __init__(self, thresh):
99+
Transform.__init__(self)
100+
self.thresh = thresh
101+
102+
The ``transform`` method is where the real work happens: It takes an N
103+
x 1 ``numpy`` array and returns a transformed copy. Since the range
104+
of the Mercator scale is limited by the user-specified threshold, the
105+
input array must be masked to contain only valid values.
106+
``matplotlib`` will handle masked arrays and remove the out-of-range
107+
data from the plot. Importantly, the transformation should return an
108+
array that is the same shape as the input array, since these values
109+
need to remain synchronized with values in the other dimension.::
110+
111+
def transform(self, a):
112+
masked = ma.masked_where((a < -self.thresh) | (a > self.thresh), a)
113+
return ma.log(ma.abs(ma.tan(masked) + 1.0 / ma.cos(masked)))
114+
115+
Lastly for the transformation class, define a method to get the
116+
inverse transformation::
117+
118+
def inverted(self):
119+
return MercatorLatitudeScale.InvertedMercatorLatitudeTransform(self.thresh)
120+
121+
The inverse transformation class follows the same pattern, but
122+
obviously the mathematical operation performed is different::
123+
124+
class InvertedMercatorLatitudeTransform(Transform):
125+
input_dims = 1
126+
output_dims = 1
127+
128+
def __init__(self, thresh):
129+
Transform.__init__(self)
130+
self.thresh = thresh
131+
132+
def transform(self, a):
133+
return npy.arctan(npy.sinh(a))
134+
135+
def inverted(self):
136+
return MercatorLatitudeScale.MercatorLatitudeTransform(self.thresh)
137+
138+
Now we're back to methods for the ``MercatorLatitudeScale`` class.
139+
Any keyword arguments passed to ``set_xscale`` and ``set_yscale`` will
140+
be passed along to the scale's constructor. In the case of
141+
``MercatorLatitudeScale``, the ``thresh`` keyword argument specifies
142+
the degree at which to crop the plot data. The constructor also
143+
creates a local instance of the ``Transform`` class defined above,
144+
which is made available through its ``get_transform`` method::
145+
146+
def __init__(self, axis, **kwargs):
147+
thresh = kwargs.pop("thresh", (85 / 180.0) * npy.pi)
148+
if thresh >= npy.pi / 2.0:
149+
raise ValueError("thresh must be less than pi/2")
150+
self.thresh = thresh
151+
self._transform = self.MercatorLatitudeTransform(thresh)
152+
153+
def get_transform(self):
154+
return self._transform
155+
156+
The ``limit_range_for_scale`` method must be provided to limit the
157+
bounds of the axis to the domain of the function. In the case of
158+
Mercator, the bounds should be limited to the threshold that was
159+
passed in. Unlike the autoscaling provided by the tick locators, this
160+
range limiting will always be adhered to, whether the axis range is set
161+
manually, determined automatically or changed through panning and
162+
zooming::
163+
164+
def limit_range_for_scale(self, vmin, vmax, minpos):
165+
return max(vmin, -self.thresh), min(vmax, self.thresh)
166+
167+
Lastly, the ``set_default_locators_and_formatters`` method sets up the
168+
locators and formatters to use with the scale. It may be that the new
169+
scale requires new locators and formatters. Doing so is outside the
170+
scope of this document, but there are many examples in ``ticker.py``.
171+
The Mercator example uses a fixed locator from -90 to 90 degrees and a
172+
custom formatter class to put convert the radians to degrees and put a
173+
degree symbol after the value::
174+
175+
def set_default_locators_and_formatters(self, axis):
176+
class DegreeFormatter(Formatter):
177+
def __call__(self, x, pos=None):
178+
# \u00b0 : degree symbol
179+
return u"%d\u00b0" % ((x / npy.pi) * 180.0)
180+
181+
deg2rad = npy.pi / 180.0
182+
axis.set_major_locator(FixedLocator(
183+
npy.arange(-90, 90, 10) * deg2rad))
184+
axis.set_major_formatter(DegreeFormatter())
185+
axis.set_minor_formatter(DegreeFormatter())
186+
187+
Now that the Scale class has been defined, it must be registered so
188+
that ``matplotlib`` can find it::
189+
190+
register_scale(MercatorLatitudeScale)
191+
192+
.. _Mercator: http://en.wikipedia.org/wiki/Mercator_projection

0 commit comments

Comments
 (0)