Source code for pygimli.viewer.mpl.colorbar

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Define special colorbar behavior."""
import numpy as np
from packaging import version

import pygimli as pg
from . import saveFigure, updateAxes
from . utils import prettyFloat
from pygimli.core.logger import renameKwarg


[docs] def autolevel(z, nLevs, logScale=None, zMin=None, zMax=None): """Create nLevs bins for the data array z based on matplotlib ticker. Examples -------- >>> import numpy as np >>> from pygimli.viewer.mpl import autolevel >>> x = np.linspace(1, 10, 100) >>> autolevel(x, 3) array([ 1. , 5.5, 10. ]) >>> x = np.linspace(1, 1000, 100) >>> autolevel(x, 4, logScale=True) array([ 1., 10., 100., 1000.]) """ import matplotlib.ticker as ticker locator = None if logScale: locator = ticker.LogLocator() else: locator = ticker.LinearLocator(numticks=nLevs) # locator = ticker.MaxNLocator(nBins=nLevs + 1) # locator = ticker.MaxNLocator(nBins='auto') if zMin is None: zMin = min(z) if logScale is True and zMin < 3e-16: zMin = pg.core.epsilon(abs(z)) if zMax is None: zMax = max(z) # print('autolevel: ', z) # print('autolevel:', zMin, zMax, min(z), max(z)) if logScale: ## logscale ticker behaves weird levs = np.geomspace(zMin, zMax, nLevs) else: levs = locator.tick_values(zMin, zMax) #print(levs) return levs
[docs] def cmapFromName(cmapname='jet', ncols=256, bad=None, **kwargs): """Get a colormap either from name or from keyworld list. See http://matplotlib.org/examples/color/colormaps_reference.html Parameters ---------- cmapname : str Name for the colormap. ncols : int Amount of colors. bad : [r,g,b,a] Default color for bad values [nan, inf] [white] ** kwargs : cMap : str Name for the colormap cmap : str colormap name (old) Returns ------- cMap: matplotlib Colormap """ if not isinstance(cmapname, str): return cmapname import matplotlib as mpl if not bad: bad = [1.0, 1.0, 1.0, 0.0] renameKwarg('cmap', 'cMap', kwargs) cMapName = kwargs.pop('cMap', cmapname) cMap = None if cMapName is None: cMapName = 'viridis' if cMapName == 'b2r': pg.warn("Don't use manual b2r cMap, use MPL internal 'RdBu' instead.") cMap = "RdBu_r" else: try: import copy cMap = copy.copy(mpl.colormaps.get_cmap(cMapName).resampled(ncols)) except BaseException as e: pg.warn("Could not retrieve colormap ", cMapName, e) cMap.set_bad(bad) return cMap
[docs] def findAndMaskBestClim(dataIn, cMin=None, cMax=None, dropColLimitsPerc=5, logScale=False): """TODO Documentme.""" data = np.asarray(dataIn) if min(data) < 0: logScale = False if logScale: data = np.log10(data) xHist = np.histogram(data, bins=100)[1] if cMin is None: cMin = xHist[dropColLimitsPerc] if logScale: cMin = pow(10.0, cMin) if cMax is None: cMax = xHist[100 - dropColLimitsPerc] if logScale: cMax = pow(10.0, cMax) if logScale: data = pow(10.0, data) data[np.where(data < cMin)] = cMin data[np.where(data > cMax)] = cMax return data, cMin, cMax
[docs] def updateColorBar(cbar, gci=None, cMin=None, cMax=None, cMap=None, logScale=None, nCols=256, nLevs=5, levels=None, label=None, **kwargs): """Update colorbar values. Update limits and label of a given colorbar. Parameters ---------- cbar: matplotlib colorbar gci : matplotlib graphical instance cMin: float cMax: float cLog: bool cMap: matplotlib colormap nCols: int [None] Number of colors. If not set its number of levels. nLevs: int Number of color levels for the colorbar, can be different from the number of colors. levels: iterable Levels for the colorbar, overwrite nLevs. label: str Colorbar name. """ import matplotlib as mpl # pg._g('update colorbar: ', cbar, gci, cMin, cMax, cMap, # logScale, ', nCols:', nCols, nLevs, ', label:', label, levels) if gci is not None: if min(gci.get_array()) < 1e12: norm = mpl.colors.Normalize(vmin=min(gci.get_array()), vmax=max(gci.get_array())) gci.set_norm(norm) if cbar is not None: #cbar.on_mappable_changed(gci) cbar.update_normal(gci) mappable = gci else: mappable = cbar.mappable if levels is not None: nLevs = len(levels) if cMap is not None: if isinstance(cMap, str): if nCols is None: nCols = nLevs cMap = cmapFromName(cMap, ncols=nCols, bad=[1.0, 1.0, 1.0, 0.0] ) # does not work. why?? # cMap.set_under('yellow') # cMap.set_over('cyan') # mappable.get_norm().clip=False mappable.set_cmap(cMap) needLevelUpdate = False if levels is not None: cMin = levels[0] cMax = levels[-1] needLevelUpdate = True if cMin is not None or cMax is not None or nLevs is not None: needLevelUpdate = True if logScale is not None: needLevelUpdate = True if cMin is None: cMin = mappable.get_clim()[0] if cMax is None: cMax = mappable.get_clim()[1] if logScale: if cMin < 1e-12: cMin = min(filter(lambda _x: _x > 0.0, mappable.get_array())) norm = mpl.colors.LogNorm(vmin=cMin, vmax=cMax) else: norm = mpl.colors.Normalize(vmin=cMin, vmax=cMax) mappable.set_norm(norm) if needLevelUpdate is True: if cbar is not None: setCbarLevels(cbar, cMin, cMax, nLevs, levels) if label is not None: cbar.set_label(label) else: setCbarLevels(mappable, cMin, cMax, nLevs, levels) return cbar
[docs] def createColorBar(gci, orientation='horizontal', size=0.2, pad=None, **kwargs): """Create a Colorbar. Shortcut to create a matplotlib colorbar within the ax for a given patchset. The colorbar is stored in the axes object as __cBar__ to avoid duplicates. Parameters ---------- gci: matplotlib graphical instance orientation: string size: float pad: float **kwargs : onlyColorSet: bool (False) If set to true, only the gci aka mappable values are changed and no colorBar will be created. Forwarded to updateColorBar """ #pg._y('createColorBar', kwargs) from mpl_toolkits.axes_grid1 import make_axes_locatable cbarTarget = pg.plt cax = None divider = None # if hasattr(patches, 'figure'): # cbarTarget = patches.figure ax = kwargs.pop('ax', None) if ax is None: try: if hasattr(gci, 'axes'): ax = gci.axes elif hasattr(gci, 'get_axes'): ax = gci.get_axes() elif hasattr(gci, 'ax'): # deprecated since MPL 3.3 ax = gci.ax except: pass cbar = None if hasattr(ax, '__cBar__'): cbar = ax.__cBar__ #pg._y('update', kwargs) updateColorBar(cbar, gci, **kwargs) else: if kwargs.pop('onlyColorSet', False) == False: # pg._y(kwargs) divider = make_axes_locatable(ax) if divider: if orientation == 'horizontal': if pad is None: pad = 0.5 cax = divider.append_axes("bottom", size=size, pad=pad) else: if pad is None: pad = 0.1 cax = divider.append_axes("right", size=size, pad=pad) cbar = cbarTarget.colorbar(gci, cax=cax, orientation=orientation) #store the cbar into the axes to reuse it on the next call ax.__cBar__ = cbar updateColorBar(cbar, **kwargs) else: #pg._y('update', kwargs) updateColorBar(None, gci=gci, **kwargs) return cbar
[docs] def createColorBarOnly(cMin=1, cMax=100, logScale=False, cMap=None, nLevs=5, label=None, orientation='horizontal', savefig=None, ax=None, **kwargs): """Create figure with a colorbar. Parameters ---------- **kwargs: Forwarded to mpl.colorbar.ColorbarBase. Returns ------- fig: The created figure. Examples -------- >>> # import pygimli as pg >>> # from pygimli.viewer.mpl import createColorBarOnly >>> # createColorBarOnly(cMin=0.2, cMax=5, logScale=False, >>> # cMap='b2r', >>> # nLevs=7, >>> # label=r'Ratio', >>> # orientation='horizontal') >>> # pg.wait() """ from matplotlib.colors import LogNorm, Normalize from matplotlib.colorbar import ColorbarBase if ax is None: fig = pg.plt.figure() if orientation == 'horizontal': ax = fig.add_axes([0.035, 0.6, 0.93, 0.05]) else: ax = fig.add_axes([0.30, 0.02, 0.22, 0.96]) norm = None if cMin > 0 and logScale is True: norm = LogNorm(vmin=cMin, vmax=cMax) else: norm = Normalize(vmin=cMin, vmax=cMax) cmap = cmapFromName(cMap) kwargs.pop('colorBar', False) # often False for multiple plots kwargs.pop('xlabel', False) kwargs.pop('ylabel', False) aspect = kwargs.pop('aspect', None) levels = kwargs.pop('levels', None) cbar = ColorbarBase(ax, norm=norm, cmap=cmap, orientation=orientation, **kwargs) # cbar.labelpad = -20 # cbar.ax.yaxis.set_label_position('left') if levels is not None: kwargs['levels'] = levels if aspect is not None: ax.set_aspect(aspect) updateColorBar(cbar, cMin=cMin, cMax=cMax, nLevs=nLevs, label=label, **kwargs) # updateColorBar(cbar, **kwargs) if savefig is not None: saveFigure(ax.figure, savefig) return ax
[docs] def setCbarLevels(cbar, cMin=None, cMax=None, nLevs=5, levels=None): """Set colorbar levels given a number of levels and min/max values.""" import matplotlib as mpl import matplotlib.ticker as ticker if hasattr(cbar, 'mappable'): mappable = cbar.mappable else: # cbar might be a mappable itself mappable = cbar if cMin is None: cMin = mappable.get_clim()[0] if cMax is None: cMax = mappable.get_clim()[1] if cMin == cMax: cMin *= 0.999 cMax *= 1.001 norm = None if hasattr(mappable, 'mappable'): norm = mappable.norm elif hasattr(mappable, 'norm'): norm = mappable.norm # norm.clip = True if levels is not None: cbarLevels = levels else: if isinstance(norm, mpl.colors.LogNorm): cbarLevels = np.logspace(np.log10(cMin), np.log10(cMax), nLevs) else: # if cMax < cMin: cbarLevels = np.linspace(cMin, cMax, nLevs) # FIXME: [10.1, 10.2, 10.3] mapped to [10 10 10] if np.all(np.array(cbarLevels) < 1e-2): # roundValue not used at all! pg.debug("All values smaller than 1e-4, avoiding additional rounding.") roundValue = False else: roundValue = True # cbarLevelsString = [] # for i in cbarLevels: # cbarLevelsString.append(prettyFloat(i, roundValue)) mappable.set_clim(vmin=cMin, vmax=cMax) if hasattr(cbar, 'set_ticks'): # cbar is a ColorBar with ticks cbar.set_ticks(cbarLevels) # cbar.set_ticklabels(cbarLevelsString) try: cbar.ax.figure.draw_without_rendering() #cbar._draw_all() # work but dunno how long this will exists except: cbar.draw_all() # removed by mpl-3.8 # necessary since mpl 3.0 cbar.ax.minorticks_off() @ticker.FuncFormatter def pfMajorFormatter(x, pos): return prettyFloat(x) % x try: # mpl 3.5 if cbar.orientation == 'horizontal': cbar.ax.xaxis.set_major_formatter(pfMajorFormatter) else: cbar.ax.yaxis.set_major_formatter(pfMajorFormatter) except Exception as e: pg.warn(e)
[docs] def setMappableData(mappable, dataIn, cMin=None, cMax=None, logScale=None, **kwargs): """Change the data values for a given mappable.""" import matplotlib as mpl data = dataIn if not isinstance(data, np.ma.core.MaskedArray): data = np.array(dataIn) # set bad value color to white if mappable.get_cmap() is not None: try: import copy # from mpl 3.3 # cm_ = copy.copy(mappable.get_cmap()).set_bad([1.0, 1.0, 1.0, 0.]) # mappable.set_cmap(cm_) pass except: # old prior mpl 3.3 # not needed anymore mappable.get_cmap().set_bad([1.0, 1.0, 1.0, 0.0]) if cMin is None: cMin = data.min() if cMax is None: cMax = data.max() oldLog = None if cMin <= 0.0: oldLog = isinstance(mappable.norm, mpl.colors.LogNorm) if oldLog is True or logScale is True: if cMax > 0: cMin = min(data[data > 0.0]) data = np.ma.masked_array(data, data <= 0.0) else: # if all data are negative switch to lin scale return setMappableData(mappable, dataIn, cMin, cMax, logScale=False, **kwargs) if logScale is True: mappable.set_norm(mpl.colors.LogNorm(vmin=cMin, vmax=cMax)) elif logScale is False: mappable.set_norm(mpl.colors.Normalize(vmin=cMin, vmax=cMax)) # pg._g(oldLog, logScale, cMin, cMax, mappable.norm, data) mappable.set_array(data) mappable.set_clim(cMin, cMax) if mappable.colorbar is not None: updateColorBar(mappable.colorbar, cMin=cMin, cMax=cMax, **kwargs)
[docs] def addCoverageAlpha(patches, coverage, dropThreshold=0.4): """Add alpha values to the colors of a polygon collection. Parameters ---------- patches : 2D mpl mappable coverage : array coverage values. Maximum coverage mean no opaqueness. dropThreshold : float relative minimum coverage """ patches.set_antialiaseds(True) # generate individual color values here patches.update_scalarmappable() cols = patches.get_facecolor() C = np.asarray(coverage) # print(np.min(C), np.max(C)) if (np.min(C) < 0.) | (np.max(C) > 1.) | (np.max(C) < 0.5): nn, hh = np.histogram(C, 50) nnn = nn.cumsum(axis=0) / float(len(C)) # print("min-max nnn ", min(nnn), max(nnn)) mi = hh[np.min(np.where(nnn > 0.02)[0])] if np.min(nnn) > dropThreshold: ma = np.max(C) else: ma = hh[np.max(np.where(nnn < dropThreshold)[0])] # mi = hh[min(np.where(nnn > 0.2)[0])] # ma = hh[max(np.where(nnn < 0.7)[0])] C = (C - mi) / (ma - mi) C[np.where(C < 0.)] = 0.0 C[np.where(C > 0.95)] = 1.0 # else: # print('taking the values directly') import matplotlib as mpl if version.parse(mpl.__version__) >= version.parse("3.4"): patches.set_alpha(C) patches.set_snap(True) else: cols[:, 3] = C patches.set_facecolors(cols) if hasattr(patches, 'ax'): updateAxes(patches.ax) elif hasattr(patches, 'get_axes'): updateAxes(patches.get_axes())