diff --git a/pyfpdb/GuiTourneyGraphViewer.py b/pyfpdb/GuiTourneyGraphViewer.py new file mode 100644 index 00000000..e213e1f4 --- /dev/null +++ b/pyfpdb/GuiTourneyGraphViewer.py @@ -0,0 +1,319 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +#Copyright 2008-2010 Carl Gherardi +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, version 3 of the License. +# +#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 Affero General Public License +#along with this program. If not, see . +#In the "official" distribution you can find the license in agpl-3.0.txt. + +import threading +import pygtk +pygtk.require('2.0') +import gtk +import os +import sys +import traceback +from time import * +from datetime import datetime +#import pokereval + +import locale +lang=locale.getdefaultlocale()[0][0:2] +if lang=="en": + def _(string): return string +else: + import gettext + try: + trans = gettext.translation("fpdb", localedir="locale", languages=[lang]) + trans.install() + except IOError: + def _(string): return string + +import fpdb_import +import Database +import Filters +import Charset + +try: + import matplotlib + matplotlib.use('GTKCairo') + from matplotlib.figure import Figure + from matplotlib.backends.backend_gtk import FigureCanvasGTK as FigureCanvas + from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar + from matplotlib.font_manager import FontProperties + from numpy import arange, cumsum + from pylab import * +except ImportError, inst: + print _("""Failed to load libs for graphing, graphing will not function. Please + install numpy and matplotlib if you want to use graphs.""") + print _("""This is of no consequence for other parts of the program, e.g. import + and HUD are NOT affected by this problem.""") + print "ImportError: %s" % inst.args + +class GuiTourneyGraphViewer (threading.Thread): + + def __init__(self, querylist, config, parent, debug=True): + """Constructor for GraphViewer""" + self.sql = querylist + self.conf = config + self.debug = debug + self.parent = parent + #print "start of GraphViewer constructor" + self.db = Database.Database(self.conf, sql=self.sql) + + + filters_display = { "Heroes" : True, + "Sites" : True, + "Games" : False, + "Limits" : False, + "LimitSep" : False, + "LimitType" : False, + "Type" : False, + "UseType" : 'tour', + "Seats" : False, + "SeatSep" : False, + "Dates" : True, + "Groups" : False, + "Button1" : True, + "Button2" : True + } + + self.filters = Filters.Filters(self.db, self.conf, self.sql, display = filters_display) + self.filters.registerButton1Name("Refresh _Graph") + self.filters.registerButton1Callback(self.generateGraph) + self.filters.registerButton2Name("_Export to File") + self.filters.registerButton2Callback(self.exportGraph) + + self.mainHBox = gtk.HBox(False, 0) + self.mainHBox.show() + + self.leftPanelBox = self.filters.get_vbox() + + self.hpane = gtk.HPaned() + self.hpane.pack1(self.leftPanelBox) + self.mainHBox.add(self.hpane) + # hierarchy: self.mainHBox / self.hpane / self.graphBox / self.canvas / self.fig / self.ax + + self.graphBox = gtk.VBox(False, 0) + self.graphBox.show() + self.hpane.pack2(self.graphBox) + self.hpane.show() + + self.fig = None + #self.exportButton.set_sensitive(False) + self.canvas = None + + + self.db.rollback() + + def get_vbox(self): + """returns the vbox of this thread""" + return self.mainHBox + #end def get_vbox + + def clearGraphData(self): + + try: + try: + if self.canvas: + self.graphBox.remove(self.canvas) + except: + pass + + if self.fig != None: + self.fig.clear() + self.fig = Figure(figsize=(5,4), dpi=100) + if self.canvas is not None: + self.canvas.destroy() + + self.canvas = FigureCanvas(self.fig) # a gtk.DrawingArea + except: + err = traceback.extract_tb(sys.exc_info()[2])[-1] + print _("***Error: ")+err[2]+"("+str(err[1])+"): "+str(sys.exc_info()[1]) + raise + + def generateGraph(self, widget, data): + try: + self.clearGraphData() + + sitenos = [] + playerids = [] + + sites = self.filters.getSites() + heroes = self.filters.getHeroes() + siteids = self.filters.getSiteIds() + + # Which sites are selected? + for site in sites: + if sites[site] == True: + sitenos.append(siteids[site]) + _hname = Charset.to_utf8(heroes[site]) + result = self.db.get_player_id(self.conf, site, _hname) + if result is not None: + playerids.append(int(result)) + + if not sitenos: + #Should probably pop up here. + print _("No sites selected - defaulting to PokerStars") + self.db.rollback() + return + + if not playerids: + print _("No player ids found") + self.db.rollback() + return + + #Set graph properties + self.ax = self.fig.add_subplot(111) + + #Get graph data from DB + starttime = time() + green = self.getData(playerids, sitenos) + print _("Graph generated in: %s") %(time() - starttime) + + + #Set axis labels and grid overlay properites + self.ax.set_xlabel(_("Tournaments"), fontsize = 12) + self.ax.set_ylabel("$", fontsize = 12) + self.ax.grid(color='g', linestyle=':', linewidth=0.2) + if green == None or green == []: + self.ax.set_title(_("No Data for Player(s) Found")) + green = ([ 0., 0., 0., 0., 500., 1000., 900., 800., + 700., 600., 500., 400., 300., 200., 100., 0., + 500., 1000., 1000., 1000., 1000., 1000., 1000., 1000., + 1000., 1000., 1000., 1000., 1000., 1000., 875., 750., + 625., 500., 375., 250., 125., 0., 0., 0., + 0., 500., 1000., 900., 800., 700., 600., 500., + 400., 300., 200., 100., 0., 500., 1000., 1000.]) + red = ([ 0., 0., 0., 0., 500., 1000., 900., 800., + 700., 600., 500., 400., 300., 200., 100., 0., + 0., 0., 0., 0., 0., 0., 125., 250., + 375., 500., 500., 500., 500., 500., 500., 500., + 500., 500., 375., 250., 125., 0., 0., 0., + 0., 500., 1000., 900., 800., 700., 600., 500., + 400., 300., 200., 100., 0., 500., 1000., 1000.]) + blue = ([ 0., 0., 0., 0., 500., 1000., 900., 800., + 700., 600., 500., 400., 300., 200., 100., 0., + 0., 0., 0., 0., 0., 0., 125., 250., + 375., 500., 625., 750., 875., 1000., 875., 750., + 625., 500., 375., 250., 125., 0., 0., 0., + 0., 500., 1000., 900., 800., 700., 600., 500., + 400., 300., 200., 100., 0., 500., 1000., 1000.]) + + self.ax.plot(green, color='green', label=_('Tournaments: %d\nProfit: $%.2f') %(len(green), green[-1])) + #self.ax.plot(blue, color='blue', label=_('Showdown: $%.2f') %(blue[-1])) + #self.ax.plot(red, color='red', label=_('Non-showdown: $%.2f') %(red[-1])) + self.graphBox.add(self.canvas) + self.canvas.show() + self.canvas.draw() + + #TODO: Do something useful like alert user + #print "No hands returned by graph query" + else: + self.ax.set_title(_("Tournament Results")) + + #Draw plot + self.ax.plot(green, color='green', label=_('Tournaments: %d\nProfit: $%.2f') %(len(green), green[-1])) + #self.ax.plot(blue, color='blue', label=_('Showdown: $%.2f') %(blue[-1])) + #self.ax.plot(red, color='red', label=_('Non-showdown: $%.2f') %(red[-1])) + if sys.version[0:3] == '2.5': + self.ax.legend(loc='upper left', shadow=True, prop=FontProperties(size='smaller')) + else: + self.ax.legend(loc='upper left', fancybox=True, shadow=True, prop=FontProperties(size='smaller')) + + self.graphBox.add(self.canvas) + self.canvas.show() + self.canvas.draw() + #self.exportButton.set_sensitive(True) + except: + err = traceback.extract_tb(sys.exc_info()[2])[-1] + print _("***Error: ")+err[2]+"("+str(err[1])+"): "+str(sys.exc_info()[1]) + + #end of def showClicked + + def getData(self, names, sites): + tmp = self.sql.query['tourneyResults'] + print "DEBUG: getData" + start_date, end_date = self.filters.getDates() + + #Buggered if I can find a way to do this 'nicely' take a list of integers and longs + # and turn it into a tuple readale by sql. + # [5L] into (5) not (5,) and [5L, 2829L] into (5, 2829) + nametest = str(tuple(names)) + sitetest = str(tuple(sites)) + + #Must be a nicer way to deal with tuples of size 1 ie. (2,) - which makes sql barf + tmp = tmp.replace("", nametest) + tmp = tmp.replace("", sitetest) + tmp = tmp.replace("", start_date) + tmp = tmp.replace("", end_date) + tmp = tmp.replace(",)", ")") + + print "DEBUG: sql query:" + print tmp + self.db.cursor.execute(tmp) + #returns (HandId,Winnings,Costs,Profit) + winnings = self.db.cursor.fetchall() + self.db.rollback() + + if len(winnings) == 0: + return None + + green = map(lambda x:float(x[0]), winnings) + #blue = map(lambda x: float(x[1]) if x[2] == True else 0.0, winnings) + #red = map(lambda x: float(x[1]) if x[2] == False else 0.0, winnings) + greenline = cumsum(green) + #blueline = cumsum(blue) + #redline = cumsum(red) + return (greenline/100) + + def exportGraph (self, widget, data): + if self.fig is None: + return # Might want to disable export button until something has been generated. + + dia_chooser = gtk.FileChooserDialog(title=_("Please choose the directory you wish to export to:"), + action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER, + buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OK,gtk.RESPONSE_OK)) + dia_chooser.set_destroy_with_parent(True) + dia_chooser.set_transient_for(self.parent) + try: + dia_chooser.set_filename(self.exportFile) # use previously chosen export path as default + except: + pass + + response = dia_chooser.run() + + if response <> gtk.RESPONSE_OK: + print _('Closed, no graph exported') + dia_chooser.destroy() + return + + # generate a unique filename for export + now = datetime.now() + now_formatted = now.strftime("%Y%m%d%H%M%S") + self.exportFile = dia_chooser.get_filename() + "/fpdb" + now_formatted + ".png" + dia_chooser.destroy() + + #print "DEBUG: self.exportFile = %s" %(self.exportFile) + self.fig.savefig(self.exportFile, format="png") + + #display info box to confirm graph created + diainfo = gtk.MessageDialog(parent=self.parent, + flags=gtk.DIALOG_DESTROY_WITH_PARENT, + type=gtk.MESSAGE_INFO, + buttons=gtk.BUTTONS_OK, + message_format=_("Graph created")) + diainfo.format_secondary_text(self.exportFile) + diainfo.run() + diainfo.destroy() + + #end of def exportGraph diff --git a/pyfpdb/SQL.py b/pyfpdb/SQL.py index abc812a8..87857908 100644 --- a/pyfpdb/SQL.py +++ b/pyfpdb/SQL.py @@ -3027,6 +3027,9 @@ class Sql: #elif db_server == 'sqlite': # self.query['playerStatsByPosition'] = """ """ + #################################### + # Cash Game Graph query + #################################### self.query['getRingProfitAllHandsPlayerIdSite'] = """ SELECT hp.handId, hp.totalProfit, hp.sawShowdown FROM HandsPlayers hp @@ -3043,6 +3046,28 @@ class Sql: GROUP BY h.startTime, hp.handId, hp.sawShowdown, hp.totalProfit ORDER BY h.startTime""" + #################################### + # Tourney Results query + #################################### + self.query['tourneyResults'] = """ + SELECT (tp.winnings - tt.buyIn - tt.fee) as profit, tp.koCount, tp.rebuyCount, tp.addOnCount, tt.buyIn, tt.fee + FROM TourneysPlayers tp + INNER JOIN Players pl ON (pl.id = tp.playerId) + INNER JOIN Tourneys t ON (t.id = tp.tourneyId) + INNER JOIN TourneyTypes tt ON (tt.id = t.tourneyTypeId) + WHERE pl.id in + AND pl.siteId in + AND t.startTime > '' + AND t.startTime < '' + GROUP BY t.startTime, tp.tourneyId, tp.winningsCurrency, + tp.winnings, tp.koCount, + tp.rebuyCount, tp.addOnCount, + tt.buyIn, tt.fee + ORDER BY t.startTime""" + + #AND gt.type = 'ring' + # + # #################################### # Session stats query diff --git a/pyfpdb/fpdb.pyw b/pyfpdb/fpdb.pyw index 385d07ef..61af3c25 100755 --- a/pyfpdb/fpdb.pyw +++ b/pyfpdb/fpdb.pyw @@ -122,6 +122,7 @@ import GuiTourneyViewer import GuiPositionalStats import GuiAutoImport import GuiGraphViewer +import GuiTourneyGraphViewer import GuiSessionViewer import SQL import Database @@ -778,6 +779,7 @@ class fpdb: + @@ -817,6 +819,7 @@ class fpdb: ('autoimp', None, _('_Auto Import and HUD'), _('A'), 'Auto Import and HUD', self.tab_auto_import), ('hudConfigurator', None, _('_HUD Configurator'), _('H'), 'HUD Configurator', self.diaHudConfigurator), ('graphs', None, _('_Graphs'), _('G'), 'Graphs', self.tabGraphViewer), + ('tourneygraphs', None, _('Tourney Graphs'), None, 'TourneyGraphs', self.tabTourneyGraphViewer), ('ringplayerstats', None, _('Ring _Player Stats (tabulated view, not on pgsql)'), _('P'), 'Ring Player Stats (tabulated view)', self.tab_ring_player_stats), ('tourneyplayerstats', None, _('_Tourney Player Stats (tabulated view, not on pgsql)'), _('T'), 'Tourney Player Stats (tabulated view, mysql only)', self.tab_tourney_player_stats), ('tourneyviewer', None, _('Tourney _Viewer'), None, 'Tourney Viewer)', self.tab_tourney_viewer_stats), @@ -1066,6 +1069,13 @@ You can find the full license texts in agpl-3.0.txt, gpl-2.0.txt, gpl-3.0.txt an gv_tab = new_gv_thread.get_vbox() self.add_and_display_tab(gv_tab, _("Graphs")) + def tabTourneyGraphViewer(self, widget, data=None): + """opens a graph viewer tab""" + new_gv_thread = GuiTourneyGraphViewer.GuiTourneyGraphViewer(self.sql, self.config, self.window) + self.threads.append(new_gv_thread) + gv_tab = new_gv_thread.get_vbox() + self.add_and_display_tab(gv_tab, _("Tourney Graphs")) + def __init__(self): # no more than 1 process can this lock at a time: self.lock = interlocks.InterProcessLock(name="fpdb_global_lock")