Skip to content

Instantly share code, notes, and snippets.

@Joshua1989
Last active April 2, 2019 19:23
Show Gist options
  • Save Joshua1989/31ba6b5a48e066314bf54d4948d7606a to your computer and use it in GitHub Desktop.
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
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')
@Joshua1989
Copy link
Author

Joshua1989 commented Jan 31, 2017

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.

fg_helper_demo

Dependencies: python3, tkinter, clipboard

How to use

  1. Run the python script

  2. A transparent yellow window will cover the whole screen and come to topmost, this is your drawing board

    • Ctrl+Left Button click to add a new variable node
    • Option+Left Button click to add a new function node
    • To add an edge between nodes, simply click two endpoints sequentially
  3. After building the graph, press Esc to quit doing nothing, or press Enter to generate corresponding tikz code.

  4. If you press Enter, the tikz code is already in your clipboard, just paste it to everywhere you want!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment