@@ -585,8 +585,9 @@ def __repr__(self):
585585 if callable (data ):
586586 string += f' { key !r} : <function>,\n '
587587 else :
588- string += (f' { key !r} : [{ data [0 ][2 ]:.3f} , '
589- f'..., { data [- 1 ][1 ]:.3f} ],\n ' )
588+ string += (
589+ f' { key !r} : [{ data [0 ][2 ]:.3f} , ..., { data [- 1 ][1 ]:.3f} ],\n '
590+ )
590591 return type (self ).__name__ + '({\n ' + string + '})'
591592
592593 @docstring .add_snippets
@@ -1230,7 +1231,8 @@ def __repr__(self):
12301231 'ListedColormap({\n '
12311232 f" 'name': { self .name !r} ,\n "
12321233 f" 'colors': { [mcolors .to_hex (color ) for color in self .colors ]} ,\n "
1233- '})' )
1234+ '})'
1235+ )
12341236
12351237 def __init__ (self , * args , alpha = None , ** kwargs ):
12361238 """
@@ -1840,9 +1842,10 @@ class DiscreteNorm(mcolors.BoundaryNorm):
18401842 # WARNING: Must be child of BoundaryNorm. Many methods in ColorBarBase
18411843 # test for class membership, crucially including _process_values(), which
18421844 # if it doesn't detect BoundaryNorm will try to use DiscreteNorm.inverse().
1845+ @warnings ._rename_kwargs ('0.7' , extend = 'unique' )
18431846 def __init__ (
1844- self , levels , norm = None , step = 1.0 , extend = None ,
1845- clip = False , descending = False ,
1847+ self , levels , norm = None , cmap = None ,
1848+ unique = None , step = None , clip = False , descending = False ,
18461849 ):
18471850 """
18481851 Parameters
@@ -1854,18 +1857,24 @@ def __init__(
18541857 to `~DiscreteNorm.__call__` before discretization. The ``vmin``
18551858 and ``vmax`` of the normalizer are set to the minimum and
18561859 maximum values in `levels`.
1860+ cmap : `matplotlib.colors.Colormap`, optional
1861+ The colormap associated with this normalizer. This is used to
1862+ apply default `unique` and `step` settings depending on whether the
1863+ colormap is cyclic and whether distinct "extreme" colors have been
1864+ designated with `~matplotlib.colors.Colormap.set_under` and/or
1865+ `~matplotlib.colors.Colormap.set_over`.
1866+ unique : {'neither', 'both', 'min', 'max'}, optional
1867+ Which out-of-bounds regions should be assigned unique colormap
1868+ colors. The normalizer needs this information so it can ensure
1869+ the colorbar always spans the full range of colormap colors.
18571870 step : float, optional
18581871 The intensity of the transition to out-of-bounds colors as a
18591872 fraction of the adjacent step between in-bounds colors.
18601873 Default is ``1``.
1861- extend : {'neither', 'both', 'min', 'max'}, optional
1862- Which out-of-bounds regions should be assigned unique colormap
1863- colors. The normalizer needs this information so it can ensure
1864- the colorbar always spans the full range of colormap colors.
18651874 clip : bool, optional
18661875 Whether to clip values falling outside of the level bins. This
1867- only has an effect on lower colors when extend is
1868- ``'min'`` or ``'both'``, and on upper colors when extend is
1876+ only has an effect on lower colors when unique is
1877+ ``'min'`` or ``'both'``, and on upper colors when unique is
18691878 ``'max'`` or ``'both'``.
18701879 descending : bool, optional
18711880 Whether the levels are meant to be descending. This will cause
@@ -1874,11 +1883,52 @@ def __init__(
18741883
18751884 Note
18761885 ----
1877- If you are using a diverging colormap with ``extend='max'`` or
1878- ``extend='min'``, the center will get messed up. But that is very
1879- strange usage anyway... so please just don't do that :)
1880- """
1881- # Parse input
1886+ This normalizer also makes sure that your levels always span the full range
1887+ of colors in the colormap, whether `extend` is set to ``'min'``, ``'max'``,
1888+ ``'neither'``, or ``'both'``. By default, when `extend` is not ``'both'``,
1889+ matplotlib simply cuts off the most intense colors (reserved for
1890+ "out of bounds" data), even though they are not being used.
1891+
1892+ While this could also be done by limiting the number of colors in the colormap
1893+ lookup table by selecting a smaller ``N`` (see
1894+ `~matplotlib.colors.LinearSegmentedColormap`), ProPlot chooses to
1895+ always build colormaps with high resolution lookup tables and leave it
1896+ to the `~matplotlib.colors.Normalize` instance to handle *discretization*
1897+ of the color selections.
1898+
1899+ Note that this approach means if you use a diverging colormap with
1900+ ``extend='max'`` or ``extend='min'``, the central color will get messed up.
1901+ But that is very strange usage anyway... so please just don't do that :)
1902+ """
1903+ # Special properties specific to colormap type
1904+ if cmap is not None :
1905+ over = cmap ._rgba_over
1906+ under = cmap ._rgba_under
1907+ cyclic = getattr (cmap , '_cyclic' , None )
1908+ if cyclic :
1909+ # *Scale bins* as if extend='both' to omit end colors
1910+ step = 0.5
1911+ unique = 'both'
1912+
1913+ else :
1914+ # *Scale bins* as if unique='neither' because there may be *discrete
1915+ # change* between minimum color and out-of-bounds colors.
1916+ if over is not None and under is not None :
1917+ unique = 'both'
1918+ elif over is not None :
1919+ # Turn off unique bin for over-bounds colors
1920+ if unique == 'both' :
1921+ unique = 'min'
1922+ elif unique == 'max' :
1923+ unique = 'neither'
1924+ elif under is not None :
1925+ # Turn off unique bin for under-bounds colors
1926+ if unique == 'both' :
1927+ unique = 'min'
1928+ elif unique == 'max' :
1929+ unique = 'neither'
1930+
1931+ # Validate input arguments
18821932 # NOTE: This must be a subclass BoundaryNorm, so ColorbarBase will
18831933 # detect it... even though we completely override it.
18841934 if not norm :
@@ -1887,12 +1937,13 @@ def __init__(
18871937 raise ValueError ('Normalizer cannot be instance of BoundaryNorm.' )
18881938 elif not isinstance (norm , mcolors .Normalize ):
18891939 raise ValueError ('Normalizer must be instance of Normalize.' )
1890- extend = extend or 'neither'
1891- extends = ('both' , 'min' , 'max' , 'neither' )
1892- if extend not in extends :
1940+ if unique is None :
1941+ unique = 'neither'
1942+ uniques = ('both' , 'min' , 'max' , 'neither' )
1943+ if unique not in uniques :
18931944 raise ValueError (
1894- f'Unknown extend option { extend !r} . Options are: '
1895- + ', ' .join (map (repr , extends )) + '.'
1945+ f'Unknown unique option { unique !r} . Options are: '
1946+ + ', ' .join (map (repr , uniques )) + '.'
18961947 )
18971948
18981949 # Ensure monotonically increasing levels
@@ -1909,8 +1960,9 @@ def __init__(
19091960 # 2 color coordinates for out-of-bounds color bins.
19101961 # For *same* out-of-bounds colors, looks like [0, 0, ..., 1, 1]
19111962 # For *unique* out-of-bounds colors, looks like [0, X, ..., 1 - X, 1]
1912- # NOTE: For cyclic colormaps, _build_discrete_norm sets extend to
1913- # 'both' and step to 0.5 so that we omit end colors.
1963+ # NOTE: Ensure out-of-bounds bin values correspond to out-of-bounds
1964+ # coordinate in case user used set_under or set_over to apply discrete
1965+ # change. See lukelbd/proplot#190
19141966 # NOTE: Critical that we scale the bin centers in "physical space"
19151967 # and *then* translate to color coordinates so that nonlinearities in
19161968 # the normalization stay intact. If we scaled the bin centers in
@@ -1921,9 +1973,11 @@ def __init__(
19211973 mids = np .zeros ((levels .size + 1 ,))
19221974 mids [1 :- 1 ] = 0.5 * (levels [1 :] + levels [:- 1 ])
19231975 mids [0 ], mids [- 1 ] = mids [1 ], mids [- 2 ]
1924- if extend in ('min' , 'both' ):
1976+ if step is None :
1977+ step = 1.0
1978+ if unique in ('min' , 'both' ):
19251979 mids [0 ] += step * (mids [1 ] - mids [2 ])
1926- if extend in ('max' , 'both' ):
1980+ if unique in ('max' , 'both' ):
19271981 mids [- 1 ] += step * (mids [- 2 ] - mids [- 3 ])
19281982 if vcenter is None :
19291983 mids = _interpolate_basic (
@@ -1937,15 +1991,18 @@ def __init__(
19371991 mids [mids >= vcenter ] = _interpolate_basic (
19381992 mids [mids >= vcenter ], vcenter , np .max (mids ), vcenter , vmax ,
19391993 )
1994+ eps = 1e-10 # mids and dest are numpy.float64
19401995 dest = norm (mids )
1996+ dest [0 ] -= eps
1997+ dest [- 1 ] += eps
19411998
19421999 # Attributes
19432000 # NOTE: If clip is True, we clip values to the centers of the end
19442001 # bins rather than vmin/vmax to prevent out-of-bounds colors from
19452002 # getting an in-bounds bin color due to landing on a bin edge.
1946- # NOTE: With extend ='min' the minimimum in-bounds and out-of-bounds
2003+ # NOTE: With unique ='min' the minimimum in-bounds and out-of-bounds
19472004 # colors are the same so clip=True will have no effect. Same goes
1948- # for extend ='max' with maximum colors.
2005+ # for unique ='max' with maximum colors.
19492006 # WARNING: For some reason must clip manually for LogNorm, or
19502007 # end up with unpredictable fill value, weird "out-of-bounds" colors
19512008 self ._bmin = np .min (mids )
@@ -2272,6 +2329,34 @@ def _get_cmap(name=None, lut=None):
22722329 return cmap
22732330
22742331
2332+ def _to_proplot_colormap (cmap ):
2333+ """
2334+ Translate the input argument to a ProPlot colormap subclass.
2335+ """
2336+ cmap_orig = cmap
2337+ if isinstance (cmap , (ListedColormap , LinearSegmentedColormap )):
2338+ pass
2339+ elif isinstance (cmap , mcolors .LinearSegmentedColormap ):
2340+ cmap = LinearSegmentedColormap (
2341+ cmap .name , cmap ._segmentdata , cmap .N , cmap ._gamma
2342+ )
2343+ elif isinstance (cmap , mcolors .ListedColormap ):
2344+ cmap = ListedColormap (
2345+ cmap .colors , cmap .name , cmap .N
2346+ )
2347+ elif isinstance (cmap , mcolors .Colormap ): # base class
2348+ pass
2349+ else :
2350+ raise ValueError (
2351+ f'Invalid colormap type { type (cmap ).__name__ !r} . '
2352+ 'Must be instance of matplotlib.colors.Colormap.'
2353+ )
2354+ cmap ._rgba_bad = cmap_orig ._rgba_bad
2355+ cmap ._rgba_under = cmap_orig ._rgba_under
2356+ cmap ._rgba_over = cmap_orig ._rgba_over
2357+ return cmap
2358+
2359+
22752360class ColormapDatabase (dict ):
22762361 """
22772362 Dictionary subclass used to replace the matplotlib
@@ -2353,25 +2438,8 @@ def __setitem__(self, key, item):
23532438 """
23542439 if not isinstance (key , str ):
23552440 raise KeyError (f'Invalid key { key !r} . Must be string.' )
2356- if isinstance (item , (ListedColormap , LinearSegmentedColormap )):
2357- pass
2358- elif isinstance (item , mcolors .LinearSegmentedColormap ):
2359- item = LinearSegmentedColormap (
2360- item .name , item ._segmentdata , item .N , item ._gamma
2361- )
2362- elif isinstance (item , mcolors .ListedColormap ):
2363- item = ListedColormap (
2364- item .colors , item .name , item .N
2365- )
2366- elif isinstance (item , mcolors .Colormap ): # base class
2367- pass
2368- else :
2369- raise ValueError (
2370- f'Invalid colormap { item } . Must be instance of '
2371- 'matplotlib.colors.ListedColormap or '
2372- 'matplotlib.colors.LinearSegmentedColormap.'
2373- )
23742441 key = self ._sanitize_key (key , mirror = False )
2442+ item = _to_proplot_colormap (item )
23752443 super ().__setitem__ (key , item )
23762444
23772445 def __contains__ (self , item ):
0 commit comments