Repository: bokeh/bokeh

Issue:
`nonselection_color` (etc.) doesn't apply after PR #9153
See e.g. `examples/app/selection_histogram.py`.

Ref: #9153 


Discussion:
cc @solstag 

I'd regard this as a failure of test coverage, primarily. We should make sure the fix includes additional tests. 
@solstag do you have any thoughts? In a nutshell, code like this:
```
p.scatter(x, y, radius=radii,
          fill_color=colors, fill_alpha=0.6)
```
Produces a non-selection glyph with a fill_alpha 0f 0.6 instead of the trait_default of 0.1 Changing the order of where values are looked for seems to generate the desired result:
```
diff --git a/bokeh/plotting/helpers.py b/bokeh/plotting/helpers.py
index 4dd5294e8..17d7aafe0 100644
--- a/bokeh/plotting/helpers.py
+++ b/bokeh/plotting/helpers.py
@@ -377,10 +377,10 @@ def _pop_visuals(glyphclass, props, prefix="", defaults={}, trait_defaults={}):
             result[pname] = props.pop(prefix+pname)
         elif trait not in glyphprops and prefix+trait in props:
             result[pname] = props[prefix+trait]
-        elif pname in defaults:
-            result[pname] = defaults[pname]
         elif trait in trait_defaults:
             result[pname] = trait_defaults[trait]
+        elif pname in defaults:
+            result[pname] = defaults[pname]
         if trait not in glyphprops:
             traits.add(trait)
     for trait in traits:
```
But in this case maybe a name like `override_defaults` (because they override the glyph when used as a source of defaults) would better?

Resolving patch:
diff --git a/bokeh/plotting/helpers.py b/bokeh/plotting/helpers.py
--- a/bokeh/plotting/helpers.py
+++ b/bokeh/plotting/helpers.py
@@ -313,7 +313,7 @@ def _pop_renderer_args(kwargs):
     return result
 
 
-def _pop_visuals(glyphclass, props, prefix="", defaults={}, trait_defaults={}):
+def _pop_visuals(glyphclass, props, prefix="", defaults={}, override_defaults={}):
     """
     Applies basic cascading logic to deduce properties for a glyph.
 
@@ -337,14 +337,16 @@ def _pop_visuals(glyphclass, props, prefix="", defaults={}, trait_defaults={}):
         prefix (str) :
             Prefix used when accessing `props`. Ex: 'selection_'
 
+        override_defaults (dict) :
+            Explicitly provided fallback based on '{trait}', in case property
+            not set in `props`.
+            Ex. 'width' here may be used for 'selection_line_width'.
+
         defaults (dict) :
-            Property fallback, in case prefixed property not in `props`.
+            Property fallback, in case prefixed property not in `props` or
+            `override_defaults`.
             Ex. 'line_width' here may be used for 'selection_line_width'.
 
-        trait_defaults (dict) :
-            Fallback based on '{trait}', in case property not in `defaults`.
-            Ex. 'width' here may be used for 'selection_line_width'.
-
     Returns:
         result (dict) :
             Resulting properties for the instance (no prefixes).
@@ -365,7 +367,8 @@ def is_visual(ft):
 
     defaults = defaults.copy()
     defaults.setdefault('text_color', 'black')
-    trait_defaults = trait_defaults.copy()
+
+    trait_defaults = {}
     trait_defaults.setdefault('color', get_default_color())
     trait_defaults.setdefault('alpha', 1.0)
 
@@ -373,14 +376,27 @@ def is_visual(ft):
     glyphprops = glyphclass.properties()
     for pname in filter(is_visual, glyphprops):
         _, trait = split_feature_trait(pname)
+
+        # e.g. "line_color", "selection_fill_alpha"
         if prefix+pname in props:
             result[pname] = props.pop(prefix+pname)
+
+        # e.g. "nonselection_alpha"
         elif trait not in glyphprops and prefix+trait in props:
             result[pname] = props[prefix+trait]
+
+        # e.g. an alpha to use for nonselection if none is provided
+        elif trait in override_defaults:
+            result[pname] = override_defaults[trait]
+
+        # e.g use values off the main glyph
         elif pname in defaults:
             result[pname] = defaults[pname]
+
+        # e.g. not specificed anywhere else
         elif trait in trait_defaults:
             result[pname] = trait_defaults[trait]
+
         if trait not in glyphprops:
             traits.add(trait)
     for trait in traits:
@@ -964,7 +980,7 @@ def func(self, **kwargs):
             raise RuntimeError(_GLYPH_SOURCE_MSG % nice_join(incompatible_literal_spec_values, conjuction="and"))
 
         # handle the nonselection glyph, we always set one
-        nsglyph_ca = _pop_visuals(glyphclass, kwargs, prefix='nonselection_', defaults=glyph_ca, trait_defaults={'alpha':0.1})
+        nsglyph_ca = _pop_visuals(glyphclass, kwargs, prefix='nonselection_', defaults=glyph_ca, override_defaults={'alpha':0.1})
 
         # handle the selection glyph, if any properties were given
         if any(x.startswith('selection_') for x in kwargs):


Test patch:
diff --git a/tests/unit/bokeh/plotting/test_helpers.py b/tests/unit/bokeh/plotting/test_helpers.py
--- a/tests/unit/bokeh/plotting/test_helpers.py
+++ b/tests/unit/bokeh/plotting/test_helpers.py
@@ -27,6 +27,7 @@
     CategoricalAxis,
     CategoricalScale,
     CDSView,
+    Circle,
     ColumnDataSource,
     DataRange1d,
     DatetimeAxis,
@@ -70,6 +71,51 @@ def all_combinations(lst):
 # Private API
 #-----------------------------------------------------------------------------
 
+class Test__pop_visuals(object):
+
+    def test_basic_prop(self):
+        kwargs = dict(fill_alpha=0.7, line_alpha=0.8, line_color="red")
+        ca = bph._pop_visuals(Circle, kwargs)
+        assert ca['fill_alpha'] == 0.7
+        assert ca['line_alpha'] == 0.8
+        assert ca["line_color"] == "red"
+        assert ca["fill_color"] == "#1f77b4"
+        assert set(ca) == { "fill_color", "line_color", "fill_alpha", "line_alpha" }
+
+    def test_basic_trait(self):
+        kwargs = dict(fill_alpha=0.7, alpha=0.8, color="red")
+        ca = bph._pop_visuals(Circle, kwargs)
+        assert ca['fill_alpha'] == 0.7
+        assert ca['line_alpha'] == 0.8
+        assert ca["line_color"] == "red"
+        assert ca["fill_color"] == "red"
+        assert set(ca) == { "fill_color", "line_color", "fill_alpha", "line_alpha" }
+
+    def test_override_defaults_with_prefix(self):
+        glyph_kwargs = dict(fill_alpha=1, line_alpha=1)
+        kwargs=dict(alpha=0.6)
+        ca = bph._pop_visuals(Circle, kwargs, prefix='nonselection_', defaults=glyph_kwargs, override_defaults={'alpha':0.1})
+        assert ca['fill_alpha'] == 0.1
+        assert ca['line_alpha'] == 0.1
+
+    def test_defaults(self):
+        kwargs = dict(fill_alpha=0.7, line_alpha=0.8, line_color="red")
+        ca = bph._pop_visuals(Circle, kwargs, defaults=dict(line_color="blue", fill_color="green"))
+        assert ca['fill_alpha'] == 0.7
+        assert ca['line_alpha'] == 0.8
+        assert ca["line_color"] == "red"
+        assert ca["fill_color"] == "green"
+        assert set(ca) == { "fill_color", "line_color", "fill_alpha", "line_alpha" }
+
+    def test_override_defaults(self):
+        kwargs = dict(fill_alpha=0.7, line_alpha=0.8)
+        ca = bph._pop_visuals(Circle, kwargs, defaults=dict(line_color="blue", fill_color="green"), override_defaults=dict(color="white"))
+        assert ca['fill_alpha'] == 0.7
+        assert ca['line_alpha'] == 0.8
+        assert ca["line_color"] == "white"
+        assert ca["fill_color"] == "white"
+        assert set(ca) == { "fill_color", "line_color", "fill_alpha", "line_alpha" }
+
 @pytest.mark.parametrize('key', LEGEND_KWS)
 def test__pop_legend_kwarg(key):
     kws = {'foo': 10, key: 'bar'}