Last active
April 2, 2019 19:23
-
-
Save Joshua1989/31ba6b5a48e066314bf54d4948d7606a to your computer and use it in GitHub Desktop.
Fast tikz Factor Graph drawing based on python3+tkinter, just run it, build your graph by clicking and hit return, then the tikz code is in your clipboard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from tkinter import * | |
from string import Template | |
import clipboard | |
root = Tk() | |
# Make the window transparent, bring it to topmost | |
root.wait_visibility(root) | |
root.attributes('-alpha', 0.5) | |
# Note sometimes this causes other windows to hide, still figuring what happened | |
root.attributes('-topmost', True) | |
root.after_idle(root.attributes,'-topmost',False) | |
# Resize the window to cover the whole screen | |
maxW, maxH = root.winfo_screenwidth(), root.winfo_screenheight() | |
root.geometry("{0}x{1}+0+0".format(maxW, maxH)) | |
canvas = Canvas(root, width=maxW, height=maxH, borderwidth=0, highlightthickness=0, bg="yellow") | |
canvas.grid() | |
# Define basic drawing functions | |
def _create_circle(self, x, y, r, **kwargs): | |
return self.create_oval(x-r, y-r, x+r, y+r, **kwargs) | |
Canvas.create_circle = _create_circle | |
def _create_square(self, x, y, r, **kwargs): | |
return self.create_rectangle(x-r, y-r, x+r, y+r, **kwargs) | |
Canvas.create_square = _create_square | |
def _create_edge(self, p1, p2, **kwargs): | |
return self.create_line(p1[0], p1[1], p2[0], p2[1], **kwargs) | |
Canvas.create_edge = _create_edge | |
# Callback of mouse click | |
# There are two types of nodes in factor graph | |
# click_pts[0] - variable node, circle shape | |
# click_pts[1] - function node, square shape | |
click_pts = [[],[]] | |
# Each element in edges is a pair of pairs ((type1, index1), (type2, index2)) | |
# meaning the index1-th type1 node connects to index2-th type2 node | |
edges = [] | |
radius = 10 # Radius of nodes | |
# Function to check whether the given position overlaps with existing nodes | |
# If there is no overlap, return False and [] | |
# If there is unique overlap node, return True and the overlapping node in format (tyoe, index) | |
# If there are two or more overlap nodes, it is impossible by design, return None, None | |
def check_overlap(x,y,r=5*radius): | |
def dist(p1,p2): | |
return (p1[0]-p2[0])**2 + (p1[1]-p2[1])**2 | |
overlap_info = [ (i,j) for i in range(2) for j in range(len(click_pts[i])) if dist((x,y),click_pts[i][j]) < r**2 ] | |
if not overlap_info: | |
return False, [] | |
elif len(overlap_info) == 1: | |
return True, overlap_info[0] | |
else: | |
print('impossible case: {0} overlaps'.format(len(overlap_info))) | |
return None, None | |
# Function to add new variable node | |
def callback_circle(event): | |
is_overlap, overlap_idx = check_overlap(event.x,maxH-event.y) | |
# If there is no overlap node, add new variable node | |
if is_overlap == False: | |
click_pts[0].append((event.x,maxH-event.y)) | |
canvas.create_circle(event.x, event.y, radius, fill="red") | |
canvas.create_text(event.x,event.y,text='{0}'.format(len(click_pts[0])), fill='white') | |
print('add circle {0} at ({1},{2})'.format(len(click_pts[0]), event.x, event.y)) | |
# If there is unique overlap node, cancel adding new node and show the overlap node | |
elif is_overlap == True: | |
names = {0:'circle', 1:'square'} | |
print('overlap with {0}-th {1}'.format(overlap_idx[1]+1, names[overlap_idx[0]])) | |
# If there are more than one overlap node, do nothing | |
# Bind to Ctrl+Left Button | |
root.bind("<Control-Button-1>", callback_circle) | |
# Function to add new function node | |
def callback_square(event): | |
is_overlap, overlap_idx = check_overlap(event.x,maxH-event.y) | |
# If there is no overlap node, add new function node | |
if is_overlap == False: | |
click_pts[1].append((event.x,maxH-event.y)) | |
canvas.create_square(event.x, event.y, radius, fill="blue") | |
canvas.create_text(event.x,event.y,text='{0}'.format(len(click_pts[1])), fill='white') | |
print('add square {0} at ({1},{2})'.format(len(click_pts[1]), event.x, event.y)) | |
# If there is unique overlap node, cancel adding new node and show the overlap node | |
elif is_overlap == True: | |
names = {0:'circle', 1:'square'} | |
print('overlap with {0}-th {1}'.format(overlap_idx[1]+1, names[overlap_idx[0]])) | |
# If there are more than one overlap node, do nothing | |
# Bind to Option+Left Button | |
root.bind("<Option-Button-1>", callback_square) | |
head = False # True if the first endpoint of an edge is assigned | |
p1, p2 = None, None # (type,index) information for two endpoints of the edge | |
# Function to add edges | |
def callback_edge(event): | |
global head, p1, p2 | |
def get_pt(idx): | |
return (click_pts[idx[0]][idx[1]][0], maxH-click_pts[idx[0]][idx[1]][1]) | |
is_overlap, overlap_idx = check_overlap(event.x,maxH-event.y) | |
names = {0:'circle', 1:'square'} | |
# If there is a node in current position and the first endpoint is not assigned | |
# assign the first endpoint | |
if is_overlap == True and head == False: | |
head, p1, p2 = True, overlap_idx, None | |
print('choose start point as {0}-th {1}'.format(overlap_idx[1]+1, names[overlap_idx[0]])) | |
# If there is a node in current position and the first endpoint is assigned | |
# If the second endpoint is different from the first one, draw an edge | |
elif is_overlap == True and head == True and overlap_idx != p1: | |
head, p2 = False, overlap_idx | |
edges.append((p1,p2)) | |
canvas.create_edge(get_pt(p1), get_pt(p2), width=3) | |
print('choose end point as {0}-th {1}'.format(overlap_idx[1]+1, names[overlap_idx[0]])) | |
# If there is a node in current position and the first endpoint is assigned | |
# If the second endpoint is same as the first one, cancel adding edge | |
elif is_overlap == True and head == True and overlap_idx == p1: | |
head, p2 = False, None | |
print('start and end points are same, cancel adding edge') | |
# Bind to Left Button | |
root.bind("<Button-1>", callback_edge) | |
# Functions to exit the program | |
confirm = False # True if finished properly | |
def Return(event): | |
global confirm | |
confirm = True | |
root.destroy() | |
root.bind("<Return>", Return) | |
def Exit(event): | |
root.destroy() | |
root.bind("<Escape>", Exit) | |
# Main loop of the program | |
root.mainloop() | |
# Post-processing | |
if not confirm: | |
# If exit improperly, do nothing | |
print('exit without output') | |
elif confirm: | |
template = Template(''' | |
\\begin{center} | |
\\begin{tikzpicture}[scale=1, auto, swap] | |
\\tikzstyle{var}=[circle, draw, thick, minimum size=20pt, inner sep=0pt, fill=yellow, font=\small] | |
\\tikzstyle{fun}=[rectangle, draw, thick, minimum size=20pt, inner sep=0pt, fill=cyan, font=\small] | |
\\tikzstyle{edge} = [draw, thick, -] | |
\\def\\mylen{0.35}; | |
% First we draw the variable nodes | |
\\foreach \\pos/\\name/\\mathname in { $VN_Info} | |
\\node[var] (\\name) at \\pos {\\mathname}; | |
% Then we draw the function nodes | |
\\foreach \\pos/\\name/\\mathname in { $FN_Info} | |
\\node[fun] (\\name) at \\pos {\\mathname}; | |
% Connect vertices with edges and draw weights | |
\\foreach \\source/ \\dest in { $Edge_Info} | |
\\path[edge] (\\source) -- (\\dest); | |
\\end{tikzpicture} | |
\\end{center}''') | |
# Find bounding box of all nodes | |
all_pts = click_pts[0] + click_pts[1] | |
bottom = min(all_pts ,key=lambda x:x[1])[1] | |
top = max(all_pts ,key=lambda x:x[1])[1] | |
left = min(all_pts ,key=lambda x:x[0])[0] | |
right = max(all_pts ,key=lambda x:x[0])[0] | |
# Align coordinates of all nodes to grid | |
max_grid_num = 25 # Grid resolution | |
unit = 1.0 * max(top-bottom,right-left) / max_grid_num | |
click_pts[0] = [ (round(1.0*(x-left)/unit), round(1.0*(y-bottom)/unit)) for x,y in click_pts[0] ] | |
click_pts[1] = [ (round(1.0*(x-left)/unit), round(1.0*(y-bottom)/unit)) for x,y in click_pts[1] ] | |
# Generate code for variable nodes | |
par_list = [ '{{({0}*\\mylen,{1}*\\mylen)/X{2}/$ X_{{{2}}} $}}'.format(pt[0],pt[1],i) for i,pt in enumerate(click_pts[0],1) ] | |
VN_Info = (',\n'+'\t'*10).join(par_list) | |
# Generate code for function nodes | |
par_list = [ '{{({0}*\\mylen,{1}*\\mylen)/F{2}/$ F_{{{2}}} $}}'.format(pt[0],pt[1],i) for i,pt in enumerate(click_pts[1],1) ] | |
FN_Info = (',\n'+'\t'*10).join(par_list) | |
# Generate code for edges | |
names = {0:'X', 1:'F'} | |
par_list = [ '{0}{1}/{2}{3}'.format(names[p1[0]], p1[1]+1, names[p2[0]], p2[1]+1) for p1, p2 in edges ] | |
Edge_Info = (',\n'+'\t'*9).join(par_list) | |
# Replace code segments to the template and send to clipboard | |
tikz_code = template.substitute(VN_Info=VN_Info, FN_Info=FN_Info, Edge_Info=Edge_Info) | |
clipboard.copy(tikz_code) | |
print('the tikz code is in the clipboard') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Recently I am taking a class which draws a lot graphs on the board, I found it is annoying to use latex to note down these simultaneously, especially figuring out the coordinates. So this motivates me to implement this short python code to achieve tikz fast graph drawing.
Dependencies: python3, tkinter, clipboard
How to use
Run the python script
A transparent yellow window will cover the whole screen and come to topmost, this is your drawing board
After building the graph, press Esc to quit doing nothing, or press Enter to generate corresponding tikz code.
If you press Enter, the tikz code is already in your clipboard, just paste it to everywhere you want!