├── GUI_image.PNG ├── README.md └── pyplot_editor.py /GUI_image.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LunaKet/Matplotlib_Graph_Editor_GUI/568ed1082d9eda21407e24356cbe5f7f6807d48b/GUI_image.PNG -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Matplotlib_Graph_Editor_GUI 2 | A GUI to make quick fixes to graphs in matplotlib. Uses the pysimplegui package for GUI implementation. Created because I was dissatisfied with spyder's innate graphing GUI. Intended to create a user-friendly and fun experience for graphing in python, and approaches the utility of Matlab's graphing GUI. 3 | 4 | This has a number of common adjustments to pyplot, such as axis labels, title, and legend options. It is continually being updated with new features. The program is completely dynamic and any changes are reflected immediately in the graph. 5 | 6 | ![GUI](https://github.com/alailink/Matplotlib_Graph_Editor_GUI/blob/master/GUI_image.PNG) 7 | 8 | for usage: 9 | ```python 10 | import pyplot_editor as pe 11 | 12 | ###create fig here 13 | 14 | pe.gui(fig) 15 | ``` 16 | 17 | To change the graph theme, it must be done beforehand, which can be accomplished like this: 18 | 19 | ```python 20 | import pyplot_editor as pe 21 | 22 | with plt.style.context('dark_background'): 23 | #... 24 | #create fig and axes plots 25 | #... 26 | pe.gui(fig) 27 | ``` 28 | -------------------------------------------------------------------------------- /pyplot_editor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Jun 3 15:25:17 2020 4 | 5 | @author: Clay 6 | 7 | Create tabs for axes, with basic axes setting not dependent on the graph 8 | -size axis labels 9 | -legend labels?? 10 | -shared x/y axis? 11 | -standard, log, scales 12 | -Rotation was being tricky, save it for later 13 | -all of the stats 14 | break-out window for options, with rescalable window for graph 15 | save button 16 | red bounding box for current plot selection? not a plot option, but a canvas-level draw so it doesn't save with the figure? 17 | 18 | Tips: 19 | -For titles, special formatting can be used as described here -- https://matplotlib.org/1.3.1/users/mathtext.html 20 | - example, "This is the title $e = x^{random} + y_x$" 21 | -mathematical expression go in between $--$ 22 | -it will occassionally break the program. Don't close the $ until you're done 23 | -To change the style, axes *must* be created after global style changes, therefore before passed into gui. 24 | with plt.style.context('dark_background'): 25 | #... 26 | #create fig and axes plots 27 | #... 28 | gui(fig) 29 | -shared axis labels for each row be accomplished by \ 30 | choosing far left-most plot and leaving right plots blank \ 31 | similarly, shared x labels for multiple columns by choosing bottom 32 | -figsize / dpi 33 | 34 | Update notes: 35 | -converting the growing list of if/elif statements to a switch/case-style class improved program idle speed 5-fold (.18s to .0000004s) 36 | and active speed from .18s to .13s (updating the graph is probably the bottleneck now) 37 | -created a second window for the graph. Hope to make it dynamically expand in the future. 38 | 39 | """ 40 | #import time 41 | 42 | 43 | from tkinter import * 44 | from random import randint 45 | import PySimpleGUI as sg 46 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, FigureCanvasAgg 47 | from matplotlib.figure import Figure 48 | from matplotlib.backends import _backend_tk 49 | import tkinter as Tk 50 | import random 51 | import numpy as np 52 | import matplotlib 53 | from matplotlib import pyplot as plt 54 | plt.style.use('dark_background') 55 | 56 | 57 | 58 | def gui(*args): 59 | if len(args)<1: 60 | fig = Figure(figsize = (10,6)) 61 | ax = fig.add_subplot(121) 62 | ax.set_xlabel("X axis") 63 | ax.set_ylabel("Y axis") 64 | pts = 1000 65 | 66 | mean1, mean2, var1, var2 = 4,7,1,1 67 | x = [random.gauss(4,1) for _ in range(pts)] 68 | y = [random.gauss(7,1) for _ in range(pts)] 69 | bins = np.linspace(0, 10, 100) 70 | ax.hist(x, bins, alpha=0.5, label='x', color='pink') 71 | ax.hist(y, bins, alpha=0.5, label='y', color='deepskyblue') 72 | # ax.plot([1,2,3,4,5,6],[2,2,5,5,3,4]) 73 | ax.legend(loc='upper right') 74 | ax.grid(True) 75 | ax.tick_params(labelcolor='white', top='on', bottom='on', left='on', right='on') 76 | 77 | ax2 = fig.add_subplot(122) 78 | x = [i for i in range(1000)] 79 | y = [random.gauss(7,1) for _ in range(pts)] 80 | ax2.plot(x,y) 81 | ax2.grid(False) 82 | 83 | #Otherwise grab the figure passed into the function 84 | if len(args)==1: 85 | fig = args[0] 86 | #These are for global xlabels and other options later. A hidden axes 87 | globalax = fig.add_subplot(111, frame_on=False) #ends up being "final" axes of fig.axes[-1] for global settings 88 | globalax.grid(False) 89 | globalax.tick_params(labelcolor='none', top=False, bottom=False, left=False, right=False) 90 | 91 | #getting info for dynamic GUI rendering listbox 92 | CURRENT_SUBPLOT = "global" 93 | subplot_strs = [f"subplot_{i}" for i in range(len(fig.axes))] 94 | subplot_strs = ["global"] + subplot_strs[:-1] 95 | 96 | column_legend = [[sg.Radio('TL', 'legendary', enable_events=True, key='-TL-'), sg.Radio('T', 'legendary', enable_events=True,key='-T-'), 97 | sg.Radio('TR', 'legendary', enable_events=True,key='-TR-')], 98 | [sg.Radio('ML', 'legendary', enable_events=True,key='-ML-'), sg.Radio('M', 'legendary', enable_events=True,key='-M-'), 99 | sg.Radio('MR', 'legendary', enable_events=True,key='-MR-'),sg.Radio('No Legend', 'legendary', enable_events=True,key='-NOLEGEND-')], 100 | [sg.Radio('BL', 'legendary', enable_events=True,key='-BL-'),sg.Radio('B', 'legendary', enable_events=True,key='-B-'), 101 | sg.Radio('BR', 'legendary', enable_events=True,key='-BR-')]] 102 | 103 | 104 | 105 | #the main settings column for the program 106 | column1_frame = [[sg.Text('X Label'), 107 | sg.Input(key='-XLABEL-', enable_events=True, size=(30,14))], 108 | [sg.Text('Y Label'), 109 | sg.Input(key='-YLABEL-', enable_events=True, size=(30,14))], 110 | [sg.Text('XLimits', key='XLIM'), 111 | sg.Input(key='-XMIN-', enable_events=True, size=(4,14)), 112 | sg.Input(key='-XMAX-', enable_events=True, size=(4,14)), 113 | sg.Text('YLimits'), 114 | sg.Input(key='-YMIN-', enable_events=True, size=(4,14)), 115 | sg.Input(key='-YMAX-', enable_events=True, size=(4,14))], 116 | [sg.Text('Ticks', key='-TICKTEXT-'), sg.Checkbox("left", enable_events=True, key='-LEFTTICK-', default=True), 117 | sg.Checkbox("bottom", enable_events=True, key='-BOTTOMTICK-', default=True), 118 | sg.Checkbox("right", enable_events=True, key='-RIGHTTICK-', default=True), 119 | sg.Checkbox("top", enable_events=True, key='-TOPTICK-', default=True)], 120 | [sg.Checkbox("Grid", enable_events=True, key='-GRID-', pad=((50,0),0)), 121 | sg.Checkbox("Frame", enable_events=True, key='-FRAME-', default=True), 122 | sg.Checkbox("Frame-Part", enable_events=True, key='-FRAMEPART-', default=False)], 123 | [sg.Text("Legend"), sg.Column(column_legend)]] 124 | column1 = [[sg.Text('Choose subplot:', justification='center', font='Helvetica 14', key='-text2-')], 125 | [sg.Listbox(values=subplot_strs, key='-SUBPLOT-', size=(20,3), enable_events=True)], 126 | [sg.Text('Title', justification='center', font='Helvetica 14', key='-OUT-'), sg.Input(key='-TITLE-', enable_events=True, size=(32,14))], 127 | [sg.Text('Font size', pad=(0,(14,0)), justification='center', font='Helvetica 12', key='-OUT2-'),sg.Slider(range=(1, 32), key='-TITLESIZE-', enable_events = True, 128 | pad=(0,0), default_value=12, size=(24, 15), 129 | orientation='h', font=("Helvetica", 10))], 130 | [sg.Frame('General Axes Options, global', [[sg.Column(column1_frame)]], key='-AXESBOX-', pad=(0,(14,0)))], 131 | [sg.B('Save', key='-SAVE-')]] 132 | 133 | 134 | #And here is where we create the layout 135 | sg.theme('DarkBlue') 136 | layout = [[sg.Text('Matplotlib Editor', size=(20, 1), justification='center', font='Helvetica 20')], 137 | [sg.Column(column1)]] 138 | 139 | layout2 = [[sg.Canvas(size=(fig.get_figwidth()*100, fig.get_figheight()*100), background_color='black', key='canvas')]] 140 | 141 | 142 | #[sg.Listbox(values=pyplot.style.available, size=(20, 6), key='-STYLE-', enable_events=True)] 143 | window = sg.Window('Simple GUI to envision ROC curves', layout) 144 | window.Finalize() # needed to access the canvas element prior to reading the window 145 | 146 | window_g = sg.Window("Graphing", layout2, resizable=True) 147 | window_g.Finalize() 148 | canvas_elem = window_g['canvas'] 149 | graph = FigureCanvasTkAgg(fig, master=canvas_elem.TKCanvas) 150 | canvas = canvas_elem.TKCanvas 151 | 152 | def update_graph(): 153 | graph.draw() 154 | figure_x, figure_y, figure_w, figure_h = fig.bbox.bounds 155 | figure_w, figure_h = int(figure_w), int(figure_h) 156 | photo = Tk.PhotoImage(master=canvas, width=figure_w, height=figure_h) 157 | canvas.image = photo 158 | canvas.pack(fill="both", expand=True) 159 | canvas.create_image(fig.get_figwidth()*100 / 2, fig.get_figheight()*100 / 2, image=photo) 160 | #canvas.update(size=(size(window_g)[0],size(window_g)[1])) 161 | figure_canvas_agg = FigureCanvasAgg(fig) 162 | figure_canvas_agg.draw() 163 | _backend_tk.blit(photo, figure_canvas_agg.get_renderer()._renderer, (0, 1, 2, 3)) 164 | 165 | update_graph() 166 | 167 | def frame_set(ax, value): 168 | ax.spines["top"].set_visible(value) 169 | ax.spines["right"].set_visible(value) 170 | ax.spines["bottom"].set_visible(value) 171 | ax.spines["left"].set_visible(value) 172 | 173 | 174 | ### THIS IS THE MAIN BULK OF THE PROGRAM -- MANAGING EVENTS FOR THE GUI #### 175 | class event_manager_class: 176 | 177 | def switch(self, event): 178 | if event != 'Exit' or None: 179 | event = event[1:-1] 180 | #print(event) 181 | getattr(self, event)() 182 | 183 | def TITLE(self): 184 | if CURRENT_SUBPLOT == 'global': 185 | fig.suptitle(values['-TITLE-']) 186 | else: 187 | fig.axes[CURRENT_SUBPLOT].set_title(values['-TITLE-']) 188 | 189 | def TITLESIZE(self): 190 | fontsize = int(values['-TITLESIZE-']) 191 | if CURRENT_SUBPLOT == 'global': 192 | if type(fig._suptitle) is matplotlib.text.Text: 193 | fig.suptitle(values['-TITLE-'], fontsize=fontsize) 194 | else: 195 | fig.axes[CURRENT_SUBPLOT].set_title(fig.axes[CURRENT_SUBPLOT].get_title(), fontsize=fontsize) 196 | def XLABEL(self): 197 | if CURRENT_SUBPLOT == 'global': 198 | globalax.set_xlabel(values['-XLABEL-']) 199 | else: 200 | fig.axes[CURRENT_SUBPLOT].set_xlabel(values['-XLABEL-']) 201 | def YLABEL(self): 202 | if CURRENT_SUBPLOT == 'global': 203 | globalax.set_ylabel(values['-YLABEL-']) 204 | else: 205 | fig.axes[CURRENT_SUBPLOT].set_ylabel(values['-YLABEL-']) 206 | def XMIN(self): 207 | try: limit_update = float(values['-XMIN-']) 208 | except ValueError: return 209 | if CURRENT_SUBPLOT == 'global': 210 | [sub.set_xlim(xmin=limit_update) for sub in fig.axes] 211 | else: 212 | fig.axes[CURRENT_SUBPLOT].set_xlim(xmin=limit_update) 213 | def XMAX(self): 214 | try: limit_update = float(values['-XMAX-']) 215 | except ValueError: return 216 | if CURRENT_SUBPLOT == 'global': 217 | [sub.set_xlim(xmax=limit_update) for sub in fig.axes] 218 | else: 219 | fig.axes[CURRENT_SUBPLOT].set_xlim(xmax=limit_update) 220 | def YMIN(self): 221 | try: limit_update = float(values['-YMIN-']) 222 | except ValueError: return 223 | if CURRENT_SUBPLOT == 'global': 224 | [sub.set_ylim(ymin=limit_update) for sub in fig.axes] 225 | else: 226 | fig.axes[CURRENT_SUBPLOT].set_ylim(ymin=limit_update) 227 | def YMAX(self): 228 | try: limit_update = float(values['-YMAX-']) 229 | except ValueError: return 230 | if CURRENT_SUBPLOT == 'global': 231 | [sub.set_ylim(ymax=limit_update) for sub in fig.axes] 232 | else: 233 | fig.axes[CURRENT_SUBPLOT].set_ylim(ymax=limit_update) 234 | def LEFTTICK(self): 235 | if CURRENT_SUBPLOT == 'global': 236 | [sub.tick_params(left=values['-LEFTTICK-']) for sub in fig.axes[0:-1]] 237 | else: 238 | fig.axes[CURRENT_SUBPLOT].tick_params(left=values['-LEFTTICK-']) 239 | def TOPTICK(self): 240 | if CURRENT_SUBPLOT == 'global': 241 | [sub.tick_params(top=values['-TOPTICK-']) for sub in fig.axes[0:-1]] 242 | else: 243 | fig.axes[CURRENT_SUBPLOT].tick_params(top=values['-TOPTICK-']) 244 | def RIGHTTICK(self): 245 | if CURRENT_SUBPLOT == 'global': 246 | [sub.tick_params(right=values['-RIGHTTICK-']) for sub in fig.axes[0:-1]] 247 | else: 248 | fig.axes[CURRENT_SUBPLOT].tick_params(right=values['-RIGHTTICK-']) 249 | def BOTTOMTICK(self): 250 | if CURRENT_SUBPLOT == 'global': 251 | [sub.tick_params(bottom=values['-BOTTOMTICK-']) for sub in fig.axes[0:-1]] 252 | else: 253 | fig.axes[CURRENT_SUBPLOT].tick_params(bottom=values['-BOTTOMTICK-']) 254 | def GRID(self): 255 | if CURRENT_SUBPLOT == 'global': 256 | [sub.grid(values['-GRID-']) for sub in fig.axes[0:-1]] 257 | else: 258 | fig.axes[CURRENT_SUBPLOT].grid(values['-GRID-']) 259 | def FRAME(self): 260 | if CURRENT_SUBPLOT == 'global': 261 | [frame_set(sub, values['-FRAME-']) for sub in fig.axes[0:-1]] 262 | else: 263 | frame_set(fig.axes[CURRENT_SUBPLOT], values['-FRAME-']) 264 | def FRAMEPART(self): 265 | if CURRENT_SUBPLOT == 'global': 266 | [sub.spines["top"].set_visible(values['-FRAMEPART-']) for sub in fig.axes[0:-1]] 267 | [sub.spines["right"].set_visible(values['-FRAMEPART-']) for sub in fig.axes[0:-1]] 268 | else: 269 | fig.axes[CURRENT_SUBPLOT].spines["top"].set_visible(values['-FRAMEPART-']) 270 | fig.axes[CURRENT_SUBPLOT].spines["right"].set_visible(values['-FRAMEPART-']) 271 | #Radio Buttons for legend 272 | def TL(self): 273 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=2) for sub in fig.axes[0:-1]] 274 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=2) 275 | def T(self): 276 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=9) for sub in fig.axes[0:-1]] 277 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=9) 278 | def TR(self): 279 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=1) for sub in fig.axes[0:-1]] 280 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=1) 281 | def ML(self): 282 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=6) for sub in fig.axes[0:-1]] 283 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=6) 284 | def M(self): 285 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=10) for sub in fig.axes[0:-1]] 286 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=10) 287 | def MR(self): 288 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=7) for sub in fig.axes[0:-1]] 289 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=7) 290 | def BL(self): 291 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=3) for sub in fig.axes[0:-1]] 292 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=3) 293 | def B(self): 294 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=8) for sub in fig.axes[0:-1]] 295 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=8) 296 | def BR(self): 297 | if CURRENT_SUBPLOT == 'global': [sub.legend(loc=4) for sub in fig.axes[0:-1]] 298 | else: fig.axes[CURRENT_SUBPLOT].legend(loc=4) 299 | def NOLEGEND(self): 300 | if CURRENT_SUBPLOT == 'global': [sub.legend().remove() for sub in fig.axes[0:-1]] 301 | else: fig.axes[CURRENT_SUBPLOT].legend().remove() 302 | def SAVE(self): 303 | fname = sg.popup_get_file('Save figure', save_as=True) 304 | fig.savefig(fname) 305 | 306 | ################################################################################################ 307 | 308 | event_manager = event_manager_class() 309 | 310 | while True: 311 | event, values = window.read(timeout=10) 312 | #print(event) 313 | #tic = time.perf_counter() 314 | 315 | if event == 'Exit' or event is None: 316 | break 317 | elif event == '__TIMEOUT__': 318 | pass 319 | #This gets a special elif bc I need to change the global variable CURRENT_SUBPLOT 320 | elif event == '-SUBPLOT-': 321 | idx = values['-SUBPLOT-'][0][-1] 322 | if idx.isdigit(): 323 | CURRENT_SUBPLOT = int(idx) 324 | else: 325 | CURRENT_SUBPLOT = 'global' 326 | window['-AXESBOX-'].Update("General Axes Options, " + values['-SUBPLOT-'][0]) 327 | else: 328 | event_manager.switch(event) 329 | update_graph() 330 | #toc = time.perf_counter() 331 | #print(toc-tic) 332 | 333 | #window['-OUT-'].update(CURRENT_SUBPLOT) 334 | 335 | 336 | window.close() 337 | window_g.close() 338 | 339 | --------------------------------------------------------------------------------