Completed
Pull Request — master (#416)
by Ryan
01:27
created

  A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 226
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 20
dl 0
loc 226
rs 10
c 0
b 0
f 0
1
# Copyright (c) 2008-2015 MetPy Developers.
2
# Distributed under the terms of the BSD 3-Clause License.
3
# SPDX-License-Identifier: BSD-3-Clause
4
"""Create Station-model plots."""
5
6
try:
7
    from enum import Enum
8
except ImportError:
9
    from enum34 import Enum
10
11
import numpy as np
12
13
from .wx_symbols import (current_weather, high_clouds, low_clouds, mid_clouds,
14
                         pressure_tendency, sky_cover, wx_symbol_font)
15
from ..cbook import is_string_like
16
from ..package_tools import Exporter
17
18
exporter = Exporter(globals())
19
20
21
@exporter.export
22
class StationPlot(object):
23
    """Make a standard meteorological station plot.
24
25
    Plots values, symbols, or text spaced around a central location. Can also plot wind
26
    barbs as the center of the location.
27
    """
28
29
    location_names = dict(C=(0, 0), N=(0, 1), NE=(1, 1), E=(1, 0), SE=(1, -1), S=(0, -1),
30
                          SW=(-1, -1), W=(-1, 0), NW=(-1, 1))
31
32
    def __init__(self, ax, x, y, fontsize=10, spacing=None, transform=None):
33
        """Initialize the StationPlot with items that do not change.
34
35
        This sets up the axes and station locations. The `fontsize` and `spacing`
36
        are also specified here to ensure that they are consistent between individual
37
        station elements.
38
39
        Parameters
40
        ----------
41
        ax : matplotlib.axes.Axes
42
            The :class:`~matplotlib.axes.Axes` for plotting
43
        x : array_like
44
            The x location of the stations in the plot
45
        y : array_like
46
            The y location of the stations in the plot
47
        fontsize : int
48
            The fontsize to use for drawing text
49
        spacing : int
50
            The spacing, in points, that corresponds to a single increment between
51
            station plot elements.
52
        transform : matplotlib.transforms.Transform (or compatible)
53
            The default transform to apply to the x and y positions when plotting.
54
        """
55
        self.ax = ax
56
        self.x = x
57
        self.y = y
58
        self.fontsize = fontsize
59
        self.spacing = fontsize if spacing is None else spacing
60
        self.transform = transform
61
        self.items = dict()
62
        self.barbs = None
63
64
    def plot_symbol(self, location, codes, symbol_mapper, **kwargs):
65
        """At the specified location in the station model plot a set of symbols.
66
67
        This specifies that at the offset `location`, the data in `codes` should be
68
        converted to unicode characters (for our :data:`wx_symbol_font`) using `symbol_mapper`,
69
        and plotted.
70
71
        Additional keyword arguments given will be passed onto the actual plotting
72
        code; this is useful for specifying things like color or font properties.
73
74
        If something has already been plotted at this location, it will be replaced.
75
76
        Parameters
77
        ----------
78
        location : str or tuple[float, float]
79
            The offset (relative to center) to plot this parameter. If str, should be one of
80
            'C', 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', or 'NW'. Otherwise, should be a tuple
81
            specifying the number of increments in the x and y directions; increments
82
            are multiplied by `spacing` to give offsets in x and y relative to the center.
83
        codes : array_like
84
            The numeric values that should be converted to unicode characters for plotting.
85
        symbol_mapper : callable
86
            Controls converting data values to unicode code points for the
87
            :data:`wx_symbol_font` font. This should take a value and return a single unicode
88
            character. See :mod:`metpy.plots.wx_symbols` for included mappers.
89
        kwargs
90
            Additional keyword arguments to use for matplotlib's plotting functions.
91
92
93
        .. plot::
94
95
           import matplotlib.pyplot as plt
96
           import numpy as np
97
           from math import ceil
98
99
           from metpy.plots import StationPlot
100
           from metpy.plots.wx_symbols import current_weather, current_weather_auto
101
           from metpy.plots.wx_symbols import low_clouds, mid_clouds, high_clouds
102
           from metpy.plots.wx_symbols import sky_cover, pressure_tendency
103
104
105
           def plot_symbols(mapper, name, nwrap=12, figsize=(10, 1.4)):
106
107
               # Determine how many symbols there are and layout in rows of nwrap
108
               # if there are more than nwrap symbols
109
               num_symbols = len(mapper)
110
               codes = np.arange(len(mapper))
111
               ncols = nwrap
112
               if num_symbols <= nwrap:
113
                   nrows = 1
114
                   x = np.linspace(0, 1, len(mapper))
115
                   y = np.ones_like(x)
116
                   ax_height = 0.8
117
               else:
118
                   nrows = int(ceil(num_symbols / ncols))
119
                   x = np.tile(np.linspace(0, 1, ncols), nrows)[:num_symbols]
120
                   y = np.repeat(np.arange(nrows, 0, -1), ncols)[:num_symbols]
121
                   figsize = (10, 1 * nrows + 0.4)
122
                   ax_height = 0.8 + 0.018 * nrows
123
124
               fig = plt.figure(figsize=figsize,  dpi=300)
125
               ax = fig.add_axes([0, 0, 1, ax_height])
126
               ax.set_title(name, size=20)
127
               ax.xaxis.set_ticks([])
128
               ax.yaxis.set_ticks([])
129
               ax.set_frame_on(False)
130
131
               # Plot
132
               sp = StationPlot(ax, x, y, fontsize=36)
133
               sp.plot_symbol('C', codes, mapper)
134
               sp.plot_parameter((0, -1), codes, fontsize=18)
135
136
               ax.set_ylim(-0.05, nrows + 0.5)
137
138
               plt.show()
139
140
141
           plot_symbols(current_weather, "Current Weather Symbols")
142
           plot_symbols(current_weather_auto, "Current Weather Auto Reported Symbols")
143
           plot_symbols(low_clouds, "Low Cloud Symbols")
144
           plot_symbols(mid_clouds, "Mid Cloud Symbols")
145
           plot_symbols(high_clouds, "High Cloud Symbols")
146
           plot_symbols(sky_cover, "Sky Cover Symbols")
147
           plot_symbols(pressure_tendency, "Pressure Tendency Symbols")
148
149
        See Also
150
        --------
151
        plot_barb, plot_parameter, plot_text
152
        """
153
        # Make sure we use our font for symbols
154
        kwargs['fontproperties'] = wx_symbol_font.copy()
155
        return self.plot_parameter(location, codes, symbol_mapper, **kwargs)
156
157
    def plot_parameter(self, location, parameter, formatter='.0f', **kwargs):
158
        """At the specified location in the station model plot a set of values.
159
160
        This specifies that at the offset `location`, the data in `parameter` should be
161
        plotted. The conversion of the data values to a string is controlled by `formatter`.
162
163
        Additional keyword arguments given will be passed onto the actual plotting
164
        code; this is useful for specifying things like color or font properties.
165
166
        If something has already been plotted at this location, it will be replaced.
167
168
        Parameters
169
        ----------
170
        location : str or tuple[float, float]
171
            The offset (relative to center) to plot this parameter. If str, should be one of
172
            'C', 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', or 'NW'. Otherwise, should be a tuple
173
            specifying the number of increments in the x and y directions; increments
174
            are multiplied by `spacing` to give offsets in x and y relative to the center.
175
        parameter : array_like
176
            The numeric values that should be plotted
177
        formatter : str or callable, optional
178
            How to format the data as a string for plotting. If a string, it should be
179
            compatible with the :func:`format` builtin. If a callable, this should take a
180
            value and return a string. Defaults to '0.f'.
181
        kwargs
182
            Additional keyword arguments to use for matplotlib's plotting functions.
183
184
185
        See Also
186
        --------
187
        plot_barb, plot_symbol, plot_text
188
        """
189
        text = self._to_string_list(parameter, formatter)
190
        return self.plot_text(location, text, **kwargs)
191
192
    def plot_text(self, location, text, **kwargs):
193
        """At the specified location in the station model plot a collection of text.
194
195
        This specifies that at the offset `location`, the strings in `text` should be
196
        plotted.
197
198
        Additional keyword arguments given will be passed onto the actual plotting
199
        code; this is useful for specifying things like color or font properties.
200
201
        If something has already been plotted at this location, it will be replaced.
202
203
        Parameters
204
        ----------
205
        location : str or tuple[float, float]
206
            The offset (relative to center) to plot this parameter. If str, should be one of
207
            'C', 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', or 'NW'. Otherwise, should be a tuple
208
            specifying the number of increments in the x and y directions; increments
209
            are multiplied by `spacing` to give offsets in x and y relative to the center.
210
        text : list (or array) of strings
211
            The strings that should be plotted
212
        kwargs
213
            Additional keyword arguments to use for matplotlib's plotting functions.
214
215
        See Also
216
        --------
217
        plot_barb, plot_parameter, plot_symbol
218
        """
219
        location = self._handle_location(location)
220
221
        # Pass transform if necessary
222
        if 'transform' not in kwargs and self.transform:
223
            kwargs['transform'] = self.transform
224
225
        text_collection = self.ax.scattertext(self.x, self.y, text, loc=location,
226
                                              size=kwargs.pop('fontsize', self.fontsize),
227
                                              **kwargs)
228
        if location in self.items:
229
            self.items[location].remove()
230
        self.items[location] = text_collection
231
        return text_collection
232
233
    def plot_barb(self, u, v, **kwargs):
234
        r"""At the center of the station model plot wind barbs.
235
236
        Additional keyword arguments given will be passed onto matplotlib's
237
        :meth:`~matplotlib.axes.Axes.barbs` function; this is useful for specifying things
238
        like color or line width.
239
240
        Parameters
241
        ----------
242
        u : array-like
243
            The data to use for the u-component of the barbs.
244
        v : array-like
245
            The data to use for the v-component of the barbs.
246
        kwargs
247
            Additional keyword arguments to pass to matplotlib's
248
            :meth:`~matplotlib.axes.Axes.barbs` function.
249
250
        See Also
251
        --------
252
        plot_parameter, plot_symbol, plot_text
253
        """
254
        # Handle transforming our center points. CartoPy doesn't like 1D barbs
255
        # TODO: This can be removed for cartopy > 0.14.3
256
        if hasattr(self.ax, 'projection') and (self.transform or 'transform' in kwargs):
257
            trans = kwargs.pop('transform', None) or self.transform
258
            x, y, _ = self. ax.projection.transform_points(trans, self.x, self.y).T
259
        else:
260
            x, y = self.x, self.y
261
262
        # Empirically determined
263
        pivot = 0.51 * np.sqrt(self.fontsize)
264
        length = 1.95 * np.sqrt(self.fontsize)
265
        defaults = dict(sizes=dict(spacing=.15, height=0.5, emptybarb=0.35),
266
                        length=length, pivot=pivot)
267
        defaults.update(kwargs)
268
269
        # Remove old barbs
270
        if self.barbs:
271
            self.barbs.remove()
272
273
        self.barbs = self.ax.barbs(x, y, u, v, **defaults)
274
275
    @staticmethod
276
    def _to_string_list(vals, fmt):
277
        """Convert a sequence of values to a list of strings."""
278
        if not callable(fmt):
279
            def formatter(s):
280
                """Turn a format string into a callable."""
281
                if hasattr(s, 'units'):
282
                    s = np.asscalar(s)
283
                return format(s, fmt)
284
        else:
285
            formatter = fmt
286
287
        return [formatter(v) if np.isfinite(v) else '' for v in vals]
288
289
    def _handle_location(self, location):
290
        """Process locations to get a consistent set of tuples for location."""
291
        if is_string_like(location):
292
            location = self.location_names[location]
293
        xoff, yoff = location
294
        return xoff * self.spacing, yoff * self.spacing
295
296
297
@exporter.export
298
class StationPlotLayout(dict):
299
    r"""make a layout to encapsulate plotting using :class:`StationPlot`.
300
301
    This class keeps a collection of offsets, plot formats, etc. for a parameter based
302
    on its name. This then allows a dictionary of data (or any object that allows looking
303
    up of arrays based on a name) to be passed to :meth:`plot()` to plot the data all at once.
304
305
    See Also
306
    --------
307
    StationPlot
308
    """
309
310
    class PlotTypes(Enum):
311
        r"""Different plotting types for the layout.
312
313
        Controls how items are displayed (e.g. converting values to symbols).
314
        """
315
316
        value = 1
317
        symbol = 2
318
        text = 3
319
        barb = 4
320
321
    def add_value(self, location, name, fmt='.0f', units=None, **kwargs):
322
        r"""Add a numeric value to the station layout.
323
324
        This specifies that at the offset `location`, data should be pulled from the data
325
        container using the key `name` and plotted. The conversion of the data values to
326
        a string is controlled by `fmt`. The units required for plotting can also
327
        be passed in using `units`, which will cause the data to be converted before
328
        plotting.
329
330
        Additional keyword arguments given will be passed onto the actual plotting
331
        code; this is useful for specifying things like color or font properties.
332
333
        Parameters
334
        ----------
335
        location : str or tuple[float, float]
336
            The offset (relative to center) to plot this value. If str, should be one of
337
            'C', 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', or 'NW'. Otherwise, should be a tuple
338
            specifying the number of increments in the x and y directions.
339
        name : str
340
            The name of the parameter, which is used as a key to pull data out of the
341
            data container passed to :meth:`plot`.
342
        fmt : str or callable, optional
343
            How to format the data as a string for plotting. If a string, it should be
344
            compatible with the :func:`format` builtin. If a callable, this should take a
345
            value and return a string. Defaults to '0.f'.
346
        units : pint-compatible unit, optional
347
            The units to use for plotting. Data will be converted to this unit before
348
            conversion to a string. If not specified, no conversion is done.
349
        kwargs
350
            Additional keyword arguments to use for matplotlib's plotting functions.
351
352
        See Also
353
        --------
354
        add_barb, add_symbol, add_text
355
        """
356
        self[location] = (self.PlotTypes.value, name, (fmt, units, kwargs))
357
358
    def add_symbol(self, location, name, symbol_mapper, **kwargs):
359
        r"""Add a symbol to the station layout.
360
361
        This specifies that at the offset `location`, data should be pulled from the data
362
        container using the key `name` and plotted. Data values will converted to glyphs
363
        appropriate for MetPy's symbol font using the callable `symbol_mapper`.
364
365
        Additional keyword arguments given will be passed onto the actual plotting
366
        code; this is useful for specifying things like color or font properties.
367
368
        Parameters
369
        ----------
370
        location : str or tuple[float, float]
371
            The offset (relative to center) to plot this value. If str, should be one of
372
            'C', 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', or 'NW'. Otherwise, should be a tuple
373
            specifying the number of increments in the x and y directions.
374
        name : str
375
            The name of the parameter, which is used as a key to pull data out of the
376
            data container passed to :meth:`plot`.
377
        symbol_mapper : callable
378
            Controls converting data values to unicode code points for the
379
            :data:`wx_symbol_font` font. This should take a value and return a single unicode
380
            character. See :mod:`metpy.plots.wx_symbols` for included mappers.
381
        kwargs
382
            Additional keyword arguments to use for matplotlib's plotting functions.
383
384
        See Also
385
        --------
386
        add_barb, add_text, add_value
387
        """
388
        self[location] = (self.PlotTypes.symbol, name, (symbol_mapper, kwargs))
389
390
    def add_text(self, location, name, **kwargs):
391
        r"""Add a text field to the  station layout.
392
393
        This specifies that at the offset `location`, data should be pulled from the data
394
        container using the key `name` and plotted directly as text with no conversion
395
        applied.
396
397
        Additional keyword arguments given will be passed onto the actual plotting
398
        code; this is useful for specifying things like color or font properties.
399
400
        Parameters
401
        ----------
402
        location : str or tuple(float, float)
403
            The offset (relative to center) to plot this value. If str, should be one of
404
            'C', 'N', 'NE', 'E', 'SE', 'S', 'SW', 'W', or 'NW'. Otherwise, should be a tuple
405
            specifying the number of increments in the x and y directions.
406
        name : str
407
            The name of the parameter, which is used as a key to pull data out of the
408
            data container passed to :meth:`plot`.
409
        kwargs
410
            Additional keyword arguments to use for matplotlib's plotting functions.
411
412
        See Also
413
        --------
414
        add_barb, add_symbol, add_value
415
        """
416
        self[location] = (self.PlotTypes.text, name, kwargs)
417
418
    def add_barb(self, u_name, v_name, units=None, **kwargs):
419
        r"""Add a wind barb to the center of the station layout.
420
421
        This specifies that u- and v-component data should be pulled from the data
422
        container using the keys `u_name` and `v_name`, respectively, and plotted as
423
        a wind barb at the center of the station plot. If `units` are given, both
424
        components will be converted to these units.
425
426
        Additional keyword arguments given will be passed onto the actual plotting
427
        code; this is useful for specifying things like color or line width.
428
429
        Parameters
430
        ----------
431
        u_name : str
432
            The name of the parameter for the u-component for `barbs`, which is used as
433
            a key to pull data out of the data container passed to :meth:`plot`.
434
        v_name : str
435
            The name of the parameter for the v-component for `barbs`, which is used as
436
            a key to pull data out of the data container passed to :meth:`plot`.
437
        units : pint-compatible unit, optional
438
            The units to use for plotting. Data will be converted to this unit before
439
            conversion to a string. If not specified, no conversion is done.
440
        kwargs
441
            Additional keyword arguments to use for matplotlib's
442
            :meth:`~matplotlib.axes.Axes.barbs` function.
443
444
        See Also
445
        --------
446
        add_symbol, add_text, add_value
447
        """
448
        # Not sure if putting the v_name as a plot-specific option is appropriate,
449
        # but it seems simpler than making name code in plot handle tuples
450
        self['barb'] = (self.PlotTypes.barb, (u_name, v_name), (units, kwargs))
451
452
    def names(self):
453
        """Get the list of names used by the layout.
454
455
        Returns
456
        -------
457
        list[str]
458
            the list of names of variables used by the layout
459
        """
460
        ret = []
461
        for item in self.values():
462
            if item[0] == self.PlotTypes.barb:
463
                ret.extend(item[1])
464
            else:
465
                ret.append(item[1])
466
        return ret
467
468
    def plot(self, plotter, data_dict):
469
        """Plot a collection of data using this layout for a station plot.
470
471
        This function iterates through the entire specified layout, pulling the fields named
472
        in the layout from `data_dict` and plotting them using `plotter` as specified
473
        in the layout. Fields present in the layout, but not in `data_dict`, are ignored.
474
475
        Parameters
476
        ----------
477
        plotter : StationPlot
478
            :class:`StationPlot` to use to plot the data. This controls the axes,
479
            spacing, station locations, etc.
480
        data_dict : dict[str, array-like]
481
            Data container that maps a name to an array of data. Data from this object
482
            will be used to fill out the station plot.
483
        """
484
        def coerce_data(dat, u):
485
            try:
486
                return dat.to(u).magnitude
487
            except AttributeError:
488
                return dat
489
490
        for loc, info in self.items():
491
            typ, name, args = info
492
            if typ == self.PlotTypes.barb:
493
                # Try getting the data
494
                u_name, v_name = name
495
                u_data = data_dict.get(u_name)
496
                v_data = data_dict.get(v_name)
497
498
                # Plot if we have the data
499
                if not (v_data is None or u_data is None):
500
                    units, kwargs = args
501
                    plotter.plot_barb(coerce_data(u_data, units), coerce_data(v_data, units),
502
                                      **kwargs)
503
            else:
504
                # Check that we have the data for this location
505
                data = data_dict.get(name)
506
                if data is not None:
507
                    # If we have it, hand it to the appropriate method
508
                    if typ == self.PlotTypes.value:
509
                        fmt, units, kwargs = args
510
                        plotter.plot_parameter(loc, coerce_data(data, units), fmt, **kwargs)
511
                    elif typ == self.PlotTypes.symbol:
512
                        mapper, kwargs = args
513
                        plotter.plot_symbol(loc, data, mapper, **kwargs)
514
                    elif typ == self.PlotTypes.text:
515
                        plotter.plot_text(loc, data, **args)
516
517
    def __repr__(self):
518
        """Return string representation of layout."""
519
        return ('{' +
520
                ', '.join('{0}: ({1[0].name}, {1[1]}, ...)'.format(loc, info)
521
                          for loc, info in sorted(self.items())) +
522
                '}')
523
524
525
with exporter:
526
    #: :desc: Simple station plot layout
527
    simple_layout = StationPlotLayout()
528
    simple_layout.add_barb('eastward_wind', 'northward_wind', 'knots')
529
    simple_layout.add_value('NW', 'air_temperature', units='degC')
530
    simple_layout.add_value('SW', 'dew_point_temperature', units='degC')
531
    simple_layout.add_value('NE', 'air_pressure_at_sea_level', units='mbar',
532
                            fmt=lambda v: format(10 * v, '03.0f')[-3:])
533
    simple_layout.add_symbol('C', 'cloud_coverage', sky_cover)
534
    simple_layout.add_symbol('W', 'present_weather', current_weather)
535
536
    #: Full NWS station plot `layout`__
537
    #:
538
    #: __ http://oceanservice.noaa.gov/education/yos/resource/JetStream/synoptic/wxmaps.htm
539
    nws_layout = StationPlotLayout()
540
    nws_layout.add_value((-1, 1), 'air_temperature', units='degF')
541
    nws_layout.add_symbol((0, 2), 'high_cloud_type', high_clouds)
542
    nws_layout.add_symbol((0, 1), 'medium_cloud_type', mid_clouds)
543
    nws_layout.add_symbol((0, -1), 'low_cloud_type', low_clouds)
544
    nws_layout.add_value((1, 1), 'air_pressure_at_sea_level', units='mbar',
545
                         fmt=lambda v: format(10 * v, '03.0f')[-3:])
546
    nws_layout.add_value((-2, 0), 'visibility_in_air', fmt='.0f', units='miles')
547
    nws_layout.add_symbol((-1, 0), 'present_weather', current_weather)
548
    nws_layout.add_symbol((0, 0), 'cloud_coverage', sky_cover)
549
    nws_layout.add_value((1, 0), 'tendency_of_air_pressure', units='mbar',
550
                         fmt=lambda v: ('-' if v < 0 else '') + format(10 * abs(v), '02.0f'))
551
    nws_layout.add_symbol((2, 0), 'tendency_of_air_pressure_symbol', pressure_tendency)
552
    nws_layout.add_barb('eastward_wind', 'northward_wind', units='knots')
553
    nws_layout.add_value((-1, -1), 'dew_point_temperature', units='degF')
554
555
    # TODO: Fix once we have the past weather symbols converted
556
    nws_layout.add_symbol((1, -1), 'past_weather', current_weather)
557