Source code for pandastable.dialogs

#!/usr/bin/env python
"""
    Dialog classes.
    Created Oct 2014
    Copyright (C) Damien Farrell

    This program is free software; you can redistribute it and/or
    modify it under the terms of the GNU General Public License
    as published by the Free Software Foundation; either version 3
    of the License, or (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""

from __future__ import absolute_import, division, print_function
import sys,os,types
import platform
from datetime import datetime
try:
    from tkinter import *
    from tkinter.ttk import *
    from tkinter import filedialog, messagebox, simpledialog
    from tkinter.simpledialog import Dialog
    from tkinter import Scale
    from tkinter.scrolledtext import ScrolledText
except:
    from Tkinter import *
    from ttk import *
    from tkSimpleDialog import Dialog
    from Tkinter import Scale
    import tkFileDialog as filedialog
    import tkSimpleDialog as simpledialog
    import tkMessageBox as messagebox
    from ScrolledText import ScrolledText

from collections import OrderedDict
import webbrowser
import numpy as np
import pandas as pd
from .data import TableModel
from . import util, images

[docs]def setGeometry(win, width=None): """Set window geometry to center of screen""" winsize = getBestGeometry(win, width) win.geometry(winsize) return
[docs]def getBestGeometry(win, width=None): """Calculate optimal geometry from screen size or given width""" ws = win.winfo_screenwidth() hs = win.winfo_screenheight() if width == None: w = ws/1.4; h = hs*0.8 else: w = width h = width / 2 x = (ws/2)-(w/2); y = (hs/2)-(h/2) g = '%dx%d+%d+%d' % (w,h,x,y) return g
[docs]def getParentGeometry(parent): x = parent.winfo_rootx() y = parent.winfo_rooty() w = parent.winfo_width() h = parent.winfo_height() return x,y,w,h
[docs]def getDictfromTkVars(opts, tkvars, widgets): kwds = {} for i in opts: if not i in tkvars: continue if opts[i]['type'] == 'listbox': items = widgets[i].curselection() kwds[i] = [widgets[i].get(j) for j in items] #print (items, kwds[i]) else: try: kwds[i] = int(tkvars[i].get()) except: kwds[i] = tkvars[i].get() return kwds
[docs]def pickColor(parent, oldcolor): import tkinter.colorchooser ctuple, newcolor = tkinter.colorchooser.askcolor(title='pick a color', initialcolor=oldcolor, parent=parent) if ctuple == None: return None return str(newcolor)
[docs]def dialogFromOptions(parent, opts, groups=None, callback=None, sticky='news', layout='horizontal'): """Auto create tk vars and widgets for corresponding options and and return the enclosing frame""" tkvars = {} widgets = {} dialog = Frame(parent) if groups == None: groups = {'options': opts.keys()} c=0 row=0 scol=1 for g in groups: if g == 'hidden': continue if layout=='horizontal': row=0; c+=1 side=LEFT fill=Y else: c=0; row+=1 side=TOP fill=X frame = LabelFrame(dialog, text=g) #frame.grid(row=row,column=c,sticky=sticky) frame.pack(side=side,fill=fill,expand=False) for i in groups[g]: w=None opt = opts[i] if opt['type'] == 'entry': if 'label' in opt: label=opt['label'] else: label=i if 'width' in opt: w=opt['width'] else: w=6 Label(frame,text=label).pack() if type(opts[i]['default']) is int: tkvars[i] = v = IntVar() else: tkvars[i] = v = StringVar() v.set(opts[i]['default']) w = Entry(frame,textvariable=v, width=w, command=callback) elif opt['type'] == 'scrolledtext': w = ScrolledText(frame, width=20, wrap=WORD) tkvars[i] = None elif opt['type'] == 'checkbutton': tkvars[i] = v = IntVar() v.set(opts[i]['default']) w = Checkbutton(frame,text=opt['label'], variable=v) elif opt['type'] == 'combobox': if 'label' in opt: label=opt['label'] else: label = i if 'width' in opt: w=opt['width'] else: w=16 Label(frame,text=label).pack() tkvars[i] = v = StringVar() v.set(opts[i]['default']) w = Combobox(frame, values=opt['items'], textvariable=v,width=w, validatecommand=callback,validate='key') w.set(opt['default']) if 'tooltip' in opt: ToolTip.createToolTip(w, opt['tooltip']) elif opt['type'] == 'spinbox': if 'label' in opt: label=opt['label'] else: label = i Label(frame,text=label).pack() tkvars[i] = v = StringVar() w = Spinbox(frame, values=opt['items'], textvariable=v,width=w, validatecommand=callback,validate='key') w.set(opt['default']) #if 'tooltip' in opt: # ToolTip.createToolTip(w, opt['tooltip']) elif opt['type'] == 'listbox': if 'label' in opt: label=opt['label'] else: label = i Label(frame,text=label).pack() w,v = addListBox(frame, values=opt['items'],width=12) tkvars[i] = v #add widget instead of var elif opt['type'] == 'radio': Label(frame,text=label).pack() if 'label' in opt: label=opt['label'] else: label = i Label(frame,text=label).pack() tkvars[i] = v = StringVar() for item in opt['items']: w = Radiobutton(frame, text=item, variable=v, value=item).pack() elif opt['type'] == 'scale': fr,to=opt['range'] tkvars[i] = v = DoubleVar() v.set(opts[i]['default']) w = Scale(frame,label=opt['label'], from_=fr,to=to, orient='horizontal', resolution=opt['interval'], variable=v) elif opt['type'] == 'colorchooser': tkvars[i] = v = StringVar() clr = opts[i]['default'] v.set(clr) def func(var): clr=var.get() new=pickColor(parent,clr) if new != None: var.set(new) w = Button(frame, text=opt['label'], command=lambda v=v : func(v)) if w != None: w.pack(fill=BOTH,expand=1,pady=1) widgets[i] = w row+=1 return dialog, tkvars, widgets
[docs]def addButton(frame, name, callback, img=None, tooltip=None, side=TOP, compound=None, width=None, padding=1): """Add a button with image, toolip to a tkinter frame""" #style = ttk.Style() #style.configure('TButton', padding=1) if img==None: b = Button(frame, text=name, command=callback) else: b = Button(frame, text=name, command=callback, width=width, image=img, compound=compound, padding=padding) b.image = img b.pack(side=side,fill=X,padx=padding) if tooltip != None: ToolTip.createToolTip(b, tooltip) return
[docs]def applyStyle(w): """Apply style to individual widget to prevent widget color issues on linux""" plf = util.checkOS() if plf in ['linux','darwin']: bg = Style().lookup('TLabel.label', 'background') w.configure(fg='black', bg=bg, activeforeground='white', activebackground='#0174DF') return
[docs]def setWidgetStyles(widgets): """set styles of list of widgets""" style = Style() bg = style.lookup('TLabel.label', 'background') for w in widgets: try: w.configure(fg='black', bg=bg) except: pass return
[docs]class Progress(): """ threaded progress bar for tkinter gui """ def __init__(self, parent, side=LEFT): import threading self.maximum = 100 self.interval = 10 self.progressbar = Progressbar(parent, orient=HORIZONTAL, mode="indeterminate", maximum=self.maximum) #self.progressbar.grid(row=row, column=column, # columnspan=columnspan, sticky="we") self.progressbar.pack(fill=X,side=side) self.thread = threading.Thread() self.thread.__init__(target=self.progressbar.start(self.interval), args=()) self.thread.start()
[docs] def pb_stop(self): """ stops the progress bar """ if not self.thread.isAlive(): VALUE = self.progressbar["value"] self.progressbar.stop() self.progressbar["value"] = VALUE
[docs] def pb_start(self): """ starts the progress bar """ if not self.thread.isAlive(): VALUE = self.progressbar["value"] self.progressbar.configure(mode="indeterminate", maximum=self.maximum, value=VALUE) self.progressbar.start(self.interval)
[docs] def pb_clear(self): """ stops the progress bar """ if not self.thread.isAlive(): self.progressbar.stop() self.progressbar.configure(mode="determinate", value=0)
[docs] def pb_complete(self): """ stops the progress bar and fills it """ if not self.thread.isAlive(): self.progressbar.stop() self.progressbar.configure(mode="determinate", maximum=self.maximum, value=self.maximum)
[docs]class MultipleValDialog(Dialog): """Simple dialog to get multiple values""" def __init__(self, parent, title=None, initialvalues=None, labels=None, types=None, tooltips=None, width=14, **kwargs): if labels != None and types is not None: self.initialvalues = initialvalues self.labels = labels self.types = types self.tooltips = tooltips self.maxwidth = width Dialog.__init__(self, parent, title) #super(MultipleValDialog, self).__init__(parent, title) return
[docs] def body(self, master): r=0 self.vrs=[];self.entries=[] for i in range(len(self.labels)): Label(master, text=self.labels[i]).grid(row=r,column=0,sticky='news') if self.types[i] in ['int','checkbutton']: self.vrs.append(IntVar()) else: self.vrs.append(StringVar()) default = self.initialvalues[i] if self.types[i] == 'password': s='*' else: s=None if self.types[i] == 'combobox': self.vrs[i].set(default[0]) w = Combobox(master, values=default, textvariable=self.vrs[i],width=14) self.entries.append(w) elif self.types[i] == 'listbox': f,w = addListBox(master, values=default,width=14) self.entries.append(f) self.vrs[i] = w #add widget instead of var elif self.types[i] == 'checkbutton': self.vrs[i].set(default) w = Checkbutton(master, text='', variable=self.vrs[i]) self.entries.append(w) else: if default == None: default='' self.vrs[i].set(default) self.entries.append(Entry(master, textvariable=self.vrs[i], width=self.maxwidth, show=s)) self.entries[i].grid(row=r, column=1,padx=2,pady=2,sticky='ew') if self.tooltips != None: ToolTip.createToolTip(self.entries[i], self.tooltips[i]) r+=1 s=Style() bg = s.lookup('TLabel.label', 'background') self.configure(background=bg) master.configure(background=bg) self.option_add("*background", bg) self.option_add("*foreground", 'black') return self.entries[0] # initial focus
[docs] def apply(self): self.result = True self.results = [] for i in range(len(self.labels)): if self.types[i] == 'listbox': self.results.append(self.vrs[i].getSelectedItem()) else: self.results.append(self.vrs[i].get()) return
[docs] def getResults(self, null=None): """Return a dict of options/values""" res = dict(zip(self.labels,self.results)) #replace null values with None if null != None: for r in res: if res[r] == null: res[r] = None for r in res: try: res[r] = int(res[r]) except: pass return res
[docs]class ToolTip(object): """Tooltip class for tkinter widgets""" def __init__(self, widget): self.widget = widget self.tipwindow = None self.id = None self.x = self.y = 0
[docs] def showtip(self, text, event=None): "Display text in tooltip window" self.text = text if self.tipwindow or not self.text: return x, y, cx, cy = self.widget.bbox("insert") x = x + event.x x = x + self.widget.winfo_rootx() + 25 y = y + cy + self.widget.winfo_rooty() + 10 self.tipwindow = tw = Toplevel(self.widget) tw.wm_overrideredirect(1) tw.wm_geometry("+%d+%d" % (x, y)) try: # For Mac OS tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "noActivates") except TclError: pass label = Label(tw, text=self.text, justify=LEFT, background="#ffffe0", relief=SOLID, borderwidth=1, font=("tahoma", "8", "normal"), foreground='black') label.pack(ipadx=1)
[docs] def hidetip(self, event=None): """Hide tooltip""" tw = self.tipwindow self.tipwindow = None if tw: tw.destroy()
[docs] @classmethod def createToolTip(self, widget, text): """Create a tooltip for a widget""" toolTip = ToolTip(widget) def enter(event): toolTip.showtip(text, event) def leave(event): toolTip.hidetip(event) widget.bind('<Enter>', enter) widget.bind('<Leave>', leave) return
[docs]class ProgressDialog(Toplevel): def __init__(self): Toplevel.__init__() prog = Progressbar(self, orient='horizontal', length=200, mode='indeterminate')
[docs]class ImportDialog(Frame): """Provides a frame for figure canvas and MPL settings""" def __init__(self, parent=None, filename=None): from .core import Table self.parent = parent self.filename = filename self.df = None self.main = Toplevel() self.master = self.main self.main.title('Text Import') self.main.protocol("WM_DELETE_WINDOW", self.quit) self.main.grab_set() self.main.transient(parent) #setGeometry(self.main, width=900) delimiters = [',',r'\t',' ',';','/','&','|','^','+','-'] encodings = ['utf-8','ascii','iso8859_15','cp037','cp1252','big5','euc_jp'] timeformats = ['infer','%d/%m/%Y','%Y/%m/%d','%Y/%d/%m', '%Y-%m-%d %H:%M:%S','%Y-%m-%d %H:%M', '%d-%m-%Y %H:%M:%S','%d-%m-%Y %H:%M'] grps = {'formats':['delimiter','decimal','comment'], 'data':['header','skiprows','skipinitialspace', 'skip_blank_lines','numbers_as_string', 'parse_dates','time format','encoding','names'], 'other':['rowsperfile']} grps = OrderedDict(sorted(grps.items())) opts = self.opts = {'delimiter':{'type':'combobox','default':',', 'items':delimiters, 'tooltip':'seperator'}, 'header':{'type':'entry','default':0,'label':'header', 'tooltip':'position of column header'}, #'index_col':{'type':'entry','default':'','label':'index col', # 'tooltip':''}, 'decimal':{'type':'combobox','default':'.','items':['.',','], 'tooltip':'decimal point symbol'}, 'comment':{'type':'entry','default':'#','label':'comment', 'tooltip':'comment symbol'}, 'skipinitialspace':{'type':'checkbutton','default':0,'label':'skip initial space', 'tooltip':'skip initial space'}, 'skiprows':{'type':'entry','default':0,'label':'skiprows', 'tooltip':'rows to skip'}, 'skip_blank_lines': {'type':'checkbutton','default':0,'label':'skip blank lines', 'tooltip':'do not use blank lines'}, 'numbers_as_string': {'type':'checkbutton','default':0,'label':'numbers as string', 'tooltip':'import numbers as string'}, 'parse_dates': {'type':'checkbutton','default':1,'label':'parse dates', 'tooltip':'try to parse date/time columns'}, 'time format': {'type':'combobox','default':'','items':timeformats, 'tooltip':'date/time format'}, 'encoding':{'type':'combobox','default':'utf-8','items':encodings, 'tooltip':'file encoding'}, #'prefix':{'type':'entry','default':None,'label':'prefix', # 'tooltip':''} 'rowsperfile':{'type':'entry','default':'','label':'rows per file', 'tooltip':'rows to read'}, 'names':{'type':'entry','default':'','label':'column names', 'tooltip':'col labels'}, } bf = Frame(self.main) bf.pack(side=LEFT,fill=BOTH) optsframe, self.tkvars, w = dialogFromOptions(bf, opts, grps, sticky='nwe', layout='vertical') self.m = PanedWindow(self.main, orient=VERTICAL) self.m.pack(side=LEFT,fill=BOTH,expand=1) self.textpreview = ScrolledText(self.main, width=100, height=10, bg='white') self.m.add(self.textpreview, weight=3) tf = Frame(self.main) self.m.add(tf, weight=1) self.previewtable = Table(tf,rows=0,columns=0,editable=False,enable_menus=False) self.previewtable.show() self.update() optsframe.pack(side=TOP,fill=BOTH) b = Button(bf, text="Update preview", command=self.update) b.pack(side=TOP,fill=X,pady=2) b = Button(bf, text="Import", command=self.doImport) b.pack(side=TOP,fill=X,pady=2) b = Button(bf, text="Cancel", command=self.quit) b.pack(side=TOP,fill=X,pady=2) self.main.wait_window() return
[docs] def showText(self, encoding='utf-8'): """Show text contents""" with open(self.filename, 'r', encoding=encoding) as stream: try: text = stream.read() except: text = 'failed to preview this file, change the file encoding and then press update preview' self.textpreview.delete('1.0', END) self.textpreview.insert('1.0', text) return
[docs] def update(self): """Reload previews""" kwds = {} other = ['rowsperfile','time format','numbers_as_string'] for i in self.opts: if i in other: continue try: val = self.tkvars[i].get() except: val = None if val == '': val = None if self.opts[i]['type'] == 'checkbutton': val = bool(val) elif type(self.opts[i]['default']) != int: try: val=int(val) except: pass kwds[i] = val self.kwds = kwds as_string = self.tkvars['numbers_as_string'].get() timeformat = self.tkvars['time format'].get() dateparse = lambda x: datetime.strptime(x, timeformat) self.showText(encoding=kwds['encoding']) if as_string == True: temp = pd.read_csv(self.filename, chunksize=50, **kwds).get_chunk() cols = temp.columns self.converters = {col: str for col in cols} else: self.converters = None try: f = self.textfilereader = pd.read_csv(self.filename, chunksize=500, on_bad_lines='skip', warn_bad_lines=False, date_parser=dateparse, converters=self.converters, **kwds) except Exception as e: print ('read csv error') print (e) f = None if f is not None: try: df = f.get_chunk() except: print ('parser error') df = pd.DataFrame() else: df = pd.DataFrame() model = TableModel(dataframe=df) self.previewtable.updateModel(model) self.previewtable.showIndex() self.previewtable.redraw() return
[docs] def doImport(self): """Do the import""" self.update() self.df = pd.read_csv(self.filename, converters=self.converters, **self.kwds) self.quit() return
[docs] def quit(self): self.main.destroy() return
[docs]class CombineDialog(Frame): """Provides a frame for setting up merge/combine operations""" def __init__(self, parent=None, df1=None, df2=None): self.parent = parent self.main = Toplevel() self.master = self.main self.main.title('Merge/Join/Concat') self.main.protocol("WM_DELETE_WINDOW", self.quit) self.main.grab_set() self.main.transient(parent) self.main.resizable(width=False, height=False) self.df1 = df1 self.df2 = df2 self.merged = None wf = Frame(self.main) wf.pack(side=LEFT,fill=BOTH) f=Frame(wf) f.pack(side=TOP,fill=BOTH) ops = ['merge','concat'] self.opvar = StringVar() w = Combobox(f, values=ops, textvariable=self.opvar,width=14 ) w.set('merge') Label(f,text='operation:').pack() w.pack() #buttons to add for each op. #merge: left, right, how, suff1, suff2 #concat assumes homogeneous dfs how = ['inner','outer','left','right'] grps = {'merge': ['left_on','right_on','suffix1','suffix2','how'], 'concat': ['join','ignore_index','verify_integrity']} self.grps = grps = OrderedDict(sorted(grps.items())) cols1 = list(df1.columns) cols2 = list(df2.columns) opts = self.opts = {'left_on':{'type':'listbox','default':'', 'items':cols1, 'tooltip':'left column'}, 'right_on':{'type':'listbox','default':'', 'items':cols2, 'tooltip':'right column'}, #'left_index':{'type':'checkbutton','default':0,'label':'use left index'}, #'right_index':{'type':'checkbutton','default':0,'label':'use right index'}, 'suffix1':{'type':'entry','default':'_1','label':'left suffix'}, 'suffix2':{'type':'entry','default':'_2','label':'right suffix'}, 'how':{'type':'combobox','default':'inner', 'items':how, 'tooltip':'how to merge'}, 'join':{'type':'combobox','default':'inner', 'items':['inner','outer'], 'tooltip':'how to join'}, 'ignore_index':{'type':'checkbutton','default':0,'label':'ignore index', 'tooltip':'do not use the index values on the concatenation axis'}, 'verify_integrity':{'type':'checkbutton','default':0,'label':'check duplicates'}, } optsframe, self.tkvars, w = dialogFromOptions(wf, opts, grps, sticky='new') optsframe.pack(side=TOP,fill=BOTH) bf = Frame(wf) bf.pack(side=TOP,fill=BOTH) b = Button(bf, text="Apply", command=self.apply) b.pack(side=LEFT,fill=X,expand=1,pady=1) b = Button(bf, text="Close", command=self.quit) b.pack(side=LEFT,fill=X,expand=1,pady=1) b = Button(bf, text="Help", command=self.help) b.pack(side=LEFT,fill=X,expand=1,pady=1) self.main.wait_window() return
[docs] def apply(self): """Apply operation""" kwds = {} method = self.opvar.get() for i in self.opts: if i not in self.grps[method]: continue if self.opts[i]['type'] == 'listbox': val = self.tkvars[i].getSelectedItem() else: val = self.tkvars[i].get() if val == '': val=None kwds[i] = val #print (kwds) if method == 'merge': s=(kwds['suffix1'],kwds['suffix2']) del kwds['suffix1'] del kwds['suffix2'] m = pd.merge(self.df1,self.df2,on=None,suffixes=s, **kwds) elif method == 'concat': m = pd.concat([self.df1,self.df2], **kwds) print (m) #if successful ask user to replace table and close if len(m) > 0: self.getResult(m) else: f = self.tbf = Frame(self.main) f.pack(side=LEFT,fill=BOTH) st = ScrolledText(f, bg='white', fg='black') st.pack(in_=f, fill=BOTH, expand=1) msg = 'result is empty, check your columns. column types might differ' st.insert(END, msg) return
[docs] def getResult(self, df): """Show result of merge and let user choose to replace current table""" self.result = df from . import core if hasattr(self, 'tbf'): self.tbf.destroy() f = self.tbf = Frame(self.main) f.pack(side=LEFT,fill=BOTH) newtable = core.Table(f, dataframe=df, showstatusbar=1) newtable.adjustColumnWidths() newtable.show() bf = Frame(f) bf.grid(row=4,column=0,columnspan=2,sticky='news',padx=2,pady=2) b = Button(bf, text="Copy Table", command=lambda : self.result.to_clipboard(sep=',')) b.pack(side=RIGHT) b = Button(bf, text="Replace Current Table", command=self.replaceTable) b.pack(side=RIGHT) return
[docs] def replaceTable(self): """replace parent table""" n = messagebox.askyesno("Replace with merged", "Are you sure?", parent=self.main) if not n: return df = self.result model = TableModel(dataframe=df) self.parent.updateModel(model) self.parent.redraw() return
[docs] def help(self): link='http://pandas.pydata.org/pandas-docs/stable/merging.html' webbrowser.open(link,autoraise=1) return
[docs] def quit(self): self.main.destroy() return
[docs]class BaseDialog(Frame): """Generic dialog - inherit from this and customise the createWidgets and apply methods.""" def __init__(self, parent=None, df=None, title=''): self.parent = parent self.main = Toplevel() self.master = self.main x,y,w,h = getParentGeometry(self.parent) self.main.geometry('+%d+%d' %(x+w/2-200,y+h/2-200)) self.main.title(title) self.main.protocol("WM_DELETE_WINDOW", self.quit) self.main.grab_set() self.main.transient(parent) self.main.resizable(width=False, height=False) self.df = df self.result = None return
[docs] def createWidgets(self, m): """Override this""" cols = list(self.df.columns) f = LabelFrame(m, text='frame') f.pack(side=LEFT,fill=BOTH,padx=2) self.buttonsFrame() return
[docs] def buttonsFrame(self): bf = Frame(self.main) bf.pack(side=TOP,fill=BOTH) b = Button(bf, text="Apply", command=self.apply) b.pack(side=LEFT,fill=X,expand=1,pady=2) b = Button(bf, text="Close", command=self.quit) b.pack(side=LEFT,fill=X,expand=1,pady=2) b = Button(bf, text="Help", command=self.help) b.pack(side=LEFT,fill=X,expand=1,pady=2) return
[docs] def apply(self): """Code to run when Apply is pressed""" return
[docs] def help(self): """Help button code""" return
[docs] def quit(self): self.main.destroy() return
[docs]class CrosstabDialog(BaseDialog): def __init__(self, parent=None, df=None, title=''): BaseDialog.__init__(self, parent, df, title) m = Frame(self.main) m.pack(side=TOP) self.createWidgets(m) return
[docs] def createWidgets(self, m): """Create a set of grp-agg-func options together""" cols = list(self.df.columns) funcs = ['mean','median','sum','size','count','std','first','last', 'min','max','var'] f = LabelFrame(m, text='crosstab') f.pack(side=LEFT,fill=BOTH,padx=2) w,self.colsvar = addListBox(f, values=cols,width=14, label='columns') w.pack(side=LEFT,padx=2) self.vars = OrderedDict() w,self.rowsvar = addListBox(f, values=cols,width=14,label='rows') w.pack(side=LEFT,padx=2) valcols = list(self.df.select_dtypes(include=[np.float64,np.int32,np.int64])) w,self.valsvar = addListBox(f, values=valcols,width=14,label='values') w.pack(side=LEFT,padx=2) w,self.funcvar = addListBox(f, values=funcs,width=14,label='functions') w.pack(side=LEFT,padx=2) self.marginsvar = BooleanVar() w = Checkbutton(self.main,text='add row/column subtotals', variable=self.marginsvar) w.pack(padx=2,pady=2) self.buttonsFrame() return
[docs] def apply(self): """Apply crosstab""" cols = self.colsvar.getSelectedItem() rows = self.rowsvar.getSelectedItem() vals = self.valsvar.getSelectedItem() funcs = self.funcvar.getSelectedItem() margins = self.marginsvar.get() df = self.df rowdata = [df[r] for r in rows] coldata = [df[c] for c in cols] if vals == '': vals=None else: vals=df[vals] if funcs == '': funcs=None elif len(funcs) != len(cols): funcs = [funcs[0] for i in cols] self.result = pd.crosstab(rowdata, coldata, values=vals, aggfunc=funcs, margins=margins) self.parent.createChildTable(self.result, '', index=True) return
[docs] def help(self): link='https://pandas.pydata.org/pandas-docs/stable/generated/pandas.crosstab.html' webbrowser.open(link,autoraise=1) return
[docs]class AggregateDialog(BaseDialog): """Provides a frame for split-apply-combine operations""" def __init__(self, parent=None, df=None): BaseDialog.__init__(self, parent, df) m = Frame(self.main) m.pack(side=TOP) self.createWidgets(m) f = Frame(self.main) f.pack(side=TOP) self.mapcolfuncs = BooleanVar() w = Checkbutton(f,text='map columns to functions', variable=self.mapcolfuncs) w.pack(padx=2,pady=2) ToolTip.createToolTip(w, 'do 1-1 mapping of cols to functions') self.keepcols = BooleanVar() self.keepcols.set(False) w = Checkbutton(f,text='set grouping column as index', variable=self.keepcols) w.pack(padx=2,pady=2) #self.main.wait_window() return
[docs] def createWidgets(self, m): """Create a set of grp-agg-func options together""" cols = list(self.df.columns) funcs = ['mean','median','sum','size','count','std','first','last', 'min','max','var'] f = LabelFrame(m, text='group by') f.pack(side=LEFT,fill=BOTH,padx=2) w,self.grpvar = addListBox(f, values=cols,width=14, label='columns') w.pack() self.vars = OrderedDict() f = LabelFrame(m, text='aggregate on') f.pack(side=LEFT,fill=BOTH,padx=2) w,self.aggvar = addListBox(f, values=cols,width=14,label='columns') w.pack(side=LEFT,padx=2) w,self.funcvar = addListBox(f, values=funcs,width=14,label='functions') w.pack(side=LEFT,padx=2) self.buttonsFrame() return
[docs] def apply(self): """Apply operation""" funcs = self.funcvar.getSelectedItem() grpcols = self.grpvar.getSelectedItem() agg = self.aggvar.getSelectedItem() mapfuncs = self.mapcolfuncs.get() keepcols = self.keepcols.get() #print (grpcols, agg, funcs) if mapfuncs == True: aggdict = (dict(zip(agg,funcs))) else: aggdict = {} if len(funcs)==1: funcs=funcs[0] for a in agg: aggdict[a] = funcs #print (aggdict) self.result = self.df.groupby(grpcols,as_index=keepcols).agg(aggdict) self.parent.createChildTable(self.result, 'aggregated', index=True) #self.quit() return
[docs] def copyResult(self, ): if self.result is not None: self.parent.createChildTable(self.result, 'aggregated', index=True)
[docs] def help(self): link='http://pandas.pydata.org/pandas-docs/stable/groupby.html' webbrowser.open(link,autoraise=1) return
[docs] def quit(self): self.main.destroy() return
[docs]def addListBox(parent, values=[], width=10, height=6, label=''): """Add an EasyListBox""" frame=Frame(parent) Label(frame, text=label).grid(row=0) yScroll = Scrollbar(frame, orient = VERTICAL) yScroll.grid(row = 1, column = 1, sticky = N+S) listItemSelected = lambda index: index lbx = EasyListbox(frame, width, height, yScroll.set, listItemSelected) lbx.grid(row = 1, column = 0, sticky = N+S+E+W) frame.columnconfigure(0, weight = 1) frame.rowconfigure(0, weight = 1) yScroll["command"] = lbx.yview for i in values: lbx.insert(END, i) return frame, lbx
[docs]def getListBoxSelection(w): items = w.curselection() return [w.get(j) for j in items]
[docs]class AutoScrollbar(Scrollbar): """A scrollbar that hides itself if it's not needed. only \ works if you use the grid geometry manager."""
[docs] def set(self, lo, hi): if float(lo) <= 0.0 and float(hi) >= 1.0: self.tk.call("grid", "remove", self) else: self.grid() Scrollbar.set(self, lo, hi)
[docs] def pack(self, **kw): """pack""" raise TclError("cannot use pack with this widget") return
[docs] def place(self, **kw): """place""" raise TclError("cannot use place with this widget") return
[docs]class VerticalScrolledFrame(Frame): """A pure Tkinter scrollable frame \ see http://tkinter.unpythonic.net/wiki/VerticalScrolledFrame. \ Use the 'interior' attribute to place widgets inside the scrollable frame. """ def __init__(self, parent, height=None, width=None, *args, **kw): Frame.__init__(self, parent, *args, **kw) # create a canvas object and a vertical scrollbar for scrolling it #vscrollbar = AutoScrollbar(self, orient=VERTICAL) vscrollbar = Scrollbar(self, orient=VERTICAL) vscrollbar.pack(fill=Y, side=RIGHT, expand=FALSE) #vscrollbar.grid(row=0,column=2,rowspan=1,sticky='news',pady=0) canvas = Canvas(self, bd=0, highlightthickness=0, height=height, width=width, yscrollcommand=vscrollbar.set) canvas.pack(side=LEFT, fill=BOTH, expand=TRUE) #canvas.grid(row=0,column=0,rowspan=1,sticky='ns',pady=0) vscrollbar.config(command=canvas.yview) # reset the view canvas.xview_moveto(0) canvas.yview_moveto(0) # create a frame inside the canvas which will be scrolled with it self.interior = interior = Frame(canvas) interior_id = canvas.create_window(0, 0, window=interior, anchor=NW) # track changes to the canvas and frame width and sync them, # also updating the scrollbar def _configure_interior(event): # update the scrollbars to match the size of the inner frame size = (interior.winfo_reqwidth(), interior.winfo_reqheight()) canvas.config(scrollregion="0 0 %s %s" % size) if interior.winfo_reqwidth() != canvas.winfo_width(): # update the canvas's width to fit the inner frame canvas.config(width=interior.winfo_reqwidth()) interior.bind('<Configure>', _configure_interior) def _configure_canvas(event): if interior.winfo_reqwidth() != canvas.winfo_width(): # update the inner frame's width to fill the canvas canvas.itemconfigure(interior_id, width=canvas.winfo_width()) canvas.bind('<Configure>', _configure_canvas) return
[docs]class EasyListbox(Listbox): """Customised list box to replace useless default one""" def __init__(self, parent, width, height, yscrollcommand, listItemSelected): self._listItemSelected = listItemSelected Listbox.__init__(self, parent, width = width, height = height, yscrollcommand = yscrollcommand, selectmode = MULTIPLE, exportselection=0) self.bind("<<ListboxSelect>>", self.triggerListItemSelected) self.configure(background='white', foreground='black', selectbackground='#0174DF', selectforeground='white') return
[docs] def triggerListItemSelected(self, event): """Strategy method to respond to an item selection in the list box. \ Runs the client's listItemSelected method with the selected index. """ if self.size() == 0: return widget = event.widget indexes = widget.curselection()
#self._listItemSelected(index)
[docs] def getSelectedIndex(self): """Returns the index of the selected item or -1 if no item is selected.""" tup = self.curselection() if len(tup) == 0: return -1 else: return tup
[docs] def getSelectedItem(self): """Returns the selected item or the empty string if no item is selected.""" index = self.getSelectedIndex() if index == -1: return "" else: return [self.get(i) for i in index]
[docs] def setSelectedIndex(self, index): """Selects the item at the index if it's in the range.""" if index < 0 or index >= self.size(): return self.selection_set(index, index)
[docs] def clear(self): """Deletes all items from the list box.""" while self.size() > 0: self.delete(0)
[docs] def getIndex(self, item): """Returns the index of item if it's in the list box.""" tup = self.get(0, self.size() - 1) if item in tup: return tup.index(item) else: return -1
[docs]class SimpleEditor(Frame): """Simple text editor""" def __init__(self, parent=None, width=100, height=40, font=None): Frame.__init__(self, parent) st = self.text = ScrolledText(self, width=width, height=height, bg='white', fg='black') st.pack(in_=self, fill=BOTH, expand=1) if font == None: if 'Windows' in platform.system(): font = ('Courier New',10) else: font = 'monospace 10' st.config(font=font) btnform = Frame(self) btnform.pack(fill=BOTH) Button(btnform, text='Save', command=self.onSave).pack(side=LEFT) #Button(frm, text='Cut', command=self.onCut).pack(side=LEFT) #Button(frm, text='Paste', command=self.onPaste).pack(side=LEFT) Button(btnform, text='Find', command=self.onFind).pack(side=LEFT) Button(btnform, text='Clear', command=self.onClear).pack(side=LEFT) self.target='' return
[docs] def onSave(self): """Save text""" filename = filedialog.asksaveasfilename(defaultextension='.txt', initialdir=os.path.expanduser('~'), filetypes=(('Text files', '*.txt'), ('All files', '*.*'))) if filename: with open(filename, 'w') as stream: stream.write(self.text.get('1.0',END)) return
[docs] def onClear(self): """Clear text""" self.text.delete('1.0',END) return
[docs] def onFind(self): self.target = simpledialog.askstring('SimpleEditor', 'Search String?', initialvalue=self.target) if self.target: where = self.text.search(self.target, INSERT, END, nocase=True) if where: pastit = '{}+{}c'.format(where, len(self.target)) self.text.tag_add(SEL, where, pastit) self.text.mark_set(INSERT, pastit) self.text.see(INSERT) self.text.focus()
[docs]class FindReplaceDialog(Frame): """Find/replace dialog.""" def __init__(self, table): parent = table.parentframe Frame.__init__(self, parent) self.parent = parent self.table = table self.coords = [] self.current = 0 #coords of found cells self.setup() return
[docs] def setup(self): sf = self sfont = "Helvetica 10 bold" Label(sf, text='Enter Search String:', font=sfont).pack(side=TOP,fill=X) self.searchvar = StringVar() e = Entry(sf, textvariable=self.searchvar) self.searchvar.trace("w", self.updated) e.bind('<Return>', self.find) e.pack(fill=BOTH,side=TOP,expand=1,padx=2,pady=2) Label(sf, text='Replace With:', font=sfont).pack(side=TOP,fill=X) self.replacevar = StringVar() e = Entry(sf, textvariable=self.replacevar) e.pack(fill=BOTH,side=TOP,expand=1,padx=2,pady=2) f = Frame(sf) f.pack(side=TOP, fill=BOTH, padx=2, pady=2) addButton(f, 'Find Next', self.findNext, None, None, side=LEFT) addButton(f, 'Find All', self.findAll, None, None, side=LEFT) addButton(f, 'Replace All', self.replace, None, None, side=LEFT) addButton(f, 'Clear', self.clear, None, None, side=LEFT) addButton(f, 'Close', self.close, None, None, side=LEFT) f = Frame(sf) f.pack(side=TOP, fill=BOTH, padx=2, pady=2) self.casevar = BooleanVar() cb=Checkbutton(f, text= 'case sensitive', variable=self.casevar, command=self.updated) cb.pack(side=LEFT) return
[docs] def updated(self, name='', index='', mode=''): """Widgets changed so run search again""" self.search_changed=True return
[docs] def find(self): """Do string search. Creates a masked dataframe for results and then stores each cell coordinate in a list.""" table = self.table df = table.model.df df = df.astype('object').astype('str') s = self.searchvar.get() case = self.casevar.get() self.search_changed = False self.clear() if s == '': return found = pd.DataFrame() for col in df: found[col] = df[col].str.contains(s, na=False, case=case) #set the masked dataframe so that highlighted cells are shown on redraw table.highlighted = found i=0 self.coords = [] for r,row in found.iterrows(): j=0 for col,val in row.iteritems(): if val is True: #print (r,col,val, i, j) self.coords.append((i,j)) j+=1 i+=1 self.current = 0 return
[docs] def findAll(self): """Highlight all found cells""" table = self.table table.delete('temprect') df = table.model.df self.find() table.redraw() return
[docs] def findNext(self): """Show next cell of search results""" table = self.table s = self.searchvar.get() if len(self.coords)==0 or self.search_changed == True: self.find() if len(self.coords)==0: return idx = self.current i,j = self.coords[idx] table.movetoSelection(row=i,col=j,offset=3) table.redraw() table.drawSelectedRect(i, j, color='red') self.current+=1 if self.current>=len(self.coords): self.current=0 return
[docs] def replace(self): """Replace all instances of search text""" table = self.table table.storeCurrent() df = table.model.df s=self.searchvar.get() r=self.replacevar.get() case = self.casevar.get() #import re #r = re.compile(re.escape(r), re.IGNORECASE) table.model.df = df.replace(s,r,regex=True) table.redraw() self.search_changed = True return
[docs] def clear(self): self.coords = [] self.table.delete('temprect') self.table.highlighted = None self.table.redraw() return
[docs] def close(self): self.clear() self.table.searchframe=None self.destroy() return
[docs]class QueryDialog(Frame): """Use string query to filter. Will not work with spaces in column names, so these would need to be converted first.""" def __init__(self, table): parent = table.parentframe Frame.__init__(self, parent) self.parent = parent self.table = table self.setup() self.filters = [] return
[docs] def setup(self): qf = self sfont = "Helvetica 10 bold" Label(qf, text='Enter String Query:', font=sfont).pack(side=TOP,fill=X) self.queryvar = StringVar() e = Entry(qf, textvariable=self.queryvar, font="Courier 12 bold") e.bind('<Return>', self.query) e.pack(fill=BOTH,side=TOP,expand=1,padx=2,pady=2) self.fbar = Frame(qf) self.fbar.pack(side=TOP,fill=BOTH,expand=1,padx=2,pady=2) f = Frame(qf) f.pack(side=TOP, fill=BOTH, padx=2, pady=2) addButton(f, 'find', self.query, images.filtering(), 'apply filters', side=LEFT) addButton(f, 'add manual filter', self.addFilter, images.add(), 'add manual filter', side=LEFT) addButton(f, 'close', self.close, images.cross(), 'close', side=LEFT) self.applyqueryvar = BooleanVar() self.applyqueryvar.set(True) c = Checkbutton(f, text='show filtered only', variable=self.applyqueryvar, command=self.query) c.pack(side=LEFT,padx=2) addButton(f, 'color rows', self.colorResult, images.color_swatch(), 'color filtered rows', side=LEFT) self.queryresultvar = StringVar() l = Label(f,textvariable=self.queryresultvar, font=sfont) l.pack(side=RIGHT) return
[docs] def close(self): self.destroy() self.table.qframe = None self.table.showAll()
[docs] def query(self, evt=None): """Do query""" table = self.table s = self.queryvar.get() if table.filtered == True: table.model.df = table.dataframe df = table.model.df mask = None #string query first if s!='': try: mask = df.eval(s) except: mask = df.eval(s, engine='python') #add any filters from widgets if len(self.filters)>0: mask = self.applyFilter(df, mask) if mask is None: table.showAll() self.queryresultvar.set('') return #apply the final mask self.filtdf = filtdf = df[mask] self.queryresultvar.set('%s rows found' %len(filtdf)) if self.applyqueryvar.get() == 1: #replace current dataframe but keep a copy! table.dataframe = table.model.df.copy() table.delete('rowrect') table.multiplerowlist = [] table.model.df = filtdf table.filtered = True else: idx = filtdf.index rows = table.multiplerowlist = table.getRowsFromIndex(idx) if len(rows)>0: table.currentrow = rows[0] table.redraw() return
[docs] def addFilter(self): """Add a filter using widgets""" df = self.table.model.df fb = FilterBar(self, self.fbar, list(df.columns)) fb.pack(side=TOP, fill=BOTH, expand=1, padx=2, pady=2) self.filters.append(fb) return
[docs] def applyFilter(self, df, mask=None): """Apply the widget based filters, returns a boolean mask""" if mask is None: mask = df.index==df.index for f in self.filters: col, val, op, b = f.getFilter() try: val = float(val) except: pass #print (col, val, op, b) if op == 'contains': m = df[col].str.contains(str(val)) elif op == 'equals': m = df[col]==val elif op == 'not equals': m = df[col]!=val elif op == '>': m = df[col]>val elif op == '<': m = df[col]<val elif op == 'is empty': m = df[col].isnull() elif op == 'not empty': m = ~df[col].isnull() elif op == 'excludes': m = -df[col].str.contains(val) elif op == 'starts with': m = df[col].str.startswith(val) elif op == 'has length': m = df[col].str.len()>val elif op == 'is number': m = df[col].astype('object').str.isnumeric() elif op == 'is lowercase': m = df[col].astype('object').str.islower() elif op == 'is uppercase': m = df[col].astype('object').str.isupper() else: continue if b == 'AND': mask = mask & m elif b == 'OR': mask = mask | m elif b == 'NOT': mask = mask ^ m return mask
[docs] def colorResult(self): """Color filtered rows in main table""" table=self.table if not hasattr(self.table,'dataframe') or not hasattr(self, 'filtdf'): return clr = self.table.getaColor('#dcf1fc') if clr is None: return df = table.model.df = table.dataframe idx = self.filtdf.index rows = table.multiplerowlist = table.getRowsFromIndex(idx) table.setRowColors(rows, clr, cols='all') return
[docs] def update(self): df = self.table.model.df cols = list(df.columns) for f in self.filters: f.update(cols) return
[docs]class FilterBar(Frame): """Class providing filter widgets""" operators = ['contains','excludes','equals','not equals','>','<','is empty','not empty', 'starts with','ends with','has length','is number','is lowercase','is uppercase'] booleanops = ['AND','OR','NOT'] def __init__(self, parent, parentframe, cols): Frame.__init__(self, parentframe) self.parent = parent self.filtercol = StringVar() initial = cols[0] self.filtercolmenu = Combobox(self, textvariable = self.filtercol, values = cols, #initialitem = initial, width = 14) self.filtercolmenu.grid(row=0,column=1,sticky='news',padx=2,pady=2) self.operator = StringVar() #self.operator.set('equals') operatormenu = Combobox(self, textvariable = self.operator, values = self.operators, width = 10) operatormenu.grid(row=0,column=2,sticky='news',padx=2,pady=2) self.filtercolvalue=StringVar() valsbox = Entry(self,textvariable=self.filtercolvalue,width=26) valsbox.grid(row=0,column=3,sticky='news',padx=2,pady=2) #valsbox.bind("<Return>", self.parent.callback) self.booleanop = StringVar() self.booleanop.set('AND') booleanopmenu = Combobox(self, textvariable = self.booleanop, values = self.booleanops, width = 6) booleanopmenu.grid(row=0,column=0,sticky='news',padx=2,pady=2) #disable the boolean operator if it's the first filter #if self.index == 0: # booleanopmenu.component('menubutton').configure(state=DISABLED) img = images.cross() cb = Button(self,text='-', image=img, command=self.close) cb.image = img cb.grid(row=0,column=5,sticky='news',padx=2,pady=2) return
[docs] def close(self): """Destroy and remove from parent""" self.parent.filters.remove(self) self.destroy() return
[docs] def getFilter(self): """Get filter values for this instance""" col = self.filtercol.get() val = self.filtercolvalue.get() op = self.operator.get() booleanop = self.booleanop.get() return col, val, op, booleanop
[docs] def update(self, cols): self.filtercolmenu['values'] = cols return
[docs]class BaseTable(Canvas): """Basic table class based on tk canvas. inherit from this to add your own functionality""" def __init__(self, parent=None, width=280, height=190, rows=2, cols=2, **kwargs): Canvas.__init__(self, parent, bg='white', width=width, height=height ) self.parent = parent self.rows = rows self.cols = cols self.selectedrows = [0] self.selectedcols = [0] self.top = .1 self.bottom =.9 self.height = height self.width = width self.doBindings() self.redraw() self.update_callback = None return
[docs] def update(self): if self.update_callback != None: self.update_callback()
[docs] def doBindings(self): self.bind("<Button-1>",self.handle_left_click) self.bind('<B1-Motion>', self.handle_mouse_drag) return
[docs] def redraw(self): self.drawGrid() self.drawMultipleCells(self.selectedrows, self.selectedcols) return
[docs] def drawGrid(self): self.delete('gridline') self.delete('currentrect') self.delete('multiplesel') rows = self.rows h = self.height w = self.width for row in range(rows+1): y = row*h/rows self.create_line(1,y,w,y, tag='gridline', fill='gray', width=2) for col in range(self.cols+1): x = col*w/self.cols self.create_line(x,1,x,rows*h, tag='gridline', fill='gray', width=2) return
[docs] def handle_left_click(self, event): """Respond to a single press""" #self.clearSelected() #which row and column is the click inside? row = self.get_row_clicked(event) col = self.get_col_clicked(event) self.focus_set() #print (row, col) self.delete('multiplesel') self.delete('currentrect') self.drawSelectedRect(row, col, tags='currentrect') self.startrow = row self.startcol = col self.selectedrows = [row] self.selectedcols = [col] self.update() return
[docs] def handle_mouse_drag(self, event): """Handle mouse moved with button held down, multiple selections""" rowover = self.get_row_clicked(event) colover = self.get_col_clicked(event) self.endrow = rowover self.endcol = colover rows = [rowover] cols = [colover] if colover == None or rowover == None: return #draw the selected rows if self.endrow != self.startrow: if self.endrow < self.startrow: rows = list(range(self.endrow, self.startrow+1)) else: rows = list(range(self.startrow, self.endrow+1)) if self.endcol != self.startcol: if self.endcol < self.startcol: cols = list(range(self.endcol, self.startcol+1)) else: cols = list(range(self.startcol, self.endcol+1)) self.drawMultipleCells(rows,cols) self.selectedrows = rows self.selectedcols = cols self.update() return
[docs] def get_row_clicked(self, event): """Get row where event on canvas occurs""" h = self.height/self.rows #get coord on canvas, not window, need this if scrolling y = int(self.canvasy(event.y)) row = int(int(y)/h) return row
[docs] def get_col_clicked(self, event): """Get column where event on the canvas occurs""" w = self.width/self.cols x = int(self.canvasx(event.x)) col =int(int(x)/w) return col
[docs] def getCellCoords(self, row, col): """Get x-y coordinates to drawing a cell in a given row/col""" h = self.height/self.rows x_start=0 y_start=0 #get nearest rect co-ords for that row/col w = self.width/self.cols x1 = w*col y1=y_start+h*row x2=x1+w y2=y1+h return x1,y1,x2,y2
[docs] def drawSelectedRect(self, row, col, color='#c2c2d6',pad=4, tags=''): """User has clicked to select area""" if col >= self.cols: return w=1 pad=pad x1,y1,x2,y2 = self.getCellCoords(row,col) #print (x1,y1,x2,y2) rect = self.create_rectangle(x1+pad+w,y1+pad+w,x2-pad-w,y2-pad-w, outline='gray50', fill=color, width=w, tag=tags) return
[docs] def drawMultipleCells(self, rows, cols): """Draw more than one row selection""" self.delete('multiplesel') self.delete('currentrect') for col in cols: for row in rows: #print (row, col) self.drawSelectedRect(row, col, tags='multiplesel') return