Completed
Pull Request — master (#552)
by Ryan
01:26
created

StationPlot._make_kwargs()   A

Complexity

Conditions 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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