Repository: bokeh/bokeh

Issue:
Colorbar Axis



Discussion:
Estimate: 1 day

OK, giving up the pretense. #1 priority after 0.8...

It would be good to get an idea of the structure that you have in mind. Judging by the title, the implementation will be a subclass of Axis in <code>bokeh/models/axes.py</code>?

For reference, matplotlib makes it pretty simple to add a colorbar - but invariably there are specific parms to pass to get exactly what you want.

<pre>
# matplotlib example
plt.streamplot(X, Y, U, V, color=U, linewidth=2, cmap=plt.cm.autumn)
plt.colorbar()
# bokeh?
plotting.output_file("image.html", title='bokeh colorbar example')
plot = plotting.figure(plot_height=600, plot_width=1200, x_range=[0,10], y_range=[0,10])
plot.image(image=[data], x=[0], y=[0], dw=[10], dh=[10], palette='Spectral11')
plot.colorbar()
plotting.save(obj=plot)
</pre>

We use a constraint solver (kiwi.js) to handle the layout of things on the canvas. The issue is not s much the python spelling, or even the actual drawing of a colorbar. I don't expect any of that would take more than a few hours. The issue is that some fairly low level parts of BokehJS dealing with setting up and maintaining layout constraints will need to be extended and pushed beyond where they have been utilized up to this point. Realistically a few new abstractions need to need to be added to keep the complexity and repeated code from spiraling out of hand, and that may take a few days (I'm estimating for me, it would probably be longer for anyone else). If you are potentially interested in working on this, I would be happy to write up an outline of work tasks in more detail. 

An outline of steps/tasks would be very useful to me and hopefully also help you clarify what you have in mind. I have some time I can dedicate towards this but there is clearly a lot of effort required to get to a solid understanding of the python code and even more so the BokehJS part.

I made an interactive on selection colour bar in combination with a colour wash. It's a bit of a work around, but it doesn't use any callbacks:
http://simonbiggs.github.io/electronfactors

It would be great if something like this functionality was packaged in.

@bryevdv Any updates on this?

This will not be in `0.11` proper, but `0.11` has a new mechanism for extending Bokeh with User Defined Models, so I very much hope to be able to create a custom Colorbar axis model that people can easily use until one gets added to Bokeh core library. To support this feature fully really depends on some refactoring of our canvas layout, which I hope to turn to in the next few weeks. We are prioritizing the best we can with the resources we have. 


Resolving patch:
diff --git a/bokeh/models/annotations.py b/bokeh/models/annotations.py
--- a/bokeh/models/annotations.py
+++ b/bokeh/models/annotations.py
@@ -16,8 +16,11 @@
     AngleSpec, Angle, FontSizeSpec, ColorSpec
 )
 
+from .formatters import TickFormatter, BasicTickFormatter
+from .mappers import ColorMapper
 from .renderers import Renderer, GlyphRenderer
 from .sources import DataSource, ColumnDataSource
+from .tickers import Ticker, BasicTicker
 
 @abstract
 class Annotation(Renderer):
@@ -123,6 +126,139 @@ class Legend(Annotation):
         Dict(String, List(Instance(GlyphRenderer))), lambda d: list(d.items())
     )
 
+class ColorBar(Annotation):
+    """ Render a color bar based on a color mapper for a plot.
+    """
+
+    location = Either(Enum(LegendLocation), Tuple(Float, Float),
+        default="top_right", help="""
+    The location where the ColorBar should draw itself. It's either one of
+    ``bokeh.core.enums.LegendLocation``'s enumerated values, or a ``(x, y)``
+    tuple indicating an absolute location absolute location in screen
+    coordinates (pixels from the bottom-left corner).
+    """)
+
+    orientation = Enum(Orientation, default="vertical", help="""
+    Whether the legend entries should be placed vertically or horizontally
+    when they are layed out.
+    """)
+
+    legend_height = Either(Auto, Int(), help="""
+    The height (in pixels) that the rendered legend should occupy.
+    """)
+
+    legend_width = Either(Auto, Int(), help="""
+    The width (in pixels) that the rendered legend should occupy.
+    """)
+
+    title = String(help="""
+    The title text to render.
+    """)
+
+    title_props = Include(TextProps, help="""
+    The %s values for the title text.
+    """)
+
+    title_text_font_size = Override(default={'value': "10pt"})
+
+    title_text_font_style = Override(default="italic")
+
+    title_standoff = Int(2, help="""
+    The distance (in pixels) to separate the title from the ColorBar.
+    """)
+
+    ticker = Instance(Ticker, default=lambda: BasicTicker(), help="""
+    A Ticker to use for computing locations of axis components.
+    """)
+
+    formatter = Instance(TickFormatter, default=lambda: BasicTickFormatter(), help="""
+    A TickFormatter to use for formatting the visual appearance
+    of ticks.
+    """)
+
+    color_mapper = Instance(ColorMapper, help="""
+    A color mapper containing a color palette to render.
+
+    .. warning::
+        If the `low` and `high` attributes of the ColorMapper aren't set, ticks
+        and tick labels won't be rendered.
+    """)
+
+    legend_margin = Int(30, help="""
+    Amount of margin around the legend.
+    """)
+
+    legend_padding = Int(10, help="""
+    Amount of padding around the contents of the legend.
+    """)
+
+    label_standoff = Int(5, help="""
+    The distance (in pixels) to separate the tick labels from the ColorBar.
+    """)
+
+    major_label_props = Include(TextProps, help="""
+    The %s of the major tick labels.
+    """)
+
+    major_label_text_align = Override(default="center")
+
+    major_label_text_baseline = Override(default="middle")
+
+    major_label_text_font_size = Override(default={'value': "8pt"})
+
+    major_tick_props = Include(LineProps, help="""
+    The %s of the major ticks.
+    """)
+
+    major_tick_line_color = Override(default="#ffffff")
+
+    major_tick_in = Int(default=5, help="""
+    The distance in pixels that major ticks should extend into the
+    main plot area.
+    """)
+
+    major_tick_out = Int(default=0, help="""
+    The distance in pixels that major ticks should extend out of the
+    main plot area.
+    """)
+
+    minor_tick_props = Include(LineProps, help="""
+    The %s of the minor ticks.
+    """)
+
+    minor_tick_line_color = Override(default=None)
+
+    minor_tick_in = Int(default=0, help="""
+    The distance in pixels that minor ticks should extend into the
+    main plot area.
+    """)
+
+    minor_tick_out = Int(default=0, help="""
+    The distance in pixels that major ticks should extend out of the
+    main plot area.
+    """)
+
+    bar_props = Include(LineProps, help="""
+    The %s for the colorbar outline.
+    """)
+
+    bar_line_color = Override(default=None)
+
+    border_props = Include(LineProps, help="""
+    The %s for the colorbar border outline.
+    """)
+
+    border_line_color = Override(default=None)
+
+    background_props = Include(FillProps, help="""
+    The %s for the colorbar background style.
+    """)
+
+    background_fill_color = Override(default="#ffffff")
+
+    background_fill_alpha = Override(default=0.95)
+
+
 def _DEFAULT_ARROW():
     from .arrow_heads import OpenHead
     return OpenHead()
diff --git a/sphinx/source/docs/user_guide/source_examples/plotting_color_bars.py b/sphinx/source/docs/user_guide/source_examples/plotting_color_bars.py
new file mode 100644
--- /dev/null
+++ b/sphinx/source/docs/user_guide/source_examples/plotting_color_bars.py
@@ -0,0 +1,26 @@
+import numpy as np
+from matplotlib.mlab import bivariate_normal
+
+from bokeh.plotting import figure, output_file, show
+from bokeh.models import LogColorMapper, LogTicker, ColorBar
+
+output_file('color_bar.html')
+
+N = 100
+X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]
+Z1 = bivariate_normal(X, Y, 0.1, 0.2, 1.0, 1.0) +  \
+    0.1 * bivariate_normal(X, Y, 1.0, 1.0, 0.0, 0.0)
+image = Z1 * 1e6
+
+color_mapper = LogColorMapper(palette="Viridis256", low=1, high=1e7)
+
+plot = figure(x_range=(0,1), y_range=(0,1), toolbar_location=None)
+plot.image(image=[image], color_mapper=color_mapper,
+           dh=[1.0], dw=[1.0], x=[0], y=[0])
+
+color_bar = ColorBar(color_mapper=color_mapper, ticker=LogTicker(),
+                     label_standoff=12, border_line_color=None, location=(0,0))
+
+plot.add_layout(color_bar, 'right')
+
+show(plot)


Test patch:
diff --git a/bokeh/models/tests/test_annotations.py b/bokeh/models/tests/test_annotations.py
--- a/bokeh/models/tests/test_annotations.py
+++ b/bokeh/models/tests/test_annotations.py
@@ -1,53 +1,17 @@
 from __future__ import  absolute_import
 
-from itertools import chain
-
 from bokeh.models.annotations import (
-    Legend, Arrow, BoxAnnotation, Span, LabelSet, Label, Title
+    Legend, ColorBar, Arrow, BoxAnnotation, Span, LabelSet, Label, Title
+)
+from bokeh.models import (
+    ColumnDataSource, ArrowHead, BasicTicker, BasicTickFormatter
 )
-from bokeh.models import ColumnDataSource, ArrowHead
-from bokeh.core.enums import NamedColor as Color, LineJoin, LineCap, TextAlign
-
-FILL = ["fill_color", "fill_alpha"]
-LINE = ["line_color", "line_width", "line_alpha", "line_join", "line_cap",
-        "line_dash", "line_dash_offset"]
-TEXT = ["text_font", "text_font_size", "text_font_style", "text_color",
-        "text_alpha", "text_align", "text_baseline"]
-ANGLE = ["angle", "angle_units"]
-PROPS = ["name", "tags", "visible"]
-
-def prefix(prefix, props):
-    return [prefix + p for p in props]
-
-def check_props(annotation, *props):
-    expected = set(chain(PROPS, *props))
-    found = set(annotation.properties())
-    missing = expected.difference(found)
-    extra = found.difference(expected)
-    assert len(missing) == 0, "Properties missing: {0}".format(", ".join(sorted(missing)))
-    assert len(extra) == 0, "Extra properties: {0}".format(", ".join(sorted(extra)))
-
-def check_fill(annotation, prefix="", fill_color='#ffffff', fill_alpha=1.0):
-    assert getattr(annotation, prefix + "fill_color") == fill_color
-    assert getattr(annotation, prefix + "fill_alpha") == fill_alpha
-
-def check_line(annotation, prefix="", line_color=Color.black, line_width=1.0, line_alpha=1.0):
-    assert getattr(annotation, prefix + "line_color") == line_color
-    assert getattr(annotation, prefix + "line_width") == line_width
-    assert getattr(annotation, prefix + "line_alpha") == line_alpha
-    assert getattr(annotation, prefix + "line_join") == LineJoin.miter
-    assert getattr(annotation, prefix + "line_cap") == LineCap.butt
-    assert getattr(annotation, prefix + "line_dash") == []
-    assert getattr(annotation, prefix + "line_dash_offset") == 0
 
-def check_text(annotation, prefix="", font_size='12pt', baseline='bottom', font_style='normal'):
-    assert getattr(annotation, prefix + "text_font") == "helvetica"
-    assert getattr(annotation, prefix + "text_font_size") == {"value": font_size}
-    assert getattr(annotation, prefix + "text_font_style") == font_style
-    assert getattr(annotation, prefix + "text_color") == "#444444"
-    assert getattr(annotation, prefix + "text_alpha") == 1.0
-    assert getattr(annotation, prefix + "text_align") == TextAlign.left
-    assert getattr(annotation, prefix + "text_baseline") == baseline
+from .utils.property_utils import (
+    FILL, LINE, TEXT, ANGLE, prefix,
+    check_properties_existence, check_fill_properties,
+    check_line_properties, check_text_properties
+)
 
 def test_Legend():
     legend = Legend()
@@ -61,11 +25,12 @@ def test_Legend():
     assert legend.legend_padding == 10
     assert legend.legend_spacing == 3
     assert legend.legends == []
-    yield check_line, legend, "border_", "#e5e5e5", 1.0, 0.5
-    yield check_text, legend, "label_", "10pt", "middle"
-    yield check_fill, legend, "background_", "#ffffff", 0.95
-    yield (check_props, legend, [
+    yield check_line_properties, legend, "border_", "#e5e5e5", 1.0, 0.5
+    yield check_text_properties, legend, "label_", "10pt", "middle"
+    yield check_fill_properties, legend, "background_", "#ffffff", 0.95
+    yield (check_properties_existence, legend, [
         "plot",
+        "visible",
         "location",
         "orientation",
         "label_standoff",
@@ -82,6 +47,61 @@ def test_Legend():
         prefix('border_', LINE),
         prefix('background_', FILL))
 
+def test_ColorBar():
+    color_bar = ColorBar()
+    assert color_bar.plot is None
+    assert color_bar.location == 'top_right'
+    assert color_bar.orientation == 'vertical'
+    assert color_bar.legend_height == 'auto'
+    assert color_bar.legend_width == 'auto'
+    assert color_bar.title is None
+    assert color_bar.title_standoff == 2
+    assert isinstance(color_bar.ticker, BasicTicker)
+    assert isinstance(color_bar.formatter, BasicTickFormatter)
+    assert color_bar.color_mapper is None
+    assert color_bar.legend_margin == 30
+    assert color_bar.legend_padding == 10
+    assert color_bar.label_standoff == 5
+    assert color_bar.major_tick_in == 5
+    assert color_bar.major_tick_out == 0
+    assert color_bar.minor_tick_in == 0
+    assert color_bar.minor_tick_out == 0
+    yield check_text_properties, color_bar, "title_", "10pt", "bottom", "italic"
+    yield check_text_properties, color_bar, "major_label_", "8pt", "middle", "normal", "center"
+    yield check_line_properties, color_bar, "major_tick_", "#ffffff"
+    yield check_line_properties, color_bar, "minor_tick_", None
+    yield check_line_properties, color_bar, "bar_", None
+    yield check_line_properties, color_bar, "border_", None
+    yield check_fill_properties, color_bar, "background_", "#ffffff", 0.95
+    yield (check_properties_existence, color_bar, [
+        "plot",
+        "level",
+        "visible",
+        "location",
+        "orientation",
+        "legend_height",
+        "legend_width",
+        "title",
+        "title_standoff",
+        "ticker",
+        "formatter",
+        "color_mapper",
+        "legend_margin",
+        "legend_padding",
+        "label_standoff",
+        "major_tick_in",
+        "major_tick_out",
+        "minor_tick_in",
+        "minor_tick_out"],
+        prefix('title_', TEXT),
+        prefix('major_label_', TEXT),
+        prefix('major_tick_', LINE),
+        prefix('minor_tick_', LINE),
+        prefix('bar_', LINE),
+        prefix('border_', LINE),
+        prefix('background_', FILL)
+    )
+
 def test_Arrow():
     arrow = Arrow()
     assert arrow.plot is None
@@ -96,10 +116,11 @@ def test_Arrow():
     assert arrow.source is None
     assert arrow.x_range_name == "default"
     assert arrow.y_range_name == "default"
-    yield check_line, arrow
-    yield (check_props, arrow, [
+    yield check_line_properties, arrow
+    yield (check_properties_existence, arrow, [
         "plot",
         "level",
+        "visible",
         "x_start",
         "y_start",
         "start_units",
@@ -127,11 +148,12 @@ def test_BoxAnnotation():
     assert box.x_range_name == 'default'
     assert box.y_range_name == 'default'
     assert box.level == 'annotation'
-    yield check_line, box, "", '#cccccc', 1, 0.3
-    yield check_fill, box, "", "#fff9ba", 0.4
-    yield (check_props, box, [
+    yield check_line_properties, box, "", '#cccccc', 1, 0.3
+    yield check_fill_properties, box, "", "#fff9ba", 0.4
+    yield (check_properties_existence, box, [
         "render_mode",
         "plot",
+        "visible",
         "left",
         "left_units",
         "right",
@@ -161,12 +183,13 @@ def test_Label():
     assert label.render_mode == 'canvas'
     assert label.x_range_name == 'default'
     assert label.y_range_name == 'default'
-    yield check_text, label
-    yield check_fill, label, "background_", None, 1.0
-    yield check_line, label, "border_", None, 1.0, 1.0
-    yield (check_props, label, [
+    yield check_text_properties, label
+    yield check_fill_properties, label, "background_", None, 1.0
+    yield check_line_properties, label, "border_", None, 1.0, 1.0
+    yield (check_properties_existence, label, [
         "plot",
         "level",
+        "visible",
         "x",
         "y",
         "x_units",
@@ -201,11 +224,12 @@ def test_LabelSet():
     assert label_set.y_range_name == 'default'
     assert isinstance(label_set.source, ColumnDataSource)
     assert label_set.source.data == {}
-    yield check_text, label_set
-    yield check_fill, label_set, "background_", None, 1.0
-    yield check_line, label_set, "border_", None, 1.0, 1.0
-    yield (check_props, label_set, [
+    yield check_text_properties, label_set
+    yield check_fill_properties, label_set, "background_", None, 1.0
+    yield check_line_properties, label_set, "border_", None, 1.0, 1.0
+    yield (check_properties_existence, label_set, [
         "plot",
+        "visible",
         "level",
         "x",
         "y",
@@ -235,9 +259,10 @@ def test_Span():
     assert line.y_range_name == 'default'
     assert line.level == 'annotation'
     assert line.render_mode == 'canvas'
-    yield check_line, line, "", 'black', 1.0
-    yield (check_props, line, [
+    yield check_line_properties, line, "", 'black', 1.0
+    yield (check_properties_existence, line, [
         "plot",
+        "visible",
         "location",
         "location_units",
         "dimension",
@@ -259,10 +284,11 @@ def test_Title():
     assert title.text_font_style == 'bold'
     assert title.text_color == '#444444'
     assert title.text_alpha == 1.0
-    yield check_fill, title, "background_", None, 1.0
-    yield check_line, title, "border_", None, 1.0, 1.0
-    yield (check_props, title, [
+    yield check_fill_properties, title, "background_", None, 1.0
+    yield check_line_properties, title, "border_", None, 1.0, 1.0
+    yield (check_properties_existence, title, [
         "plot",
+        "visible",
         "level",
         "text",
         "align",
diff --git a/bokeh/models/tests/test_glyphs.py b/bokeh/models/tests/test_glyphs.py
--- a/bokeh/models/tests/test_glyphs.py
+++ b/bokeh/models/tests/test_glyphs.py
@@ -1,5 +1,11 @@
 from __future__ import absolute_import
 
+from .utils.property_utils import (
+    FILL, LINE, TEXT, GLYPH, MARKER,
+    check_properties_existence, check_fill_properties,
+    check_line_properties, check_text_properties, check_marker_properties
+)
+
 from bokeh.models.glyphs import (
     AnnularWedge, Annulus, Arc,
     Bezier,
@@ -51,49 +57,6 @@
     ButtonType, MapType,
     Color, NamedIcon)
 
-FILL = ["fill_color", "fill_alpha"]
-LINE = ["line_color", "line_width", "line_alpha", "line_join", "line_cap", "line_dash", "line_dash_offset"]
-TEXT = ["text_font", "text_font_size", "text_font_style", "text_color", "text_alpha", "text_align", "text_baseline"]
-
-PROPS = ["name", "tags"]
-GLYPH = ["visible"]
-MARKER = ["x", "y", "size", "angle", "angle_units"]
-
-def check_props(glyph, *props):
-    expected = set(sum((PROPS, GLYPH) + props, []))
-    found = set(glyph.properties())
-    missing = expected.difference(found)
-    extra = found.difference(expected)
-    assert len(missing) == 0, "Properties missing: {0}".format(", ".join(sorted(missing)))
-    assert len(extra) == 0, "Extra properties: {0}".format(", ".join(sorted(extra)))
-
-def check_fill(glyph):
-    assert glyph.fill_color == Color.gray
-    assert glyph.fill_alpha == 1.0
-
-def check_line(glyph):
-    assert glyph.line_color == Color.black
-    assert glyph.line_width == 1
-    assert glyph.line_alpha == 1.0
-    assert glyph.line_join == LineJoin.miter
-    assert glyph.line_cap == LineCap.butt
-    assert glyph.line_dash == []
-    assert glyph.line_dash_offset == 0
-
-def check_text(glyph):
-    assert glyph.text_font == "helvetica"
-    assert glyph.text_font_size == {"value": "12pt"}
-    assert glyph.text_font_style == FontStyle.normal
-    assert glyph.text_color == "#444444"
-    assert glyph.text_alpha == 1.0
-    assert glyph.text_align == TextAlign.left
-    assert glyph.text_baseline == TextBaseline.bottom
-
-def check_marker(marker):
-    assert marker.x is None
-    assert marker.y is None
-    assert marker.size == 4
-
 def test_AnnularWedge():
     glyph = AnnularWedge()
     assert glyph.x is None
@@ -103,9 +66,9 @@ def test_AnnularWedge():
     assert glyph.start_angle is None
     assert glyph.end_angle is None
     assert glyph.direction == "anticlock"
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "inner_radius",
@@ -117,7 +80,7 @@ def test_AnnularWedge():
         "end_angle",
         "end_angle_units",
         "direction",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Annulus():
     glyph = Annulus()
@@ -125,16 +88,16 @@ def test_Annulus():
     assert glyph.y is None
     assert glyph.inner_radius is None
     assert glyph.outer_radius is None
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "inner_radius",
         "inner_radius_units",
         "outer_radius",
         "outer_radius_units",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Arc():
     glyph = Arc()
@@ -144,8 +107,8 @@ def test_Arc():
     assert glyph.start_angle is None
     assert glyph.end_angle is None
     assert glyph.direction == "anticlock"
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "radius",
@@ -155,7 +118,7 @@ def test_Arc():
         "end_angle",
         "end_angle_units",
         "direction",
-    ], LINE)
+    ], LINE, GLYPH)
 
 def test_Bezier():
     glyph = Bezier()
@@ -167,8 +130,8 @@ def test_Bezier():
     assert glyph.cy0 is None
     assert glyph.cx1 is None
     assert glyph.cy1 is None
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x0",
         "y0",
         "x1",
@@ -177,7 +140,7 @@ def test_Bezier():
         "cy0",
         "cx1",
         "cy1",
-    ], LINE)
+    ], LINE, GLYPH)
 
 def test_Gear():
     glyph = Gear()
@@ -189,9 +152,9 @@ def test_Gear():
     assert glyph.pressure_angle == 20
     assert glyph.shaft_size == 0.3
     assert glyph.internal == False
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "angle",
@@ -201,7 +164,7 @@ def test_Gear():
         "pressure_angle",
         "shaft_size",
         "internal",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_HBar():
     glyph = HBar()
@@ -209,14 +172,14 @@ def test_HBar():
     assert glyph.height is None
     assert glyph.left == 0
     assert glyph.right is None
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "y",
         "height",
         "left",
         "right",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Image():
     glyph = Image()
@@ -226,7 +189,7 @@ def test_Image():
     assert glyph.dw is None
     assert glyph.dh is None
     assert glyph.dilate == False
-    yield (check_props, glyph, [
+    yield (check_properties_existence, glyph, [
         "image",
         "x",
         "y",
@@ -236,7 +199,7 @@ def test_Image():
         "dh_units",
         "dilate",
         "color_mapper",
-    ])
+    ], GLYPH)
 
 def test_ImageRGBA():
     glyph = ImageRGBA()
@@ -248,7 +211,7 @@ def test_ImageRGBA():
     assert glyph.rows == None
     assert glyph.cols == None
     assert glyph.dilate == False
-    yield (check_props, glyph, [
+    yield (check_properties_existence, glyph, [
         "image",
         "x",
         "y",
@@ -259,7 +222,7 @@ def test_ImageRGBA():
         "rows",
         "cols",
         "dilate",
-    ])
+    ], GLYPH)
 
 def test_ImageURL():
     glyph = ImageURL()
@@ -274,7 +237,7 @@ def test_ImageURL():
     assert glyph.retry_attempts == 0
     assert glyph.retry_timeout == 0
     assert glyph.global_alpha == 1.0
-    yield (check_props, glyph, [
+    yield (check_properties_existence, glyph, [
         "url",
         "x",
         "y",
@@ -289,27 +252,27 @@ def test_ImageURL():
         "retry_attempts",
         "retry_timeout",
         "global_alpha",
-    ])
+    ], GLYPH)
 
 def test_Line():
     glyph = Line()
     assert glyph.x is None
     assert glyph.y is None
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
-    ], LINE)
+    ], LINE, GLYPH)
 
 def test_MultiLine():
     glyph = MultiLine()
     assert glyph.xs is None
     assert glyph.ys is None
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "xs",
         "ys",
-    ], LINE)
+    ], LINE, GLYPH)
 
 def test_Oval():
     glyph = Oval()
@@ -318,9 +281,9 @@ def test_Oval():
     assert glyph.width is None
     assert glyph.height is None
     assert glyph.angle == 0
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "width",
@@ -329,29 +292,29 @@ def test_Oval():
         "height_units",
         "angle",
         "angle_units",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Patch():
     glyph = Patch()
     assert glyph.x is None
     assert glyph.y is None
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Patches():
     glyph = Patches()
     assert glyph.xs is None
     assert glyph.ys is None
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "xs",
         "ys",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Quad():
     glyph = Quad()
@@ -359,14 +322,14 @@ def test_Quad():
     assert glyph.right is None
     assert glyph.bottom is None
     assert glyph.top is None
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "left",
         "right",
         "bottom",
         "top",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Quadratic():
     glyph = Quadratic()
@@ -376,15 +339,15 @@ def test_Quadratic():
     assert glyph.y1 is None
     assert glyph.cx is None
     assert glyph.cy is None
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x0",
         "y0",
         "x1",
         "y1",
         "cx",
         "cy",
-    ], LINE)
+    ], LINE, GLYPH)
 
 def test_Ray():
     glyph = Ray()
@@ -392,15 +355,15 @@ def test_Ray():
     assert glyph.y is None
     assert glyph.angle is None
     assert glyph.length is None
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "angle",
         "angle_units",
         "length",
         "length_units",
-    ], LINE)
+    ], LINE, GLYPH)
 
 def test_Rect():
     glyph = Rect()
@@ -410,9 +373,9 @@ def test_Rect():
     assert glyph.height is None
     assert glyph.angle == 0
     assert glyph.dilate == False
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "width",
@@ -422,7 +385,7 @@ def test_Rect():
         "angle",
         "angle_units",
         "dilate",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Segment():
     glyph = Segment()
@@ -430,13 +393,13 @@ def test_Segment():
     assert glyph.y0 is None
     assert glyph.x1 is None
     assert glyph.y1 is None
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x0",
         "y0",
         "x1",
         "y1"
-    ], LINE)
+    ], LINE, GLYPH)
 
 def test_Text():
     glyph = Text()
@@ -444,8 +407,8 @@ def test_Text():
     assert glyph.y is None
     assert glyph.text == "text"
     assert glyph.angle == 0
-    yield check_text, glyph
-    yield (check_props, glyph, [
+    yield check_text_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "text",
@@ -453,7 +416,7 @@ def test_Text():
         "angle_units",
         "x_offset",
         "y_offset"
-    ], TEXT)
+    ], TEXT, GLYPH)
 
 def test_VBar():
     glyph = VBar()
@@ -461,14 +424,14 @@ def test_VBar():
     assert glyph.width is None
     assert glyph.top is None
     assert glyph.bottom == 0
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "width",
         "top",
         "bottom",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Wedge():
     glyph = Wedge()
@@ -478,9 +441,9 @@ def test_Wedge():
     assert glyph.start_angle is None
     assert glyph.end_angle is None
     assert glyph.direction == "anticlock"
-    yield check_fill, glyph
-    yield check_line, glyph
-    yield (check_props, glyph, [
+    yield check_fill_properties, glyph
+    yield check_line_properties, glyph
+    yield (check_properties_existence, glyph, [
         "x",
         "y",
         "radius",
@@ -490,100 +453,100 @@ def test_Wedge():
         "end_angle",
         "end_angle_units",
         "direction",
-    ], FILL, LINE)
+    ], FILL, LINE, GLYPH)
 
 def test_Asterisk():
     marker = Asterisk()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_Circle():
     marker = Circle()
-    yield check_marker, marker
+    yield check_marker_properties, marker
     assert marker.radius == None
-    yield check_fill, marker
-    yield check_line, marker
-    yield (check_props, marker, [
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield (check_properties_existence, marker, [
         "radius",
         "radius_units",
         "radius_dimension",
-    ], MARKER, FILL, LINE)
+    ], MARKER, FILL, LINE, GLYPH)
 
 def test_CircleCross():
     marker = CircleCross()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_CircleX():
     marker = CircleX()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_Cross():
     marker = Cross()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_Diamond():
     marker = Diamond()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_DiamondCross():
     marker = DiamondCross()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_InvertedTriangle():
     marker = InvertedTriangle()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_Square():
     marker = Square()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_SquareCross():
     marker = SquareCross()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_SquareX():
     marker = SquareX()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_Triangle():
     marker = Triangle()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
 
 def test_X():
     marker = X()
-    yield check_marker, marker
-    yield check_fill, marker
-    yield check_line, marker
-    yield check_props, marker, MARKER, FILL, LINE
+    yield check_marker_properties, marker
+    yield check_fill_properties, marker
+    yield check_line_properties, marker
+    yield check_properties_existence, marker, MARKER, FILL, LINE, GLYPH
diff --git a/bokeh/models/tests/utils/__init__.py b/bokeh/models/tests/utils/__init__.py
new file mode 100644
diff --git a/bokeh/models/tests/utils/property_utils.py b/bokeh/models/tests/utils/property_utils.py
new file mode 100644
--- /dev/null
+++ b/bokeh/models/tests/utils/property_utils.py
@@ -0,0 +1,52 @@
+from itertools import chain
+
+from bokeh.core.enums import NamedColor as Color, LineJoin, LineCap
+
+# enums
+FILL = ["fill_color", "fill_alpha"]
+LINE = ["line_color", "line_width", "line_alpha", "line_join", "line_cap", "line_dash", "line_dash_offset"]
+TEXT = ["text_font", "text_font_size", "text_font_style", "text_color", "text_alpha", "text_align", "text_baseline"]
+
+ANGLE = ["angle", "angle_units"]
+
+PROPS = ["name", "tags"]
+GLYPH = ["visible"]
+MARKER = ["x", "y", "size", "angle", "angle_units"]
+
+def prefix(prefix, props):
+    return [prefix + p for p in props]
+
+def check_properties_existence(model, *props):
+    expected = set(chain(PROPS, *props))
+    found = set(model.properties())
+    missing = expected.difference(found)
+    extra = found.difference(expected)
+    assert len(missing) == 0, "Properties missing: {0}".format(", ".join(sorted(missing)))
+    assert len(extra) == 0, "Extra properties: {0}".format(", ".join(sorted(extra)))
+
+def check_fill_properties(model, prefix="", fill_color=Color.gray, fill_alpha=1.0):
+    assert getattr(model, prefix + "fill_color") == fill_color
+    assert getattr(model, prefix + "fill_alpha") == fill_alpha
+
+def check_line_properties(model, prefix="", line_color=Color.black, line_width=1.0, line_alpha=1.0):
+    assert getattr(model, prefix + "line_color") == line_color
+    assert getattr(model, prefix + "line_width") == line_width
+    assert getattr(model, prefix + "line_alpha") == line_alpha
+    assert getattr(model, prefix + "line_join") == LineJoin.miter
+    assert getattr(model, prefix + "line_cap") == LineCap.butt
+    assert getattr(model, prefix + "line_dash") == []
+    assert getattr(model, prefix + "line_dash_offset") == 0
+
+def check_text_properties(model, prefix="", font_size='12pt', baseline='bottom', font_style='normal', align="left"):
+    assert getattr(model, prefix + "text_font") == "helvetica"
+    assert getattr(model, prefix + "text_font_size") == {"value": font_size}
+    assert getattr(model, prefix + "text_font_style") == font_style
+    assert getattr(model, prefix + "text_color") == "#444444"
+    assert getattr(model, prefix + "text_alpha") == 1.0
+    assert getattr(model, prefix + "text_align") == align
+    assert getattr(model, prefix + "text_baseline") == baseline
+
+def check_marker_properties(marker):
+    assert marker.x is None
+    assert marker.y is None
+    assert marker.size == 4
diff --git a/bokehjs/test/models/annotations/color_bar.coffee b/bokehjs/test/models/annotations/color_bar.coffee
new file mode 100644
--- /dev/null
+++ b/bokehjs/test/models/annotations/color_bar.coffee
@@ -0,0 +1,275 @@
+{expect} = require "chai"
+utils = require "../../utils"
+sinon = require 'sinon'
+proxyquire = require "proxyquire"
+
+SidePanel = utils.require("core/layout/side_panel").Model
+LinearColorMapper = utils.require("models/mappers/linear_color_mapper").Model
+LinearMapper = utils.require("models/mappers/linear_mapper").Model
+LogColorMapper = utils.require("models/mappers/log_color_mapper").Model
+LogMapper = utils.require("models/mappers/log_mapper").Model
+LogTicker = utils.require("models/tickers/log_ticker").Model
+{Viridis} = utils.require("palettes/palettes")
+Plot = utils.require("models/plots/plot").Model
+Range1d = utils.require("models/ranges/range1d").Model
+{Document} = utils.require "document"
+
+###
+(LC) sinon doesn't appear to stub functions (vs object methods). This
+work-around using proxyrequire does some `require` hackery to stub the
+`text` module imported by `models/annotations/color_bar` so that
+text.get_text_height can be stubbed.
+###
+textStub = {}
+ColorBar = proxyquire('../../../src/coffee/models/annotations/color_bar',
+                      {"../../core/util/text": textStub})
+
+describe "ColorBar module", ->
+
+  afterEach ->
+    utils.unstub_canvas()
+    textStub.get_text_height.restore()
+
+  beforeEach ->
+    utils.stub_canvas()
+    stub = sinon.stub(textStub, 'get_text_height')
+    stub.returns({'height': 15, 'ascent': 10, 'descent': 5})
+
+    @plot = new Plot({
+       x_range: new Range1d({start: 0, end: 1})
+       y_range: new Range1d({start: 0, end: 1})
+    })
+
+    @color_bar = new ColorBar.Model()
+
+  describe "ColorBar.Model", ->
+
+    beforeEach ->
+      # Stub solver computed values with deterministic frame height and width
+      frame_stub = sinon.stub(@plot.plot_canvas.frame, 'get')
+      frame_stub.withArgs('height').returns(500)
+      frame_stub.withArgs('width').returns(500)
+
+    describe "ColorBar.Model._title_extent method", ->
+
+      it "_title_height should return 0 if there is no title", ->
+        title_height = @color_bar._title_extent()
+        expect(title_height).to.be.equal(0)
+
+      it "_title_height should calculate title height plus title_standoff if there is a title", ->
+        @color_bar.title = "I'm a title"
+        @color_bar.title_standoff = 5
+        title_height = @color_bar._title_extent()
+        expect(title_height).to.be.equal(20)
+
+    describe "ColorBar.Model._tick_extent method", ->
+      it "Should return zero if either low or high are unset", ->
+        @color_bar.color_mapper = new LinearColorMapper({palette: Viridis.Viridis10})
+        expect(@color_bar._tick_extent()).to.be.equal(0)
+
+      it "Should return major_tick_out if both low and high are set", ->
+        @color_bar.color_mapper = new LinearColorMapper({low: 0, high: 10, palette: Viridis.Viridis10})
+        @color_bar.major_tick_out = 6
+        expect(@color_bar._tick_extent()).to.be.equal(6)
+
+    describe "ColorBar.Model._tick_coordinate_mapper method", ->
+
+      it "LinearColorMapper should yield LinearMapper instance with correct state", ->
+        @color_bar.color_mapper = new LinearColorMapper({low: 0, high: 10, palette: Viridis.Viridis10})
+        mapper = @color_bar._tick_coordinate_mapper(100) #length of scale dimension
+        expect(mapper).to.be.instanceof(LinearMapper)
+        expect(mapper.get('mapper_state')).to.be.deep.equal [10, 0]
+
+      it "LogColorMapper should yield LogMapper instance with correct state", ->
+        @color_bar.color_mapper = new LogColorMapper({low: 0, high: 10, palette: Viridis.Viridis10})
+        mapper = @color_bar._tick_coordinate_mapper(100) #length of scale dimension
+        expect(mapper).to.be.instanceof(LogMapper)
+        expect(mapper.get('mapper_state')).to.be.deep.equal [100, 0, 2.302585092994046, 0]
+
+    describe "ColorBar.Model._computed_image_dimensions method", ->
+
+        describe "ColorBar.orientation = 'vertical' in plot frame", ->
+
+          beforeEach ->
+            @plot.add_layout(@color_bar)
+
+          it "Should use set `legend_width` and `legend_height` if set", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis3})
+            @color_bar.legend_width = 100
+            @color_bar.legend_height = 200
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            expect(image_dimensions.width).to.be.equal(100)
+            expect(image_dimensions.height).to.be.equal(200)
+
+          it "Should return height = 0.30 * frame_height for 'short' palette", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis3})
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            expect(image_dimensions.width).to.be.equal(25)
+            expect(image_dimensions.height).to.be.equal(150)
+
+          it "Should return height = palette.length * 25 for 'medium' palette", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis10})
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            expect(image_dimensions.width).to.be.equal(25)
+            expect(image_dimensions.height).to.be.equal(250)
+
+          it "Should return height = 0.80 * plot.height for 'long' palette", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis256})
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            expect(image_dimensions.width).to.be.equal(25)
+            expect(image_dimensions.height).to.be.equal(380)
+
+        describe "ColorBar.orientation = 'vertical' in side frame", ->
+
+          beforeEach ->
+            @plot.add_layout(@color_bar, 'right')
+            document = new Document()
+            document.add_root(@plot)
+
+          it "Should return height = plot.height - 2 * legend_padding for any palette in side panel", ->
+
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis3})
+            @color_bar.title = "I'm a title"
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            expect(image_dimensions.width).to.be.equal(25)
+            # height = 500 (plot.height) - 2 * 10 (color_bar.legend_padding) - 17 (title_height)
+            expect(image_dimensions.height).to.be.equal(463)
+
+        describe "ColorBar.orientation = 'horizontal'", ->
+
+          beforeEach ->
+            @color_bar.orientation = 'horizontal'
+            @plot.add_layout(@color_bar)
+
+          it "Should use set `legend_width` and `legend_height` if set", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis3})
+            @color_bar.legend_width = 100
+            @color_bar.legend_height = 200
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            expect(image_dimensions.width).to.be.equal(100)
+            expect(image_dimensions.height).to.be.equal(200)
+
+          it "Should return width = 0.30 * plot.width for 'short' palette", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis3})
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            expect(image_dimensions.width).to.be.equal(150)
+            expect(image_dimensions.height).to.be.equal(25)
+
+          it "Should return width = palette.length * 25 for 'medium' palette", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis10})
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            expect(image_dimensions.width).to.be.equal(250)
+            expect(image_dimensions.height).to.be.equal(25)
+
+          it "Should return width = 0.80 * plot.width for 'long' palette", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis256})
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            # width = 500 (plot.width) * 0.8 - 2 * 10 (color_bar.legend_padding)
+            expect(image_dimensions.width).to.be.equal(380)
+            expect(image_dimensions.height).to.be.equal(25)
+
+        describe "ColorBar.orientation = 'horizontal' in side frame", ->
+
+          beforeEach ->
+            @color_bar.orientation = 'horizontal'
+            @plot.add_layout(@color_bar, 'below')
+            document = new Document()
+            document.add_root(@plot)
+
+          it "Should return width = plot.width - 2 * legend_padding for any palette in side panel", ->
+            @color_bar.color_mapper = new LinearColorMapper({low: 1, high: 100, palette: Viridis.Viridis10})
+            @color_bar.title = "I'm a title"
+
+            image_dimensions = @color_bar._computed_image_dimensions()
+            # width = 500 (plot.width) - 2 * 10 (color_bar.legend_padding)
+            expect(image_dimensions.width).to.be.equal(480)
+            expect(image_dimensions.height).to.be.equal(25)
+
+    describe "ColorBar.Model._tick_coordinates method", ->
+
+      beforeEach ->
+        @plot.add_layout(@color_bar)
+
+      it "Should correctly determine tick coords and labels for LinearColorMapper", ->
+        @color_bar.color_mapper = new LinearColorMapper({low: 0, high: 10, palette: Viridis.Viridis10})
+        @color_bar.legend_height = 100
+
+        tick_coords = @color_bar._tick_coordinates()
+
+        expect(tick_coords.major[1]).to.be.deep.equal(new Float64Array([0, 20, 40, 60, 80, 100]))
+        expect(tick_coords.major_labels).to.be.deep.equal([0, 2, 4, 6, 8, 10])
+
+      it "Should correctly determine tick coords and labels for LogColorMapper", ->
+        @color_bar.color_mapper = new LogColorMapper({low: 0, high: 1000, palette: Viridis.Viridis10})
+        @color_bar.legend_height = 100
+
+        tick_coords = @color_bar._tick_coordinates()
+
+        expect(tick_coords.major[1]).to.be.deep.equal(new Float64Array([0, 76.70099985546604, 86.73533304426542, 92.60504167945479, 96.76966623306478, 100]))
+        expect(tick_coords.major_labels).to.be.deep.equal([0, 200, 400, 600, 800, 1000])
+
+  describe "ColorBar.View", ->
+
+    afterEach ->
+      @_set_canvas_image_stub.restore()
+
+    beforeEach ->
+      @_set_canvas_image_stub = sinon.stub(ColorBar.View.prototype, '_set_canvas_image')
+
+      @color_bar.color_mapper = new LinearColorMapper({low: 0, high: 10, palette: Viridis.Viridis10})
+
+      @plot.add_layout(@color_bar, 'right')
+      document = new Document()
+      document.add_root(@plot)
+
+      @plot_canvas_view = new @plot.plot_canvas.default_view({ model: @plot.plot_canvas })
+
+      @color_bar_view = new @color_bar.default_view({
+        model: @color_bar
+        plot_model: @plot.plot_canvas
+        plot_view: @plot_canvas_view
+      })
+
+    it "Should reset scale image if color_mapper changes", ->
+      # Reset spy count to zero (method was called during view initialization)
+      @_set_canvas_image_stub.reset()
+      @color_bar.color_mapper.set('palette', Viridis.Viridis3)
+      expect(@_set_canvas_image_stub.called).to.be.true
+
+    it "ColorBar.View._get_image_offset method", ->
+      @color_bar.title = "I'm a title"
+      expect(@color_bar_view._get_image_offset()).to.be.deep.equal({ x: 10, y: 27 })
+
+    it "ColorBar.View._get_label_extent method (orientation='vertical')", ->
+      # Note: ctx.measureText is stubbed to return {'width': 1, 'ascent': 1} in test/utils
+      expect(@color_bar_view._get_label_extent()).to.be.equal(6)
+
+    it "ColorBar.View._get_label_extent method (orientation='horizontal')", ->
+      @color_bar_view.model.orientation = "horizontal"
+      expect(@color_bar_view._get_label_extent()).to.be.equal(20)
+
+    it "ColorBar.View.compute_legend_dimensions method (orientation='vertical')", ->
+      # Note: ctx.measureText is stubbed to return {'width': 1, 'ascent': 1} in test/utils
+      @color_bar.legend_height = 100
+      @color_bar.legend_width = 25
+
+      expect(@color_bar_view.compute_legend_dimensions()).to.be.deep.equal({ height: 120, width: 51 })
+
+    it "ColorBar.View.compute_legend_dimensions method (orientation='horizontal')", ->
+      @color_bar.orientation = "horizontal"
+      @color_bar.legend_height = 25
+      @color_bar.legend_width = 100
+
+      expect(@color_bar_view.compute_legend_dimensions()).to.be.deep.equal({ height: 65, width: 120 })
+
+    it "ColorBar.View._get_size method", ->
+      expect(@color_bar_view._get_size()).to.be.equal(51)
diff --git a/bokehjs/test/models/annotations/index.coffee b/bokehjs/test/models/annotations/index.coffee
--- a/bokehjs/test/models/annotations/index.coffee
+++ b/bokehjs/test/models/annotations/index.coffee
@@ -1,2 +1,3 @@
 require "./annotation"
+require "./color_bar"
 require "./legend"
diff --git a/bokehjs/test/size.coffee b/bokehjs/test/size.coffee
--- a/bokehjs/test/size.coffee
+++ b/bokehjs/test/size.coffee
@@ -11,7 +11,7 @@ LIMITS = {
   "css/bokeh.min.css":         74
   "js/bokeh-compiler.min.js":  178
   "js/bokeh-widgets.min.js":   290
-  "js/bokeh.min.js":           907
+  "js/bokeh.min.js":           918
 }
 
 for filename, maxsize of LIMITS
diff --git a/bokehjs/test/utils.coffee b/bokehjs/test/utils.coffee
--- a/bokehjs/test/utils.coffee
+++ b/bokehjs/test/utils.coffee
@@ -12,6 +12,7 @@ stub_canvas = () ->
     beginPath: () -> null
     clearRect: () -> null
     clip: () -> null
+    drawImage: () -> null
     fillRect: () -> null
     fillText: () -> null
     lineTo: () -> null
diff --git a/tests/integration/annotations/screenshots/base__test_color_bar__test_color_bar_placement_and_render.png b/tests/integration/annotations/screenshots/base__test_color_bar__test_color_bar_placement_and_render.png
new file mode 100644
Binary files /dev/null and b/tests/integration/annotations/screenshots/base__test_color_bar__test_color_bar_placement_and_render.png differ
diff --git a/tests/integration/annotations/test_color_bar.py b/tests/integration/annotations/test_color_bar.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/annotations/test_color_bar.py
@@ -0,0 +1,66 @@
+from __future__ import absolute_import
+
+from bokeh.io import save
+from bokeh.models import (
+    Plot, ColorBar, LogTicker, Range1d, LinearColorMapper, LogColorMapper
+)
+from selenium.webdriver.common.action_chains import ActionChains
+from tests.integration.utils import has_no_console_errors
+
+import pytest
+pytestmark = pytest.mark.integration
+
+HEIGHT = 600
+WIDTH = 600
+
+def create_vertical_color_bar_with_log_cmap(height='auto', width='auto'):
+    color_mapper = LogColorMapper(low=1, high=100000, palette='Viridis256')
+    color_bar = ColorBar(orientation='vertical',
+                         color_mapper=color_mapper,
+                         legend_height=height,
+                         legend_width=width,
+                         ticker=LogTicker(),
+                         label_standoff=12)
+    return color_bar
+
+def create_horizontal_color_bar_with_linear_cmap(height='auto', width='auto'):
+    color_mapper = LinearColorMapper(low=0, high=100, palette='Spectral10')
+    color_bar = ColorBar(orientation='horizontal',
+                         color_mapper=color_mapper,
+                         legend_height=height,
+                         legend_width=width)
+    return color_bar
+
+@pytest.mark.screenshot
+def test_color_bar_placement_and_render(output_file_url, selenium, screenshot):
+    plot = Plot(height=HEIGHT, width=WIDTH,
+                x_range=Range1d(0,10), y_range=Range1d(0,10),
+                toolbar_location=None)
+
+    bar_vertical_right_panel = create_vertical_color_bar_with_log_cmap()
+    bar_vertical_right_panel.location = (0, 0)
+
+    bar_vertical_in_frame = create_vertical_color_bar_with_log_cmap()
+    bar_vertical_in_frame.location = "top_right"
+    bar_vertical_in_frame.title = "Dummy Title"
+    bar_vertical_in_frame.title_standoff = 7
+
+    bar_horizontal_below_panel = create_horizontal_color_bar_with_linear_cmap()
+    bar_horizontal_below_panel.location = (0, 0)
+
+    bar_horizontal_in_frame = create_horizontal_color_bar_with_linear_cmap()
+    bar_horizontal_in_frame.location = "bottom_left"
+    bar_horizontal_in_frame.title = "Dummy Title"
+
+    plot.add_layout(bar_vertical_right_panel, 'right')
+    plot.add_layout(bar_vertical_in_frame)
+    plot.add_layout(bar_horizontal_below_panel, 'below')
+    plot.add_layout(bar_horizontal_in_frame)
+
+    # Save the plot and start the test
+    save(plot)
+    selenium.get(output_file_url)
+    assert has_no_console_errors(selenium)
+
+    # Take screenshot
+    assert screenshot.is_valid()