# Copyright (c) 2018,2019 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
"""Declarative plotting tools."""
import contextlib
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from traitlets import (Any, Bool, Float, HasTraits, Instance, Int, List, observe, Tuple,
Unicode, Union)
from . import ctables
from . import wx_symbols
from .cartopy_utils import import_cartopy
from .station_plot import StationPlot
from ..calc import reduce_point_density
from ..package_tools import Exporter
from ..units import units
ccrs = import_cartopy()
exporter = Exporter(globals())
_areas = {
'105': (-129.3, -22.37, 17.52, 53.78),
'local': (-92., -64., 28.5, 48.5),
'wvaac': (120.86, -15.07, -53.6, 89.74),
'tropsfc': (-100., -55., 8., 33.),
'epacsfc': (-155., -75., -20., 33.),
'ofagx': (-100., -80., 20., 35.),
'ahsf': (-105., -30., -5., 35.),
'ehsf': (-145., -75., -5., 35.),
'shsf': (-125., -75., -20., 5.),
'tropful': (-160., 0., -20., 50.),
'tropatl': (-115., 10., 0., 40.),
'subtrop': (-90., -20., 20., 60.),
'troppac': (-165., -80., -25., 45.),
'gulf': (-105., -70., 10., 40.),
'carib': (-100., -50., 0., 40.),
'sthepac': (-170., -70., -60., 0.),
'opcahsf': (-102., -20., 0., 45.),
'opcphsf': (175., -70., -28., 45.),
'wwe': (-106., -50., 18., 54.),
'world': (-24., -24., -90., 90.),
'nwwrd1': (-180., 180., -90., 90.),
'nwwrd2': (0., 0., -90., 90.),
'afna': (-135.02, -23.04, 10.43, 40.31),
'awna': (-141.03, -18.58, 7.84, 35.62),
'medr': (-178., -25., -15., 5.),
'pacsfc': (129., -95., -5., 18.),
'saudi': (4.6, 92.5, -13.2, 60.3),
'natlmed': (-30., 70., 0., 65.),
'ncna': (-135.5, -19.25, 8., 37.7),
'ncna2': (-133.5, -20.5, 10., 42.),
'hpcsfc': (-124., -26., 15., 53.),
'atlhur': (-96., -6., 4., 3.),
'nam': (-134., 3., -4., 39.),
'sam': (-120., -20., -60., 20.),
'samps': (-148., -36., -28., 12.),
'eur': (-16., 80., 24., 52.),
'afnh': (-155.19, 18.76, -6.8, -3.58),
'awnh': (-158.94, 15.35, -11.55, -8.98),
'wwwus': (-127.7, -59., 19.8, 56.6),
'ccfp': (-130., -65., 22., 52.),
'llvl': (-119.6, -59.5, 19.9, 44.5),
'llvl2': (-125., -32.5, 5., 46.),
'llvl_e': (-89., -59.5, 23.5, 44.5),
'llvl_c': (-102.4, -81.25, 23.8, 51.6),
'llvl_w': (-119.8, -106.5, 19.75, 52.8),
'ak_artc': (163.7, -65.3, 17.5, 52.6),
'fxpswna': (-80.5, 135., -1., 79.),
'fxpsnna': (-80.5, 54., -1., 25.5),
'fxpsna': (-72.6, 31.4, -3.6, 31.),
'natl_ps': (-80.5, 54., -1., 25.5),
'fxpsena': (-45., 54., 11., 25.5),
'fxpsnp': (155.5, -106.5, 22.5, 47.),
'npac_ps': (155.5, -106.5, 22.5, 47.),
'fxpsus': (-120., -59., 20., 44.5),
'fxmrwrd': (58., 58., -70., 70.),
'fxmrwr2': (-131., -131., -70., 70.),
'nwmrwrd': (70., 70., -70., 70.),
'wrld_mr': (58., 58., -70., 70.),
'fxmr110': (-180., -110., -20., 50.5),
'fxmr180': (110., -180., -20., 50.5),
'fxmrswp': (97.5, -147.5, -36., 45.5),
'fxmrus': (-162.5, -37.5, -28., 51.2),
'fxmrea': (-40., 20., -20., 54.2),
'fxmrjp': (100., -160., 0., 45.),
'icao_a': (-137.4, -12.6, -54., 67.),
'icao_b': (-52.5, -16., -62.5, 77.5),
'icao_b1': (-125., 40., -45.5, 62.7),
'icao_c': (-35., 70., -45., 75.),
'icao_d': (-15., 132., -27., 63.),
'icao_e': (25., 180., -54., 40.),
'icao_f': (100., -110., -52.7, 50.),
'icao_g': (34.8, 157.2, -0.8, 13.7),
'icao_h': (-79.1, 56.7, 1.6, 25.2),
'icao_i': (166.24, -60.62, -6.74, 33.32),
'icao_j': (106.8, -101.1, -27.6, 0.8),
'icao_k': (3.3, 129.1, -11.1, 6.7),
'icao_m': (100., -110., -10., 70.),
'icao_eu': (-21.6, 68.4, 21.4, 58.7),
'icao_me': (17., 70., 10., 44.),
'icao_as': (53., 108., 00., 36.),
'icao_na': (-54.1, 60.3, 17.2, 50.7),
'nhem': (-135., 45., -15., -15.),
'nhem_ps': (-135., 45., -15., -15.),
'nhem180': (135., -45., -15., -15.),
'nhem155': (160., -20., -15., -15.),
'nhem165': (150., -30., -15., -15.),
'nh45_ps': (-90., 90., -15., -15.),
'nhem0': (-45., 135., -15., -15.),
'shem_ps': (88., -92., 30., 30.),
'hfo_gu': (160., -130., -30., 40.),
'natl': (-110., 20.1, 15., 70.),
'watl': (-84., -38., 25., 46.),
'tatl': (-90., -15., -10., 35.),
'npac': (102., -110., -12., 60.),
'spac': (102., -70., -60., 20.),
'tpac': (-165., -75., -10., 40.),
'epac': (-134., -110., 12., 75.),
'wpac': (130., -120., 0., 63.),
'mpac': (128., -108., 15., 71.95),
'opcsfp': (128.89, -105.3, 3.37, 16.77),
'opcsfa': (-55.5, 75., -8.5, 52.6),
'opchur': (-99., -15., 1., 50.05),
'us': (-119., -56., 19., 47.),
'spcus': (-116.4, -63.9, 22.1, 47.2),
'afus': (-119.04, -63.44, 23.1, 44.63),
'ncus': (-124.2, -40.98, 17.89, 47.39),
'nwus': (-118., -55.5, 17., 46.5),
'awips': (-127., -59., 20., 50.),
'bwus': (-124.6, -46.7, 13.1, 43.1),
'usa': (-118., -62., 22.8, 45.),
'usnps': (-118., -62., 18., 51.),
'uslcc': (-118., -62., 20., 51.),
'uswn': (-129., -45., 17., 53.),
'ussf': (-123.5, -44.5, 13., 32.1),
'ussp': (-126., -49., 13., 54.),
'whlf': (-123.8, -85.9, 22.9, 50.2),
'chlf': (-111., -79., 27.5, 50.5),
'centus': (-105.4, -77., 24.7, 47.6),
'ehlf': (-96.2, -62.7, 22., 49.),
'mehlf': (-89.9, -66.6, 23.8, 49.1),
'bosfa': (-87.5, -63.5, 34.5, 50.5),
'miafa': (-88., -72., 23., 39.),
'chifa': (-108., -75., 34., 50.),
'dfwfa': (-106.5, -80.5, 22., 40.),
'slcfa': (-126., -98., 29.5, 50.5),
'sfofa': (-129., -111., 30., 50.),
'g8us': (-116., -58., 19., 56.),
'wsig': (155., -115., 18., 58.),
'esig': (-80., -30., 25., 51.),
'eg8': (-79., -13., 24., 52.),
'west': (-125., -90., 25., 55.),
'cent': (-107.4, -75.3, 24.3, 49.7),
'east': (-100.55, -65.42, 24.57, 47.2),
'nwse': (-126., -102., 38.25, 50.25),
'swse': (-126., -100., 28.25, 40.25),
'ncse': (-108., -84., 38.25, 50.25),
'scse': (-108.9, -84., 24., 40.25),
'nese': (-89., -64., 37.25, 47.25),
'sese': (-90., -66., 28.25, 40.25),
'afwh': (170.7, 15.4, -48.6, 69.4),
'afeh': (-9.3, -164.6, -48.6, 69.4),
'afpc': (80.7, -74.6, -48.6, 69.4),
'ak': (-179., -116.4, 49., 69.),
'ak2': (-180., -106., 42., 73.),
'nwak': (-180., -110., 50., 60.),
'al': (-95., -79., 27., 38.),
'ar': (-100.75, -84.75, 29.5, 40.5),
'ca': (-127.75, -111.75, 31.5, 42.5),
'co': (-114., -98., 33.5, 44.5),
'ct': (-81.25, -65.25, 36., 47.),
'dc': (-85., -69., 33.35, 44.35),
'de': (-83.75, -67.75, 33.25, 44.25),
'fl': (-90., -74., 23., 34.),
'ga': (-92., -76., 27.5, 38.5),
'hi': (-161.5, -152.5, 17., 23.),
'nwxhi': (-166., -148., 14., 26.),
'ia': (-102., -86., 36.5, 47.5),
'id': (-123., -107., 39.25, 50.25),
'il': (-97.75, -81.75, 34.5, 45.5),
'in': (-94.5, -78.5, 34.5, 45.5),
'ks': (-106.5, -90.5, 33.25, 44.25),
'ky': (-93., -77., 31.75, 42.75),
'la': (-100.75, -84.75, 25.75, 36.75),
'ma': (-80.25, -64.25, 36.75, 47.75),
'md': (-85.25, -69.25, 33.75, 44.75),
'me': (-77.75, -61.75, 39.5, 50.5),
'mi': (-93., -77., 37.75, 48.75),
'mn': (-102., -86., 40.5, 51.5),
'mo': (-101., -85., 33., 44.),
'ms': (-98., -82., 27., 38.),
'mt': (-117., -101., 41.5, 52.5),
'nc': (-87.25, -71.25, 30., 41.),
'nd': (-107.5, -91.5, 42.25, 53.25),
'ne': (-107.5, -91.5, 36.25, 47.25),
'nh': (-79.5, -63.5, 38.25, 49.25),
'nj': (-82.5, -66.5, 34.75, 45.75),
'nm': (-114.25, -98.25, 29., 40.),
'nv': (-125., -109., 34., 45.),
'ny': (-84., -68., 37.25, 48.25),
'oh': (-91., -75., 34.5, 45.5),
'ok': (-105.25, -89.25, 30.25, 41.25),
'or': (-128., -112., 38.75, 49.75),
'pa': (-86., -70., 35.5, 46.5),
'ri': (-79.75, -63.75, 36., 47.),
'sc': (-89., -73., 28.5, 39.5),
'sd': (-107.5, -91.5, 39., 50.),
'tn': (-95., -79., 30., 41.),
'tx': (-107., -91., 25.4, 36.5),
'ut': (-119., -103., 34., 45.),
'va': (-86.5, -70.5, 32.25, 43.25),
'vt': (-80.75, -64.75, 38.25, 49.25),
'wi': (-98., -82., 38.5, 49.5),
'wv': (-89., -73., 33., 44.),
'wy': (-116., -100., 37.75, 48.75),
'az': (-119., -103., 29., 40.),
'wa': (-128., -112., 41.75, 52.75),
'abrfc': (-108., -88., 30., 42.),
'ab10': (-106.53, -90.28, 31.69, 40.01),
'cbrfc': (-117., -103., 28., 46.),
'cb10': (-115.69, -104.41, 29.47, 44.71),
'lmrfc': (-100., -77., 26., 40.),
'lm10': (-97.17, -80.07, 28.09, 38.02),
'marfc': (-83.5, -70., 35.5, 44.),
'ma10': (-81.27, -72.73, 36.68, 43.1),
'mbrfc': (-116., -86., 33., 53.),
'mb10': (-112.8, -89.33, 35.49, 50.72),
'ncrfc': (-108., -76., 34., 53.),
'nc10': (-104.75, -80.05, 35.88, 50.6),
'nerfc': (-84., -61., 39., 49.),
'ne10': (-80.11, -64.02, 40.95, 47.62),
'nwrfc': (-128., -105., 35., 55.),
'nw10': (-125.85, -109.99, 38.41, 54.46),
'ohrfc': (-92., -75., 34., 44.),
'oh10': (-90.05, -77.32, 35.2, 42.9),
'serfc': (-94., -70., 22., 40.),
'se10': (-90.6, -73.94, 24.12, 37.91),
'wgrfc': (-112., -88., 21., 42.),
'wg10': (-108.82, -92.38, 23.99, 39.18),
'nwcn': (-133.5, -10.5, 32., 56.),
'cn': (-120.4, -14., 37.9, 58.6),
'ab': (-119.6, -108.2, 48.6, 60.4),
'bc': (-134.5, -109., 47.2, 60.7),
'mb': (-102.4, -86.1, 48.3, 60.2),
'nb': (-75.7, -57.6, 42.7, 49.6),
'nf': (-68., -47., 45., 62.),
'ns': (-67., -59., 43., 47.5),
'nt': (-131.8, -33.3, 57.3, 67.8),
'on': (-94.5, -68.2, 41.9, 55.),
'pe': (-64.6, -61.7, 45.8, 47.1),
'qb': (-80., -49.2, 44.1, 60.9),
'sa': (-111.2, -97.8, 48.5, 60.3),
'yt': (-142., -117., 59., 70.5),
'ag': (-80., -53., -56., -20.),
'ah': (60., 77., 27., 40.),
'afrca': (-25., 59.4, -36., 41.),
'ai': (-14.3, -14.1, -8., -7.8),
'alba': (18., 23., 39., 43.),
'alge': (-9., 12., 15., 38.),
'an': (10., 25., -20., -5.),
'antl': (-70., -58., 11., 19.),
'antg': (-86., -65., 17., 25.),
'atg': (-62., -61.6, 16.9, 17.75),
'au': (101., 148., -45., -6.5),
'azor': (-27.6, -23., 36., 41.),
'ba': (-80.5, -72.5, 22.5, 28.5),
'be': (-64.9, -64.5, 32.2, 32.6),
'bel': (2.5, 6.5, 49.4, 51.6),
'bf': (113., 116., 4., 5.5),
'bfa': (-6., 3., 9., 15.1),
'bh': (-89.3, -88.1, 15.7, 18.5),
'bi': (29., 30.9, -4.6, -2.2),
'bj': (0., 5., 6., 12.6),
'bn': (50., 51., 25.5, 27.1),
'bo': (-72., -50., -24., -8.),
'bots': (19., 29.6, -27., -17.),
'br': (-62.5, -56.5, 12.45, 13.85),
'bt': (71.25, 72.6, -7.5, -5.),
'bu': (22., 30., 40., 45.),
'bv': (3., 4., -55., -54.),
'bw': (87., 93., 20.8, 27.),
'by': (19., 33., 51., 60.),
'bz': (-75., -30., -35., 5.),
'cais': (-172., -171., -3., -2.),
'nwcar': (-120., -50., -15., 35.),
'cari': (-103., -53., 3., 36.),
'cb': (13., 25., 7., 24.),
'ce': (14., 29., 2., 11.5),
'cg': (10., 20., -6., 5.),
'ch': (-80., -66., -56., -15.),
'ci': (85., 145., 14., 48.5),
'cm': (7.5, 17.1, 1., 14.),
'colm': (-81., -65., -5., 14.),
'cr': (-19., -13., 27., 30.),
'cs': (-86.5, -81.5, 8.2, 11.6),
'cu': (-85., -74., 19., 24.),
'cv': (-26., -22., 14., 18.),
'cy': (32., 35., 34., 36.),
'cz': (8.9, 22.9, 47.4, 52.4),
'dj': (41.5, 44.1, 10.5, 13.1),
'dl': (4.8, 16.8, 47., 55.),
'dn': (8., 11., 54., 58.6),
'do': (-61.6, -61.2, 15.2, 15.8),
'dr': (-72.2, -68., 17.5, 20.2),
'eg': (24., 37., 21., 33.),
'eq': (-85., -74., -7., 3.),
'er': (50., 57., 22., 26.6),
'es': (-90.3, -87.5, 13., 14.6),
'et': (33., 49., 2., 19.),
'fa': (-8., -6., 61., 63.),
'fg': (-55., -49., 1., 7.),
'fi': (20.9, 35.1, 59., 70.6),
'fj': (176., -179., 16., 19.),
'fk': (-61.3, -57.5, -53., -51.),
'fn': (0., 17., 11., 24.),
'fr': (-5., 11., 41., 51.5),
'gb': (-17.1, -13.5, 13., 14.6),
'gc': (-82.8, -77.6, 17.9, 21.1),
'gh': (-4.5, 1.5, 4., 12.),
'gi': (-8., -4., 35., 38.),
'gl': (-56.7, 14., 58.3, 79.7),
'glp': (-64.2, -59.8, 14.8, 19.2),
'gm': (144.5, 145.1, 13., 14.),
'gn': (2., 16., 3.5, 15.5),
'go': (8., 14.5, -4.6, 3.),
'gr': (20., 27.6, 34., 42.),
'gu': (-95.6, -85., 10.5, 21.1),
'gw': (-17.5, -13.5, 10.8, 12.8),
'gy': (-62., -55., 0., 10.),
'ha': (-75., -71., 18., 20.),
'he': (-6.1, -5.5, -16.3, -15.5),
'hk': (113.5, 114.7, 22., 23.),
'ho': (-90., -83., 13., 16.6),
'hu': (16., 23., 45.5, 49.1),
'ic': (43., 45., -13.2, -11.),
'icel': (-24.1, -11.5, 63., 67.5),
'ie': (-11.1, -4.5, 50., 55.6),
'inda': (67., 92., 4.2, 36.),
'indo': (95., 141., -8., 6.),
'iq': (38., 50., 29., 38.),
'ir': (44., 65., 25., 40.),
'is': (34., 37., 29., 34.),
'iv': (-9., -2., 4., 11.),
'iw': (34.8, 35.6, 31.2, 32.6),
'iy': (6.6, 20.6, 35.6, 47.2),
'jd': (34., 39.6, 29., 33.6),
'jm': (-80., -76., 16., 19.),
'jp': (123., 155., 24., 47.),
'ka': (131., 155., 1., 9.6),
'kash': (74., 78., 32., 35.),
'kb': (172., 177., -3., 3.2),
'khm': (102., 108., 10., 15.),
'ki': (105.2, 106.2, -11., -10.),
'kn': (32.5, 42.1, -6., 6.),
'kna': (-62.9, -62.4, 17., 17.5),
'ko': (124., 131.5, 33., 43.5),
'ku': (-168., -155., -24.1, -6.1),
'kw': (46.5, 48.5, 28.5, 30.5),
'laos': (100., 108., 13.5, 23.1),
'lb': (34.5, 37.1, 33., 35.),
'lc': (60.9, 61.3, 13.25, 14.45),
'li': (-12., -7., 4., 9.),
'ln': (-162.1, -154.9, -4.2, 6.),
'ls': (27., 29.6, -30.6, -28.),
'lt': (9.3, 9.9, 47., 47.6),
'lux': (5.6, 6.6, 49.35, 50.25),
'ly': (8., 26., 19., 35.),
'maar': (-63.9, -62.3, 17., 18.6),
'made': (-17.3, -16.5, 32.6, 33.),
'mala': (100., 119.6, 1., 8.),
'mali': (-12.5, 6., 8.5, 25.5),
'maur': (57.2, 57.8, -20.7, -19.9),
'maut': (-17.1, -4.5, 14.5, 28.1),
'mc': (-13., -1., 25., 36.),
'mg': (43., 50.6, -25.6, -12.),
'mh': (160., 172., 4.5, 12.1),
'ml': (14.3, 14.7, 35.8, 36.),
'mmr': (92., 102., 7.5, 28.5),
'mong': (87.5, 123.1, 38.5, 52.6),
'mr': (-61.2, -60.8, 14.3, 15.1),
'mu': (113., 114., 22., 23.),
'mv': (70.1, 76.1, -6., 10.),
'mw': (32.5, 36.1, -17., -9.),
'mx': (-119., -83., 13., 34.),
'my': (142.5, 148.5, 9., 25.),
'mz': (29., 41., -26.5, -9.5),
'nama': (11., 25., -29.5, -16.5),
'ncal': (158., 172., -23., -18.),
'ng': (130., 152., -11., 0.),
'ni': (2., 14.6, 3., 14.),
'nk': (-88., -83., 10.5, 15.1),
'nl': (3.5, 7.5, 50.5, 54.1),
'no': (3., 35., 57., 71.5),
'np': (80., 89., 25., 31.),
'nw': (166.4, 167.4, -1., 0.),
'nz': (165., 179., -48., -33.),
'om': (52., 60., 16., 25.6),
'os': (9., 18., 46., 50.),
'pf': (-154., -134., -28., -8.),
'ph': (116., 127., 4., 21.),
'pi': (-177.5, -167.5, -9., 1.),
'pk': (60., 78., 23., 37.),
'pl': (14., 25., 48.5, 55.),
'pm': (-83., -77., 7., 10.),
'po': (-10., -4., 36.5, 42.5),
'pr': (-82., -68., -20., 5.),
'pt': (-130.6, -129.6, -25.56, -24.56),
'pu': (-67.5, -65.5, 17.5, 18.5),
'py': (-65., -54., -32., -17.),
'qg': (7., 12., -2., 3.),
'qt': (50., 52., 24., 27.),
'ra': (60., -165., 25., 55.),
're': (55., 56., -21.5, -20.5),
'riro': (-18., -12., 17.5, 27.5),
'ro': (19., 31., 42.5, 48.5),
'rw': (29., 31., -3., -1.),
'saud': (34.5, 56.1, 15., 32.6),
'sb': (79., 83., 5., 10.),
'seyc': (55., 56., -5., -4.),
'sg': (-18., -10., 12., 17.),
'si': (39.5, 52.1, -4.5, 13.5),
'sk': (109.5, 119.3, 1., 7.),
'sl': (-13.6, -10.2, 6.9, 10.1),
'sm': (-59., -53., 1., 6.),
'sn': (10., 25., 55., 69.6),
'so': (156., 167., -12., -6.),
'sp': (-10., 6., 35., 44.),
'sr': (103., 105., 1., 2.),
'su': (21.5, 38.5, 3.5, 23.5),
'sv': (30.5, 33.1, -27.5, -25.3),
'sw': (5.9, 10.5, 45.8, 48.),
'sy': (35., 42.6, 32., 37.6),
'tanz': (29., 40.6, -13., 0.),
'td': (-62.1, -60.5, 10., 11.6),
'tg': (-0.5, 2.5, 5., 12.),
'th': (97., 106., 5., 21.),
'ti': (-71.6, -70.6, 21., 22.),
'tk': (-173., -171., -11.5, -7.5),
'to': (-178.5, -170.5, -22., -15.),
'tp': (6., 7.6, 0., 2.),
'ts': (7., 13., 30., 38.),
'tu': (25., 48., 34.1, 42.1),
'tv': (176., 180., -11., -5.),
'tw': (120., 122., 21.9, 25.3),
'ug': (29., 35., -3.5, 5.5),
'uk': (-11., 5., 49., 60.),
'ur': (24., 41., 44., 55.),
'uy': (-60., -52., -35.5, -29.5),
'vanu': (167., 170., -21., -13.),
'vi': (-65.5, -64., 16.6, 19.6),
'vk': (13.8, 25.8, 46.75, 50.75),
'vn': (-75., -60., -2., 14.),
'vs': (102., 110., 8., 24.),
'wk': (166.1, 167.1, 18.8, 19.8),
'ye': (42.5, 54.1, 12.5, 19.1),
'yg': (13.5, 24.6, 40., 47.),
'za': (16., 34., -36., -22.),
'zb': (21., 35., -20., -7.),
'zm': (170.5, 173.5, -15., -13.),
'zr': (12., 31.6, -14., 6.),
'zw': (25., 34., -22.9, -15.5)
}
def lookup_projection(projection_code):
"""Get a Cartopy projection based on a short abbreviation."""
import cartopy.crs as ccrs
projections = {'lcc': ccrs.LambertConformal(central_latitude=40, central_longitude=-100,
standard_parallels=[30, 60]),
'ps': ccrs.NorthPolarStereo(central_longitude=-100),
'mer': ccrs.Mercator()}
return projections[projection_code]
def lookup_map_feature(feature_name):
"""Get a Cartopy map feature based on a name."""
import cartopy.feature as cfeature
from . import cartopy_utils
name = feature_name.upper()
try:
feat = getattr(cfeature, name)
scaler = cfeature.AdaptiveScaler('110m', (('50m', 50), ('10m', 15)))
except AttributeError:
feat = getattr(cartopy_utils, name)
scaler = cfeature.AdaptiveScaler('20m', (('5m', 5), ('500k', 1)))
return feat.with_scale(scaler)
class Panel(HasTraits):
"""Draw one or more plots."""
[docs]@exporter.export
class PanelContainer(HasTraits):
"""Collects panels and set complete figure related settings (e.g., size)."""
size = Union([Tuple(Union([Int(), Float()]), Union([Int(), Float()])),
Instance(type(None))], default_value=None)
size.__doc__ = """This trait takes a tuple of (width, height) to set the size of the
figure.
This trait defaults to None and will assume the default `matplotlib.pyplot.figure` size.
"""
panels = List(Instance(Panel))
panels.__doc__ = """A list of panels to plot on the figure.
This trait must contain at least one panel to plot on the figure."""
@property
def panel(self):
"""Provide simple access for a single panel."""
return self.panels[0]
@panel.setter
def panel(self, val):
self.panels = [val]
@observe('panels')
def _panels_changed(self, change):
for panel in change.new:
panel.parent = self
panel.observe(self.refresh, names=('_need_redraw'))
@property
def figure(self):
"""Provide access to the underlying figure object."""
if not hasattr(self, '_fig'):
self._fig = plt.figure(figsize=self.size)
return self._fig
[docs] def refresh(self, _):
"""Refresh the rendering of all panels."""
# First make sure everything is properly constructed
self.draw()
# Trigger the graphics refresh
self.figure.canvas.draw()
# Flush out interactive events--only ok on Agg for newer matplotlib
with contextlib.suppress(NotImplementedError):
self.figure.canvas.flush_events()
[docs] def draw(self):
"""Draw the collection of panels."""
for panel in self.panels:
with panel.hold_trait_notifications():
panel.draw()
[docs] def save(self, *args, **kwargs):
"""Save the constructed graphic as an image file.
This method takes a string for saved file name. Additionally, the same arguments and
keyword arguments that `matplotlib.pyplot.savefig` does.
"""
self.draw()
self.figure.savefig(*args, **kwargs)
[docs] def show(self):
"""Show the constructed graphic on the screen."""
self.draw()
plt.show()
[docs]@exporter.export
class MapPanel(Panel):
"""Set figure related elements for an individual panel.
Parameters that need to be set include collecting all plotting types
(e.g., contours, wind barbs, etc.) that are desired to be in a given panel.
Additionally, traits can be set to plot map related features (e.g., coastlines, borders),
projection, graphics area, and title.
"""
parent = Instance(PanelContainer)
layout = Tuple(Int(), Int(), Int(), default_value=(1, 1, 1))
layout.__doc__ = """A tuple that contains the description (nrows, ncols, index) of the
panel position; default value is (1, 1, 1).
This trait is set to describe the panel position and the default is for a single panel. For
example, a four-panel plot will have two rows and two columns with the tuple setting for
the upper-left panel as (2, 2, 1), upper-right as (2, 2, 2), lower-left as (2, 2, 3), and
lower-right as (2, 2, 4). For more details see the documentation for
`matplotlib.figure.Figure.add_subplot`.
"""
plots = List(Any())
plots.__doc__ = """A list of handles that represent the plots (e.g., `ContourPlot`,
`FilledContourPlot`, `ImagePlot`) to put on a given panel.
This trait collects the different plots, including contours and images, that are intended
for a given panel.
"""
_need_redraw = Bool(default_value=True)
area = Union([Unicode(), Tuple(Float(), Float(), Float(), Float())], allow_none=True,
default_value=None)
area.__doc__ = """A tuple or string value that indicates the graphical area of the plot.
The tuple value coresponds to longitude/latitude box based on the projection of the map
with the format (west-most longitude, east-most longitude, south-most latitude,
north-most latitude). This tuple defines a box from the lower-left to the upper-right
corner.
This trait can also be set with a string value associated with the named geographic regions
within MetPy. The tuples associated with the names are based on a PlatteCarree projection.
For a CONUS region, the following strings can be used: 'us', 'spcus', 'ncus', and 'afus'.
For regional plots, US postal state abbreviations can be used.
"""
projection = Union([Unicode(), Instance('cartopy.crs.Projection')], default_value='data')
projection.__doc__ = """A string for a pre-defined projection or a Cartopy projection
object.
There are three pre-defined projections that can be called with a short name:
Lambert conformal conic ('lcc'), Mercator ('mer'), or polar-stereographic ('ps').
Additionally, this trait can be set to a Cartopy projection object.
"""
layers = List(Union([Unicode(), Instance('cartopy.feature.Feature')]),
default_value=['coastline'])
layers.__doc__ = """A list of strings for a pre-defined feature layer or a Cartopy Feature object.
Like the projection, there are a couple of pre-defined feature layers that can be called
using a short name. The pre-defined layers are: 'coastline', 'states', 'borders', 'lakes',
'land', 'ocean', 'rivers', 'usstates', and 'uscounties'. Additionally, this can accept
Cartopy Feature objects.
"""
title = Unicode()
title.__doc__ = """A string to set a title for the figure.
This trait sets a user-defined title that will plot at the top center of the figure.
"""
@observe('plots')
def _plots_changed(self, change):
"""Handle when our collection of plots changes."""
for plot in change.new:
plot.parent = self
plot.observe(self.refresh, names=('_need_redraw'))
self._need_redraw = True
@observe('parent')
def _parent_changed(self, _):
"""Handle when the parent is changed."""
self.ax = None
@property
def _proj_obj(self):
"""Return the projection as a Cartopy object.
Handles looking up a string for the projection, or if the projection
is set to ``'data'`` looks at the data for the projection.
"""
if isinstance(self.projection, str):
if self.projection == 'data':
if isinstance(self.plots[0].griddata, tuple):
return self.plots[0].griddata[0].metpy.cartopy_crs
else:
return self.plots[0].griddata.metpy.cartopy_crs
else:
return lookup_projection(self.projection)
else:
return self.projection
@property
def _layer_features(self):
"""Iterate over all map features and return as Cartopy objects.
Handle converting names of maps to auto-scaling map features.
"""
for item in self.layers:
if isinstance(item, str):
feat = lookup_map_feature(item)
else:
feat = item
yield feat
@observe('area')
def _set_need_redraw(self, _):
"""Watch traits and set the need redraw flag as necessary."""
self._need_redraw = True
@property
def ax(self):
"""Get the :class:`matplotlib.axes.Axes` to draw on.
Creates a new instance if necessary.
"""
# If we haven't actually made an instance yet, make one with the right size and
# map projection.
if getattr(self, '_ax', None) is None:
self._ax = self.parent.figure.add_subplot(*self.layout, projection=self._proj_obj)
return self._ax
@ax.setter
def ax(self, val):
"""Set the :class:`matplotlib.axes.Axes` to draw on.
Clears existing state as necessary.
"""
if getattr(self, '_ax', None) is not None:
self._ax.cla()
self._ax = val
[docs] def refresh(self, changed):
"""Refresh the drawing if necessary."""
self._need_redraw = changed.new
[docs] def draw(self):
"""Draw the panel."""
# Only need to run if we've actually changed.
if self._need_redraw:
# Set the extent as appropriate based on the area. One special case for 'global'
if self.area == 'global':
self.ax.set_global()
elif self.area is not None:
# Try to look up if we have a string
if isinstance(self.area, str):
area = _areas[self.area.lower()]
# Otherwise, assume we have a tuple to use as the extent
else:
area = self.area
self.ax.set_extent(area, ccrs.PlateCarree())
# Draw all of the plots.
for p in self.plots:
with p.hold_trait_notifications():
p.draw()
# Add all of the maps
for feat in self._layer_features:
self.ax.add_feature(feat)
# Use the set title or generate one.
title = self.title or ',\n'.join(plot.name for plot in self.plots)
self.ax.set_title(title)
self._need_redraw = False
[docs]@exporter.export
class Plots2D(HasTraits):
"""The highest level class related to plotting 2D data.
This class collects all common methods no matter whether plotting a scalar variable or
vector. Primary settings common to all types of 2D plots are time and level.
"""
parent = Instance(Panel)
_need_redraw = Bool(default_value=True)
level = Union([Int(allow_none=True, default_value=None), Instance(units.Quantity)])
level.__doc__ = """The level of the field to be plotted.
This is a value with units to choose the desired plot level. For example, selecting the
850-hPa level, set this parameter to ``850 * units.hPa``
"""
time = Instance(datetime, allow_none=True)
time.__doc__ = """Set the valid time to be plotted as a datetime object.
If a forecast hour is to be plotted the time should be set to the valid future time, which
can be done using the `~datetime.datetime` and `~datetime.timedelta` objects
from the Python standard library.
"""
plot_units = Unicode(allow_none=True, default_value=None)
plot_units.__doc__ = """The desired units to plot the field in.
Setting this attribute will convert the units of the field variable to the given units for
plotting using the MetPy Units module.
"""
scale = Float(default_value=1e0)
scale.__doc__ = """Scale the field to be plotted by the value given.
This attribute will scale the field by multiplying by the scale. For example, to
scale vorticity to be whole values for contouring you could set the scale to 1e5, such that
the data values will be scaled by 10^5.
"""
@property
def _cmap_obj(self):
"""Return the colormap object.
Handle convert the name of the colormap to an object from matplotlib or metpy.
"""
try:
return ctables.registry.get_colortable(self.colormap)
except KeyError:
return plt.get_cmap(self.colormap)
@property
def _norm_obj(self):
"""Return the normalization object.
Converts the tuple image range to a matplotlib normalization instance.
"""
return plt.Normalize(*self.image_range)
[docs] def clear(self):
"""Clear the plot.
Resets all internal state and sets need for redraw.
"""
if getattr(self, 'handle', None) is not None:
if getattr(self.handle, 'collections', None) is not None:
self.clear_collections()
else:
self.clear_handle()
self._need_redraw = True
[docs] def clear_handle(self):
"""Clear the handle to the plot instance."""
self.handle.remove()
self.handle = None
[docs] def clear_collections(self):
"""Clear the handle collections to the plot instance."""
for col in self.handle.collections:
col.remove()
self.handle = None
@observe('parent')
def _parent_changed(self, _):
"""Handle setting the parent object for the plot."""
self.clear()
@observe('level', 'time')
def _update_data(self, _=None):
"""Handle updating the internal cache of data.
Responds to changes in various subsetting parameters.
"""
self._griddata = None
self.clear()
# Can't be a Traitlet because notifications don't work with arrays for traits
# notification never happens
@property
def data(self):
"""Xarray dataset that contains the field to be plotted."""
return self._data
@data.setter
def data(self, val):
self._data = val
self._update_data()
@property
def name(self):
"""Generate a name for the plot."""
if isinstance(self.field, tuple):
ret = ''
ret += ' and '.join(f for f in self.field)
else:
ret = self.field
if self.level is not None:
ret += f'@{self.level:d}'
return ret
[docs]@exporter.export
class PlotScalar(Plots2D):
"""Defines the common elements of 2D scalar plots for single scalar value fields.
Most of the other traits here are for one or more of the specific plots. Currently this
allows too many options for `ContourPlot` since it does not user image_range, for
example. Similar issues for `ImagePlot` and `FilledContourPlot`.
"""
field = Unicode()
field.__doc__ = """Name of the field to be plotted.
This is the name of the variable from the dataset that is to be plotted. An example,
from a model grid file that uses the THREDDS convention for naming would be
`Geopotential_height_isobaric` or `Temperature_isobaric`. For GOES-16/17 satellite data it
might be `Sectorized_CMI`. To check for the variables available within a dataset, list the
variables with the following command assuming the dataset was read using xarray as `ds`,
`list(ds)`
"""
@observe('field')
def _update_data(self, _=None):
"""Handle updating the internal cache of data.
Responds to changes in various subsetting parameters.
"""
self._griddata = None
self.clear()
@property
def griddata(self):
"""Return the internal cached data."""
if getattr(self, '_griddata', None) is None:
if self.field:
data = self.data.metpy.parse_cf(self.field)
elif not hasattr(self.data.metpy, 'x'):
# Handles the case where we have a dataset but no specified field
raise ValueError('field attribute has not been set.')
else:
data = self.data
subset = {'method': 'nearest'}
if self.level is not None:
subset[data.metpy.vertical.name] = self.level
if self.time is not None:
subset[data.metpy.time.name] = self.time
data_subset = data.metpy.sel(**subset).squeeze()
if self.plot_units is not None:
data_subset = data_subset.metpy.convert_units(self.plot_units)
self._griddata = data_subset * self.scale
return self._griddata
@property
def plotdata(self):
"""Return the data for plotting.
The data array, x coordinates, and y coordinates.
"""
x = self.griddata.metpy.x
y = self.griddata.metpy.y
return x, y, self.griddata
[docs] def draw(self):
"""Draw the plot."""
if self._need_redraw:
if getattr(self, 'handle', None) is None:
self._build()
if getattr(self, 'colorbar', None) is not None:
self.parent.ax.figure.colorbar(
self.handle, orientation=self.colorbar, pad=0, aspect=50)
self._need_redraw = False
class ContourTraits(HasTraits):
"""Represents common contour traits."""
contours = Union([List(Float()), Int()], default_value=25)
contours.__doc__ = """A list of values to contour or an integer number of contour levels.
This parameter sets contour or colorfill values for a plot. Values can be entered either
as a list of values or as an integer with the number of contours to be plotted (as per
matplotlib documentation). A list can be generated by using square brackets or creating
a numpy 1D array and converting it to a list with the `~numpy.ndarray.tolist` method.
"""
clabels = Bool(default_value=False)
clabels.__doc__ = """A boolean (True/False) on whether to plot contour labels.
To plot contour labels set this trait to ``True``, the default value is ``False``.
"""
class ColorfillTraits(HasTraits):
"""Represent common colorfill traits."""
colormap = Unicode(allow_none=True, default_value=None)
colormap.__doc__ = """The string name for a Matplolib or MetPy colormap.
For example, the Blue-Purple colormap from Matplotlib can be accessed using 'BuPu'.
"""
image_range = Union([Tuple(Int(allow_none=True), Int(allow_none=True)),
Instance(plt.Normalize)], default_value=(None, None))
image_range.__doc__ = """A tuple of min and max values that represent the range of values
to color the rasterized image.
The min and max values entered as a tuple will be converted to a
`matplotlib.colors.Normalize` instance for plotting.
"""
colorbar = Unicode(default_value=None, allow_none=True)
colorbar.__doc__ = """A string (horizontal/vertical) on whether to add a colorbar to the plot.
To add a colorbar associated with the plot, set the trait to ``horizontal`` or
``vertical``,specifying the orientation of the produced colorbar. The default value is
``None``.
"""
[docs]@exporter.export
class ImagePlot(PlotScalar, ColorfillTraits):
"""Make raster image using `~matplotlib.pyplot.imshow` for satellite or colored image."""
@observe('colormap', 'image_range')
def _set_need_redraw(self, _):
"""Handle changes to attributes that just need a simple redraw."""
if hasattr(self, 'handle'):
self.handle.set_cmap(self._cmap_obj)
self.handle.set_norm(self._norm_obj)
self._need_redraw = True
@observe('colorbar')
def _set_need_rebuild(self, _):
"""Handle changes to attributes that need to regenerate everything."""
# Because matplotlib doesn't let you just change these properties, we need
# to trigger a clear and re-call of contour()
self.clear()
@property
def plotdata(self):
"""Return the data for plotting.
The data array, x coordinates, and y coordinates.
"""
x = self.griddata.metpy.x
y = self.griddata.metpy.y
# At least currently imshow with cartopy does not like this
if 'degree' in x.units:
x = x.data
x[x > 180] -= 360
return x, y, self.griddata
def _build(self):
"""Build the plot by calling any plotting methods as necessary."""
x, y, imdata = self.plotdata
# We use min/max for y and manually figure out origin to try to avoid upside down
# images created by images where y[0] > y[-1]
extents = (x[0], x[-1], y.min(), y.max())
origin = 'upper' if y[0] > y[-1] else 'lower'
self.handle = self.parent.ax.imshow(imdata, extent=extents, origin=origin,
cmap=self._cmap_obj, norm=self._norm_obj,
transform=imdata.metpy.cartopy_crs)
[docs]@exporter.export
class ContourPlot(PlotScalar, ContourTraits):
"""Make contour plots by defining specific traits."""
linecolor = Unicode('black')
linecolor.__doc__ = """A string value to set the color of plotted contours; default is
black.
This trait can be set to any Matplotlib color
(https://matplotlib.org/3.1.0/gallery/color/named_colors.html)
"""
linewidth = Int(2)
linewidth.__doc__ = """An integer value to set the width of plotted contours; default value
is 2.
This trait changes the thickness of contour lines with a higher value plotting a thicker
line.
"""
linestyle = Unicode('solid', allow_none=True)
linestyle.__doc__ = """A string value to set the linestyle (e.g., dashed); default is
solid.
The valid string values are those of Matplotlib which are solid, dashed, dotted, and
dashdot.
"""
@observe('contours', 'linecolor', 'linewidth', 'linestyle', 'clabels')
def _set_need_rebuild(self, _):
"""Handle changes to attributes that need to regenerate everything."""
# Because matplotlib doesn't let you just change these properties, we need
# to trigger a clear and re-call of contour()
self.clear()
def _build(self):
"""Build the plot by calling any plotting methods as necessary."""
x, y, imdata = self.plotdata
self.handle = self.parent.ax.contour(x, y, imdata, self.contours,
colors=self.linecolor, linewidths=self.linewidth,
linestyles=self.linestyle,
transform=imdata.metpy.cartopy_crs)
if self.clabels:
self.handle.clabel(inline=1, fmt='%.0f', inline_spacing=8,
use_clabeltext=True)
[docs]@exporter.export
class FilledContourPlot(PlotScalar, ColorfillTraits, ContourTraits):
"""Make color-filled contours plots by defining appropriate traits."""
@observe('contours', 'colorbar', 'colormap')
def _set_need_rebuild(self, _):
"""Handle changes to attributes that need to regenerate everything."""
# Because matplotlib doesn't let you just change these properties, we need
# to trigger a clear and re-call of contour()
self.clear()
def _build(self):
"""Build the plot by calling any plotting methods as necessary."""
x, y, imdata = self.plotdata
self.handle = self.parent.ax.contourf(x, y, imdata, self.contours,
cmap=self._cmap_obj, norm=self._norm_obj,
transform=imdata.metpy.cartopy_crs)
[docs]@exporter.export
class PlotVector(Plots2D):
"""Defines common elements for 2D vector plots.
This class collects common elements including the field trait, which is a tuple argument
accepting two strings, for plotting 2D vector fields.
"""
field = Tuple(Unicode(), Unicode())
field.__doc__ = """A tuple containing the two components of the vector field from the
dataset in the form (east-west component, north-south component).
For a wind barb plot each component of the wind must be specified and should be of the form
(u-wind, v-wind).
"""
pivot = Unicode('middle')
pivot.__doc__ = """A string setting the pivot point of the vector. Default value is
'middle'.
This trait takes the values of the keyword argument from `matplotlin.pyplot.barbs`:
'tip' or 'middle'.
"""
skip = Tuple(Int(), Int(), default_value=(1, 1))
skip.__doc__ = """A tuple of integers to indicate the number of grid points to skip between
plotting vectors. Default is (1, 1).
This trait is to be used to reduce the number of vectors plotted in the (east-west,
north-south) components. The two values can be set to the same or different integer values
depending on what is desired.
"""
earth_relative = Bool(default_value=True)
earth_relative.__doc__ = """A boolean value to indicate whether the vector to be plotted
is earth- or grid-relative. Default value is `True`, indicating that vectors are
earth-relative.
Common gridded meteorological datasets including GFS and NARR output contain wind
components that are earth-relative. The primary expection is NAM output with wind
components that are grid-relative. For any grid-relative vectors set this trait to `False`.
"""
color = Unicode(default_value='black')
color.__doc__ = """A string value that controls the color of the vectors. Default value is
black.
This trait can be set to any named color from
`Matplotlibs Colors <https://matplotlib.org/3.1.0/gallery/color/named_colors.html>`
"""
@observe('field')
def _update_data(self, _=None):
"""Handle updating the internal cache of data.
Responds to changes in various subsetting parameters.
"""
self._griddata_u = None
self._griddata_v = None
self.clear()
@property
def griddata(self):
"""Return the internal cached data."""
if getattr(self, '_griddata_u', None) is None:
if self.field[0]:
u = self.data.metpy.parse_cf(self.field[0])
v = self.data.metpy.parse_cf(self.field[1])
else:
raise ValueError('field attribute not set correctly')
subset = {'method': 'nearest'}
if self.level is not None:
subset[u.metpy.vertical.name] = self.level
if self.time is not None:
subset[u.metpy.time.name] = self.time
data_subset_u = u.metpy.sel(**subset).squeeze()
data_subset_v = v.metpy.sel(**subset).squeeze()
if self.plot_units is not None:
data_subset_u = data_subset_u.metpy.convert_units(self.plot_units)
data_subset_v = data_subset_v.metpy.convert_units(self.plot_units)
self._griddata_u = data_subset_u
self._griddata_v = data_subset_v
return (self._griddata_u, self._griddata_v)
@property
def plotdata(self):
"""Return the data for plotting.
The data array, x coordinates, and y coordinates.
"""
x = self.griddata[0].metpy.x
y = self.griddata[0].metpy.y
if self.earth_relative:
x, y, _ = ccrs.PlateCarree().transform_points(self.griddata[0].metpy.cartopy_crs,
*np.meshgrid(x, y)).T
x = x.T
y = y.T
else:
if 'degree' in x.units:
x, y, _ = self.griddata[0].metpy.cartopy_crs.transform_points(
ccrs.PlateCarree(), *np.meshgrid(x, y)).T
x = x.T
y = y.T
if x.ndim == 1:
xx, yy = np.meshgrid(x, y)
else:
xx, yy = x, y
return xx, yy, self.griddata[0], self.griddata[1]
[docs] def draw(self):
"""Draw the plot."""
if self._need_redraw:
if getattr(self, 'handle', None) is None:
self._build()
self._need_redraw = False
[docs]@exporter.export
class BarbPlot(PlotVector):
"""Make plots of wind barbs on a map with traits to refine the look of plotted elements."""
barblength = Float(default_value=7)
barblength.__doc__ = """A float value that changes the length of the wind barbs. Default
value is 7.
This trait corresponds to the keyword length in `matplotlib.pyplot.barbs`.
"""
@observe('barblength', 'pivot', 'skip', 'earth_relative', 'color')
def _set_need_rebuild(self, _):
"""Handle changes to attributes that need to regenerate everything."""
# Because matplotlib doesn't let you just change these properties, we need
# to trigger a clear and re-call of contour()
self.clear()
def _build(self):
"""Build the plot by calling needed plotting methods as necessary."""
x, y, u, v = self.plotdata
if self.earth_relative:
transform = ccrs.PlateCarree()
else:
transform = u.metpy.cartopy_crs
wind_slice = (slice(None, None, self.skip[0]), slice(None, None, self.skip[1]))
self.handle = self.parent.ax.barbs(
x[wind_slice], y[wind_slice],
u.values[wind_slice], v.values[wind_slice],
color=self.color, pivot=self.pivot, length=self.barblength,
transform=transform)
[docs]@exporter.export
class PlotObs(HasTraits):
"""The highest level class related to plotting observed surface and upperair data.
This class collects all common methods no matter whether plotting a upper-level or
surface data using station plots.
List of Traits:
* level
* time
* fields
* locations (optional)
* time_window (optional)
* formats (optional)
* colors (optional)
* plot_units (optional)
* vector_field (optional)
* vector_field_color (optional)
* vector_field_length (optional)
* vector_plot_units (optional)
* reduce_points (optional)
"""
parent = Instance(Panel)
_need_redraw = Bool(default_value=True)
level = Union([Int(allow_none=True), Instance(units.Quantity)], default_value=None)
level.__doc__ = """The level of the field to be plotted.
This is a value with units to choose the desired plot level. For example, selecting the
850-hPa level, set this parameter to ``850 * units.hPa``. For surface data, parameter
must be set to `None`.
"""
time = Instance(datetime, allow_none=True)
time.__doc__ = """Set the valid time to be plotted as a datetime object.
If a forecast hour is to be plotted the time should be set to the valid future time, which
can be done using the `~datetime.datetime` and `~datetime.timedelta` objects
from the Python standard library.
"""
time_window = Instance(timedelta, default_value=timedelta(minutes=0), allow_none=True)
time_window.__doc__ = """Set a range to look for data to plot as a timedelta object.
If this parameter is set, it will subset the data provided to be within the time and plus
or minus the range value given. If there is more than one observation from a given station
then it will keep only the most recent one for plotting purposes. Default value is to have
no range. (optional)
"""
fields = List(Unicode())
fields.__doc__ = """Name of the scalar or symbol fields to be plotted.
List of parameters to be plotted around station plot (e.g., temperature, dewpoint, skyc).
"""
locations = List(default_value=['C'])
locations.__doc__ = """List of strings for scalar or symbol field plotting locations.
List of parameters locations for plotting parameters around the station plot (e.g.,
NW, NE, SW, SE, W, C). (optional)
"""
formats = List(default_value=[None])
formats.__doc__ = """List of the scalar, symbol, and text field data formats. (optional)
List of scalar parameters formmaters or mapping values (if symbol) for plotting text and/or
symbols around the station plot (e.g., for pressure variable
```lambda v: format(10 * v, '.0f')[-3:]```).
For symbol mapping the following options are available to be put in as a string:
current_weather, sky_cover, low_clouds, mid_clouds, high_clouds, and pressure_tendency.
For plotting text, use the format setting of 'text'.
"""
colors = List(Unicode(), default_value=['black'])
colors.__doc__ = """List of the scalar and symbol field colors.
List of strings that represent the colors to be used for the variable being plotted.
(optional)
"""
vector_field = List(default_value=[None], allow_none=True)
vector_field.__doc__ = """List of the vector field to be plotted.
List of vector components to combined and plotted from the center of the station plot
(e.g., wind components). (optional)
"""
vector_field_color = Unicode('black', allow_none=True)
vector_field_color.__doc__ = """String color name to plot the vector. (optional)"""
vector_field_length = Int(default_value=None, allow_none=True)
vector_field_length.__doc__ = """Integer value to set the length of the plotted vector.
(optional)
"""
reduce_points = Float(default_value=0)
reduce_points.__doc__ = """Float to reduce number of points plotted. (optional)"""
plot_units = List(default_value=[None], allow_none=True)
plot_units.__doc__ = """A list of the desired units to plot the fields in.
Setting this attribute will convert the units of the field variable to the given units for
plotting using the MetPy Units module, provided that units are attached to the DataFrame.
"""
vector_plot_units = Unicode(default_value=None, allow_none=True)
vector_plot_units.__doc__ = """The desired units to plot the vector field in.
Setting this attribute will convert the units of the field variable to the given units for
plotting using the MetPy Units module, provided that units are attached to the DataFrame.
"""
[docs] def clear(self):
"""Clear the plot.
Resets all internal state and sets need for redraw.
"""
if getattr(self, 'handle', None) is not None:
self.handle.ax.cla()
self.handle = None
self._need_redraw = True
@observe('parent')
def _parent_changed(self, _):
"""Handle setting the parent object for the plot."""
self.clear()
@observe('fields', 'level', 'time', 'vector_field', 'time_window')
def _update_data(self, _=None):
"""Handle updating the internal cache of data.
Responds to changes in various subsetting parameters.
"""
self._obsdata = None
self.clear()
# Can't be a Traitlet because notifications don't work with arrays for traits
# notification never happens
@property
def data(self):
"""Pandas dataframe that contains the fields to be plotted."""
return self._data
@data.setter
def data(self, val):
self._data = val
self._update_data()
@property
def name(self):
"""Generate a name for the plot."""
ret = ''
ret += ' and '.join(f for f in self.fields)
if self.level is not None:
ret += f'@{self.level:d}'
return ret
@property
def obsdata(self):
"""Return the internal cached data."""
if getattr(self, '_obsdata', None) is None:
# Use a copy of data so we retain all of the original data passed in unmodified
data = self.data
# Subset for a particular level if given
if self.level is not None:
mag = getattr(self.level, 'magnitude', self.level)
data = data[data.pressure == mag]
# Subset for our particular time
if self.time is not None:
# If data are not currently indexed by time, we need to do so choosing one of
# the columns we're looking for
if not isinstance(data.index, pd.DatetimeIndex):
time_vars = ['valid', 'time', 'valid_time', 'date_time', 'date']
dim_times = [time_var for time_var in time_vars if
time_var in list(self.data)]
if not dim_times:
raise AttributeError(
'Time variable not found. Valid variable names are:'
f'{time_vars}')
data = data.set_index(dim_times[0])
if not isinstance(data.index, pd.DatetimeIndex):
# Convert our column of interest to a datetime
data = data.reset_index()
time_index = pd.to_datetime(data[dim_times[0]])
data = data.set_index(time_index)
# Works around the fact that traitlets 4.3 insists on sending us None by
# default because timedelta(0) is Falsey.
window = timedelta(minutes=0) if self.time_window is None else self.time_window
# Indexes need to be properly sorted for the slicing below to work; the
# error you get if that's not the case really convoluted, which is why
# we don't rely on users doing it.
data = data.sort_index()
data = data[self.time - window:self.time + window]
# Look for the station column
stn_vars = ['station', 'stn', 'station_id', 'stid']
dim_stns = [stn_var for stn_var in stn_vars if stn_var in list(self.data)]
if not dim_stns:
raise AttributeError('Station variable not found. Valid variable names are: '
f'{stn_vars}')
else:
dim_stn = dim_stns[0]
# Make sure we only use one observation per station
self._obsdata = data.groupby(dim_stn).tail(1)
return self._obsdata
@property
def plotdata(self):
"""Return the data for plotting.
The data arrays, x coordinates, and y coordinates.
"""
plot_data = {}
for dim_name in list(self.obsdata):
if dim_name.find('lat') != -1:
lat = self.obsdata[dim_name]
elif dim_name.find('lon') != -1:
lon = self.obsdata[dim_name]
else:
plot_data[dim_name] = self.obsdata[dim_name]
return lon.values, lat.values, plot_data
[docs] def draw(self):
"""Draw the plot."""
if self._need_redraw:
if getattr(self, 'handle', None) is None:
self._build()
self._need_redraw = False
@observe('colors', 'formats', 'locations', 'reduce_points', 'vector_field_color')
def _set_need_rebuild(self, _):
"""Handle changes to attributes that need to regenerate everything."""
# Because matplotlib doesn't let you just change these properties, we need
# to trigger a clear and re-call of contour()
self.clear()
def _build(self):
"""Build the plot by calling needed plotting methods as necessary."""
lon, lat, data = self.plotdata
# Use the cartopy map projection to transform station locations to the map and
# then refine the number of stations plotted by setting a radius
if self.parent._proj_obj == ccrs.PlateCarree():
scale = 1.
else:
scale = 100000.
point_locs = self.parent._proj_obj.transform_points(ccrs.PlateCarree(), lon, lat)
subset = reduce_point_density(point_locs, self.reduce_points * scale)
self.handle = StationPlot(self.parent.ax, lon[subset], lat[subset], clip_on=True,
transform=ccrs.PlateCarree(), fontsize=10)
for i, ob_type in enumerate(self.fields):
field_kwargs = {}
if len(self.locations) > 1:
location = self.locations[i]
else:
location = self.locations[0]
if len(self.colors) > 1:
field_kwargs['color'] = self.colors[i]
else:
field_kwargs['color'] = self.colors[0]
if len(self.formats) > 1:
field_kwargs['formatter'] = self.formats[i]
else:
field_kwargs['formatter'] = self.formats[0]
if len(self.plot_units) > 1:
field_kwargs['plot_units'] = self.plot_units[i]
else:
field_kwargs['plot_units'] = self.plot_units[0]
if hasattr(self.data, 'units') and (field_kwargs['plot_units'] is not None):
parameter = units.Quantity(data[ob_type][subset].values,
self.data.units[ob_type])
else:
parameter = data[ob_type][subset]
if field_kwargs['formatter'] is not None:
mapper = getattr(wx_symbols, str(field_kwargs['formatter']), None)
if mapper is not None:
field_kwargs.pop('formatter')
self.handle.plot_symbol(location, parameter, mapper, **field_kwargs)
else:
if self.formats[i] == 'text':
self.handle.plot_text(location, parameter, color=field_kwargs['color'])
else:
self.handle.plot_parameter(location, parameter, **field_kwargs)
else:
field_kwargs.pop('formatter')
self.handle.plot_parameter(location, parameter, **field_kwargs)
if self.vector_field[0] is not None:
vector_kwargs = {}
vector_kwargs['color'] = self.vector_field_color
vector_kwargs['plot_units'] = self.vector_plot_units
if hasattr(self.data, 'units') and (vector_kwargs['plot_units'] is not None):
u = units.Quantity(data[self.vector_field[0]][subset].values,
self.data.units[self.vector_field[0]])
v = units.Quantity(data[self.vector_field[1]][subset].values,
self.data.units[self.vector_field[1]])
else:
vector_kwargs.pop('plot_units')
u = data[self.vector_field[0]][subset]
v = data[self.vector_field[1]][subset]
if self.vector_field_length is not None:
vector_kwargs['length'] = self.vector_field_length
self.handle.plot_barb(u, v, **vector_kwargs)