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 dem node_renderer zugeordnet ist, muss eine Spalte mit dem Namen "index" haben, die die eindeutigen Indizes der Knoten enthält.

  • Die ColumnDataSource, die dem edge_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- und width-Attribute der Ellipse festlegt

  • das fill_color-Attribut der Ellipse festlegt

[1]:
import math

from bokeh.io import output_notebook, show
from bokeh.plotting import figure


output_notebook()
Loading BokehJS ...
[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_rendereroder 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)