Repository: bokeh/bokeh

Issue:
[Feature Request] ‘from_networkx’ function for a fixed layout
# Summary
Currently (ver 0.13.0) Bokeh, `from_networkx` function always requests a layout function. 
Personally I think that it is more useful to be able to convert a graph structure using **a fixed layout** as well as layout functions.

# Detail
## Use Cases
I think that this feature is particularly useful in the following scenes.

(A) The case where **the user wants to specify a fixed layout that customizes the position by oneself** instead of using a layout function.
- For example, a transportation network associating nodes with geographical positions.

(B) The case where **the user wants to calculate a layout before calling `from_networkx`** since the calculation time is too long.	
 - Currently, layout is calculated every time `from_networkx` is called. Therefore, it takes time to try to recreate `GraphRenderer` many times. It will become a large problem in the case of a large graph.

I think that it’s a general case to use a fixed layout.

## Workaround
I currently handle a fixed layout by the following methods. But they are **NOT** fundamental solutions.

1. Specify a layout function (eg `nx.spring_layout function`, etc.) as **a dummy** and then update the `layout_provider` attribute with a fixed layout.
    -  [Code Example](http://nbviewer.jupyter.org/github/komo-fr/networkx2bokeh_note/blob/master/notebooks/networkx2bokeh_layout_for_custom_position.ipynb)
2. Prepare a function that returns a fixed layout (dict) and pass it in the argument `layout_function`.
    - [Code Example](http://nbviewer.jupyter.org/github/komo-fr/networkx2bokeh_note/blob/master/notebooks/use_function_return_layout.ipynb#Workaround)

### Problem
Workaround (1) solves use case (A), but it  does **NOT** solve use case (B).
It seems that workaround (2) can solve both use case (A) and (B), **BUT** I think that it’s a bother.
Therefore I hope for a fundamental solution.

# Expected Result
1. Be able to specify a fixed layout in `from_networkx`
2. Or, be able to use `from_networkx` without a layout function

# Solution
I imagine there are several methods of implementation as below, but I don't really have a strong opinion.
If Bokeh has an API design policy and future vision, I would like to follow them.

## Before
https://github.com/bokeh/bokeh/blob/e46ef320ff33be0b64be9d8fbd2eea2ad86aa24c/bokeh/models/graphs.py#L35-L109

## After
### Approach 1. Add a new function for graphs with a fixed layout
- Add a new function apart from the existing `from_networkx`.
- The difference from `from_networkx` is that it is passed a fixed layout (dict) as an argument.

[Code & Usage Example](http://nbviewer.jupyter.org/github/komo-fr/networkx2bokeh_note/blob/master/notebooks/draft_from_networkx_for_fixed_layout.ipynb#Approach-1.-Add-a-new-function-for-graphs-with-fixed-layout)

### Approach 2. Update `from_networkx` to handle both fixed layout and layout function
- Rename `layout_function` to `layout` and allow to receive both dictionary and function.
- If `layout`  is a function, execute the function and calculate the layout. Otherwise, set it directly to `graph_renderer.layout_provider` in `from_networkx`.
- It is **NOT** backward compatible with 0.13.0 since the argument name changes.

[Code & Usage Example](http://nbviewer.jupyter.org/github/komo-fr/networkx2bokeh_note/blob/master/notebooks/draft_from_networkx_for_fixed_layout.ipynb#Approach-2.-Update-from_networkx-to-handle-both-fixed-layout-and-layout-function)

### Approach 3. Update `from_networkx` so that it can be called without a layout function
- Make `layout_function` an optional argument.
- The user manually sets a fixed layout after calling `from_networkx`. 

[Code & Usage Example](http://nbviewer.jupyter.org/github/komo-fr/networkx2bokeh_note/blob/master/notebooks/draft_from_networkx_for_fixed_layout.ipynb#Approach-3.-Update-from_networkx-so-that--it-can-be-called-without-a-layout-function)

## Or Other Approaches
Other people may have better ideas.

I'm often bothered by this problem, so I'm interested in sending a PR.


Discussion:
I'd suggest:

2b) which is exactly as 2) above but doesn't rename the parameter. I think it is slighltly off ideal, but it is simplest for users and maintains backwards compat. It's a bit of a reach, but a "pure mathematical" definition of a function is just a particular kind of mapping, so I think we an stretch a little and justify a fixed layout to be a kind of "layout_function"

Otherwise:

3b) check for `layout` in kwargs and use that in preference to `layout_function`, but use `layout_function` if it is given. Add a check to make sure both are not specified. We can later deprecate `layout_function`
@komo-fr also worth adding: If you'd like this in 1.0 then Friday is a pretty firm deadline, but it can of course always go in a later release if you are not free before then. 

Resolving patch:
diff --git a/bokeh/models/graphs.py b/bokeh/models/graphs.py
--- a/bokeh/models/graphs.py
+++ b/bokeh/models/graphs.py
@@ -40,7 +40,8 @@ def from_networkx(graph, layout_function, **kwargs):
 
         Args:
             graph (networkx.Graph) : a networkx graph to render
-            layout_function (function) : a networkx layout function
+            layout_function (function or dict) : a networkx layout function or mapping of node keys to positions.
+            The position is a two element sequence containing the x and y coordinate.
 
         Returns:
             instance (GraphRenderer)
@@ -103,7 +104,17 @@ def from_networkx(graph, layout_function, **kwargs):
         graph_renderer.node_renderer.data_source.data = node_source.data
         graph_renderer.edge_renderer.data_source.data = edge_source.data
 
-        graph_layout = layout_function(graph, **kwargs)
+        if callable(layout_function):
+            graph_layout = layout_function(graph, **kwargs)
+        else:
+            graph_layout = layout_function
+
+            node_keys = graph_renderer.node_renderer.data_source.data['index']
+            if set(node_keys) != set(layout_function.keys()):
+                from warnings import warn
+                warn("Node keys in 'layout_function' don't match node keys in the graph. "
+                     "These nodes may not be displayed correctly.")
+
         graph_renderer.layout_provider = StaticLayoutProvider(graph_layout=graph_layout)
 
         return graph_renderer


Test patch:
diff --git a/bokeh/models/tests/test_graphs.py b/bokeh/models/tests/test_graphs.py
--- a/bokeh/models/tests/test_graphs.py
+++ b/bokeh/models/tests/test_graphs.py
@@ -84,3 +84,38 @@ def test_from_networkx_with_bad_attributes():
         assert renderer.edge_renderer.data_source.data["start"] == [0]
         assert renderer.edge_renderer.data_source.data["end"] == [1]
         assert renderer.edge_renderer.data_source.data["attr_1"] == [10]
+
+def test_from_networkx_fixed_layout():
+    G = nx.Graph()
+    G.add_nodes_from([0, 1, 2])
+    G.add_edges_from([[0, 1], [0, 2]])
+
+    fixed_layout = {0: [0, 1],
+                    1: [-1, 0],
+                    2: [1, 0]}
+
+    renderer = from_networkx(G, fixed_layout)
+    assert renderer.node_renderer.data_source.data["index"] == [0, 1, 2]
+    assert renderer.edge_renderer.data_source.data["start"] == [0, 0]
+    assert renderer.edge_renderer.data_source.data["end"] == [1, 2]
+
+    gl = renderer.layout_provider.graph_layout
+    assert set(gl.keys()) == set([0, 1, 2])
+    assert renderer.layout_provider.graph_layout[0] == fixed_layout[0]
+    assert renderer.layout_provider.graph_layout[1] == fixed_layout[1]
+    assert renderer.layout_provider.graph_layout[2] == fixed_layout[2]
+
+def test_from_networkx_with_missing_layout():
+    G = nx.Graph()
+    G.add_nodes_from([0, 1, 2])
+    G.add_edges_from([[0, 1], [0, 2]])
+
+    missing_fixed_layout = {0: [0, 1],
+                            1: [-1, 0]}
+
+    with pytest.warns(UserWarning):
+        renderer = from_networkx(G, missing_fixed_layout)
+        gl = renderer.layout_provider.graph_layout
+        assert set(gl.keys()) == set([0, 1])
+        assert renderer.layout_provider.graph_layout[0] == missing_fixed_layout[0]
+        assert renderer.layout_provider.graph_layout[1] == missing_fixed_layout[1]