Repository: holoviz/holoviews

Issue:
Bokeh Arrow plot broken?
I can run the Matplotlib example gallery/demos/matplotlib/dropdown_economic.ipynb but the Bokeh version gallery/demos/bokeh/dropdown_economic.ipynb  gives me a traceback when run with holoviews=1.11.2.post9+gee503cd and bokeh=1.0.4:

```
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
~/anaconda/envs/pyviz/lib/python3.7/site-packages/IPython/core/formatters.py in __call__(self, obj, include, exclude)
    968 
    969             if method is not None:
--> 970                 return method(include=include, exclude=exclude)
    971             return None
    972         else:

~/holoviews_git/holoviews/core/dimension.py in _repr_mimebundle_(self, include, exclude)
   1423         combined and returned.
   1424         """
-> 1425         return Store.render(self)
   1426 
   1427 

~/holoviews_git/holoviews/core/options.py in render(cls, obj)
   1507         data, metadata = {}, {}
   1508         for hook in hooks:
-> 1509             ret = hook(obj)
   1510             if ret is None:
   1511                 continue

~/holoviews_git/holoviews/ipython/display_hooks.py in pprint_display(obj)
    270     if not ip.display_formatter.formatters['text/plain'].pprint:
    271         return None
--> 272     return display(obj, raw_output=True)
    273 
    274 

~/holoviews_git/holoviews/ipython/display_hooks.py in display(obj, raw_output, **kwargs)
    246     elif isinstance(obj, (HoloMap, DynamicMap)):
    247         with option_state(obj):
--> 248             output = map_display(obj)
    249     elif isinstance(obj, Plot):
    250         output = render(obj)

~/holoviews_git/holoviews/ipython/display_hooks.py in wrapped(element)
    140         try:
    141             max_frames = OutputSettings.options['max_frames']
--> 142             mimebundle = fn(element, max_frames=max_frames)
    143             if mimebundle is None:
    144                 return {}, {}

~/holoviews_git/holoviews/ipython/display_hooks.py in map_display(vmap, max_frames)
    200         return None
    201 
--> 202     return render(vmap)
    203 
    204 

~/holoviews_git/holoviews/ipython/display_hooks.py in render(obj, **kwargs)
     63         renderer = renderer.instance(fig='png')
     64 
---> 65     return renderer.components(obj, **kwargs)
     66 
     67 

~/holoviews_git/holoviews/plotting/bokeh/renderer.py in components(self, obj, fmt, comm, **kwargs)
    247         # Bokeh has to handle comms directly in <0.12.15
    248         comm = False if bokeh_version < '0.12.15' else comm
--> 249         return super(BokehRenderer, self).components(obj,fmt, comm, **kwargs)
    250 
    251 

~/holoviews_git/holoviews/plotting/renderer.py in components(self, obj, fmt, comm, **kwargs)
    319             plot = obj
    320         else:
--> 321             plot, fmt = self._validate(obj, fmt)
    322 
    323         widget_id = None

~/holoviews_git/holoviews/plotting/renderer.py in _validate(self, obj, fmt, **kwargs)
    218         if isinstance(obj, tuple(self.widgets.values())):
    219             return obj, 'html'
--> 220         plot = self.get_plot(obj, renderer=self, **kwargs)
    221 
    222         fig_formats = self.mode_formats['fig'][self.mode]

~/holoviews_git/holoviews/plotting/bokeh/renderer.py in get_plot(self_or_cls, obj, doc, renderer, **kwargs)
    132             curdoc().theme = self_or_cls.theme
    133         doc.theme = self_or_cls.theme
--> 134         plot = super(BokehRenderer, self_or_cls).get_plot(obj, renderer, **kwargs)
    135         plot.document = doc
    136         return plot

~/holoviews_git/holoviews/plotting/renderer.py in get_plot(self_or_cls, obj, renderer, **kwargs)
    205             init_key = tuple(v if d is None else d for v, d in
    206                              zip(plot.keys[0], defaults))
--> 207             plot.update(init_key)
    208         else:
    209             plot = obj

~/holoviews_git/holoviews/plotting/plot.py in update(self, key)
    594         if len(self) == 1 and ((key == 0) or (key == self.keys[0])) and not self.drawn:
    595             return self.initialize_plot()
--> 596         item = self.__getitem__(key)
    597         self.traverse(lambda x: setattr(x, '_updated', True))
    598         return item

~/holoviews_git/holoviews/plotting/plot.py in __getitem__(self, frame)
    256         if isinstance(frame, int) and frame > len(self):
    257             self.param.warning("Showing last frame available: %d" % len(self))
--> 258         if not self.drawn: self.handles['fig'] = self.initialize_plot()
    259         if not isinstance(frame, tuple):
    260             frame = self.keys[frame]

~/holoviews_git/holoviews/plotting/bokeh/element.py in initialize_plot(self, ranges, plot, plots)
   1858             if self.tabs:
   1859                 subplot.overlaid = False
-> 1860             child = subplot.initialize_plot(ranges, plot, plots)
   1861             if isinstance(element, CompositeOverlay):
   1862                 # Ensure that all subplots are in the same state

~/holoviews_git/holoviews/plotting/bokeh/element.py in initialize_plot(self, ranges, plot, plots, source)
   1057         self.handles['plot'] = plot
   1058 
-> 1059         self._init_glyphs(plot, element, ranges, source)
   1060         if not self.overlaid:
   1061             self._update_plot(key, plot, style_element)

~/holoviews_git/holoviews/plotting/bokeh/element.py in _init_glyphs(self, plot, element, ranges, source, data, mapping, style)
   1235                 self._update_glyph(renderer, properties, mapping.get(key, {}), glyph,
   1236                                    source, source.data)
-> 1237         if self.colorbar:
   1238             for k, v in list(self.handles.items()):
   1239                 if not k.endswith('color_mapper'):

AttributeError: 'ArrowPlot' object has no attribute 'colorbar'

:HoloMap   [country]
   :Overlay
      .Curve.I   :Curve   [year]   (gdp)
      .Scatter.I :Scatter   [year]   (gdp,unem)
      .Arrow.I   :Arrow   [x,y]
      .Arrow.II  :Arrow   [x,y]
      .Arrow.III :Arrow   [x,y]
      .Arrow.IV  :Arrow   [x,y]
```


Discussion:
Looks like it works in v1.11.0 but was broken in v1.11.1; not sure why it's working on the website (I thought the website was just updated?). It looks like https://github.com/ioam/holoviews/pull/3396 is the commit that broke it; maybe most plots have self.colorbar but Arrows don't?

Resolving patch:
diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py
--- a/holoviews/plotting/bokeh/annotation.py
+++ b/holoviews/plotting/bokeh/annotation.py
@@ -235,6 +235,7 @@ def get_data(self, element, ranges, style):
         return ({'label': label_data},
                 {'arrow': arrow_opts, 'label': label_mapping}, style)
 
+
     def _init_glyph(self, plot, mapping, properties, key):
         """
         Returns a Bokeh glyph object.
@@ -246,14 +247,15 @@ def _init_glyph(self, plot, mapping, properties, key):
             arrow_start = mapping.pop('arrow_start')
             start = arrow_start(**properties) if arrow_start else None
             end = arrow_end(**properties) if arrow_end else None
-            glyph = Arrow(start=start, end=end, **dict(**mapping))
+            renderer = Arrow(start=start, end=end, **dict(**mapping))
+            glyph = renderer
         else:
             properties = {p if p == 'source' else 'text_'+p: v
                           for p, v in properties.items()}
-            glyph, _ = super(ArrowPlot, self)._init_glyph(
+            renderer, glyph = super(ArrowPlot, self)._init_glyph(
                 plot, mapping, properties, 'text_1')
-        plot.renderers.append(glyph)
-        return None, glyph
+        plot.renderers.append(renderer)
+        return renderer, glyph
 
     def get_extents(self, element, ranges=None, range_type='combined'):
         return None, None, None, None
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -1234,7 +1234,7 @@ def _init_glyphs(self, plot, element, ranges, source, data=None, mapping=None, s
             with abbreviated_exception():
                 self._update_glyph(renderer, properties, mapping.get(key, {}), glyph,
                                    source, source.data)
-        if self.colorbar:
+        if getattr(self, 'colorbar', False):
             for k, v in list(self.handles.items()):
                 if not k.endswith('color_mapper'):
                     continue


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/testannotationplot.py b/holoviews/tests/plotting/bokeh/testannotationplot.py
--- a/holoviews/tests/plotting/bokeh/testannotationplot.py
+++ b/holoviews/tests/plotting/bokeh/testannotationplot.py
@@ -1,6 +1,6 @@
 import numpy as np
 
-from holoviews.element import HLine, VLine, Text, Labels
+from holoviews.element import HLine, VLine, Text, Labels, Arrow
 
 from .testplot import TestBokehPlot, bokeh_renderer
 
@@ -55,7 +55,7 @@ def test_text_plot_rotation(self):
         plot = bokeh_renderer.get_plot(text)
         glyph = plot.handles['glyph']
         self.assertEqual(glyph.angle, np.pi/2.)
-    
+
     def test_text_plot_rotation_style(self):
         text = Text(0, 0, 'Test').options(angle=90)
         plot = bokeh_renderer.get_plot(text)
@@ -63,6 +63,42 @@ def test_text_plot_rotation_style(self):
         self.assertEqual(glyph.angle, np.pi/2.)
 
 
+class TestArrowPlot(TestBokehPlot):
+
+    def _compare_arrow_plot(self, plot, start, end):
+        arrow_glyph = plot.handles['arrow_glyph']
+        label_glyph = plot.handles['label_glyph']
+        label_cds = plot.handles['label_source']
+        x0, y0 = start
+        x1, y1 = end
+        self.assertEqual(label_glyph.x, 'x')
+        self.assertEqual(label_glyph.y, 'y')
+        self.assertEqual(label_cds.data, {'x': [x0], 'y': [y0], 'text': ['Test']})
+        self.assertEqual(arrow_glyph.x_start, x0)
+        self.assertEqual(arrow_glyph.x_end, x1)
+        self.assertEqual(arrow_glyph.y_start, y0)
+        self.assertEqual(arrow_glyph.y_end, y1)
+
+    def test_arrow_plot_left(self):
+        arrow = Arrow(0, 0, 'Test')
+        plot = bokeh_renderer.get_plot(arrow)
+        self._compare_arrow_plot(plot, (1/6., 0), (0, 0))
+
+    def test_arrow_plot_up(self):
+        arrow = Arrow(0, 0, 'Test', '^')
+        plot = bokeh_renderer.get_plot(arrow)
+        self._compare_arrow_plot(plot, (0, -1/6.), (0, 0))
+
+    def test_arrow_plot_right(self):
+        arrow = Arrow(0, 0, 'Test', '>')
+        plot = bokeh_renderer.get_plot(arrow)
+        self._compare_arrow_plot(plot, (-1/6., 0), (0, 0))
+
+    def test_arrow_plot_down(self):
+        arrow = Arrow(0, 0, 'Test', 'v')
+        plot = bokeh_renderer.get_plot(arrow)
+        self._compare_arrow_plot(plot, (0, 1/6.), (0, 0))
+
 
 class TestLabelsPlot(TestBokehPlot):