#!/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 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]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 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 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 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 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 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