def get_cmap_levels_auto(vmin, vmax, num_per_mag=10): """Initiate pseudo-log discrete colormap levels Note ---- This is a beta version and aims to Parameters ---------- vmin : float lower end of colormap (e.g. minimum value of data) vmax : float upper value of colormap (e.g. maximum value of data) """ high = float(exponent(vmax)) low = -3. if vmin == 0 else float(exponent(vmin)) lvls =[] if 1%vmax*10**(-high) == 0: low+=1 high-=1 for mag in range(int(low), int(high)): lvls.extend(np.linspace(1, 10, num_per_mag-1, endpoint=0)*10**(mag)) lvls.extend(np.linspace(1, vmax*10**(-high), num_per_mag, endpoint=1)*10**(high)) return lvls
def get_cmap_ticks_auto(lvls, num_per_mag=3): """Compute cmap ticks based on cmap levels The cmap levels may be computed automatically using :func:`get_cmap_levels_auto`. Parameters ---------- lvls : list list containing colormap levels num_per_mag : int desired number of ticks per magnitude """ low = exponent(lvls[1]) # second entry (first is 0) vmax = lvls[-1] high = exponent(vmax) ticks = [0] if 1%vmax*10**(-high) == 0: for mag in range(low, high-1): ticks.extend(np.linspace(1, 10, num_per_mag, endpoint=0)*10**(mag)) ticks.extend(np.linspace(1, 10, num_per_mag+1, endpoint=1)*10**(high-1)) else: for mag in range(low, high): ticks.extend(np.linspace(1, 10, num_per_mag, endpoint=0)*10**(mag)) ticks.extend(np.linspace(1, vmax*10**(-high), num_per_mag+1, endpoint=1)*10**(high)) return ticks
def calc_pseudolog_cmaplevels(vmin, vmax, add_zero=False): """Initiate pseudo-log discrete colormap levels Parameters ---------- vmin : float lower end of colormap (e.g. minimum value of data) vmax : float upper value of colormap (e.g. maximum value of data) add_zero : bool if True, the lower bound is set to 0 (irrelevant if vmin is 0). Returns ------- list list containing boundary array for discrete colormap (e.g. using BoundaryNorm) Example ------- >>> vmin, vmax = 0.02, 0.75 >>> vals = calc_pseudolog_cmaplevels(vmin, vmax, num_per_mag=10, add_zero=True) >>> for val in vals: print("%.4f" %val) 0.0000 0.0100 0.0126 0.0158 0.0200 0.0251 0.0316 0.0398 0.0501 0.0631 0.0794 0.1000 """ if vmin < 0: vmin = 0 if vmin == 0: vmin = 1 * 10.0**(exponent(vmax) - 2) if not add_zero: add_zero = True elif vmax < vmin: raise ValueError("Error: vmax must exceed vmin") bounds = [0] if add_zero else [] low = float(exponent(vmin)) high = float(exponent(vmax)) bounds.extend(np.arange(np.floor(vmin * 10**(-low)), 10, 1) * 10.0**(low)) if low == high: return bounds for mag in range(int(low + 1), int(high)): bounds.extend(np.linspace(1, 9, 9) * 10**(mag)) bounds.extend(np.arange(1, np.ceil(vmax * 10**(-high)), 1) * 10.0**(high)) bounds.append(vmax) return bounds
def _format_annot_heatmap(annot, annot_fmt_rows, annot_fmt_exceed): _annot = [] if not isinstance(annot_fmt_rows, list): annot_fmt_rows = [] for row in annot: mask = row[~np.isnan(row)] if len(mask) == 0: #all NaN annot_fmt_rows.append('') continue mask = mask[mask != 0] exps = exponent(mask) minexp = exps.min() if minexp < -3: annot_fmt_rows.append('.1E') elif minexp < 0: annot_fmt_rows.append('.{}f'.format(-minexp + 1)) elif minexp in [0, 1]: annot_fmt_rows.append('.1f') else: annot_fmt_rows.append('.0f') if isinstance(annot_fmt_exceed, list): exceed_val, exceed_fmt = annot_fmt_exceed else: exceed_val, exceed_fmt = None, None for i, row in enumerate(annot): rowfmt = annot_fmt_rows[i] if rowfmt == '': row_fmt = [''] * len(row) else: #numdigits = -exps.min() row_fmt = [] #lowest = np.min(row) #exp = exponent(lowest) for val in row: if np.isnan(val): valstr = '' else: #exp = exps(i) #fmt = '.{}f'.format(numdigits) if exceed_val is not None and val > exceed_val: valstr = format(val, exceed_fmt) else: valstr = format(val, rowfmt) row_fmt.append(valstr) _annot.append(row_fmt) annot = np.asarray(_annot) return annot, annot_fmt_rows
def set_map_ticks(ax, xticks=None, yticks=None, add_x=True, add_y=True): """Set or update ticks in instance of GeoAxes object (cartopy) Parameters ---------- ax : cartopy.GeoAxes map axes instance xticks : iterable, optional ticks of x-axis (longitudes) yticks : iterable, optional ticks of y-axis (latitudes) add_x : bool if True, x-axis labels are added add_y : bool if True, y-axis labels are added Returns ------- cartopy.GeoAxes modified axes instance """ lonleft, lonright = ax.get_xlim() digits = 2 - exponent(lonleft) digits = 0 if digits < 0 else digits tick_format = '.%df' %digits if not xticks: num_lonticks = 7 if lonleft == -lonright else 6 xticks = linspace(lonleft, lonright, num_lonticks) if not yticks: latleft, latright = ax.get_ylim() num_latticks = 7 if latleft == - latright else 6 yticks = linspace(latleft, latright, num_latticks) ax.set_xticks(xticks, crs=ccrs.PlateCarree()) ax.set_yticks(yticks, crs=ccrs.PlateCarree()) lon_formatter = LongitudeFormatter(number_format=tick_format, degree_symbol='', dateline_direction_label=True) lat_formatter = LatitudeFormatter(number_format=tick_format, degree_symbol='') ax.xaxis.set_major_formatter(lon_formatter) ax.yaxis.set_major_formatter(lat_formatter) return ax
def plot_scatter_aerocom(x_vals, y_vals, var_name=None, var_name_ref=None, x_name=None, y_name=None, start=None, stop=None, ts_type=None, unit=None, stations_ok=None, filter_name=None, lowlim_stats=None, highlim_stats=None, loglog=None, savefig=False, save_dir=None, save_name=None, ax=None, figsize=None, fontsize_base=10, marker='+', color='k', alpha=0.5, **kwargs): """Method that performs a scatter plot of data in AEROCOM format Parameters ---------- y_vals : ndarray 1D array (or list) of model data points (y-axis) x_vals : ndarray 1D array (or list) of observation data points (x-axis) var_name : :obj:`str`, optional name of variable that is plotted var_name_ref : :obj:`str`, optional name of variable of reference data x_name : :obj:`str`, optional Name of observation network y_name : :obj:`str`, optional Name / ID of model start : :obj:`str` or :obj`datetime` or similar start time of data stop : :obj:`str` or :obj`datetime` or similar stop time of data Returns ------- axes instance of :class:`matplotlib.axes` """ if isinstance(y_vals, list): y_vals = np.asarray(y_vals) if isinstance(x_vals, list): x_vals = np.asarray(x_vals) try: VARS = const.VARS[var_name] except: VARS = const.VARS.DEFAULT if loglog is None: loglog = VARS.scat_loglog xlim = VARS['scat_xlim'] ylim = VARS['scat_ylim'] if xlim is None or ylim is None: low = np.min([np.nanmin(x_vals), np.nanmin(y_vals)]) high = np.max([np.nanmax(x_vals), np.nanmax(y_vals)]) xlim = [low, high] ylim = [low, high] if ax is None: if figsize is None: figsize = (10,8) fig, ax = plt.subplots(figsize=figsize) if var_name is None: var_name = 'n/d' statistics = calc_statistics(y_vals, x_vals, lowlim_stats, highlim_stats) if loglog: ax.loglog(x_vals, y_vals, ls='none', color=color, marker=marker, alpha=alpha, **kwargs) else: ax.plot(x_vals, y_vals, ls='none', color=color, marker=marker, alpha=alpha, **kwargs) try: title = start_stop_str(start, stop, ts_type) if ts_type is not None: title += ' ({})'.format(ts_type) except: title = '' if not loglog: xlim[0] = 0 ylim[0] = 0 elif any(x[0] < 0 for x in [xlim, ylim]): low = 10**(float(exponent(abs(np.nanmin(y_vals))) - 1)) xlim[0] = low ylim[0] = low ax.set_xlim(xlim) ax.set_ylim(ylim) xlbl = '{}'.format(x_name) if var_name_ref is not None: xlbl += ' ({})'.format(var_name_ref) ax.set_xlabel(xlbl, fontsize=fontsize_base+4) ax.set_ylabel('{}'.format(y_name), fontsize=fontsize_base+4) ax.set_title(title, fontsize=fontsize_base+4) ax.xaxis.set_major_formatter(ScalarFormatter()) ax.yaxis.set_major_formatter(ScalarFormatter()) ax.tick_params(labelsize=fontsize_base) ax.plot(xlim, ylim, '-', color='grey') xypos = {'var_info' : (0.01, .95), 'refdata_mean' : (0.01, 0.90), 'data_mean' : (0.01, 0.86), 'nmb' : (0.01, 0.82), 'mnmb' : (0.35, 0.82), 'R' : (0.01, 0.78), 'rms' : (0.35, 0.78), 'R_kendall' : (0.01, 0.74), 'fge' : (0.35, 0.74), 'ts_type' : (0.8, 0.1), 'filter_name' : (0.8, 0.06)} var_str = var_name# + VARS.unit_str _ndig = abs(exponent(statistics['refdata_mean']) - 2) if unit is None: unit = 'N/D' if not str(unit) in ['1', 'no_unit']: var_str += ' [{}]'.format(unit) ax.annotate("{} #: {} # st: {}".format(var_str, statistics['num_valid'], stations_ok), xy=xypos['var_info'], xycoords='axes fraction', fontsize=fontsize_base+4, color='red') ax.annotate('Mean (x-data): {:.{}f}; Rng: [{:.{}f}, {:.{}f}]' .format(statistics['refdata_mean'], _ndig, np.nanmin(x_vals),_ndig, np.nanmax(x_vals), _ndig), xy=xypos['refdata_mean'], xycoords='axes fraction', fontsize=fontsize_base, color='red') ax.annotate('Mean (y-data): {:.{}f}; Rng: [{:.{}f}, {:.{}f}]' .format(statistics['data_mean'], _ndig, np.nanmin(y_vals),_ndig, np.nanmax(y_vals), _ndig), xy=xypos['data_mean'], xycoords='axes fraction', fontsize=fontsize_base, color='red') ax.annotate('NMB: {:.1f}%'.format(statistics['nmb']*100), xy=xypos['nmb'], xycoords='axes fraction', fontsize=fontsize_base, color='red') ax.annotate('MNMB: {:.1f}%'.format(statistics['mnmb']*100), xy=xypos['mnmb'], xycoords='axes fraction', fontsize=fontsize_base, color='red') ax.annotate('R (Pearson): {:.3f}'.format(statistics['R']), xy=xypos['R'], xycoords='axes fraction', fontsize=fontsize_base, color='red') ax.annotate('RMS: {:.3f}'.format(statistics['rms']), xy=xypos['rms'], xycoords='axes fraction', fontsize=fontsize_base, color='red') ax.annotate('R (Kendall): {:.3f}'.format(statistics['R_kendall']), xy=xypos['R_kendall'], xycoords='axes fraction', fontsize=fontsize_base, color='red') ax.annotate('FGE: {:.1f}'.format(statistics['fge']), xy=xypos['fge'], xycoords='axes fraction', fontsize=fontsize_base, color='red') # right lower part ax.annotate('{}'.format(ts_type), xy=xypos['ts_type'], xycoords='axes fraction', ha='center', fontsize=fontsize_base, color='black') ax.annotate('{}'.format(filter_name), xy=xypos['filter_name'], xycoords='axes fraction', ha='center', fontsize=fontsize_base, color='black') ax.set_aspect('equal') if savefig: if any([x is None for x in (save_dir, save_name)]): raise IOError fig.savefig(os.path.join(save_dir, save_name)) return ax
def test_exponent(): """Test method :func:`exponent` of :mod:`pyaerocom.utils`""" nominal = [-2, 0, 2] vals = [utils.exponent(.01), utils.exponent(4), utils.exponent(234)] npt.assert_array_equal(nominal, vals)
def plot_griddeddata_on_map(data, lons=None, lats=None, var_name=None, unit=None, xlim=(-180, 180), ylim=(-90, 90), vmin=None, vmax=None, add_zero=False, c_under=None, c_over=None, log_scale=True, discrete_norm=True, cbar_levels=None, cbar_ticks=None, add_cbar=True, cmap=None, cbar_ticks_sci=False, color_theme=COLOR_THEME, **kwargs): """Make a plot of gridded data onto a map Note ---- This is a lowlevel plotting method Parameters ---------- data : ndarray 2D data array lons : ndarray longitudes of data lats : ndarray latitudes of data var_name : :obj:`str`, optional name of variable that is plotted xlim : tuple 2-element tuple specifying plotted longitude range ylim : tuple 2-element tuple specifying plotted latitude range vmin : :obj:`float`, optional lower value of colorbar range vmax : :obj:`float`, optional upper value of colorbar range add_zero : bool if True and vmin is not 0, then, the colorbar is extended down to 0. This may be used, e.g. for logarithmic scales that should include 0. c_under : :obj:`float`, optional colour of data values smaller than ``vmin`` c_over : :obj:`float`, optional colour of data values exceeding ``vmax`` log_scale : bool if True, the value to color mapping is done in a pseudo log scale (see :func:`get_cmap_levels_auto` for implementation) discrete_norm : bool if True, color mapping will be subdivided into discrete intervals cbar_levels : iterable, optional discrete colorbar levels. Will be computed automatically, if None (and applicable) cbar_ticks : iterable, optional ticks of colorbar levels. Will be computed automatically, if None (and applicable) Returns ------- fig matplotlib figure instance containing plot result. Use ``fig.axes[0]`` to access the map axes instance (e.g. to modify the title or lon / lat range, etc.) """ kwargs['contains_cbar'] = True ax = init_map(xlim, ylim, color_theme=color_theme, **kwargs) fig = ax.figure from pyaerocom.griddeddata import GriddedData if isinstance(data, GriddedData): if not data.has_latlon_dims: from pyaerocom.exceptions import DataDimensionError raise DataDimensionError('Input data needs to have latitude and ' 'longitude dimension') if not data.ndim == 2: if not data.ndim == 3 or not 'time' in data.dimcoord_names: raise DataDimensionError( 'Input data needs to be 2 dimensional ' 'or 3D with time being the 3rd ' 'dimension') data.reorder_dimensions_tseries() data = data[0] lons = data.longitude.points lats = data.latitude.points data = data.grid.data elif not isinstance(data, np.ndarray) or not data.ndim == 2: raise IOError("Need 2D numpy array") elif not isinstance(lats, np.ndarray) or not isinstance(lons, np.ndarray): raise ValueError('Missing lats or lons input') if isinstance(data, np.ma.MaskedArray): sh = data.shape if data.mask.sum() == sh[0] * sh[1]: raise ValueError('All datapoints in input data (masked array) are ' 'invalid') _loc = ax.bbox._bbox try: ax_cbar = fig.add_axes( [_loc.x1 + .02, _loc.y0, .02, _loc.y1 - _loc.y0]) except Exception as e: ax_cbar = fig.add_axes([0.91, 0.12, .02, .8]) print(repr(e)) X, Y = meshgrid(lons, lats) dmin = np.nanmin(data) dmax = np.nanmax(data) if any([np.isnan(x) for x in [dmin, dmax]]): raise ValueError('Cannot plot map of data: all values are NaN') elif dmin == dmax: raise ValueError('Minimum value in data equals maximum value: ' '{}'.format(dmin)) if vmin is None: vmin = dmin else: if vmin < 0 and log_scale: log_scale = False if vmax is None: vmax = dmax bounds = None if cbar_levels: #user provided levels of colorbar explicitely if vmin is not None or vmax is not None: raise ValueError('Please provide either vmin/vmax OR cbar_levels') bounds = list(cbar_levels) low, high = bounds[0], bounds[-1] if add_zero and low > 0: bounds.insert(0, 0) # insert zero bound if cmap is None or isinstance(cmap, str): cmap = get_cmap_maps_aerocom(color_theme, low, high) norm = BoundaryNorm(boundaries=bounds, ncolors=cmap.N, clip=False) else: if log_scale: # no negative values allowed if vmin < 0: vmin = data[data > 0].min() if c_under is None: #special case, set c_under to indicate that there is values below 0 c_under = 'r' if cmap is None or isinstance(cmap, str): cmap = get_cmap_maps_aerocom(color_theme, vmin, vmax) if discrete_norm: #to compute upper range of colour range, round up vmax exp = float(exponent(vmax) - 1) vmax_colors = ceil(vmax / 10**exp) * 10**exp bounds = calc_pseudolog_cmaplevels(vmin=vmin, vmax=vmax_colors, add_zero=add_zero) norm = BoundaryNorm(boundaries=bounds, ncolors=cmap.N, clip=False) else: norm = LogNorm(vmin=vmin, vmax=vmax, clip=True) else: if cmap is None or isinstance(cmap, str): cmap = get_cmap_maps_aerocom(color_theme, vmin, vmax) norm = Normalize(vmin=vmin, vmax=vmax) cbar_extend = "neither" if c_under is not None: cmap.set_under(c_under) cbar_extend = "min" if bounds is not None: bounds.insert(0, bounds[0] - bounds[1]) if c_over is not None: cmap.set_over(c_over) if bounds is not None: bounds.append(bounds[-1] + bounds[-2]) if cbar_extend == "min": cbar_extend = "both" else: cbar_extend = "max" disp = ax.pcolormesh(X, Y, data, cmap=cmap, norm=norm) # ============================================================================= # fmt = None # if bounds is not None: # print(bounds) # min_mag = -exponent(bounds[1]) # min_mag = 0 if min_mag < 0 else min_mag # print(min_mag) # #fmt = "%." + str(min_mag) + "f" # ============================================================================= if add_cbar: cbar = fig.colorbar(disp, cmap=cmap, norm=norm, boundaries=bounds, extend=cbar_extend, cax=ax_cbar) if var_name is not None: var_str = var_name # + VARS.unit_str if unit is not None: if not str(unit) in ['1', 'no_unit']: var_str += ' [{}]'.format(unit) cbar.set_label(var_str) if cbar_ticks: cbar.set_ticks(cbar_ticks) if cbar_ticks_sci: lbls = [] for lbl in cbar.ax.get_yticklabels(): tstr = lbl.get_text() if bool(tstr): lbls.append('{:.1e}'.format(float(tstr))) else: lbls.append('') cbar.ax.set_yticklabels(lbls) return fig
def test_exponent(inputval, desired): """Test method :func:`exponent` of :mod:`pyaerocom.utils`""" assert mu.exponent(inputval) == desired
def plot_griddeddata_on_map(data, lons, lats, var_name=None, xlim=(-180, 180), ylim=(-90, 90), vmin=None, vmax=None, add_zero=False, c_under=None, c_over=None, log_scale=True, discrete_norm=True, cbar_levels=None, cbar_ticks=None, color_theme=COLOR_THEME, **kwargs): """Make a plot of gridded data onto a map Note ---- This is a lowlevel plotting method Parameters ---------- data : ndarray 2D data array lons : ndarray longitudes of data lats : ndarray latitudes of data var_name : :obj:`str`, optional name of variable that is plotted xlim : tuple 2-element tuple specifying plotted longitude range ylim : tuple 2-element tuple specifying plotted latitude range vmin : :obj:`float`, optional lower value of colorbar range vmax : :obj:`float`, optional upper value of colorbar range add_zero : bool if True and vmin is not 0, then, the colorbar is extended down to 0. This may be used, e.g. for logarithmic scales that should include 0. c_under : :obj:`float`, optional colour of data values smaller than ``vmin`` c_over : :obj:`float`, optional colour of data values exceeding ``vmax`` log_scale : bool if True, the value to color mapping is done in a pseudo log scale (see :func:`get_cmap_levels_auto` for implementation) discrete_norm : bool if True, color mapping will be subdivided into discrete intervals cbar_levels : iterable, optional discrete colorbar levels. Will be computed automatically, if None (and applicable) cbar_ticks : iterable, optional ticks of colorbar levels. Will be computed automatically, if None (and applicable) Returns ------- fig matplotlib figure instance containing plot result. Use ``fig.axes[0]`` to access the map axes instance (e.g. to modify the title or lon / lat range, etc.) """ kwargs['contains_cbar'] = True ax = init_map(xlim, ylim, color_theme=color_theme, **kwargs) fig = ax.figure if not isinstance(data, np.ndarray) or not data.ndim == 2: raise IOError("Need 2D numpy array") elif isinstance(data, np.ma.MaskedArray): sh = data.shape if data.mask.sum() == sh[0] * sh[1]: raise ValueError('All datapoints in input data (masked array) are ' 'invalid') _loc = ax.bbox._bbox try: ax_cbar = fig.add_axes( [_loc.x1 + .02, _loc.y0, .02, _loc.y1 - _loc.y0]) except Exception as e: ax_cbar = fig.add_axes([0.91, 0.12, .02, .8]) print(repr(e)) X, Y = meshgrid(lons, lats) cmap = copy(color_theme.cmap_map) if vmin is None: vmin = data.min() if vmax is None: vmax = data.max() bounds = None if cbar_levels: #user provided levels of colorbar explicitely bounds = cbar_levels norm = BoundaryNorm(boundaries=bounds, ncolors=cmap.N, clip=False) else: if log_scale and discrete_norm: #to compute upper range of colour range, round up vmax exp = float(exponent(vmax) - 1) vmax_colors = ceil(vmax / 10**exp) * 10**exp bounds = calc_pseudolog_cmaplevels(vmin=vmin, vmax=vmax_colors, add_zero=add_zero) norm = BoundaryNorm(boundaries=bounds, ncolors=cmap.N, clip=False) elif log_scale: norm = LogNorm(vmin=vmin, vmax=vmax, clip=True) else: norm = None cbar_extend = "neither" if c_under is not None: cmap.set_under(c_under) cbar_extend = "min" if bounds is not None: bounds.insert(0, bounds[0] - bounds[1]) if c_over is not None: cmap.set_over(c_over) if bounds is not None: bounds.append(bounds[-1] + bounds[-2]) if cbar_extend == "min": cbar_extend = "both" else: cbar_extend = "max" disp = ax.pcolormesh(X, Y, data, cmap=cmap, norm=norm) min_mag = -exponent(bounds[1]) min_mag = 0 if min_mag < 0 else min_mag cbar = fig.colorbar(disp, cmap=cmap, norm=norm, boundaries=bounds, extend=cbar_extend, cax=ax_cbar, format="%." + str(min_mag) + "f") if var_name is not None: cbar.set_label(var_name) if cbar_ticks: cbar.set_ticks(cbar_ticks) return fig