Visualisierung von Netzwerkgraphen¶
Bokeh unterstützt nativ die Erstellung von Netzwerkgraphen mit konfigurierbaren Interaktionen zwischen Kanten und Knoten.
Edge- und Node-Renderer¶
Das Hauptmerkmal von GraphRenderer
ist, dass es separate GlyphRenderer für Diagrammknoten und Diagrammkanten gibt. Dies ermöglicht das Anpassen der Knoten durch Ändern einer Eigenschaft des node_renderer
. Es ist möglich, den üblichen Kreisknoten-Glyph durch eine beliebige XYGlyph-Instanz zu ersetzen, z.B. durch einen rechteckige oder elliptische Glyphe. Ebenso können die Stileigenschaften der Kanten über die edge_renderer
-Eigenschaft geändert werden. Aktuell können Kanten jedoch nur
als MultiLine-Zeichen gerendert werden.
Für die Datenquellen, die zu diesen beiden Sub-Renderern gehören, gelten einige Anforderungen:
Die
ColumnDataSource
, die demnode_renderer
zugeordnet ist, muss eine Spalte mit dem Namen"index"
haben, die die eindeutigen Indizes der Knoten enthält.Die
ColumnDataSource
, die demedge_renderer
zugeordnet ist, enthält zwei erforderliche Spalten:"start"
und"end"
. Diese Spalten enthalten die Knotenindizes für den Anfang und das Ende der Kanten.
Es ist möglich, diesen Datenquellen zusätzliche Metadaten hinzuzufügen, um ein vektorisiertes Glyphen-Styling hinzuzufügen oder Daten für Callbacks oder Tooltips verfügbar zu machen.
Im Folgenden zeige ich ein Code-Snippet, das:
einen Knoten elliptisch darstellt
height
- undwidth
-Attribute der Ellipse festlegtdas
fill_color
-Attribut der Ellipse festlegt
[1]:
import math
from bokeh.io import output_notebook, show
from bokeh.plotting import figure
output_notebook()
[2]:
import math
from bokeh.models import Ellipse, GraphRenderer
from bokeh.palettes import Spectral8
from bokeh.plotting import figure
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.glyph = Ellipse(
height=0.1, width=0.2, fill_color="fill_color"
)
graph.node_renderer.data_source.data = dict(
index=node_indices, fill_color=Spectral8
)
graph.edge_renderer.data_source.data = dict(start=[0] * N, end=node_indices)
Durch Ausführen des obigen Code-Snippets wird kein Diagramm gerendert, da wir nicht angegeben haben, wie das Diagramm im 2D-Raum angeordnet werden soll. Wie das geht, erfahren Sie im folgenden Abschnitt.
Layout Providers¶
Bokeh verwendet ein separates LayoutProvider
-Modell, um die Koordinaten eines Graphen im kartesischen Raum anzugeben. Derzeit ist das StaticLayoutProvider
-Modell der einzige integrierte Provider, der ein Wörterbuch mit (x,y)-Koordinaten für die Knoten enthält.
In unserem Beispiel wird dem obigen Codeausschnitt ein Provider hinzugefügt mit:
[3]:
from bokeh.models import Ellipse, GraphRenderer, StaticLayoutProvider
from bokeh.palettes import Spectral8
N = 8
node_indices = list(range(N))
p = 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.add(node_indices, "index")
graph.node_renderer.data_source.add(Spectral8, "color")
graph.node_renderer.glyph = Ellipse(height=0.1, width=0.2, fill_color="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)
p.renderers.append(graph)
show(p)
Explizite Pfade¶
Standardmäßig werden vom StaticLayout
-Provider gerade Pfade zwischen den angegebenen Knoten gezeichnet. Um explizite Kantenpfade bereitzustellen, können jedoch auch Listen mit Pfaden für die edge_renderer
angegeben werden mit "xs"
und "ys"
-Spalten der ColumnDataSource
. Beachtet jedoch, dass einerseits diese Pfade in derselben Reihenfolge sein sollten wie die Punkte "start"
und "end"
und andererseits, dass es keine Validierung gibt, die mit den Knotenpositionen
übereinstimmt. Ihr solltet daher sehr vorsichtig sein, wenn ihr explizite Pfade festlegt.
Im folgenden Beispiel erweitern wir das obige Beispiel um quadratische Bezierpfade zwischen den Knoten:
[4]:
import math
from bokeh.io import show
from bokeh.models import Ellipse, GraphRenderer, StaticLayoutProvider
from bokeh.palettes import Spectral8
from bokeh.plotting import figure
N = 8
node_indices = list(range(N))
p = 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.add(node_indices, "index")
graph.node_renderer.data_source.add(Spectral8, "color")
graph.node_renderer.glyph = Ellipse(height=0.1, width=0.2, fill_color="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.0 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
p.renderers.append(graph)
show(p)
NetworkX-Integration¶
Bokeh unterstützt das schnelle Zeichnen eines Netzwerkgraphen mit seiner NetworkX-Integration. bokeh.models.graphs.from_networkx
akzeptiert ein networkx.Graph
-Objekt und eine networkx-Layout
-Methode, um eine konfigurierte GraphRenderer
-Instanz zurückzugeben.
Hier ist ein Beispiel für die Verwendung der networkx.spring_layout
-Methode zum Layout des in NetworkX integrierten Datensatzes karate_club_graph
:
[5]:
import networkx as nx
from bokeh.io import show
from bokeh.plotting import figure, from_networkx
G = nx.karate_club_graph()
p = figure(
title="Networkx Integration Demonstration",
x_range=(-1.1, 1.1),
y_range=(-1.1, 1.1),
tools="",
toolbar_location=None,
)
graph = from_networkx(G, nx.spring_layout, scale=2, center=(0, 0))
p.renderers.append(graph)
show(p)
Interaktionen¶
Ihr könnt das Auswahl- oder Überprüfungsverhalten von Diagrammen konfigurieren, indem ihr die selection_policy
- und inspection_policy
-Attribute des GraphRenderer
festlegt. Diese Attribute akzeptieren eine spezielle GraphHitTestPolicy
-Instanz.
So kann beispielsweise selection_policy=NodesAndLinkedEdges()
bewirken, dass mit einem ausgewählten Knoten auch die zugehörigen Kanten ausgewählt werden. In ähnlicher Weise werden durch inspection_policy=EdgesAndLinkedNodes()
der Start- und Endknoten einer Kante auch beim Bewegen der Maus über eine Kante mit dem HoverTool überprüft.
Mit den selection_glyph
-, nonselection_glyph
- und hover_glyph
-Attributen der Kanten- und Knotenrenderer können die Diagramminteraktionen auch dynamische visuelle Elemente hinzuzufügen.
Hier ist ein Beispiel mit solchen Knoten- und Kanteninteraktionen:
[6]:
import networkx as nx
from bokeh.io import show
from bokeh.models import (
BoxSelectTool,
Circle,
HoverTool,
MultiLine,
Plot,
Range1d,
TapTool,
)
from bokeh.models.graphs import EdgesAndLinkedNodes, NodesAndLinkedEdges
from bokeh.palettes import Spectral4
from bokeh.plotting import from_networkx
G = nx.karate_club_graph()
p = Plot(
width=400,
height=400,
x_range=Range1d(-1.1, 1.1),
y_range=Range1d(-1.1, 1.1),
)
p.title.text = "Graph Interaction Demonstration"
p.add_tools(HoverTool(tooltips=None), TapTool(), BoxSelectTool())
graph_renderer = from_networkx(G, nx.circular_layout, scale=1, center=(0, 0))
graph_renderer.node_renderer.glyph = Circle(size=15, fill_color=Spectral4[0])
graph_renderer.node_renderer.selection_glyph = Circle(
size=15, fill_color=Spectral4[2]
)
graph_renderer.node_renderer.hover_glyph = Circle(
size=15, fill_color=Spectral4[1]
)
graph_renderer.edge_renderer.glyph = MultiLine(
line_color="#CCCCCC", line_alpha=0.8, line_width=5
)
graph_renderer.edge_renderer.selection_glyph = MultiLine(
line_color=Spectral4[2], line_width=5
)
graph_renderer.edge_renderer.hover_glyph = MultiLine(
line_color=Spectral4[1], line_width=5
)
graph_renderer.selection_policy = NodesAndLinkedEdges()
graph_renderer.inspection_policy = EdgesAndLinkedNodes()
p.renderers.append(graph_renderer)
show(p)
Knoten- und Kantenattribute¶
In from_networkx
werden die Knoten- und Kantenattribute von NetworkX für den node_renderer
oder edge_renderer
umgewandelt.
Das Dataset karate_club_graph
hat beispielsweise ein Knotenattribut mit dem Namen club
. Mit from_networkx
ist es möglich, diese Informationen zu verschieben. In ähnlicher Weise können auch weitere Knoten- und Kantenattribute für Farbinformationen verwendet werden:
[7]:
import networkx as nx
from bokeh.io import show
from bokeh.models import (
BoxZoomTool,
Circle,
HoverTool,
MultiLine,
Plot,
Range1d,
ResetTool,
)
from bokeh.palettes import Spectral4
from bokeh.plotting import from_networkx
# Prepare Data
G = nx.karate_club_graph()
SAME_CLUB_COLOR, DIFFERENT_CLUB_COLOR = "black", "red"
edge_attrs = {}
for start_node, end_node, _ in G.edges(data=True):
edge_color = (
SAME_CLUB_COLOR
if G.nodes[start_node]["club"] == G.nodes[end_node]["club"]
else DIFFERENT_CLUB_COLOR
)
edge_attrs[(start_node, end_node)] = edge_color
nx.set_edge_attributes(G, edge_attrs, "edge_color")
# Show with Bokeh
p = Plot(
width=400,
height=400,
x_range=Range1d(-1.1, 1.1),
y_range=Range1d(-1.1, 1.1),
)
p.title.text = "Graph Interaction Demonstration"
node_hover_tool = HoverTool(tooltips=[("index", "@index"), ("club", "@club")])
p.add_tools(node_hover_tool, BoxZoomTool(), ResetTool())
graph_renderer = from_networkx(G, nx.spring_layout, scale=1, center=(0, 0))
graph_renderer.node_renderer.glyph = Circle(size=15, fill_color=Spectral4[0])
graph_renderer.edge_renderer.glyph = MultiLine(
line_color="edge_color", line_alpha=0.8, line_width=1
)
p.renderers.append(graph_renderer)
show(p)