# 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."""
from datetime import datetime
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
from traitlets import (Any, Bool, Float, HasTraits, Instance, Int, List, observe, Tuple,
Unicode, Union)
from . import cartopy_utils, ctables
from ..cbook import is_string_like
from ..package_tools import Exporter
from ..units import units
exporter = Exporter(globals())
_projections = {'lcc': ccrs.LambertConformal(central_latitude=40, central_longitude=-100,
standard_parallels=[30, 60]),
'ps': ccrs.NorthPolarStereo(central_longitude=-100),
'mer': ccrs.Mercator()}
_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)
}
class Panel(HasTraits):
"""Draw one or more plots."""
[docs]@exporter.export
class PanelContainer(HasTraits):
"""Hold multiple panels of plots."""
size = Union([Tuple(Int(), Int()), Instance(type(None))], default_value=None)
panels = List(Instance(Panel))
@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
try:
self.figure.canvas.flush_events()
except NotImplementedError:
pass
[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."""
self.draw()
self.figure.savefig(*args, **kwargs)
[docs] def show(self):
"""Show the constructed graphic on the screen."""
self.draw()
self.figure.show()
[docs]@exporter.export
class MapPanel(Panel):
"""Draw one or more plots on a map."""
parent = Instance(PanelContainer)
layout = Tuple(Int(), Int(), Int(), default_value=(1, 1, 1))
plots = List(Any())
_need_redraw = Bool(default_value=True)
area = Union([Unicode(), Tuple(Float(), Float(), Float(), Float())], allow_none=True,
default_value=None)
projection = Union([Unicode(), Instance(ccrs.Projection)], default_value='data')
layers = List(Union([Unicode(), Instance(cfeature.Feature)]), default_value=['coastline'])
title = Unicode()
@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 is_string_like(self.projection):
if self.projection == 'data':
return self.plots[0].griddata.metpy.cartopy_crs
else:
return _projections[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 is_string_like(item):
item = item.upper()
try:
scaler = cfeature.AdaptiveScaler('110m', (('50m', 50), ('10m', 15)))
feat = getattr(cfeature, item).with_scale(scaler)
except AttributeError:
scaler = cfeature.AdaptiveScaler('20m', (('5m', 5), ('500k', 1)))
feat = getattr(cartopy_utils, item).with_scale(scaler)
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:
# 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)
# 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 is_string_like(self.area):
area = _areas[self.area]
# Otherwise, assume we have a tuple to use as the extent
else:
area = self.area
self.ax.set_extent(area, ccrs.PlateCarree())
# Use the set title or generate one.
title = self.title or ', '.join(plot.name for plot in self.plots)
self.ax.set_title(title)
self._need_redraw = False
[docs]@exporter.export
class Plot2D(HasTraits):
"""Represent plots of 2D data."""
parent = Instance(Panel)
_need_redraw = Bool(default_value=True)
field = Unicode()
level = Union([Int(allow_none=True, default_value=None), Instance(units.Quantity)])
time = Instance(datetime, allow_none=True)
colormap = Unicode(allow_none=True, default_value=None)
image_range = Union([Tuple(Int(allow_none=True), Int(allow_none=True)),
Instance(plt.Normalize)], default_value=(None, None))
@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:
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
@observe('parent')
def _parent_changed(self, _):
"""Handle setting the parent object for the plot."""
self.clear()
@observe('field', '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):
"""Access the current data subset."""
return self._data
@data.setter
def data(self, val):
self._data = val
self._update_data()
@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
self._griddata = data.metpy.sel(**subset).squeeze()
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
if 'degree' in x.units:
import numpy as np
x, y, _ = self.griddata.metpy.cartopy_crs.transform_points(ccrs.PlateCarree(),
*np.meshgrid(x, y)).T
x = x[:, 0] % 360
y = y[0, :]
return x, y, self.griddata
@property
def name(self):
"""Generate a name for the plot."""
ret = self.field
if self.level is not None:
ret += '@{:d}'.format(self.level)
return ret
[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 ImagePlot(Plot2D):
"""Represent an image plot."""
@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
@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(Plot2D):
"""Represent a contour plot."""
linecolor = Unicode('black')
linewidth = Int(2)
contours = Union([List(Float()), Int()], default_value=25)
@observe('contours', 'linecolor', 'linewidth')
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()
[docs] def clear_handle(self):
"""Clear the handle to the plot instance."""
for col in self.handle.collections:
col.remove()
self.handle = None
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,
transform=imdata.metpy.cartopy_crs)