@@ -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
1316From 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
1723This 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
2130Creating 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