55"""
66import copy
77import inspect
8- import itertools
98import re
109from numbers import Integral
1110
@@ -1053,20 +1052,19 @@ def _add_colorbar(
10531052 # methods that construct an 'artist list' (i.e. colormap scatter object)
10541053 if np .iterable (mappable ) and len (mappable ) == 1 and isinstance (mappable [0 ], mcm .ScalarMappable ): # noqa: E501
10551054 mappable = mappable [0 ]
1056- if isinstance (mappable , mcm .ScalarMappable ):
1055+ if not isinstance (mappable , mcm .ScalarMappable ):
1056+ mappable , kwargs = cax ._parse_colorbar_arg (mappable , values , ** kwargs )
1057+ else :
10571058 pop = _pop_params (kwargs , cax ._parse_colorbar_arg , ignore_internal = True )
1058- locator_default = formatter_default = None
10591059 if pop :
10601060 warnings ._warn_proplot (
10611061 f'Input is already a ScalarMappable. '
10621062 f'Ignoring unused keyword arg(s): { pop } '
10631063 )
1064- else :
1065- result = cax ._parse_colorbar_arg (mappable , values , ** kwargs )
1066- mappable , locator_default , formatter_default , kwargs = result
10671064
10681065 # Parse 'extendsize' and 'extendfrac' keywords
10691066 # TODO: Make this auto-adjust to the subplot size
1067+ vert = kwargs ['orientation' ] == 'vertical'
10701068 if extendsize is not None and extendfrac is not None :
10711069 warnings ._warn_proplot (
10721070 f'You cannot specify both an absolute extendsize={ extendsize !r} '
@@ -1075,32 +1073,33 @@ def _add_colorbar(
10751073 extendfrac = None
10761074 if extendfrac is None :
10771075 width , height = cax ._get_size_inches ()
1078- scale = height if kwargs . get ( 'orientation' ) == 'vertical' else width
1076+ scale = height if vert else width
10791077 extendsize = units (extendsize , 'em' , 'in' )
10801078 extendfrac = extendsize / max (scale - 2 * extendsize , units (1 , 'em' , 'in' ))
10811079
10821080 # Parse the tick locators and formatters
1083- # NOTE: This auto constructs minor DiscreteLocotors from major DiscreteLocator
1084- # instances (but bypasses if labels are categorical).
1085- # NOTE: Almost always DiscreteLocator will be associated with DiscreteNorm.
1086- # But otherwise disable minor ticks or else get issues (see mpl #22233).
1087- name = 'y' if kwargs .get ('orientation' ) == 'vertical' else 'x'
1088- axis = cax .yaxis if kwargs .get ('orientation' ) == 'vertical' else cax .xaxis
1089- locator = _not_none (locator , locator_default , None )
1090- formatter = _not_none (formatter , formatter_default , 'auto' )
1081+ # NOTE: In presence of BoundaryNorm or similar handle ticks with special
1082+ # DiscreteLocator or else get issues (see mpl #22233).
1083+ norm = mappable .norm
1084+ formatter = _not_none (formatter , getattr (norm , '_labels' , None ), 'auto' )
10911085 formatter = constructor .Formatter (formatter , ** formatter_kw )
1086+ categorical = isinstance (formatter , mticker .FixedFormatter )
10921087 if locator is not None :
10931088 locator = constructor .Locator (locator , ** locator_kw )
1094- if minorlocator is not None :
1089+ if minorlocator is not None : # overrides tickminor
10951090 minorlocator = constructor .Locator (minorlocator , ** minorlocator_kw )
1096- discrete = isinstance (locator , pticker .DiscreteLocator )
1097- categorical = isinstance (formatter , mticker .FixedFormatter )
1098- tickminor = False if categorical else rc [name + 'tick.minor.visible' ]
1099- if categorical and discrete :
1100- locator = mticker .FixedLocator (np .array (locator .locs )) # convert locator
1101- if tickminor and isinstance (mappable .norm , mcolors .BoundaryNorm ):
1102- locs = np .array (locator .locs if discrete else mappable .norm .boundaries )
1103- minorlocator = pticker .DiscreteLocator (locs , minor = True )
1091+ elif tickminor is None :
1092+ tickminor = False if categorical else rc ['xy' [vert ] + 'tick.minor.visible' ]
1093+ if isinstance (norm , mcolors .BoundaryNorm ): # DiscreteNorm or BoundaryNorm
1094+ ticks = getattr (norm , '_ticks' , norm .boundaries )
1095+ segmented = isinstance (getattr (norm , '_norm' , None ), pcolors .SegmentedNorm )
1096+ if locator is None :
1097+ if categorical or segmented :
1098+ locator = mticker .FixedLocator (ticks )
1099+ else :
1100+ locator = pticker .DiscreteLocator (ticks )
1101+ if tickminor and minorlocator is None :
1102+ minorlocator = pticker .DiscreteLocator (ticks , minor = True )
11041103
11051104 # Special handling for colorbar keyword arguments
11061105 # WARNING: Critical to not pass empty major locators in matplotlib < 3.5
@@ -1118,6 +1117,7 @@ def _add_colorbar(
11181117 # set the locator and formatter axis... however this messes up colorbar lengths
11191118 # in matplotlib < 3.2. So we only apply this conditionally and in earlier
11201119 # verisons recognize that DiscreteLocator will behave like FixedLocator.
1120+ axis = cax .yaxis if vert else cax .xaxis
11211121 if not isinstance (mappable , mcontour .ContourSet ):
11221122 extend = _not_none (extend , 'neither' )
11231123 kwargs ['extend' ] = extend
@@ -1151,7 +1151,7 @@ def _add_colorbar(
11511151 obj .minorticks_on ()
11521152 else :
11531153 obj .minorticks_off ()
1154- if getattr (mappable . norm , 'descending' , None ):
1154+ if getattr (norm , 'descending' , None ):
11551155 axis .set_inverted (True )
11561156 if reverse : # potentially double reverse, although that would be weird...
11571157 axis .set_inverted (True )
@@ -1622,8 +1622,7 @@ def _parse_frame(guide, fancybox=None, shadow=None, **kwargs):
16221622
16231623 @staticmethod
16241624 def _parse_colorbar_arg (
1625- mappable , values = None , * ,
1626- norm = None , norm_kw = None , vmin = None , vmax = None , ** kwargs
1625+ mappable , values = None , norm = None , norm_kw = None , vmin = None , vmax = None , ** kwargs
16271626 ):
16281627 """
16291628 Generate a mappable from flexible non-mappable input. Useful in bridging
@@ -1688,15 +1687,15 @@ def _parse_colorbar_arg(
16881687 )
16891688
16901689 # Generate continuous normalizer, and possibly discrete normalizer. Update
1691- # outgoing locator and formatter if user does not override.
1690+ # the outgoing locator and formatter if user does not override.
16921691 norm_kw = norm_kw or {}
16931692 norm = norm or 'linear'
16941693 vmin = _not_none (vmin = vmin , norm_kw_vmin = norm_kw .pop ('vmin' , None ), default = 0 )
16951694 vmax = _not_none (vmax = vmax , norm_kw_vmax = norm_kw .pop ('vmax' , None ), default = 1 )
16961695 norm = constructor .Norm (norm , vmin = vmin , vmax = vmax , ** norm_kw )
1697- locator = formatter = None
16981696 if values is not None :
16991697 ticks = []
1698+ labels = None
17001699 for i , val in enumerate (values ):
17011700 try :
17021701 val = float (val )
@@ -1706,21 +1705,21 @@ def _parse_colorbar_arg(
17061705 val = i
17071706 ticks .append (val )
17081707 if any (isinstance (_ , str ) for _ in ticks ):
1709- formatter = mticker . FixedFormatter ( list (map (str , ticks ) ))
1708+ labels = list (map (str , ticks ))
17101709 ticks = np .arange (len (ticks ))
1711- locator = mticker .FixedLocator (ticks )
1712- else :
1713- locator = pticker .DiscreteLocator (ticks )
17141710 if len (ticks ) == 1 :
17151711 levels = [ticks [0 ] - 1 , ticks [0 ] + 1 ]
17161712 else :
17171713 levels = edges (ticks )
1718- norm , cmap , _ = Axes ._parse_discrete_norm (levels , norm , cmap )
1714+ from . import PlotAxes
1715+ norm , cmap , _ = PlotAxes ._parse_discrete_norm (
1716+ levels , norm , cmap , discrete_ticks = ticks , discrete_labels = labels
1717+ )
17191718
17201719 # Return ad hoc ScalarMappable and update locator and formatter
17211720 # NOTE: If value list doesn't match this may cycle over colors.
17221721 mappable = mcm .ScalarMappable (norm , cmap )
1723- return mappable , locator , formatter , kwargs
1722+ return mappable , kwargs
17241723
17251724 def _parse_colorbar_filled (
17261725 self , length = None , align = None , tickloc = None , ticklocation = None ,
@@ -1849,99 +1848,6 @@ def _parse_colorbar_inset(
18491848 kwargs .update ({'orientation' : 'horizontal' , 'ticklocation' : 'bottom' })
18501849 return ax , kwargs
18511850
1852- @staticmethod
1853- def _parse_discrete_norm (
1854- levels , norm , cmap , * , extend = None , min_levels = None , ** kwargs ,
1855- ):
1856- """
1857- Create a `~proplot.colors.DiscreteNorm` or `~proplot.colors.BoundaryNorm`
1858- from the input colormap and normalizer.
1859-
1860- Parameters
1861- ----------
1862- levels : sequence of float
1863- The level boundaries.
1864- norm : `~matplotlib.colors.Normalize`
1865- The continuous normalizer.
1866- cmap : `~matplotlib.colors.Colormap`
1867- The colormap.
1868- extend : str, optional
1869- The extend setting.
1870- min_levels : int, optional
1871- The minimum number of levels.
1872-
1873- Returns
1874- -------
1875- norm : `~proplot.colors.DiscreteNorm`
1876- The discrete normalizer.
1877- cmap : `~matplotlib.colors.Colormap`
1878- The possibly-modified colormap.
1879- kwargs
1880- Unused arguments.
1881- """
1882- # Reverse the colormap if input levels or values were descending
1883- # See _parse_level_list for details
1884- min_levels = _not_none (min_levels , 2 ) # 1 for contour plots
1885- unique = extend = _not_none (extend , 'neither' )
1886- under = cmap ._rgba_under
1887- over = cmap ._rgba_over
1888- cyclic = getattr (cmap , '_cyclic' , None )
1889- qualitative = isinstance (cmap , pcolors .DiscreteColormap ) # see _parse_cmap
1890- if len (levels ) < min_levels :
1891- raise ValueError (
1892- f'Invalid levels={ levels !r} . Must be at least length { min_levels } .'
1893- )
1894-
1895- # Ensure end colors are unique by scaling colors as if extend='both'
1896- # NOTE: Inside _parse_cmap should have enforced extend='neither'
1897- if cyclic :
1898- step = 0.5 # try to allocate space for unique end colors
1899- unique = 'both'
1900-
1901- # Ensure color list length matches level list length using rotation
1902- # NOTE: No harm if not enough colors, we just end up with the same
1903- # color for out-of-bounds extensions. This is a gentle failure
1904- elif qualitative :
1905- step = 0.5 # try to sample the central index for safety
1906- unique = 'both'
1907- auto_under = under is None and extend in ('min' , 'both' )
1908- auto_over = over is None and extend in ('max' , 'both' )
1909- ncolors = len (levels ) - min_levels + 1 + auto_under + auto_over
1910- colors = list (itertools .islice (itertools .cycle (cmap .colors ), ncolors ))
1911- if auto_under and len (colors ) > 1 :
1912- under , * colors = colors
1913- if auto_over and len (colors ) > 1 :
1914- * colors , over = colors
1915- cmap = cmap .copy (colors , N = len (colors ))
1916- if under is not None :
1917- cmap .set_under (under )
1918- if over is not None :
1919- cmap .set_over (over )
1920-
1921- # Ensure middle colors sample full range when extreme colors are present
1922- # by scaling colors as if extend='neither'
1923- else :
1924- step = 1.0
1925- if over is not None and under is not None :
1926- unique = 'neither'
1927- elif over is not None : # turn off over-bounds unique bin
1928- if extend == 'both' :
1929- unique = 'min'
1930- elif extend == 'max' :
1931- unique = 'neither'
1932- elif under is not None : # turn off under-bounds unique bin
1933- if extend == 'both' :
1934- unique = 'min'
1935- elif extend == 'max' :
1936- unique = 'neither'
1937-
1938- # Generate DiscreteNorm and update "child" norm with vmin and vmax from
1939- # levels. This lets the colorbar set tick locations properly!
1940- if not isinstance (norm , mcolors .BoundaryNorm ) and len (levels ) > 1 :
1941- norm = pcolors .DiscreteNorm (levels , norm = norm , unique = unique , step = step )
1942-
1943- return norm , cmap , kwargs
1944-
19451851 def _parse_legend_aligned (self , pairs , ncol = None , order = None , ** kwargs ):
19461852 """
19471853 Draw an individual legend with aligned columns. Includes support
0 commit comments