Repository: bokeh/bokeh

Issue:
ColorMapping - support NaN's and data lower than low
Make sure we correctly support the following cases:

``` python
data = [-1, 0, 1, 2]
cmap = LinearColorMapper(palette=["red", "green"], low=0, high=1)
# Expect: c_data = ["red", "red", "green", "green"]
```

``` python
data = [-1, np.NaN, 1, 2]
cmap = LinearColorMapper(palette=["red", "green"], low=0, high=1)
# Expect: c_data = ["red", "gray", "green", "green"]
```



Discussion:

Resolving patch:
diff --git a/bokeh/models/glyphs.py b/bokeh/models/glyphs.py
--- a/bokeh/models/glyphs.py
+++ b/bokeh/models/glyphs.py
@@ -333,15 +333,6 @@ def __init__(self, **kwargs):
             # Use a palette (given or default)
             palette = kwargs.pop('palette', 'Greys9')
             mapper = LinearColorMapper(palette)
-
-            reserve_val = kwargs.pop('reserve_val', None)
-            if reserve_val is not None:
-                mapper.reserve_val = reserve_val
-
-            reserve_color = kwargs.pop('reserve_color', None)
-            if reserve_color is not None:
-                mapper.reserve_color = reserve_color
-
             kwargs['color_mapper'] = mapper
 
         super(Image, self).__init__(**kwargs)
diff --git a/bokeh/models/mappers.py b/bokeh/models/mappers.py
--- a/bokeh/models/mappers.py
+++ b/bokeh/models/mappers.py
@@ -9,6 +9,7 @@
 from ..core.enums import Palette
 from .. import palettes
 
+
 @abstract
 class ColorMapper(Model):
     """ Base class for color mapper types. `ColorMapper`` is not
@@ -23,10 +24,26 @@ class ColorMapper(Model):
     any of the palettes shown in :ref:`bokeh.palettes`.
     """).accepts(Enum(Palette), lambda pal: getattr(palettes, pal))
 
+    low = Float(help="""
+    The minimum value of the range to map into the palette. Values below
+    this are clamped to ``low``.
+    """)
+
+    high = Float(help="""
+    The maximum value of the range to map into the palette. Values above
+    this are clamped to ``high``.
+    """)
+
+    nan_color = Color(default="gray", help="""
+    Color to be used if data is NaN. Default: 'gray'
+    """)
+
     def __init__(self, palette=None, **kwargs):
-        if palette is not None: kwargs['palette'] = palette
+        if palette is not None:
+            kwargs['palette'] = palette
         super(ColorMapper, self).__init__(**kwargs)
 
+
 class LinearColorMapper(ColorMapper):
     """ Map numbers in a range [*low*, *high*] linearly into a
     sequence of colors (a palette).
@@ -43,26 +60,6 @@ class LinearColorMapper(ColorMapper):
 
     """
 
-    low = Float(help="""
-    The minimum value of the range to map into the palette. Values below
-    this are clamped to ``low``.
-    """)
-
-    high = Float(help="""
-    The maximum value of the range to map into the palette. Values above
-    this are clamped to ``high``.
-    """)
-
-    # TODO: (jc) what is the color code for transparent?
-    # TODO: (bev) better docstring
-    reserve_color = Color("#ffffff", help="""
-    Used by Abstract Rendering.
-    """)
-
-    # TODO: (bev) better docstring
-    reserve_val = Float(default=None, help="""
-    Used by Abstract Rendering.
-    """)
 
 class LogColorMapper(ColorMapper):
     """ Map numbers in a range [*low*, *high*] into a
@@ -83,13 +80,3 @@ class LogColorMapper(ColorMapper):
         non-negative.
 
     """
-
-    low = Float(help="""
-    The minimum value of the range to map into the palette. Values below
-    this are clamped to ``low``.
-    """)
-
-    high = Float(help="""
-    The maximum value of the range to map into the palette. Values above
-    this are clamped to ``high``.
-    """)


Test patch:
diff --git a/bokeh/models/tests/test_mappers.py b/bokeh/models/tests/test_mappers.py
new file mode 100644
--- /dev/null
+++ b/bokeh/models/tests/test_mappers.py
@@ -0,0 +1,28 @@
+from __future__ import  absolute_import
+
+from bokeh.models.mappers import (
+    LinearColorMapper, LogColorMapper
+)
+
+from .utils.property_utils import (
+    check_properties_existence,
+)
+
+
+def test_LinearColorMapper():
+    mapper = LinearColorMapper()
+    yield (check_properties_existence, mapper, [
+        "palette",
+        "low",
+        "high",
+        "nan_color"],
+    )
+
+def test_LogColorMapper():
+    mapper = LogColorMapper()
+    yield (check_properties_existence, mapper, [
+        "palette",
+        "low",
+        "high",
+        "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
@@ -4,56 +4,102 @@ utils = require "../../utils"
 LinearColorMapper = utils.require("models/mappers/linear_color_mapper").Model
 
 
-describe "LinearColorMapper._get_values method", ->
-
-  it "Should correctly map values along linear scale with high/low unset", ->
-    palette = ["red", "green", "blue"]
-    color_mapper = new LinearColorMapper({
-        palette: palette
-      })
-
-    vals = color_mapper._get_values([99.999, 67, 50, 32, 0.0001], palette)
-    expect(vals).to.be.deep.equal(["blue", "blue", "green", "red", "red"])
-
-    vals = color_mapper._get_values([0.0001, 32, 50, 67, 99.999], palette)
-    expect(vals).to.be.deep.equal(["red", "red", "green", "blue", "blue"])
-
-    vals = color_mapper._get_values([1, 2, 3], palette)
-    expect(vals).to.be.deep.equal(["red", "green", "blue"])
-
-    vals = color_mapper._get_values([3, 2, 1], palette)
-    expect(vals).to.be.deep.equal(["blue", "green", "red"])
-
-    vals = color_mapper._get_values([0, 1, 2], palette)
-    expect(vals).to.be.deep.equal(["red", "green", "blue"])
-
-    vals = color_mapper._get_values([2, 1, 0], palette)
-    expect(vals).to.be.deep.equal(["blue", "green", "red"])
-
-    vals = color_mapper._get_values([-1, 0, 1], palette)
-    expect(vals).to.be.deep.equal(["red", "green", "blue"])
-
-    vals = color_mapper._get_values([1, 0, -1], palette)
-    expect(vals).to.be.deep.equal(["blue", "green", "red"])
-
-  it "Should correctly map values along linear scale with high/low set", ->
-    palette = ["red", "green", "blue"]
-    color_mapper = new LinearColorMapper({
-        low: 1
-        high: 3
-        palette: palette
-      })
-
-    vals = color_mapper._get_values([1, 2, 3], palette)
-    expect(vals).to.be.deep.equal(["red", "green", "blue"])
-
-  it "Should correctly map values along linear scale with high/low set in other direction", ->
-    palette = ["red", "green", "blue"]
-    color_mapper = new LinearColorMapper({
-        low: 3
-        high: 1
-        palette: palette
-      })
-
-    vals = color_mapper._get_values([1, 2, 3], palette)
-    expect(vals).to.be.deep.equal(["blue", "green", "red"])
+describe "LinearColorMapper module", ->
+
+  describe "LinearColorMapper._get_values method", ->
+
+    it "Should map values along linear scale with high/low unset", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LinearColorMapper({
+          palette: palette
+        })
+
+      vals = color_mapper._get_values([99.999, 67, 50, 32, 0.0001], palette)
+      expect(vals).to.be.deep.equal(["blue", "blue", "green", "red", "red"])
+
+      vals = color_mapper._get_values([0.0001, 32, 50, 67, 99.999], palette)
+      expect(vals).to.be.deep.equal(["red", "red", "green", "blue", "blue"])
+
+      vals = color_mapper._get_values([1, 2, 3], palette)
+      expect(vals).to.be.deep.equal(["red", "green", "blue"])
+
+      vals = color_mapper._get_values([3, 2, 1], palette)
+      expect(vals).to.be.deep.equal(["blue", "green", "red"])
+
+      vals = color_mapper._get_values([0, 1, 2], palette)
+      expect(vals).to.be.deep.equal(["red", "green", "blue"])
+
+      vals = color_mapper._get_values([2, 1, 0], palette)
+      expect(vals).to.be.deep.equal(["blue", "green", "red"])
+
+      vals = color_mapper._get_values([-1, 0, 1], palette)
+      expect(vals).to.be.deep.equal(["red", "green", "blue"])
+
+      vals = color_mapper._get_values([1, 0, -1], palette)
+      expect(vals).to.be.deep.equal(["blue", "green", "red"])
+
+    it "Should map values along linear scale with high/low set", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LinearColorMapper({
+          low: 1
+          high: 3
+          palette: palette
+        })
+
+      vals = color_mapper._get_values([1, 2, 3], palette)
+      expect(vals).to.be.deep.equal(["red", "green", "blue"])
+
+    it "Should map values along linear scale with high/low set in other direction", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LinearColorMapper({
+          low: 3
+          high: 1
+          palette: palette
+        })
+
+      vals = color_mapper._get_values([1, 2, 3], palette)
+      expect(vals).to.be.deep.equal(["blue", "green", "red"])
+
+    it "Should map data below low value to low", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LinearColorMapper({
+          low: 1
+          high: 3
+          palette: palette
+        })
+
+      vals = color_mapper._get_values([0, 1, 2], palette)
+      expect(vals).to.be.deep.equal(["red", "red", "green"])
+
+    it "Should map data above high value to high", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LinearColorMapper({
+          low: 1
+          high: 3
+          palette: palette
+        })
+
+      vals = color_mapper._get_values([2, 3, 4], palette)
+      expect(vals).to.be.deep.equal(["green", "blue", "blue"])
+
+    it "Should map data NaN to nan_color value", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LinearColorMapper({
+          low: 1
+          high: 3
+          palette: palette
+          nan_color: "gray"
+        })
+
+      vals = color_mapper._get_values([1, NaN, 3], palette)
+      expect(vals).to.be.deep.equal(["red", "gray", "blue"])
+
+    it "Should map data NaN to nan_color value when high/low not set", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LinearColorMapper({
+          palette: palette
+          nan_color: "gray"
+        })
+
+      vals = color_mapper._get_values([1, NaN, 3], palette)
+      expect(vals).to.be.deep.equal(["red", "gray", "blue"])
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
@@ -3,7 +3,7 @@ utils = require "../../utils"
 
 LogColorMapper = utils.require("models/mappers/log_color_mapper").Model
 
-describe "log_color_mapper module", ->
+describe "LogColorMapper module", ->
 
   describe "LogColorMapper.v_map_screen method", ->
 
@@ -29,3 +29,49 @@ describe "log_color_mapper module", ->
 
       buf8 = new Uint8ClampedArray(color_mapper.v_map_screen([0]))
       expect([buf8[0], buf8[1], buf8[2], buf8[3]]).to.be.deep.equal [50, 136, 189, 255]
+
+  describe "LogColorMapper._get_values method", ->
+
+    it "Should map data below low value to low", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LogColorMapper({
+          low: 1
+          high: 100
+          palette: palette
+        })
+
+      vals = color_mapper._get_values([0, 1, 10], palette)
+      expect(vals).to.be.deep.equal(["red", "red", "green"])
+
+    it "Should map data above high value to high", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LogColorMapper({
+          low: 1
+          high: 100
+          palette: palette
+        })
+
+      vals = color_mapper._get_values([10, 100, 101], palette)
+      expect(vals).to.be.deep.equal(["green", "blue", "blue"])
+
+    it "Should map data NaN to nan_color value", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LogColorMapper({
+          low: 1
+          high: 100
+          palette: palette
+          nan_color: "gray"
+        })
+
+      vals = color_mapper._get_values([1, NaN, 100], palette)
+      expect(vals).to.be.deep.equal(["red", "gray", "blue"])
+
+    it "Should map data NaN to nan_color value when high/low not set", ->
+      palette = ["red", "green", "blue"]
+      color_mapper = new LogColorMapper({
+          palette: palette
+          nan_color: "gray"
+        })
+
+      vals = color_mapper._get_values([1, NaN, 100], palette)
+      expect(vals).to.be.deep.equal(["red", "gray", "blue"])