Repository: holoviz/holoviews

Issue:
Update BoxEditCallback and RectanglesTableLinkCallback to also use Bokeh's Quad
With the changes in https://github.com/holoviz/holoviews/pull/5664, using `Quad` instead of `Rect` behind the scenes, we need also to update `BoxEditCallback`

 https://github.com/holoviz/holoviews/blob/4d0f80b22dde0108cb63b093f89a493912a982f1/holoviews/plotting/bokeh/callbacks.py#L1159




``` python
import holoviews as hv
from holoviews import annotate

hv.extension("bokeh")

rectangles = hv.Rectangles([(0, 0, 10, 10)])
box_annotate = annotate.instance()
box_annotate(rectangles, annotations=["Label"], name="Rectangles")
```

``` python-traceback
Task exception was never retrieved
future: <Task finished name='Task-9' coro=<Callback.process_on_change() done, defined at /home/shh/Repos/holoviz/holoviews/holoviews/plotting/bokeh/callbacks.py:322> exception=KeyError('x')>
Traceback (most recent call last):
  File "/home/shh/Repos/holoviz/holoviews/holoviews/plotting/bokeh/callbacks.py", line 351, in process_on_change
    self.on_msg(msg)
  File "/home/shh/Repos/holoviz/holoviews/holoviews/plotting/bokeh/callbacks.py", line 170, in on_msg
    processed_msg = self._process_msg(filtered_msg)
  File "/home/shh/Repos/holoviz/holoviews/holoviews/plotting/bokeh/callbacks.py", line 1219, in _process_msg
    for (x, y, w, h) in zip(data['x'], data['y'], data['width'], data['height']):
KeyError: 'x'

```

Edit: Also seems to be needed for `RectanglesTableLinkCallback` both in Holoviews and GeoViews:

https://github.com/holoviz/holoviews/blob/4d0f80b22dde0108cb63b093f89a493912a982f1/holoviews/plotting/bokeh/links.py#L225

https://github.com/holoviz/geoviews/blob/dbd3bfa66fb0e443b99d3a6c75fdee3409e70edd/geoviews/links.py#L237



Discussion:

Resolving patch:
diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py
--- a/holoviews/plotting/bokeh/callbacks.py
+++ b/holoviews/plotting/bokeh/callbacks.py
@@ -8,7 +8,7 @@
 
 from bokeh.models import (
     CustomJS, FactorRange, DatetimeAxis, Range1d, DataRange1d,
-    PolyDrawTool, BoxEditTool, PolyEditTool, FreehandDrawTool,
+    PolyDrawTool, PolyEditTool, FreehandDrawTool,
     PointDrawTool
 )
 from panel.io.state import state
@@ -1167,20 +1167,20 @@ def _path_initialize(self):
         data = cds.data
         element = self.plot.current_frame
 
-        xs, ys, widths, heights = [], [], [], []
+        l, b, r, t =  [], [], [], []
         for x, y in zip(data['xs'], data['ys']):
             x0, x1 = (np.nanmin(x), np.nanmax(x))
             y0, y1 = (np.nanmin(y), np.nanmax(y))
-            xs.append((x0+x1)/2.)
-            ys.append((y0+y1)/2.)
-            widths.append(x1-x0)
-            heights.append(y1-y0)
-        data = {'x': xs, 'y': ys, 'width': widths, 'height': heights}
+            l.append(x0)
+            b.append(y0)
+            r.append(x1)
+            t.append(y1)
+        data = {'left': l, 'bottom': b, 'right': r, 'top': t}
         data.update({vd.name: element.dimension_values(vd, expanded=False) for vd in element.vdims})
         cds.data.update(data)
         style = self.plot.style[self.plot.cyclic_index]
         style.pop('cmap', None)
-        r1 = plot.state.rect('x', 'y', 'width', 'height', source=cds, **style)
+        r1 = plot.state.rect('left', 'bottom', 'right', 'top', source=cds, **style)
         if plot.handles['glyph_renderer'] in self.plot.state.renderers:
             self.plot.state.renderers.remove(plot.handles['glyph_renderer'])
         data = self._process_msg({'data': data})['data']
@@ -1205,8 +1205,9 @@ def initialize(self, plot_id=None):
             renderer = self._path_initialize()
         if stream.styles:
             self._create_style_callback(cds, renderer.glyph)
-        box_tool = BoxEditTool(renderers=[renderer], **kwargs)
-        self.plot.state.tools.append(box_tool)
+        # BoxEditTool does not support Quad type only Rect
+        # box_tool = BoxEditTool(renderers=[renderer], **kwargs)
+        # self.plot.state.tools.append(box_tool)
         self._update_cds_vdims(cds.data)
         super(CDSCallback, self).initialize()
 
@@ -1214,19 +1215,12 @@ def _process_msg(self, msg):
         data = super()._process_msg(msg)
         if 'data' not in data:
             return {}
-        data = data['data']
-        x0s, x1s, y0s, y1s = [], [], [], []
-        for (x, y, w, h) in zip(data['x'], data['y'], data['width'], data['height']):
-            x0s.append(x-w/2.)
-            x1s.append(x+w/2.)
-            y0s.append(y-h/2.)
-            y1s.append(y+h/2.)
-        values = {}
-        for col in data:
-            if col in ('x', 'y', 'width', 'height'):
-                continue
-            values[col] = data[col]
-        msg = {'data': dict(values, x0=x0s, x1=x1s, y0=y0s, y1=y1s)}
+        values = dict(data['data'])
+        values['x0'] = values.pop("left")
+        values['y0'] = values.pop("bottom")
+        values['x1'] = values.pop("right")
+        values['y1'] = values.pop("top")
+        msg = {'data': values}
         self._update_cds_vdims(msg['data'])
         return self._transform(msg)
 
diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py
--- a/holoviews/plotting/bokeh/links.py
+++ b/holoviews/plotting/bokeh/links.py
@@ -233,53 +233,17 @@ class RectanglesTableLinkCallback(DataLinkCallback):
     on_target_changes = ['patching']
 
     source_code = """
-    var xs = source_cds.data[source_glyph.x.field]
-    var ys = source_cds.data[source_glyph.y.field]
-    var ws = source_cds.data[source_glyph.width.field]
-    var hs = source_cds.data[source_glyph.height.field]
-
-    var x0 = []
-    var x1 = []
-    var y0 = []
-    var y1 = []
-    for (var i = 0; i < xs.length; i++) {
-      var hw = ws[i]/2.
-      var hh = hs[i]/2.
-      x0.push(xs[i]-hw)
-      x1.push(xs[i]+hw)
-      y0.push(ys[i]-hh)
-      y1.push(ys[i]+hh)
-    }
-    target_cds.data[columns[0]] = x0
-    target_cds.data[columns[1]] = y0
-    target_cds.data[columns[2]] = x1
-    target_cds.data[columns[3]] = y1
+    target_cds.data[columns[0]] = source_cds.data[source_glyph.left.field]
+    target_cds.data[columns[1]] = source_cds.data[source_glyph.bottom.field]
+    target_cds.data[columns[2]] = source_cds.data[source_glyph.right.field]
+    target_cds.data[columns[3]] = source_cds.data[source_glyph.top.field]
     """
 
     target_code = """
-    var x0s = target_cds.data[columns[0]]
-    var y0s = target_cds.data[columns[1]]
-    var x1s = target_cds.data[columns[2]]
-    var y1s = target_cds.data[columns[3]]
-
-    var xs = []
-    var ys = []
-    var ws = []
-    var hs = []
-    for (var i = 0; i < x0s.length; i++) {
-      var x0 = Math.min(x0s[i], x1s[i])
-      var y0 = Math.min(y0s[i], y1s[i])
-      var x1 = Math.max(x0s[i], x1s[i])
-      var y1 = Math.max(y0s[i], y1s[i])
-      xs.push((x0+x1)/2.)
-      ys.push((y0+y1)/2.)
-      ws.push(x1-x0)
-      hs.push(y1-y0)
-    }
-    source_cds.data['x'] = xs
-    source_cds.data['y'] = ys
-    source_cds.data['width'] = ws
-    source_cds.data['height'] = hs
+    source_cds.data['left'] = target_cds.data[columns[0]]
+    source_cds.data['bottom'] = target_cds.data[columns[1]]
+    source_cds.data['right'] = target_cds.data[columns[2]]
+    source_cds.data['top'] = target_cds.data[columns[3]]
     """
 
     def __init__(self, root_model, link, source_plot, target_plot=None):


Test patch:
diff --git a/holoviews/tests/plotting/bokeh/test_callbacks.py b/holoviews/tests/plotting/bokeh/test_callbacks.py
--- a/holoviews/tests/plotting/bokeh/test_callbacks.py
+++ b/holoviews/tests/plotting/bokeh/test_callbacks.py
@@ -260,7 +260,7 @@ def test_box_edit_callback(self):
         self.assertEqual(source.data['bottom'], [-0.5])
         self.assertEqual(source.data['right'], [0.5])
         self.assertEqual(source.data['top'], [0.5])
-        data = {'x': [0, 1], 'y': [0, 1], 'width': [0.5, 2], 'height': [2, 0.5]}
+        data = {'left': [-0.25, 0], 'bottom': [-1, 0.75], 'right': [0.25, 2], 'top': [1, 1.25]}
         callback.on_msg({'data': data})
         element = Rectangles([(-0.25, -1, 0.25, 1), (0, 0.75, 2, 1.25)])
         self.assertEqual(box_edit.element, element)
@@ -272,11 +272,11 @@ def test_box_edit_callback_legacy(self):
         self.assertIsInstance(plot.callbacks[0], BoxEditCallback)
         callback = plot.callbacks[0]
         source = plot.handles['cds']
-        self.assertEqual(source.data['x'], [0])
-        self.assertEqual(source.data['y'], [0])
-        self.assertEqual(source.data['width'], [1])
-        self.assertEqual(source.data['height'], [1])
-        data = {'x': [0, 1], 'y': [0, 1], 'width': [0.5, 2], 'height': [2, 0.5]}
+        self.assertEqual(source.data['left'], [-0.5])
+        self.assertEqual(source.data['bottom'], [-0.5])
+        self.assertEqual(source.data['right'], [0.5])
+        self.assertEqual(source.data['top'], [0.5])
+        data = {'left': [-0.25, 0], 'bottom': [-1, 0.75], 'right': [0.25, 2], 'top': [1, 1.25]}
         callback.on_msg({'data': data})
         element = Polygons([Box(0, 0, (0.5, 2)), Box(1, 1, (2, 0.5))])
         self.assertEqual(box_edit.element, element)