1

I am using netgraph to visualize my networkx graphs. My problem now is, the visualisation of formulas are in natural way not as pretty as I would expect. Here my example graph:

import networkx as nx
from netgraph import Graph # pip install netgraph
import matplotlib.pyplot as plt

node_labels = {1: 'p→q', 2: '¬q', 3: '¬ (¬p)', 4: '¬p', 5: '¬p∧ ¬ (¬p)', 6: 'p', 7: 'q', 8: 'q∧ ¬q', 9: '¬p'}
color_map = {1: 'red', 2: 'red', 3: 'red', 4: 'red', 5: 'lightblue', 6: 'lightblue', 7: 'lightblue', 8: 'lightblue', 9: 'blue'}
edge_labels = {(3, 5): '∧I', (4, 5): '∧I', (4, 6): '¬E', (5, 6): '¬E', (1, 7): '→E', (6, 7): '→E', (2, 8): '∧I', (7, 8): '∧I', (8, 9): '¬E', (3, 9): '¬E'}

graph = nx.from_edgelist(edge_labels, nx.DiGraph())


Graph(graph, node_labels=node_labels, edge_labels=edge_labels,
      node_color=color_map, node_edge_color=color_map, arrows=True)

plt.show()

Output (direct / after zoom in):

enter image description here enter image description here:

My questions now:

  • Is their a way to adjust the font size of the nodes?
  • Is it possible to rotate the edge labels in that way they are aligned horizontally?
Martin Kunze
  • 995
  • 6
  • 16

2 Answers2

3

By default, iff node labels are plotted on top of nodes (i.e. without an offset), netgraph scales the node label font size such that all labels fit within their respective node artists. However, this behaviour can be overridden by specifying the font size explicitly , e.g. using node_label_fontdict(size=20).

The rotation of the edge labels can be turned off setting the edge_label_rotate = False flag.

enter image description here

#!/usr/bin/env python
# coding: utf-8

import matplotlib.pyplot as plt
import networkx as nx

from netgraph import Graph # pip install netgraph

node_labels = {1: 'p→q', 2: '¬q', 3: '¬ (¬p)', 4: '¬p', 5: '¬p∧ ¬ (¬p)', 6: 'p', 7: 'q', 8: 'q∧ ¬q', 9: '¬p'}
color_map = {1: 'red', 2: 'red', 3: 'red', 4: 'red', 5: 'lightblue', 6: 'lightblue', 7: 'lightblue', 8: 'lightblue', 9: 'blue'}
edge_labels = {(3, 5): '∧I', (4, 5): '∧I', (4, 6): '¬E', (5, 6): '¬E', (1, 7): '→E', (6, 7): '→E', (2, 8): '∧I', (7, 8): '∧I', (8, 9): '¬E', (3, 9): '¬E'}

graph = nx.from_edgelist(edge_labels, nx.DiGraph())

Graph(graph, node_layout='dot',
      node_labels=node_labels, node_label_fontdict=dict(size=21),
      edge_labels=edge_labels, edge_label_fontdict=dict(size=14), edge_label_rotate=False,
      node_color=color_map, node_edge_color=color_map, arrows=True
)

plt.show()
Paul Brodersen
  • 11,221
  • 21
  • 38
1

Plotly has a very nice example of network graph that can be easily readapted to your needs.

Here's the answer to your quests.

# Packages import
import networkx as nx
import plotly.graph_objs as go
# Your input data
node_labels = {1: 'p→q', 2: '¬q', 3: '¬ (¬p)', 4: '¬p', 5: '¬p∧ ¬ (¬p)', 6: 'p', 7: 'q', 8: 'q∧ ¬q', 9: '¬p'}
color_map = {1: 'red', 2: 'red', 3: 'red', 4: 'red', 5: 'lightblue', 6: 'lightblue', 7: 'lightblue', 8: 'lightblue', 9: 'blue'}
edge_labels = {(3, 5): '∧I', (4, 5): '∧I', (4, 6): '¬E', (5, 6): '¬E', (1, 7): '→E', (6, 7): '→E', (2, 8): '∧I', (7, 8): '∧I', (8, 9): '¬E', (3, 9): '¬E'}

# Create DiGraph
G=nx.DiGraph()
# Add nodes and edges
G.add_nodes_from(list(node_labels.keys()), weight=15)
G.add_edges_from(list(edge_labels.keys()))
# Create Positions
pos = nx.planar_layout(G)  # nx.random_layout(G), ...
# Create edges for plot and edge annotations
edge_x = []
edge_y = []
edge_label_x = []
edge_label_y = []
for edge in G.edges:
    x0, y0 = pos[edge[0]][0], pos[edge[0]][1]
    x1, y1 = pos[edge[1]][0], pos[edge[1]][1]
    edge_x.append(x0)
    edge_x.append(x1)
    edge_x.append(None)
    edge_y.append(y0)
    edge_y.append(y1)
    edge_y.append(None)
    edge_label_x.append((x0+x1)/2)
    edge_label_y.append((y0+y1)/2)
# Make go.Scatter for edges
edge_trace = go.Scatter(
    x=edge_x, y=edge_y,
    line=dict(width=0.5, color='#888'),
    hoverinfo='none',
    mode='lines'
)

# Create nodes for plot
node_x = []
node_y = []
node_color = []
for node in G.nodes:
    x, y = pos[node][0], pos[node][1]
    node_x.append(x)
    node_y.append(y)
    node_color.append(color_map[node])
# Make go.Scatter for nodes
node_trace = go.Scatter(
    x=node_x, y=node_y,
    mode='markers+text',
    hoverinfo='text',
    text=list(node_labels.values()),
    textposition="middle center",  # ['top left', 'top center', 'top right', 'middle left',
                                   # 'middle center', 'middle right', 'bottom left', 
                                   # 'bottom center', 'bottom right']
    textfont=dict(
        family="sans serif",
        size=10,  # <-- Here you change the node text size
        color="black"
    ),
    marker=dict(
        showscale=False,
        color=node_color,
        size=46,
        line_width=1
    )
)
# Make edge annotations
edge_annotations = []
for label, x, y in zip(list(edge_labels.values()), edge_label_x, edge_label_y):
    edge_annotations.append(
        dict(
            x=x, y=y,
            showarrow=False,
            hovertext='none',
            ax=0,
            ay=0,
            bgcolor="white",
            opacity=0.85,
            text=label,
            font=dict(
                family="sans serif",
                size=14,
                color="black",
            )
        )
    )

# Make go.Figure
fig = go.Figure(
    data=[edge_trace, node_trace],
    layout=go.Layout(
        paper_bgcolor='rgba(0,0,0,0)',
        plot_bgcolor='rgba(0,0,0,0)',
        title='<br>Network graph',
        titlefont_size=16,
        showlegend=False,
        hovermode='closest',
        xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
        yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
        autosize=False,
        width=700,
        height=700,
        margin=dict(l=50, r=50, b=100, t=100, pad=4),
    )
)
# Add edge annotations to go.Figure
for edge_annotation in edge_annotations:
    fig.add_annotation(edge_annotation)
# Show go.Figure
fig.show()

Resulting in: Network graph made with Plotly

Edit:

Adding edges directions with Plotly is a bit more tricky (see this question). Fortunately there is a github project from which we can borrow a function that adds arrowheads to edges by keeping into account node sizes.

The code now is:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri May 15 11:45:07 2020
@author: aransil
"""

import math

# Start and end are lists defining start and end points
# Edge x and y are lists used to construct the graph
# arrowAngle and arrowLength define properties of the arrowhead
# arrowPos is None, 'middle' or 'end' based on where on the edge you want the arrow to appear
# arrowLength is the length of the arrowhead
# arrowAngle is the angle in degrees that the arrowhead makes with the edge
# dotSize is the plotly scatter dot size you are using (used to even out line spacing when you have a mix of edge lengths)
def addEdge(start, end, edge_x, edge_y, lengthFrac=1, arrowPos = None, arrowLength=0.025, arrowAngle = 30, dotSize=20):

    # Get start and end cartesian coordinates
    x0, y0 = start
    x1, y1 = end

    # Incorporate the fraction of this segment covered by a dot into total reduction
    length = math.sqrt( (x1-x0)**2 + (y1-y0)**2 )
    dotSizeConversion = .0565/20 # length units per dot size
    convertedDotDiameter = dotSize * dotSizeConversion
    lengthFracReduction = convertedDotDiameter / length
    lengthFrac = lengthFrac - lengthFracReduction

    # If the line segment should not cover the entire distance, get actual start and end coords
    skipX = (x1-x0)*(1-lengthFrac)
    skipY = (y1-y0)*(1-lengthFrac)
    x0 = x0 + skipX/2
    x1 = x1 - skipX/2
    y0 = y0 + skipY/2
    y1 = y1 - skipY/2

    # Append line corresponding to the edge
    edge_x.append(x0)
    edge_x.append(x1)
    edge_x.append(None) # Prevents a line being drawn from end of this edge to start of next edge
    edge_y.append(y0)
    edge_y.append(y1)
    edge_y.append(None)

    # Draw arrow
    if not arrowPos == None:

        # Find the point of the arrow; assume is at end unless told middle
        pointx = x1
        pointy = y1

        eta = math.degrees(math.atan((x1-x0)/(y1-y0))) if y1!=y0 else 90.0

        if arrowPos == 'middle' or arrowPos == 'mid':
            pointx = x0 + (x1-x0)/2
            pointy = y0 + (y1-y0)/2

        # Find the directions the arrows are pointing
        signx = (x1-x0)/abs(x1-x0) if x1!=x0 else +1    #verify this once
        signy = (y1-y0)/abs(y1-y0) if y1!=y0 else +1    #verified

        # Append first arrowhead
        dx = arrowLength * math.sin(math.radians(eta + arrowAngle))
        dy = arrowLength * math.cos(math.radians(eta + arrowAngle))
        edge_x.append(pointx)
        edge_x.append(pointx - signx**2 * signy * dx)
        edge_x.append(None)
        edge_y.append(pointy)
        edge_y.append(pointy - signx**2 * signy * dy)
        edge_y.append(None)

        # And second arrowhead
        dx = arrowLength * math.sin(math.radians(eta - arrowAngle))
        dy = arrowLength * math.cos(math.radians(eta - arrowAngle))
        edge_x.append(pointx)
        edge_x.append(pointx - signx**2 * signy * dx)
        edge_x.append(None)
        edge_y.append(pointy)
        edge_y.append(pointy - signx**2 * signy * dy)
        edge_y.append(None)


    return edge_x, edge_y
# Packages import
import networkx as nx
import plotly.graph_objs as go
# Your input data
node_labels = {1: 'p→q', 2: '¬q', 3: '¬ (¬p)', 4: '¬p', 5: '¬p∧ ¬ (¬p)', 6: 'p', 7: 'q', 8: 'q∧ ¬q', 9: '¬p'}
color_map = {1: 'red', 2: 'red', 3: 'red', 4: 'red', 5: 'lightblue', 6: 'lightblue', 7: 'lightblue', 8: 'lightblue', 9: 'blue'}
edge_labels = {(3, 5): '∧I', (4, 5): '∧I', (4, 6): '¬E', (5, 6): '¬E', (1, 7): '→E', (6, 7): '→E', (2, 8): '∧I', (7, 8): '∧I', (8, 9): '¬E', (3, 9): '¬E'}

# General controls on the figure
NODE_SIZE = 46
LINE_WIDTH = 0.5
LINE_COLOR = '#888888'
FONT_FAMILY = 'serif'

# Create DiGraph
G=nx.DiGraph()
# Add nodes and edges
G.add_nodes_from(list(node_labels.keys()), weight=15)
G.add_edges_from(list(edge_labels.keys()))
# Create Positions
pos = nx.planar_layout(G)  # nx.random_layout(G), ...
pos[4][0] = -0.7  # <-- Changing the position of node 4 for readability
pos[9][0] = 0.9  # <-- Changing the position of node 9 for readability
for node in G.nodes:
    G.nodes[node]['pos'] = list(pos[node])

# Create Plot
# Create edges for plot and edge annotations
edge_x = []
edge_y = []
edge_label_x = []
edge_label_y = []
for edge in G.edges:
    start = G.nodes[edge[0]]['pos']
    end = G.nodes[edge[1]]['pos']
    edge_label_x.append((start[0]+end[0])/2)
    edge_label_y.append((start[1]+end[1])/2)
    edge_x, edge_y = addEdge(start, end, edge_x, edge_y, .95, 'end', .03, 20, NODE_SIZE)
# Make go.Scatter for edges
edge_trace = go.Scatter(
    x=edge_x, y=edge_y,
    mode='lines',
    line=dict(width=LINE_WIDTH, color=LINE_COLOR),
    hoverinfo='none',
)

# Create nodes for plot
node_x = []
node_y = []
node_color = []
for node in G.nodes:
    x, y = G.nodes[node]['pos']
    node_x.append(x)
    node_y.append(y)
    node_color.append(color_map[node])
# Make go.Scatter for nodes
node_trace = go.Scatter(
    x=node_x, y=node_y,
    mode='markers+text',
    hoverinfo='text',
    text=list(node_labels.values()),
    textposition="middle center",  # ['top left', 'top center', 'top right', 'middle left',
                                   # 'middle center', 'middle right', 'bottom left', 
                                   # 'bottom center', 'bottom right']
    textfont=dict(
        family=FONT_FAMILY,
        size=10,
        color="black"
    ),
    marker=dict(
        showscale=False,
        color=node_color,
        size=NODE_SIZE,
        line_width=1
    )
)
# Make edge annotations
edge_annotations = []
for label, x, y in zip(list(edge_labels.values()), edge_label_x, edge_label_y):
    edge_annotations.append(
        dict(
            x=x, y=y,
            showarrow=False,
            hovertext='none',
            ax=0,
            ay=0,
            bgcolor="white",
            opacity=0.95,
            text=label,
            font=dict(
                family=FONT_FAMILY,
                size=14,
                color="black",
            )
        )
    )

# Make go.Figure
fig = go.Figure(
    data=[edge_trace, node_trace],
    layout=go.Layout(
        paper_bgcolor='rgba(0,0,0,0)',
        plot_bgcolor='rgba(0,0,0,0)',
        title='<br>Network graph',
        titlefont_size=16,
        showlegend=False,
        hovermode='closest',
        xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
        yaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
        autosize=False,
        width=700,
        height=700,
        margin=dict(l=50, r=50, b=100, t=100, pad=4),
    )
)
# Add edge annotations to go.Figure
for edge_annotation in edge_annotations:
    fig.add_annotation(edge_annotation)
# Show go.Figure
fig.show()

Resulting in:

Network graph with edge arrows

Pietro D'Antuono
  • 352
  • 1
  • 11
  • Thanks, for first view the code looks a bit complicated but it results in a very nice solution. – Martin Kunze Apr 21 '22 at 10:14
  • I think there is a bug in your code. The connectivity in your drawing does not match the connectivity specified in the question. For example, the dark blue `¬p` node has two incoming edges. – Paul Brodersen Apr 21 '22 at 11:23
  • 1
    @PaulBrodersen there is no bug in the code, it was only a matter of better positioning the node `¬p`. Thanks for noticing though! – Pietro D'Antuono Apr 21 '22 at 12:12