Completed
Pull Request — master (#439)
by Ryan
01:36
created

  A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 233
Duplicated Lines 0 %

Importance

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