3

I am trying to add a permanent label on nodes for a networkx graph using spring_layout and bokeh library. I would like for this labels to be re-positioned as the graph scales or refreshed like what string layout does, re-positioning the nodes as the graph scales or refreshed.

I tried to create the graph, and layout, then got pos from the string_layout. However, as I call pos=nx.spring_layout(G), it will generated a set of positions for the nodes in graph G, which I can get coordinates of to put into the LabelSet. However, I have to call graph = from_networkx(G, spring_layout, scale=2, center=(0,0)) to draw the network graph. This will create a new set of position for the node. Therefore, the positions of the nodes and the labels will not be the same.

How to fix this issues?

3 Answers3

3

Thanks for asking this question. Working through it, I've realized that it is currently more work than it should be. I'd very strongly encourage you to open a GitHub issue so that we can discuss what improvements can best make this kind of thing easier for users.

Here is a complete example:

import networkx as nx

from bokeh.io import output_file, show
from bokeh.models import CustomJSTransform, LabelSet
from bokeh.models.graphs import from_networkx

from bokeh.plotting import figure

G=nx.karate_club_graph()

p = figure(x_range=(-3,3), y_range=(-3,3))
p.grid.grid_line_color = None

r = from_networkx(G, nx.spring_layout, scale=3, center=(0,0))
r.node_renderer.glyph.size=15
r.edge_renderer.glyph.line_alpha=0.2

p.renderers.append(r)

So far this is all fairly normal Bokeh graph layout code. Here is the additional part you need to add permanent labels for each node:

from bokeh.transform import transform    

# add the labels to the node renderer data source
source = r.node_renderer.data_source
source.data['names'] = [str(x*10) for x in source.data['index']]

# create a transform that can extract the actual x,y positions
code = """
    var result = new Float64Array(xs.length)
    for (var i = 0; i < xs.length; i++) {
        result[i] = provider.graph_layout[xs[i]][%s]
    }
    return result
"""
xcoord = CustomJSTransform(v_func=code % "0", args=dict(provider=r.layout_provider))
ycoord = CustomJSTransform(v_func=code % "1", args=dict(provider=r.layout_provider))

# Use the transforms to supply coords to a LabelSet 
labels = LabelSet(x=transform('index', xcoord),
                  y=transform('index', ycoord),
                  text='names', text_font_size="12px",
                  x_offset=5, y_offset=5,
                  source=source, render_mode='canvas')

p.add_layout(labels)

show(p)

Basically, since Bokeh (potentially) computes layouts in the browser, the actual node locations are only available via the "layout provider" which is currently a bit tedious to access. As I said, please open a GitHub issue to suggest making this better for users. There are probably some very quick and easy things we can do to make this much simpler for users.

The code above results in:

enter image description here

bigreddot
  • 33,642
  • 5
  • 69
  • 122
  • I should add, there are certainly other ways to do this. Since `nx` graphs generate static layouts, you could also extract the `(x,y)` coordinates *in Python* and put them in a data source for the `LabelSet`. That would avoid the JS transform, but I would regard the approach above as the most general and "correct". – bigreddot Apr 20 '18 at 18:20
  • Where is the missing link between the x, y = zip(*graph_renderer.layout_provider.graph_layout.values()) and the index or G.nodes.keys()? I will open an issue, i cannot find it. –  Apr 26 '20 at 19:33
  • @pierre please comment anything on this new existing issue (linking back here for reference) https://github.com/bokeh/bokeh/issues/9955 That said, what you have above is not feasible in general, only in some special cases. As I mentioned, the layout may only be computed *in the browser, in JavaScript*, and in that case is not available at all on the Python side. – bigreddot Apr 26 '20 at 21:16
-1

similar solution as @bigreddot.

    #Libraries for this solution
    from bokeh.plotting import figure ColumnDataSource
    from bokeh.models import LabelSet

    #Remove randomness
    import numpy as np
    np.random.seed(1337)

    #Load positions
    pos = nx.spring_layout(G)

    #Dict to df
    labels_df = pd.DataFrame.from_dict(pos).T

    #Reset index + column names
    labels_df = labels_df.reset_index()
    labels_df.columns = ["names", "x", "y"]

    graph_renderer = from_networkx(G, pos, center=(0,0))
    .
    .
    .
    plot.renderers.append(graph_renderer)

    #Set labels
    labels = LabelSet(x='x', y='y', text='names', source=ColumnDataSource(labels_df))

    #Add labels
    plot.add_layout(labels)
-2

Fixed node positions

From the networkx.spring_layout() documentation: you can add a list of nodes with a fixed position as a parameter.

import networkx as nx
import matplotlib.pyplot as plt

g = nx.Graph()
g.add_edges_from([(0,1),(1,2),(0,2),(1,3)])

pos = nx.spring_layout(g)
nx.draw(g,pos)
plt.show()

initial_graph

Then you can plot the nodes at a fixed position:

pos = nx.spring_layout(g, pos=pos, fixed=[0,1,2,3])
nx.draw(g,pos)
plt.show()

fixed_nodes

michaelg
  • 944
  • 6
  • 12
  • The question was how to accomplish this with Bokeh, not Matplotlib. Also about how to add text labels to the nodes, which this does not do at all. – bigreddot Apr 20 '18 at 18:21
  • The Bokeh function`from_networkx` is using the `spring_layout` function from `networkx` to define nodes position. So it seems easier to me to define positions in the `spring_layout` function. – michaelg Apr 21 '18 at 05:16
  • Although the title of the question is about text labels, if you read carefully the body of the question you find that the specific problem was about node positions. – michaelg Apr 21 '18 at 05:21
  • *"I am trying to add a permanent label on nodes for a networkx graph using spring_layout and bokeh library. I would like for this labels to be re-positioned as the graph scales or refreshed like what string layout does, re-positioning the nodes as the graph scales or refreshed."* This answer does not provide that, in in particular, will not help at all to keep things in sync in any case with dynamic (client generated) layout changes. – bigreddot Apr 21 '18 at 16:08
  • And if you read the rest of the question, it's clearly about `spring_layout` usage. – michaelg Apr 21 '18 at 16:55
  • No, it's really not. The ultimate goal is to accomplish a specific thing using Bokeh, and this answer does not provide that information. – bigreddot Apr 21 '18 at 17:06