Repository: bokeh/bokeh

Issue:
Allow supplying explicit edge paths to GraphRenderer
Currently when plotting a graph using the GraphRenderer and StaticLayoutProvider there is no way to supply an explicit set of paths because StaticLayoutProvider draws straight line paths from the start and end points. It is trivial to allow supplying explicit ``xs`` and ``ys`` for the MultiLine glyph, so I will open a PR to allow that shortly. 


Discussion:

Resolving patch:
diff --git a/sphinx/source/docs/user_guide/examples/graph_static_paths.py b/sphinx/source/docs/user_guide/examples/graph_static_paths.py
new file mode 100644
--- /dev/null
+++ b/sphinx/source/docs/user_guide/examples/graph_static_paths.py
@@ -0,0 +1,49 @@
+import math
+
+from bokeh.io import show, output_file
+from bokeh.plotting import figure
+from bokeh.models import GraphRenderer, StaticLayoutProvider, Oval
+from bokeh.palettes import Spectral8
+
+N = 8
+node_indices = list(range(N))
+
+plot = figure(title="Graph Layout Demonstration", x_range=(-1.1,1.1), y_range=(-1.1,1.1),
+              tools="", toolbar_location=None)
+
+graph = GraphRenderer()
+
+graph.node_renderer.data_source.data = dict(
+    index=node_indices,
+    fill_color=Spectral8)
+graph.node_renderer.glyph = Oval(height=0.1, width=0.2, fill_color="fill_color")
+
+graph.edge_renderer.data_source.data = dict(
+    start=[0]*N,
+    end=node_indices)
+
+### start of layout code
+circ = [i*2*math.pi/8 for i in node_indices]
+x = [math.cos(i) for i in circ]
+y = [math.sin(i) for i in circ]
+graph_layout = dict(zip(node_indices, zip(x, y)))
+graph.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)
+
+### Draw quadratic bezier paths
+def bezier(start, end, control, steps):
+    return [(1-s)**2*start + 2*(1-s)*s*control + s**2*end for s in steps]
+
+xs, ys = [], []
+sx, sy = graph_layout[0]
+steps = [i/100. for i in range(100)]
+for node_index in node_indices:
+    ex, ey = graph_layout[node_index]
+    xs.append(bezier(sx, ex, 0, steps))
+    ys.append(bezier(sy, ey, 0, steps))
+graph.edge_renderer.data_source.data['xs'] = xs
+graph.edge_renderer.data_source.data['ys'] = ys
+
+plot.renderers.append(graph)
+
+output_file("graph.html")
+show(plot)


Test patch:
diff --git a/bokehjs/test/models/graphs/static_layout_provider.coffee b/bokehjs/test/models/graphs/static_layout_provider.coffee
--- a/bokehjs/test/models/graphs/static_layout_provider.coffee
+++ b/bokehjs/test/models/graphs/static_layout_provider.coffee
@@ -47,6 +47,17 @@ describe "StaticLayoutProvider", ->
         expect(xs).to.be.deep.equal([ [ -1, 0 ], [ -1, 1 ], [ -1, 0 ] ])
         expect(ys).to.be.deep.equal([ [ 0, 1 ], [ 0, 0 ], [ 0, -1 ] ])
 
+      it "should return explicit edge coords if exist", ->
+        edge_source = new ColumnDataSource()
+        edge_source.data.start = [0,0,0]
+        edge_source.data.end = [1,2,3]
+        edge_source.data.xs = [ [ -1, -0.5, 0 ], [ -1, 0, 1 ], [ -1, -0.5, 0 ] ]
+        edge_source.data.ys = [ [ 0, 0.5, 1 ], [ 0, 0, 0 ], [ 0, -0.5, -1 ] ]
+
+        [xs, ys] = @layout_provider.get_edge_coordinates(edge_source)
+        expect(xs).to.be.deep.equal([ [ -1, -0.5, 0 ], [ -1, 0, 1 ], [ -1, -0.5, 0 ] ])
+        expect(ys).to.be.deep.equal([ [ 0, 0.5, 1 ], [ 0, 0, 0 ], [ 0, -0.5, -1 ] ])
+
       it "should return NaNs if coords don't exist", ->
         edge_source = new ColumnDataSource()
         edge_source.data.start = [4,4,4]
@@ -55,3 +66,14 @@ describe "StaticLayoutProvider", ->
         [xs, ys] = @layout_provider.get_edge_coordinates(edge_source)
         expect(xs).to.be.deep.equal([ [ NaN, NaN ], [ NaN, NaN ], [ NaN, NaN ] ])
         expect(ys).to.be.deep.equal([ [ NaN, NaN ], [ NaN, NaN ], [ NaN, NaN ] ])
+
+      it "should not return explicit edge coords if coords don't exist", ->
+        edge_source = new ColumnDataSource()
+        edge_source.data.start = [4,4,4]
+        edge_source.data.end = [5,6,7]
+        edge_source.data.xs = [ [ -1, -0.5, 0 ], [ -1, 0, 1 ], [ -1, -0.5, 0 ] ]
+        edge_source.data.ys = [ [ 0, 0.5, 1 ], [ 0, 0, 0 ], [ 0, -0.5, -1 ] ]
+
+        [xs, ys] = @layout_provider.get_edge_coordinates(edge_source)
+        expect(xs).to.be.deep.equal([ [ NaN, NaN ], [ NaN, NaN ], [ NaN, NaN ] ])
+        expect(ys).to.be.deep.equal([ [ NaN, NaN ], [ NaN, NaN ], [ NaN, NaN ] ])