Repository: bokeh/bokeh

Issue:
LogColorMapper?
Bokeh provides a ColorMapper base class and a [LinearColorMapper](http://bokeh.pydata.org/en/0.11.1/docs/reference/models/mappers.html), but no logarithmic color mapper as far as I can see.  Can a logarithmic mapper be provided?  matplotlib provides LogNorm and SymLogNorm, and it would be very useful if similar functionality were available in Bokeh.



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
@@ -353,7 +353,7 @@ def __init__(self, **kwargs):
     images to have a gap between them, when they should appear flush.
     """)
 
-    color_mapper = Instance(LinearColorMapper, help="""
+    color_mapper = Instance(ColorMapper, help="""
     A ``ColorMapper`` to use to map the scalar data from ``image``
     into RGBA values for display.
 
diff --git a/bokeh/models/mappers.py b/bokeh/models/mappers.py
--- a/bokeh/models/mappers.py
+++ b/bokeh/models/mappers.py
@@ -5,10 +5,9 @@
 
 from ..model import Model
 from ..core.properties import abstract
-from ..core.properties import Float, Color, Enum, Seq, Instance
+from ..core.properties import Float, Color, Enum, Seq
 from ..core.enums import Palette
 from .. import palettes
-from .transforms import Jitter
 
 @abstract
 class ColorMapper(Model):
@@ -64,3 +63,44 @@ class LinearColorMapper(ColorMapper):
     def __init__(self, palette=None, **kwargs):
         if palette is not None: kwargs['palette'] = palette
         super(LinearColorMapper, self).__init__(**kwargs)
+
+class LogColorMapper(ColorMapper):
+    """ Map numbers in a range [*low*, *high*] into a
+    sequence of colors (a palette) on a natural logarithm scale.
+
+    For example, if the range is [0, 25] and the palette is
+    ``['red', 'green', 'blue']``, the values would be mapped as
+    follows::
+
+                x < 0     : 'red'     # values < low are clamped
+       0     >= x < 2.72  : 'red'     # math.e ** 1
+       2.72  >= x < 7.39  : 'green'   # math.e ** 2
+       7.39  >= x < 20.09 : 'blue'    # math.e ** 3
+       20.09 >= x         : 'blue'    # values > high are clamped
+
+    .. warning::
+        The LogColorMapper only works for images with scalar values that are
+        non-negative.
+
+    """
+
+    palette = Seq(Color, help="""
+    A sequence of colors to use as the target palette for mapping.
+
+    This property can also be set as a ``String``, to the name of
+    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``.
+    """)
+
+    def __init__(self, palette=None, **kwargs):
+        if palette is not None: kwargs['palette'] = palette
+        super(LogColorMapper, self).__init__(**kwargs)


Test patch:
diff --git a/bokehjs/test/models/mappers/index.coffee b/bokehjs/test/models/mappers/index.coffee
--- a/bokehjs/test/models/mappers/index.coffee
+++ b/bokehjs/test/models/mappers/index.coffee
@@ -1,3 +1,4 @@
 require "./categorical_mapper"
 require "./linear_mapper"
+require "./log_color_mapper"
 require "./log_mapper"
diff --git a/bokehjs/test/models/mappers/log_color_mapper.coffee b/bokehjs/test/models/mappers/log_color_mapper.coffee
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/models/mappers/log_color_mapper.coffee
@@ -0,0 +1,27 @@
+{expect} = require "chai"
+utils = require "../../utils"
+
+LogColorMapper = utils.require("models/mappers/log_color_mapper").Model
+
+describe "log_color_mapper module", ->
+
+  describe "creation of LogColorMapper", ->
+    mapper = new LogColorMapper({
+        low: 2
+        high: 25
+        palette: ["#3288bd", "#abdda4", "#fee08b"]
+      })
+
+    it "should check and set platform endian", ->
+      expect('_little_endian' of mapper).to.be.true
+
+    it "should convert palette to Uint32Array with duplicate last value", ->
+      _palette = { '0': 3311805, '1': 11263396, '2': 16703627, '3': 16703627 }
+      expect(mapper._palette).to.be.deep.equal(_palette)
+
+    it "should map values along log scale", ->
+      buf8 = new Uint8ClampedArray(mapper.v_map_screen([2]))
+      expect([buf8[0], buf8[1], buf8[2], buf8[3]]).to.be.deep.equal [50, 136, 189, 255]
+
+      buf8 = new Uint8ClampedArray(mapper.v_map_screen([20]))
+      expect([buf8[0], buf8[1], buf8[2], buf8[3]]).to.be.deep.equal [254, 224, 139, 255]