Repository: bokeh/bokeh

Issue:
ColorMapping - color values out of high/low
``` python
data = [-1, 0, np.NaN, 1, 2]
cmap = LinearColorMapper(palette=["red", "green"], low=0, high=1, nan_color=‘gray’, high_color=‘orange’, low_color=‘pink’)
# expect: [‘pink’, ‘red’, ‘gray’, ‘green’, ‘orange’]
```

LogColorMapper should also receive this treatment.



Discussion:

Resolving patch:
diff --git a/bokeh/models/mappers.py b/bokeh/models/mappers.py
--- a/bokeh/models/mappers.py
+++ b/bokeh/models/mappers.py
@@ -74,6 +74,15 @@ class ContinuousColorMapper(ColorMapper):
     this are clamped to ``high``.
     """)
 
+    low_color = Color(default=None, help="""
+    Color to be used if data is lower than ``low`` value. If None,
+    values lower than ``low`` are mapped to the first color in the palette.
+    """)
+
+    high_color = Color(default=None, help="""
+    Color to be used if data is lower than ``high`` value. If None,
+    values lower than ``high`` are mapped to the last color in the palette.
+    """)
 
 class LinearColorMapper(ContinuousColorMapper):
     """ Map numbers in a range [*low*, *high*] linearly into a
@@ -92,6 +101,7 @@ class LinearColorMapper(ContinuousColorMapper):
     """
 
 
+
 class LogColorMapper(ContinuousColorMapper):
     """ Map numbers in a range [*low*, *high*] into a
     sequence of colors (a palette) on a natural logarithm scale.
diff --git a/examples/plotting/file/color_data_map.py b/examples/plotting/file/color_data_map.py
--- a/examples/plotting/file/color_data_map.py
+++ b/examples/plotting/file/color_data_map.py
@@ -11,13 +11,15 @@
 from bokeh.palettes import Viridis3, Viridis256
 from bokeh.plotting import figure
 
-x = np.random.random(2500) * 100
+x = np.random.random(2500) * 140 - 20
 y = np.random.normal(size=2500) * 2 + 5
 source = ColumnDataSource(dict(x=x, y=y))
 opts = dict(x='x', line_color=None, source=source)
 
 
 def make_plot(mapper, title):
+    mapper.low_color = 'blue'
+    mapper.high_color = 'red'
     p = figure(toolbar_location=None, tools='', title=title)
     color_bar = ColorBar(color_mapper=mapper, location=(0, 0))
     p.circle(
@@ -28,9 +30,9 @@ def make_plot(mapper, title):
     p.add_layout(color_bar, 'right')
     return p
 
-p1 = make_plot(LinearColorMapper(palette=Viridis256, low=0, high=100), title='Viridis256 - Linear')
-p2 = make_plot(LogColorMapper(palette=Viridis256, low=0, high=100), title='Viridis256 - Log')
-p3 = make_plot(LinearColorMapper(palette=Viridis3, low=0, high=100), title='Viridis3 - Linear')
-p4 = make_plot(LogColorMapper(palette=Viridis3, low=0, high=100), title='Viridis3 - Log')
+p1 = make_plot(LinearColorMapper(palette=Viridis256, low=0, high=100), title='Viridis256 - Linear, low/high = blue/red')
+p2 = make_plot(LogColorMapper(palette=Viridis256, low=0, high=100), title='Viridis256 - Log, low/high = blue/red')
+p3 = make_plot(LinearColorMapper(palette=Viridis3, low=0, high=100), title='Viridis3 - Linear, low/high = blue/red')
+p4 = make_plot(LogColorMapper(palette=Viridis3, low=0, high=100), title='Viridis3 - Log, low/high =, blue/red')
 
 show(gridplot([p1, p2, p3, p4], ncols=2, plot_width=400, plot_height=300, toolbar_location=None))


Test patch:
diff --git a/bokeh/models/tests/test_mappers.py b/bokeh/models/tests/test_mappers.py
--- a/bokeh/models/tests/test_mappers.py
+++ b/bokeh/models/tests/test_mappers.py
@@ -15,6 +15,8 @@ def test_LinearColorMapper():
         "palette",
         "low",
         "high",
+        "low_color",
+        "high_color",
         "nan_color"],
     )
 
@@ -25,6 +27,8 @@ def test_LogColorMapper():
         "palette",
         "low",
         "high",
+        "low_color",
+        "high_color",
         "nan_color"],
     )
 
diff --git a/bokehjs/test/models/mappers/linear_color_mapper.coffee b/bokehjs/test/models/mappers/linear_color_mapper.coffee
--- a/bokehjs/test/models/mappers/linear_color_mapper.coffee
+++ b/bokehjs/test/models/mappers/linear_color_mapper.coffee
@@ -103,3 +103,16 @@ describe "LinearColorMapper module", ->
 
       vals = color_mapper._get_values([1, NaN, 3], palette)
       expect(vals).to.be.deep.equal(["red", "gray", "blue"])
+
+    it "Should map high/low values to high_color/low_color, if provided", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LinearColorMapper({
+          low: 0
+          high: 2
+          palette: palette
+          low_color: "pink"
+          high_color: "orange"
+        })
+
+      vals = color_mapper._get_values([-1, 0, 1, 2, 3], palette)
+      expect(vals).to.be.deep.equal(["pink", "red", "green", "blue", "orange"])
diff --git a/bokehjs/test/models/mappers/log_color_mapper.coffee b/bokehjs/test/models/mappers/log_color_mapper.coffee
--- a/bokehjs/test/models/mappers/log_color_mapper.coffee
+++ b/bokehjs/test/models/mappers/log_color_mapper.coffee
@@ -75,3 +75,16 @@ describe "LogColorMapper module", ->
 
       vals = color_mapper._get_values([1, NaN, 100], palette)
       expect(vals).to.be.deep.equal(["red", "gray", "blue"])
+
+    it "Should map high/low values to high_color/low_color, if provided", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LogColorMapper({
+          low: 1
+          high: 100
+          low_color: "pink"
+          high_color: "orange"
+          palette: palette
+        })
+
+      vals = color_mapper._get_values([0.5, 1, 10, 100, 101], palette)
+      expect(vals).to.be.deep.equal(["pink", "red", "green", "blue", "orange"])