How to add a categorical legend to networkx bokeh plot

Viewed 971

I have the following code to make a bokeh plot from NetworkX

p = figure(x_range=(-1.1, 1.1), y_range=(-1.1, 1.1))
p.grid.visible = False
p.axis.visible = False

graph_renderer = from_networkx(G, nx.spring_layout, random_state=11, center=(0, 0), scale=1, k=0.5)
color_map = factor_cmap('domain_cat', factors=factors, palette=Category10_6)

graph_renderer.node_renderer.glyph = Circle(radius=0.02, fill_color=color_map, line_color=None, fill_alpha=1)
graph_renderer.edge_renderer.glyph = MultiLine(line_color='lightgray', line_alpha=0.3, line_width=2)
p.renderers.append(graph_renderer)

p.add_tools(HoverTool(tooltips='@index', show_arrow=None))

show(p)

It works great. However, I have a categorical color map for my nodes. I would like to add a legend.

When using the plotting interface you can easily add a categorical legend by just entering the source column name (https://docs.bokeh.org/en/latest/docs/user_guide/categorical.html#colors).

However, I can't figure out how to generate, even through the models interface, using Legend and LegendItem, a categorical legend.

I have tried variants of:

items = [LegendItem(label=factor, renderers=[graph_renderer.node_renderer]) for factor in factors]
legend = Legend(items=items)
p.add_layout(legend)

But this produces the following result, with an empty legend that's the correct height and console errors that read TypeError: v is undefined; can't access its "draw_legend" property.

enter image description here

1 Answers

I faced the same problem, spent hours wheeling around different solutions, but non worked out for the particular approach using the graph_renderer as you did. Anyway, I came up with a manual solution, using bokeh's Div:

from bokeh.io import show
from bokeh.models import Div

legendEntries = {'#1f77b4': '192.168.0.0/24',
 '#aec7e8': '192.168.1.128/25',
 '#ff7f0e': '192.168.0.0/23',
 '#ffbb78': '192.168.4.0/25',
 '#2ca02c': '192.168.5.0/25'}

with open ("resources/legend.css", "r") as f:
    legend_css=f.read()

with open ("resources/legend.html", "r") as f:
    legend_html=f.read()

def str_legend():
    return "<p>" + "</p><p>".join([f"<span class='legend-entry' style='background:{n}'>&nbsp;</span>{v}" for n, v in legendEntries.items()]) + "</p>"

div_legend = Div(css_classes=["legend-container"],
    text=f"<style>{legend_css}</style>{legend_html.replace('<p></p>',str_legend())}")

show(div_legend)

This script main.py plots at least a non interactive legend.

Note 1, you then need to be able to create your legendEntries dictionary from your data, but that shouldn't be a problem and is another story...

Note 2, having the html- & css-files outside the python script is more comfortable, as your editor is able to highlight according to the syntax rules ;)

Just for convenience, here is the "project" structure:

project
│   main.py  
└───resources
│   │   legend.css
│   │   legend.html

And the resources-files, legend.html:

<fieldset id="legend-1">
  <legend><h4>Legende</h4></legend>
  <p></p>
</fieldset>

and legend.css:

.legend-container {
    z-index: 2;
    top: 80px !important;
    left: 780px !important;
}

.legend-container h4, p {
    margin: 0 0 2px 0;
}

span.legend-entry {
  border: solid 0.5px black;
  border-radius: 0.8em;
  -moz-border-radius: 0.8em;
  -webkit-border-radius: 0.8em;
  display: inline-block;
  font-weight: bold;
  line-height: 1.6em;
  margin-right: 7px;
  text-align: center;
  width: 1.6em;
}

.legend-container fieldset {
    min-width: 140px;
    background: white;
}

To get this legend:

Legend Screenshot

Note 3: I used the css style of .legend-container to move the legend within the plot

Related