395395 `locator` is used to generate this many level centers at "nice" intervals.
396396 If the latter, levels are inferred using `~ultraplot.utils.edges`.
397397 This will override any `levels` input.
398+ center_levels : bool, default False
399+ If set to true, the discrete color bar bins will be centered on the level values
400+ instead of using the level values as the edges of the discrete bins. This option
401+ can be used for diverging, discrete color bars with both positive and negative
402+ data to ensure data near zero is properly represented.
398403"""
399404_auto_levels_docstring = """
400405robust : bool, float, or 2-tuple, default: :rc:`cmap.robust`
@@ -2436,8 +2441,15 @@ def _parse_color(self, x, y, c, *, apply_cycle=True, infer_rgb=False, **kwargs):
24362441 ): # noqa: E501
24372442 c = list (map (pcolors .to_hex , c )) # avoid iterating over columns
24382443 else :
2444+ center_levels = kwargs .pop ("center_levels" , None )
24392445 kwargs = self ._parse_cmap (
2440- x , y , c , plot_lines = True , default_discrete = False , ** kwargs
2446+ x ,
2447+ y ,
2448+ c ,
2449+ plot_lines = True ,
2450+ default_discrete = False ,
2451+ center_levels = center_levels ,
2452+ ** kwargs ,
24412453 ) # noqa: E501
24422454 parsers = (self ._parse_cycle ,)
24432455 pop = _pop_params (kwargs , * parsers , ignore_internal = True )
@@ -2466,6 +2478,7 @@ def _parse_cmap(
24662478 min_levels = None ,
24672479 plot_lines = False ,
24682480 plot_contours = False ,
2481+ center_levels = None ,
24692482 ** kwargs ,
24702483 ):
24712484 """
@@ -2602,6 +2615,7 @@ def _parse_cmap(
26022615 norm_kw = norm_kw ,
26032616 extend = extend ,
26042617 min_levels = min_levels ,
2618+ center_levels = center_levels ,
26052619 skip_autolev = skip_autolev ,
26062620 ** kwargs ,
26072621 )
@@ -2636,7 +2650,13 @@ def _parse_cmap(
26362650 # Then finally warn and remove unused args
26372651 if levels is not None :
26382652 norm , cmap , kwargs = self ._parse_level_norm (
2639- levels , norm , cmap , extend = extend , min_levels = min_levels , ** kwargs
2653+ levels ,
2654+ norm ,
2655+ cmap ,
2656+ center_levels = center_levels ,
2657+ extend = extend ,
2658+ min_levels = min_levels ,
2659+ ** kwargs ,
26402660 )
26412661 params = _pop_params (kwargs , * self ._level_parsers , ignore_internal = True )
26422662 if "N" in params : # use this for lookup table N instead of levels N
@@ -2855,6 +2875,7 @@ def _parse_level_num(
28552875 norm_kw = None ,
28562876 extend = None ,
28572877 symmetric = None ,
2878+ center_levels = None ,
28582879 ** kwargs ,
28592880 ):
28602881 """
@@ -2893,6 +2914,7 @@ def _parse_level_num(
28932914 locator_kw = locator_kw or {}
28942915 extend = _not_none (extend , "neither" )
28952916 levels = _not_none (levels , rc ["cmap.levels" ])
2917+ center_levels = _not_none (center_levels , rc ["colorbar.center_levels" ])
28962918 vmin = _not_none (vmin = vmin , norm_kw_vmin = norm_kw .pop ("vmin" , None ))
28972919 vmax = _not_none (vmax = vmax , norm_kw_vmax = norm_kw .pop ("vmax" , None ))
28982920 norm = constructor .Norm (norm or "linear" , ** norm_kw )
@@ -2965,6 +2987,15 @@ def _parse_level_num(
29652987 nlevels .append (olevels [- 1 ])
29662988 levels = norm .inverse (nlevels )
29672989
2990+ # Center the bin edges around the center of the bin
2991+ # rather than its edges
2992+ if center_levels :
2993+ # Shift the entire range but correct the range
2994+ # later
2995+ width = np .diff (levels )[0 ]
2996+ levels -= width * 0.5
2997+ # Add another bin edge at the width
2998+ levels = np .append (levels , levels [- 1 ] + width * np .sign (levels [- 1 ]))
29682999 return levels , kwargs
29693000
29703001 def _parse_level_vals (
@@ -2981,6 +3012,7 @@ def _parse_level_vals(
29813012 norm_kw = None ,
29823013 skip_autolev = False ,
29833014 min_levels = None ,
3015+ center_levels = None ,
29843016 ** kwargs ,
29853017 ):
29863018 """
@@ -3112,6 +3144,7 @@ def _sanitize_levels(key, array, minsize):
31123144 extend = extend ,
31133145 negative = negative ,
31143146 positive = positive ,
3147+ center_levels = center_levels ,
31153148 ** kwargs ,
31163149 )
31173150 else :
@@ -3147,6 +3180,7 @@ def _parse_level_norm(
31473180 min_levels = None ,
31483181 discrete_ticks = None ,
31493182 discrete_labels = None ,
3183+ center_levels = None ,
31503184 ** kwargs ,
31513185 ):
31523186 """
@@ -3237,6 +3271,7 @@ def _parse_level_norm(
32373271
32383272 # Generate DiscreteNorm and update "child" norm with vmin and vmax from
32393273 # levels. This lets the colorbar set tick locations properly!
3274+ center_levels = _not_none (center_levels , rc ["colorbar.center_levels" ])
32403275 if not isinstance (norm , mcolors .BoundaryNorm ) and len (levels ) > 1 :
32413276 norm = pcolors .DiscreteNorm (
32423277 levels ,
@@ -3246,7 +3281,6 @@ def _parse_level_norm(
32463281 ticks = discrete_ticks ,
32473282 labels = discrete_labels ,
32483283 )
3249-
32503284 return norm , cmap , kwargs
32513285
32523286 def _apply_plot (self , * pairs , vert = True , ** kwargs ):
@@ -3654,7 +3688,8 @@ def parametric(self, x, y, c, *, interp=0, scalex=True, scaley=True, **kwargs):
36543688 a = (x , y , c ) # pick levels from vmin and vmax, possibly limiting range
36553689 else :
36563690 a , kw ["values" ] = (), c
3657- kw = self ._parse_cmap (* a , plot_lines = True , ** kw )
3691+ center_levels = kw .pop ("center_levels" , None )
3692+ kw = self ._parse_cmap (* a , center_levels = center_levels , plot_lines = True , ** kw )
36583693 cmap , norm = kw .pop ("cmap" ), kw .pop ("norm" )
36593694
36603695 # Add collection with some custom attributes
@@ -4690,7 +4725,16 @@ def hexbin(self, x, y, weights, **kwargs):
46904725 kw = kwargs .copy ()
46914726 x , y , kw = self ._parse_1d_args (x , y , autoreverse = False , autovalues = True , ** kw )
46924727 kw .update (_pop_props (kw , "collection" )) # takes LineCollection props
4693- kw = self ._parse_cmap (x , y , y , skip_autolev = True , default_discrete = False , ** kw )
4728+ center_levels = kw .pop ("center_levels" , None )
4729+ kw = self ._parse_cmap (
4730+ x ,
4731+ y ,
4732+ y ,
4733+ skip_autolev = True ,
4734+ default_discrete = False ,
4735+ center_levels = center_levels ,
4736+ ** kw ,
4737+ )
46944738 norm = kw .get ("norm" , None )
46954739 if norm is not None and not isinstance (norm , pcolors .DiscreteNorm ):
46964740 norm .vmin = norm .vmax = None # remove nonsense values
@@ -4710,8 +4754,16 @@ def contour(self, x, y, z, **kwargs):
47104754 """
47114755 x , y , z , kw = self ._parse_2d_args (x , y , z , ** kwargs )
47124756 kw .update (_pop_props (kw , "collection" ))
4757+ center_levels = kw .pop ("center_levels" , None )
47134758 kw = self ._parse_cmap (
4714- x , y , z , min_levels = 1 , plot_lines = True , plot_contours = True , ** kw
4759+ x ,
4760+ y ,
4761+ z ,
4762+ center_levels = center_levels ,
4763+ min_levels = 1 ,
4764+ plot_lines = True ,
4765+ plot_contours = True ,
4766+ ** kw ,
47154767 )
47164768 labels_kw = _pop_params (kw , self ._add_auto_labels )
47174769 guide_kw = _pop_params (kw , self ._update_guide )
@@ -4731,7 +4783,10 @@ def contourf(self, x, y, z, **kwargs):
47314783 """
47324784 x , y , z , kw = self ._parse_2d_args (x , y , z , ** kwargs )
47334785 kw .update (_pop_props (kw , "collection" ))
4734- kw = self ._parse_cmap (x , y , z , plot_contours = True , ** kw )
4786+ center_levels = kw .pop ("center_levels" , None )
4787+ kw = self ._parse_cmap (
4788+ x , y , z , center_levels = center_levels , plot_contours = True , ** kw
4789+ )
47354790 contour_kw = _pop_kwargs (kw , "edgecolors" , "linewidths" , "linestyles" )
47364791 edgefix_kw = _pop_params (kw , self ._fix_patch_edges )
47374792 labels_kw = _pop_params (kw , self ._add_auto_labels )
@@ -4755,7 +4810,10 @@ def pcolor(self, x, y, z, **kwargs):
47554810 """
47564811 x , y , z , kw = self ._parse_2d_args (x , y , z , edges = True , ** kwargs )
47574812 kw .update (_pop_props (kw , "collection" ))
4758- kw = self ._parse_cmap (x , y , z , to_centers = True , ** kw )
4813+ center_levels = kw .pop ("center_levels" , None )
4814+ kw = self ._parse_cmap (
4815+ x , y , z , to_centers = True , center_levels = center_levels , ** kw
4816+ )
47594817 edgefix_kw = _pop_params (kw , self ._fix_patch_edges )
47604818 labels_kw = _pop_params (kw , self ._add_auto_labels )
47614819 guide_kw = _pop_params (kw , self ._update_guide )
@@ -4780,14 +4838,19 @@ def pcolormesh(self, x, y, z, **kwargs):
47804838 to_centers = edges = False
47814839 x , y , z , kw = self ._parse_2d_args (x , y , z , edges = edges , ** kwargs )
47824840 kw .update (_pop_props (kw , "collection" ))
4783- kw = self ._parse_cmap (x , y , z , to_centers = to_centers , ** kw )
4841+ center_levels = kw .pop ("center_levels" , None )
4842+ kw = self ._parse_cmap (
4843+ x , y , z , to_centers = to_centers , center_levels = center_levels , ** kw
4844+ )
47844845 edgefix_kw = _pop_params (kw , self ._fix_patch_edges )
47854846 labels_kw = _pop_params (kw , self ._add_auto_labels )
47864847 guide_kw = _pop_params (kw , self ._update_guide )
47874848 with self ._keep_grid_bools ():
47884849 m = self ._call_native ("pcolormesh" , x , y , z , ** kw )
47894850 self ._fix_patch_edges (m , ** edgefix_kw , ** kw )
47904851 self ._add_auto_labels (m , ** labels_kw )
4852+ # Add center levels to keywords
4853+ guide_kw .setdefault ("colorbar_kw" , {})["center_levels" ] = center_levels
47914854 self ._update_guide (m , queue_colorbar = False , ** guide_kw )
47924855 return m
47934856
@@ -4800,7 +4863,10 @@ def pcolorfast(self, x, y, z, **kwargs):
48004863 """
48014864 x , y , z , kw = self ._parse_2d_args (x , y , z , edges = True , ** kwargs )
48024865 kw .update (_pop_props (kw , "collection" ))
4803- kw = self ._parse_cmap (x , y , z , to_centers = True , ** kw )
4866+ center_levels = kw .pop ("center_levels" , None )
4867+ kw = self ._parse_cmap (
4868+ x , y , z , center_levels = center_levels , to_centers = True , ** kw
4869+ )
48044870 edgefix_kw = _pop_params (kw , self ._fix_patch_edges )
48054871 labels_kw = _pop_params (kw , self ._add_auto_labels )
48064872 guide_kw = _pop_params (kw , self ._update_guide )
@@ -4947,13 +5013,15 @@ def tricontour(self, *args, **kwargs):
49475013
49485014 # Update kwargs and handle cmap
49495015 kw .update (_pop_props (kw , "collection" ))
5016+ center_levels = kw .pop ("center_levels" , None )
49505017 kw = self ._parse_cmap (
49515018 triangulation .x ,
49525019 triangulation .y ,
49535020 z ,
49545021 min_levels = 1 ,
49555022 plot_lines = True ,
49565023 plot_contours = True ,
5024+ center_levels = center_levels ,
49575025 ** kw ,
49585026 )
49595027
@@ -4986,8 +5054,14 @@ def tricontourf(self, *args, **kwargs):
49865054 # Update kwargs and handle contour parameters
49875055 kw .update (_pop_props (kw , "collection" ))
49885056 contour_kw = _pop_kwargs (kw , "edgecolors" , "linewidths" , "linestyles" )
5057+ center_levels = kw .pop ("center_levels" , None )
49895058 kw = self ._parse_cmap (
4990- triangulation .x , triangulation .y , z , plot_contours = True , ** kw
5059+ triangulation .x ,
5060+ triangulation .y ,
5061+ z ,
5062+ center_levels = center_levels ,
5063+ plot_contours = True ,
5064+ ** kw ,
49915065 )
49925066
49935067 # Handle patch edges, labels, and guide parameters
@@ -5026,7 +5100,10 @@ def tripcolor(self, *args, **kwargs):
50265100
50275101 # Update kwargs and handle cmap
50285102 kw .update (_pop_props (kw , "collection" ))
5029- kw = self ._parse_cmap (triangulation .x , triangulation .y , z , ** kw )
5103+ center_levels = kw .pop ("center_levels" , None )
5104+ kw = self ._parse_cmap (
5105+ triangulation .x , triangulation .y , z , center_levels = center_levels , ** kw
5106+ )
50305107
50315108 # Handle patch edges, labels, and guide parameters
50325109 edgefix_kw = _pop_params (kw , self ._fix_patch_edges )
@@ -5053,7 +5130,10 @@ def imshow(self, z, **kwargs):
50535130 %(plot.imshow)s
50545131 """
50555132 kw = kwargs .copy ()
5056- kw = self ._parse_cmap (z , default_discrete = False , ** kw )
5133+ center_levels = kw .pop ("center_levels" , None )
5134+ kw = self ._parse_cmap (
5135+ z , center_levels = center_levels , default_discrete = False , ** kw
5136+ )
50575137 guide_kw = _pop_params (kw , self ._update_guide )
50585138 m = self ._call_native ("imshow" , z , ** kw )
50595139 self ._update_guide (m , queue_colorbar = False , ** guide_kw )
@@ -5081,7 +5161,10 @@ def spy(self, z, **kwargs):
50815161 kw = kwargs .copy ()
50825162 kw .update (_pop_props (kw , "line" )) # takes valid Line2D properties
50835163 default_cmap = pcolors .DiscreteColormap (["w" , "k" ], "_no_name" )
5084- kw = self ._parse_cmap (z , default_cmap = default_cmap , ** kw )
5164+ center_levels = kw .pop ("center_levels" , None )
5165+ kw = self ._parse_cmap (
5166+ z , center_levels = center_levels , default_cmap = default_cmap , ** kw
5167+ )
50855168 guide_kw = _pop_params (kw , self ._update_guide )
50865169 m = self ._call_native ("spy" , z , ** kw )
50875170 self ._update_guide (m , queue_colorbar = False , ** guide_kw )
0 commit comments