diff --git a/tools/TasksTransitionVisualizer/.gitignore b/tools/TasksTransitionVisualizer/.gitignore new file mode 100644 index 0000000000..00913b92a6 --- /dev/null +++ b/tools/TasksTransitionVisualizer/.gitignore @@ -0,0 +1,3 @@ + +*.pdf +testenv/ \ No newline at end of file diff --git a/tools/TasksTransitionVisualizer/README.md b/tools/TasksTransitionVisualizer/README.md new file mode 100644 index 0000000000..f719ea1a6c --- /dev/null +++ b/tools/TasksTransitionVisualizer/README.md @@ -0,0 +1,15 @@ +# 说明 + +一个简单脚本,读取tasks.json, 根据指定的前缀词筛选task,将获得的task按照next关系绘制成有向图。 + +Generate a transition graph for nodes whose name begin with a specific keyword + +``` +optional arguments: + -h, --help show this help message and exit + --keyword KEYWORD the keyword to generate the graph for, case sensitive + --nodesize [400-800] the size of nodes in the graph + --arrowsize [20-50] the size of arrows in the graph + --fontsize [5-10] the size of font in the graph + --list if list all the names of nodes matched +``` diff --git a/tools/TasksTransitionVisualizer/requirements.txt b/tools/TasksTransitionVisualizer/requirements.txt new file mode 100644 index 0000000000..7f8dfa8520 --- /dev/null +++ b/tools/TasksTransitionVisualizer/requirements.txt @@ -0,0 +1,37 @@ +backcall==0.2.0 +colorama==0.4.4 +contourpy==1.0.7 +cycler==0.11.0 +decorator==5.0.9 +ffmpeg==1.4 +fonttools==4.38.0 +importlib-resources==5.12.0 +ipykernel==5.5.5 +ipython==7.24.1 +ipython-genutils==0.2.0 +jedi==0.18.0 +jupyter-client==6.1.12 +jupyter-core==4.7.1 +kiwisolver==1.4.4 +matplotlib==3.7.1 +matplotlib-inline==0.1.2 +networkx==3.0 +numpy==1.24.2 +packaging==23.0 +parso==0.8.2 +pickleshare==0.7.5 +Pillow==9.4.0 +prompt-toolkit==3.0.18 +pydot==1.4.2 +Pygments==2.9.0 +pyparsing==3.0.9 +python-dateutil==2.8.1 +pywin32==301 +pyzmq==22.1.0 +six==1.16.0 +tornado==6.1 +traitlets==5.0.5 +urllib3==1.25.11 +wcwidth==0.2.5 +you-get==0.4.1536 +zipp==3.15.0 diff --git a/tools/TasksTransitionVisualizer/tasks_transition_visualizer.py b/tools/TasksTransitionVisualizer/tasks_transition_visualizer.py new file mode 100644 index 0000000000..efeb15a9cf --- /dev/null +++ b/tools/TasksTransitionVisualizer/tasks_transition_visualizer.py @@ -0,0 +1,122 @@ +import os +import sys +import json +import logging +import warnings +import argparse +import matplotlib.pyplot as plt +import networkx as nx +import webbrowser + +# nx.nx_pydot.graphviz_layout reports DeprecationWarning +warnings.filterwarnings('ignore', category=DeprecationWarning) + +logging.basicConfig(format='%(message)s', level=logging.INFO) + +parser = argparse.ArgumentParser( + description='Generate a transition graph for nodes whose name begin with a specific keyword') +parser.add_argument('--keyword', type=str, default='Mizuki@Roguelike', + help='the keyword to generate the graph for, case sensitive') +parser.add_argument('--nodesize', type=int, default=500, choices=range(400, 801), metavar='[400-800]', + help='the size of nodes in the graph') +parser.add_argument('--arrowsize', type=int, default=30, choices=range(20, 51), metavar='[20-50]', + help='the size of arrows in the graph') +parser.add_argument('--fontsize', type=int, default=8, choices=range(5, 11), metavar='[5-10]', + help='the size of font in the graph') +parser.add_argument('--list', action='store_true', + help='if list all the names of nodes matched') +args = parser.parse_args() + +project_root_path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), + os.pardir, os.pardir)) +task_json_path = os.path.join(project_root_path, 'resource', 'tasks.json') +try: + with open(task_json_path, 'r', encoding='utf-8') as f: + json_dict = json.load(f) +except Exception as e: + logging.error( + "Failed to read task json at {}: {}".format(task_json_path), e) + exit(-1) + +keyword = args.keyword +if keyword == "Mizuki@Roguelike": + logging.info("Default to generate graph for {}".format(keyword)) +else: + logging.info("Generating graph for %s", keyword) + + +def simplify_node(node_name): + # shorten node name for display in the graph + return node_name.replace(keyword+'@', '') + + +def recover_node(node_name): + # recover the node names, used when build graph + return keyword+'@'+node_name + +# recursively find the nodes +def search_node(node, s, visited, nodes): + # nodes without next field are not considered + if not (node in json_dict.keys() and 'next' in json_dict[node]): + return + if node in visited: + return + visited.add(node) + if node.startswith(s): + nodes.append(simplify_node(node)) + else: + return + for next_node in json_dict[node]['next']: + search_node(next_node, s, visited, nodes) + + +visited = set() +nodes = [] +for node in json_dict: + search_node(node, keyword, visited, nodes) + +if len(nodes) == 0: + logging.warning("Found 0 tasks starting with {}".format(keyword)) + exit(0) +else: + logging.info('Found {} tasks starting with {}'.format(len(nodes), keyword)) + if args.list: + logging.info('\n'.join(nodes)) +nodes.sort() + +# generate networkx.MultiDiGraph +G = {} +for node in nodes: + G[node] = json_dict[recover_node(node)]['next'] + for i in range(len(G[node])): + G[node][i] = simplify_node(G[node][i]) +G = nx.MultiDiGraph(G) +logging.info("Built graph model completed.") + +# export pdf +plt.figure(figsize=(60, 60)) +pos = nx.nx_pydot.graphviz_layout(G, prog='dot') +nx.draw_networkx_nodes(G, pos, node_size=args.nodesize, + alpha=0.8, node_color='lightblue') +nx.draw_networkx_edges(G, pos, alpha=1, arrows=True, arrowsize=args.arrowsize) +nx.draw_networkx_labels(G, pos, font_size=args.fontsize, + font_family='sans-serif') +plt.axis('off') +fig = plt.gcf() +relative_path = os.path.join( + "tools", "TasksTransitionVisualizer", "graph_{}.pdf".format(keyword)) +file_path = os.path.join(project_root_path, relative_path) + +try: + fig.savefig(file_path) + logging.info("Transition graph file for keyword {} saved to {}".format( + keyword, relative_path)) +except PermissionError: + logging.error( + "File {} is in use. Close and re-run the task.".format(file_path)) +except Exception as e: + logging.error( + "Cannot save graph file at {}. Error: {}".format(relative_path, e)) + + +webbrowser.open(file_path, new=2)