使用 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)
有没有什么简单的方法可以使用 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)