How to add a categorical legend to networkx bokeh plot
Asked Answered
D

2

6

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

Dubious answered 13/11, 2018 at 2:40 Comment(0)
T
0

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

Tying answered 16/12, 2021 at 10:52 Comment(0)
T
0

I very much sympathize with all the trouble the other answer went through, I was very frustrated by this too.

The solution I found is a bit simpler, just add a few zero-size glpyhs to your plot, and they will show up in the legend:

Assuming color_map is the dictionary of category name to color, something like this should work:

for k, v in color_map.items():
  p.circle(x=0, y=0, size=0, legend_label=k, color=v)

# display legend in top left corner (default is top right corner)
p.legend.location = "top_left"

# add a title to your legend
p.legend.title = "Legend Title"

It unfortunately will add some unnecessary glyphs to your plot, but you can make them invisible so hopefully shouldn't matter in practice..

Tripalmitin answered 2/2, 2023 at 15:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.