使用 Python 的点图节点和边的上下文菜单

Context menus for nodes and edges for dot graphs using Python

有没有什么简单的方法可以使用 Python 为点图添加节点和边的上下文菜单?这样当单击节点或边缘时,会出现上下文菜单,用户可以 select 菜单条目,然后根据条目执行 Python 代码?

我找不到任何允许 graphviz 图形的任意事件的现有库 - 我找到的最接近的是 xdot,它似乎没有满足您的要求。

所以,我做了一些应该适用于 graphviz.Digraph 对象的东西。 我能找到的在图表上显示和注册事件的最简单方法是使用 JavaScript 并将图表导出到 SVG。我已经使用 pywebview 到 运行 从 Python 到 HTML 和 JavaScript,而不依赖于浏览器。

对 Python 问题的回答中的所有 JavaScript 表示歉意,但此解决方案旨在用于 Python 项目,并且 JavaScript 似乎是唯一可行的方法。

此解决方案允许将函数回调附加到节点和边缘。边缘很薄,很难点击它们,但它是可能的,尤其是在箭头点附近。

您可以使用以下代码通过上下文菜单显示图表:

import graphviz


# import the code from the other file
import graphviz_context_menu

# create a simple graph
dot = graphviz.Digraph(comment='The Round Table', format='svg')
dot.node('A', 'King Arthur')
dot.node('B', 'Sir Bedevere the Wise')
dot.node('L', 'Sir Lancelot the Brave')
dot.edges(['AB', 'AL'])
dot.edge('B', 'L', constraint='false')


# display the graph
server = graphviz_context_menu.start_graph_server(
    dot,
    # the context menu for when a node is clicked
    node_menu={
        'Option 1': lambda node: print("option 1,", node, "clicked"),
        'Option 2': lambda node: print("option 2,", node, "clicked"),
    },
    # the context menu for when an edge is clicked
    edge_menu={
        "Edge Context Item": lambda edge: print("edge,", edge, "clicked"),
        "Another Edge Context Item": lambda edge: print("another,", edge, "clicked"),
        "Does nothing": lambda edge: None
    }
)

这依赖于同一目录中名为 graphviz_context_menu.py 的另一个文件,其内容如下:

import webview
import re

js = """
const svg = document.querySelector("#graph > svg")
const nodeMenu = document.querySelector("#node_context_menu");
const edgeMenu = document.querySelector("#edge_context_menu");

const g = svg.childNodes[1];
let selected;

function addMenu(node, menu) {
    node.addEventListener("contextmenu", e => {
        menu.style.left = `${e.pageX}px`;
        menu.style.top = `${e.pageY}px`;

        selected = node.children[0].innerHTML;

        setMenuVisible(true, menu);
        e.preventDefault();
        e.stopPropagation();
    });
}

for(let node of g.childNodes) {
    if(node.tagName === "g"){
        const nodeClass = node.attributes.class.value;
        if(nodeClass === "node"){
            addMenu(node, nodeMenu);
        }
        if(nodeClass === "edge"){
            addMenu(node, edgeMenu);
        }
    }
}

function setMenuVisible(visible, menu) {
    if(visible) {
        setMenuVisible(false);
    }
    if(menu) {
        menu.style.display = visible ? "block" : "none";
    } else {
        setMenuVisible(visible, nodeMenu);
        setMenuVisible(visible, edgeMenu);
    }
}

window.addEventListener("click", e => {
    setMenuVisible(false);
});
window.addEventListener("contextmenu", e => {
    setMenuVisible(false);
    e.preventDefault();
});


function menuClick(menuType, item) {
    if(menuType === 'edge') {
        selected = selected.replace('>','>');
    }
    pywebview.api.menu_item_clicked(menuType,selected,item);
}
"""

def make_menu(menu_info, menu_type):
    lis = '\n'.join(
        f'<li class="menu-option" onclick="menuClick(\'{menu_type}\', \'{name}\')">{name}</li>' for name in menu_info)
    return f"""
    <div class="menu" id="{menu_type}_context_menu">
      <ul class="menu-options">
        {lis}
      </ul>
    </div>
    """


style = """
.menu {
    box-shadow: 0 4px 5px 3px rgba(0, 0, 0, 0.2);
    position: absolute;
    display: none;
    background-color: white;
}
.menu-options {
    list-style: none;
    padding: 10px 0;
}
.menu-option {
    font-weight: 500;
    font-size: 14px;
    padding: 10px 40px 10px 20px;
    cursor: pointer;
    white-space: nowrap;
}
.menu-option:hover {
    background: rgba(0, 0, 0, 0.2);
}
"""



def start_graph_server(graph, node_menu, edge_menu):
    svg = graph.pipe().decode()

    match = re.search(r'<svg width="(\d+)pt" height="(\d+)pt"', svg)
    width, height = match[1], match[2]

    html = f"""<!DOCTYPE html>
                <html>
                <head>
                    <meta charset="utf-8"/>
                    <style>
                        {style}
                    </style>
                </head>
                <body>
                    <div id="graph">{svg}</div>
                    {make_menu(node_menu, 'node')}
                    {make_menu(edge_menu, 'edge')}
                    <script>{js}</script>
                </body>
                </html>
                """

    class Api:
        @staticmethod
        def menu_item_clicked(menu_type, selected, item):
            if menu_type == "node":
                callback = node_menu[item]
                callback(selected)
            elif menu_type == "edge":
                callback = edge_menu[item]
                callback(selected)
            return {}

    window = webview.create_window(
        "Graph Viewer",
        html=html,
        width=int(width) / 0.75 + 400, height=int(height) / 0.75 + 400,
        js_api=Api()
    )
    webview.start(args=window)