如何在plotly中的交互式文本标签之间添加箭头?

问题描述 投票:0回答:2

我正在创建一个应用程序,它能够以简单的方式连接文本标签,就像它可以在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()

enter image description here

我目前正在寻找

plotly
解决方案,因为它支持 Jupyter Notebook 上的动态 HTML,并且允许在没有 JavaScript 的情况下运行它。此外,我想实现这些目标:

  1. 精确连接矩形文本标签的功能,如我的图表所示
  2. 交互1:使标签可以用鼠标拖动
  3. 交互2:拖动时重绘连接文本标签的箭头
  4. 交互3:双击文本标签后重定向到某个url

对我来说最重要的是连接矩形文本标签的最小工作脚本。

matplotlib
还支持处理事件。我还想讨论
plotly
是否支持使所有这三种交互都可以进行编码。

python matplotlib plotly plotly-dash plotly-python
2个回答
1
投票

经过长时间研究代码,我可以说大多数事情都可以运行,但有一些小缺点。

首先,我发现可以实现所有想要的功能:

  • 支持矩形节点
  • 它们可以被拖动
  • 边缘更新正在工作(与 Matplotlib 注释不同)
  • 超链接需要一些定制,这并不难

交互图的构建:

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

enter image description here

自定义节点、边和标签样式

您需要定义

stylesheet
参数并在
app.layout
定义中取消注释:

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}}]

enter image description here

点击节点后导航到url

基于这个答案,只需在服务器运行之前添加回调:

@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']}"

支持 Jupyter 笔记本

要将应用程序嵌入 Jupyter Notebook,请使用 JupyterDash。这很简单,您唯一需要的就是使用不同类型的应用程序:

from jupyter_dash import JupyterDash
...
if __name__ == '__main__':
    app.run_server(mode='inline')

支持 GitHub 和
nbviewer

如果您在 GitHub 中上传应用程序,它不会显示交互式应用程序,但您可以在

nbviewer
中加载 GitHub 链接。有两个不好的方面:

  • 除非您将
    https://...
    的链接
    nbviewer
    替换为
    http://...
    ,否则它不会加载。我不满意,但这是我能找到的唯一解决方法。
  • 它不会在一个笔记本中加载多个应用程序。

支持乳胶

Dash Plotly 中没有直接的方法来渲染 Mathjax,直到一个月前发布 Dash v2.3.0。我在这些应用程序中使用的 Dash Cytoscape 仍然不支持它。我希望这个问题能够在不久的将来得到解决

文本包含在适合其形状的矩形标签中。

我可以找到任何方法来做到这一点,正如您在带有

'Very long line for testing'
的节点示例中看到的那样。目前,如果文本标签很长,更好的设计是使用圆形节点,上面有文本。


我很高兴能够找到大多数问题的解决方案,并且我仍然愿意讨论对 LaTeX/Mathjax、

nbviewer
以及标签中更好的文本包围的支持。


0
投票

我认为你能用

plotly-python
做的最好的事情就是连接两个静态矩形文本标签。然而,在 Plotly 中进行任何修改任何对象的交互操作大部分都需要回调,这意味着您需要使用
plotly-dash

由于您使用的是 Jupyter Notebook,因此您可以使用 Jupyter-Dash。如果您对这样的解决方案感兴趣,我很乐意更新我的答案并提供一个示例。

© www.soinside.com 2019 - 2024. All rights reserved.