Repository: bokeh/bokeh

Issue:
Updating of image ColorMapper
The ability to change the low/high of a ColorMapper for an already existing image (adjusting contrast/brightness) through widget callbacks on a server is desired. I could imagine this being accomplished by the following code, which incidentally doesn't throw any errors as is, it just doesn't do anything ;)

```
import numpy as np
from bokeh.plotting import figure, curdoc
from bokeh.layouts import row, layout
from bokeh.models import Slider
from bokeh.models.mappers import LinearColorMapper
from bokeh.palettes import Greys9

def change_image_contrast(attr, old, new):
    fig_im.glyph.color_mapper.update(low=graph_min_slider.value, high=graph_max_slider.value)
    fig_im.trigger('glyph', fig_im.glyph, fig_im.glyph)

graph_min_slider = Slider(title="Min", start=0, end=99, step=1, value=0)
graph_max_slider = Slider(title="Max", start=1, end=100, step=1, value=100)

graph_min_slider.on_change('value', change_image_contrast)
graph_max_slider.on_change('value', change_image_contrast)

fig = figure(plot_width=500, plot_height=500, x_range=(0, 10), y_range=(0, 10))

fig_im = fig.image(image=[np.random.randint(0, 100, (10, 10), dtype='int16')], x=[0], y=[0], dw=[10], dh=[10],
                   color_mapper=LinearColorMapper(low=0, high=100, palette=Greys9))

curdoc().add_root(layout([fig], [row([graph_min_slider, graph_max_slider])]))
```



Discussion:
ping @canavandl we should just make sure to consider this and other use-cases when adding plumbing to support colorbars. 

classifying the lack of plumbing here as a bug


Resolving patch:
diff --git a/bokeh/models/mappers.py b/bokeh/models/mappers.py
--- a/bokeh/models/mappers.py
+++ b/bokeh/models/mappers.py
@@ -16,6 +16,17 @@ class ColorMapper(Model):
 
     """
 
+    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))
+
+    def __init__(self, palette=None, **kwargs):
+        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).
@@ -32,13 +43,6 @@ class LinearColorMapper(ColorMapper):
 
     """
 
-    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``.
@@ -60,10 +64,6 @@ class LinearColorMapper(ColorMapper):
     Used by Abstract Rendering.
     """)
 
-    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.
@@ -84,13 +84,6 @@ class LogColorMapper(ColorMapper):
 
     """
 
-    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``.
@@ -100,7 +93,3 @@ class LogColorMapper(ColorMapper):
     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/glyphs/image.coffee b/bokehjs/test/models/glyphs/image.coffee
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/models/glyphs/image.coffee
@@ -0,0 +1,26 @@
+{expect} = require "chai"
+sinon = require "sinon"
+utils = require "../../utils"
+
+Image = utils.require("models/glyphs/image").Model
+ImageView = utils.require("models/glyphs/image").View
+{Blues} = utils.require("palettes/palettes")
+
+describe "image module", ->
+
+  describe "ImageView._events", ->
+
+    it "Changing color_mapper attr should call `_update_image` method on view", ->
+      spy = sinon.spy(ImageView.prototype, "_update_image")
+
+      image = new Image()
+
+      image_view = new image.default_view({
+        model: image
+        plot_model: null
+        plot_view: null
+      })
+
+      image.color_mapper.palette = Blues.Blues9
+
+      expect(spy.called).to.be.true
diff --git a/bokehjs/test/models/glyphs/index.coffee b/bokehjs/test/models/glyphs/index.coffee
--- a/bokehjs/test/models/glyphs/index.coffee
+++ b/bokehjs/test/models/glyphs/index.coffee
@@ -1 +1,2 @@
+require "./image"
 require "./image_url"
diff --git a/bokehjs/test/models/mappers/color_mapper.coffee b/bokehjs/test/models/mappers/color_mapper.coffee
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/models/mappers/color_mapper.coffee
@@ -0,0 +1,39 @@
+{expect} = require "chai"
+sinon = require "sinon"
+utils = require "../../utils"
+
+ColorMapper = utils.require("models/mappers/color_mapper").Model
+
+describe "ColorMapper module", ->
+
+  beforeEach ->
+    @color_mapper = new ColorMapper({palette: [0x31a354, 0xaddd8e, 0xf7fcb9]})
+
+  describe "ColorMapper.initializer method", ->
+
+    it "Should set `_little_endian` private attribute", ->
+      # is_little_endian is platform (browser?) dependent, so we're just checking it's set
+      expect(@color_mapper).to.have.property("_little_endian")
+
+    it "Should set `_palette` private attribute", ->
+      # This method is explicitly tested next, we're just checking it's set here
+      expect(@color_mapper).to.have.property("_palette")
+
+  describe "ColorMapper._build_palette method", ->
+
+    it "should convert palette to Uint32Array with duplicate last value", ->
+      expected = { '0': 3253076, '1': 11394446, '2': 16252089, '3': 16252089 }
+      expect(@color_mapper._build_palette([0x31a354, 0xaddd8e, 0xf7fcb9])).to.be.deep.equal(expected)
+
+  describe "Changing palette model attr should reset _build_palette private attr", ->
+
+    it "Should call `_build_palette` method and change `_palette` attr value", ->
+      spy = sinon.spy(ColorMapper.prototype, "_build_palette")
+
+      new_palette = [0x43a2ca, 0xa8ddb5, 0xe0f3db]
+      @color_mapper.palette = new_palette
+
+      expect(spy.withArgs(new_palette).calledOnce).to.be.true
+      expect(@color_mapper._palette).to.be.deep.equal({ '0': 4432586, '1': 11066805, '2': 14742491, '3': 14742491 })
+
+      spy.restore()
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,4 +1,5 @@
 require "./categorical_mapper"
+require "./color_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
--- a/bokehjs/test/models/mappers/log_color_mapper.coffee
+++ b/bokehjs/test/models/mappers/log_color_mapper.coffee
@@ -5,34 +5,27 @@ 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]))
+  describe "LogColorMapper.v_map_screen method", ->
+
+    it "Should correctly map values along log scale", ->
+      color_mapper = new LogColorMapper({
+          low: 2
+          high: 25
+          palette: ["#3288bd", "#abdda4", "#fee08b"]
+        })
+
+      buf8 = new Uint8ClampedArray(color_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]))
+      buf8 = new Uint8ClampedArray(color_mapper.v_map_screen([20]))
       expect([buf8[0], buf8[1], buf8[2], buf8[3]]).to.be.deep.equal [254, 224, 139, 255]
 
-  describe "zero value handling of LogColorMapper", ->
-    mapper = new LogColorMapper({
-        low: 0
-        high: 10
-        palette: ["#3288bd", "#abdda4", "#fee08b"]
-      })
+    it "Should correctly handle zero values", ->
+      color_mapper = new LogColorMapper({
+          low: 0
+          high: 10
+          palette: ["#3288bd", "#abdda4", "#fee08b"]
+        })
 
-    it "should map values along log scale", ->
-      buf8 = new Uint8ClampedArray(mapper.v_map_screen([0]))
+      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]