Контекстные меню для узлов и ребер для точечных графиков с использованием Python - PullRequest
0 голосов
/ 03 февраля 2020

Есть ли простой способ добавления контекстных меню для узлов и ребер для точечных графиков с использованием Python? Таким образом, при нажатии на узел или ребро появляется контекстное меню, пользователь может выбрать пункт меню и затем, в зависимости от записи, будет выполнен код Python?

1 Ответ

1 голос
/ 04 февраля 2020

Я не смог найти ни одной существующей библиотеки, которая бы допускала произвольные события для графов графиков - самым близким, что я нашел, было xdot , которое, кажется, не удовлетворяет вашим требованиям.

Итак, я сделал то, что должно работать для graphviz.Digraph объектов. Самым простым способом отображения и регистрации событий на графике, который я смог найти, было использование JavaScript и экспорт графика в SVG. Я использовал pywebview для запуска HTML и JavaScript с Python без использования браузера.

Извинения за все JavaScript в ответе на Python вопрос, но это решение предназначено для 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)
...