Я не смог найти ни одной существующей библиотеки, которая бы допускала произвольные события для графов графиков - самым близким, что я нашел, было 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)