I'm creating an app which is able to connect text labels in a simple manner like it could be done in Matplotlib:
import matplotlib.pyplot as plt
import networkx as nx
G = nx.DiGraph()
G.add_edges_from([(0,1)])
G.add_nodes_from([0, 1])
pos = {0:(0.1, 0.9), 1: (0.9, 0.5)}
fig, ax = plt.subplots()
annotations = {0: ax.annotate('Python', xy=pos[0], xycoords='data',
ha="center", va="center", bbox=dict(facecolor = "blue")),
1:ax.annotate('Programming', xy=pos[1], xycoords='data',
ha="center", va="center", bbox=dict(facecolor = "red"))}
annotations[1].draggable()
# if you don't have networkx installed, replace G.edges with [(0,1)]
for A, B in G.edges:
ax.annotate("", xy=pos[B], xycoords='data', xytext=pos[A], textcoords='data',
arrowprops=dict(arrowstyle="->", color="0.5", # shrinkA=85, shrinkB=85,
patchA=annotations[A],
patchB=annotations[B],
connectionstyle='arc3'))
plt.axis('off')
plt.show()
I'm looking for plotly
solutions at the moment since it has a support for dynamic HTML on Jupyter Notebook and allows to run it with no JavaScript. Moreover, I want to achieve these things:
The most important for me is minimal working script of connection of rectangular text labels. matplotlib
also supports handling events. I might also like to discuss if plotly
has a support for making all these three interactions possible to code.
After long time spent studying codes I could say that majority of things are working with some minor drawbacks.
To start with, I found it possible to implement all the functionality wanted:
import dash
import dash_cytoscape as cyto
from dash import html, dcc
from dash.dependencies import Input, Output
#demo for adding urls: https://stackoverflow.com/a/69700675/3044825
cyto.load_extra_layouts() #dagre layout
P1 = {'data': {'id': 'p1', 'label': 'Use Bulb'}, 'grabbable': True, 'classes': 'process'}
P2 = {'data': {'id': 'p2', 'label': 'Prod. Bulb'}, 'grabbable': True, 'classes': 'process'}
P3 = {'data': {'id': 'p3', 'label': 'Prod. Elec', 'parent': 'm1'}, 'grabbable': True, 'classes': 'process'}
P4 = {'data': {'id': 'p4', 'label': 'Very long line for testing'}, 'grabbable': True, 'classes': 'process'}
P5 = {'data': {'id': 'p5', 'label': 'Prod. Glass'}, 'grabbable': True, 'classes': 'process'}
P6 = {'data': {'id': 'p6', 'label': 'Prod. Copper'}, 'grabbable': True, 'classes': 'process'}
P7 = {'data': {'id': 'p7', 'label': 'Prod. Fuel', 'parent': 'm1'}, 'grabbable': True, 'classes': 'process'}
E1 = {'data': {'id': 'e1', 'source': 'p7', 'target': 'p3', 'label': 'Fuel'}}
E2 = {'data': {'id': 'e2', 'source': 'p3', 'target': 'p6', 'label': 'Elec.'}}
E3 = {'data': {'id': 'e3', 'source': 'p3', 'target': 'p2', 'label': 'Elec.'}}
E4 = {'data': {'id': 'e4', 'source': 'p3', 'target': 'p5', 'label': 'Elec.'}}
E5 = {'data': {'id': 'e5', 'source': 'p3', 'target': 'p1', 'label': 'Elec.'}}
E6 = {'data': {'id': 'e6', 'source': 'p6', 'target': 'p2', 'label': 'Copper'}}
E7 = {'data': {'id': 'e7', 'source': 'p5', 'target': 'p2', 'label': 'Glass'}}
E8 = {'data': {'id': 'e8', 'source': 'p2', 'target': 'p1', 'label': 'Bulb'}}
E9 = {'data': {'id': 'e9', 'source': 'p4', 'target': 'p1', 'label': 'Waste Treatment'}}
nodes = [P1, P2, P3, P4, P5, P6, P7]
edges = [E1, E2, E3, E4, E5, E6, E7, E8, E9]
app = dash.Dash(__name__)
app.layout = html.Div([
dcc.Location(id="location"),
cyto.Cytoscape(
id='cytoscape',
layout={'name': 'dagre', 'spacingFactor': 1.15},
style={'width': '100%', 'height': '900px'},
#stylesheet=stylesheet,
elements=nodes+edges,
autounselectify=True
)])
if __name__ == '__main__':
app.run_server(debug=False, port=8869) #no need for choosing a specific port if it's not in use
You'll need to define stylesheet
parameter and uncomment it in app.layout
definition:
stylesheet = [
# Group selectors
{'selector': 'node', 'style': {'content': 'data(label)', 'font-size': 8}},
{'selector': 'edge',
'style': {'content': 'data(label)',
'curve-style': 'unbundled-bezier',
'width': 1,
'line-color': 'lightblue',
'target-arrow-color': 'lightblue',
'target-arrow-shape': 'triangle',
'text-margin-x': 0,
'font-size': 8}},
# Class selectors
{'selector': '.process',
'style': {'shape': 'round-rectangle',
'background-color': 'white',
'border-color': 'black',
'border-width': 1,
'text-valign': 'center',
'height': 40,
'width': 75}}]
Based on this answer, just add a callback before server run:
@app.callback(
Output("location", "href"),
Input("cytoscape", "tapNodeData"),
prevent_initial_call=True,
)
def navigate_to_url(node_data):
return f"https://en.wikipedia.org/wiki/{node_data['label']}"
For embedding app inside Jupyter Notebook, use JupyterDash. It's simple, the only think you need is to use a different kind of app:
from jupyter_dash import JupyterDash
...
if __name__ == '__main__':
app.run_server(mode='inline')
nbviewer
If you upload your app in GitHub it won't display interactive apps but you could load your GitHub link in nbviewer
. There are two bad sides:
https://...
of nbviewer
with http://...
. I'm not satisfied but this is the only workaround I could find.There was no straight way in Dash Plotly to render Mathjax until release of Dash v2.3.0 one month ago. It's still not supported in Dash Cytoscape which I've been using in these apps. I hope this issue is going to be resolved in near future.
I could find any way to do it yet as you could see in an example of node with 'Very long line for testing'
. At the moment, if text labels occurs to be long, a better design is to use circular nodes with text above.
I'm glad I was able to find solutions to majority of my questions and I'm still open to discuss about support for LaTeX/Mathjax, nbviewer
and better text enclosion in labels.