Add 'Currencies' filter to the ring player stats viewer.
[fpdb-dooglus.git] / pyfpdb / GuiGraphViewer.py
blob36e5b93d03d69152b01c8b216abbb8f917dd67a9
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 #Copyright 2008-2011 Steffen Schaumburg
5 #This program is free software: you can redistribute it and/or modify
6 #it under the terms of the GNU Affero General Public License as published by
7 #the Free Software Foundation, version 3 of the License.
9 #This program is distributed in the hope that it will be useful,
10 #but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 #GNU General Public License for more details.
14 #You should have received a copy of the GNU Affero General Public License
15 #along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #In the "official" distribution you can find the license in agpl-3.0.txt.
18 import L10n
19 _ = L10n.get_translation()
21 import threading
22 import pygtk
23 pygtk.require('2.0')
24 import gtk
25 import os
26 import sys
27 import traceback
28 from time import *
29 from datetime import datetime
30 #import pokereval
32 import fpdb_import
33 import Database
34 import Filters
35 import Charset
37 try:
38 calluse = not 'matplotlib' in sys.modules
39 import matplotlib
40 if calluse:
41 matplotlib.use('GTKCairo')
42 from matplotlib.figure import Figure
43 from matplotlib.backends.backend_gtk import FigureCanvasGTK as FigureCanvas
44 from matplotlib.backends.backend_gtkagg import NavigationToolbar2GTKAgg as NavigationToolbar
45 from matplotlib.font_manager import FontProperties
46 from numpy import arange, cumsum
47 from pylab import *
48 except ImportError, inst:
49 print _("""Failed to load libs for graphing, graphing will not function. Please install numpy and matplotlib if you want to use graphs.""")
50 print _("""This is of no consequence for other parts of the program, e.g. import and HUD are NOT affected by this problem.""")
51 print "ImportError: %s" % inst.args
53 class GuiGraphViewer (threading.Thread):
55 def __init__(self, querylist, config, parent, debug=True):
56 """Constructor for GraphViewer"""
57 self.sql = querylist
58 self.conf = config
59 self.debug = debug
60 self.parent = parent
61 #print "start of GraphViewer constructor"
62 self.db = Database.Database(self.conf, sql=self.sql)
65 filters_display = { "Heroes" : True,
66 "Sites" : True,
67 "Games" : True,
68 "Currencies": True,
69 "Limits" : True,
70 "LimitSep" : True,
71 "LimitType" : True,
72 "Type" : False,
73 "UseType" : 'ring',
74 "Seats" : False,
75 "SeatSep" : False,
76 "Dates" : True,
77 "GraphOps" : True,
78 "Groups" : False,
79 "Button1" : True,
80 "Button2" : True
83 self.filters = Filters.Filters(self.db, self.conf, self.sql, display = filters_display)
84 self.filters.registerButton1Name(_("Refresh _Graph"))
85 self.filters.registerButton1Callback(self.generateGraph)
86 self.filters.registerButton2Name(_("_Export to File"))
87 self.filters.registerButton2Callback(self.exportGraph)
89 self.mainHBox = gtk.HBox(False, 0)
90 self.mainHBox.show()
92 self.leftPanelBox = self.filters.get_vbox()
94 self.hpane = gtk.HPaned()
95 self.hpane.pack1(self.leftPanelBox)
96 self.mainHBox.add(self.hpane)
97 # hierarchy: self.mainHBox / self.hpane / self.graphBox / self.canvas / self.fig / self.ax
99 self.graphBox = gtk.VBox(False, 0)
100 self.graphBox.show()
101 self.hpane.pack2(self.graphBox)
102 self.hpane.show()
104 self.fig = None
105 #self.exportButton.set_sensitive(False)
106 self.canvas = None
109 self.db.rollback()
111 def get_vbox(self):
112 """returns the vbox of this thread"""
113 return self.mainHBox
114 #end def get_vbox
116 def clearGraphData(self):
118 try:
119 try:
120 if self.canvas:
121 self.graphBox.remove(self.canvas)
122 except:
123 pass
125 if self.fig != None:
126 self.fig.clear()
127 self.fig = Figure(figsize=(5,4), dpi=100)
128 if self.canvas is not None:
129 self.canvas.destroy()
131 self.canvas = FigureCanvas(self.fig) # a gtk.DrawingArea
132 except:
133 err = traceback.extract_tb(sys.exc_info()[2])[-1]
134 print _("Error:")+" "+err[2]+"("+str(err[1])+"): "+str(sys.exc_info()[1])
135 raise
137 def generateGraph(self, widget, data):
138 try:
139 self.clearGraphData()
141 sitenos = []
142 playerids = []
144 sites = self.filters.getSites()
145 heroes = self.filters.getHeroes()
146 siteids = self.filters.getSiteIds()
147 limits = self.filters.getLimits()
148 games = self.filters.getGames()
149 currencies = self.filters.getCurrencies()
150 graphops = self.filters.getGraphOps()
151 names = ""
153 for i in ('show', 'none'):
154 if i in limits:
155 limits.remove(i)
156 # Which sites are selected?
157 for site in sites:
158 if sites[site] == True:
159 sitenos.append(siteids[site])
160 _hname = Charset.to_utf8(heroes[site])
161 result = self.db.get_player_id(self.conf, site, _hname)
162 if result is not None:
163 playerids.append(int(result))
164 names = names + "\n"+_hname + " on "+site
166 if not sitenos:
167 #Should probably pop up here.
168 print _("No sites selected - defaulting to PokerStars")
169 self.db.rollback()
170 return
172 if not playerids:
173 print _("No player ids found")
174 self.db.rollback()
175 return
177 if not limits:
178 print _("No limits found")
179 self.db.rollback()
180 return
182 #Set graph properties
183 self.ax = self.fig.add_subplot(111)
185 #Get graph data from DB
186 starttime = time()
187 (green, blue, red) = self.getRingProfitGraph(playerids, sitenos, limits, games, currencies, graphops['dspin'])
188 print _("Graph generated in: %s") %(time() - starttime)
192 #Set axis labels and grid overlay properites
193 self.ax.set_xlabel(_("Hands"), fontsize = 12)
194 # SET LABEL FOR X AXIS
195 self.ax.set_ylabel(graphops['dspin'], fontsize = 12)
196 self.ax.grid(color='g', linestyle=':', linewidth=0.2)
197 if green == None or green == []:
198 self.ax.set_title(_("No Data for Player(s) Found"))
199 green = ([ 0., 0., 0., 0., 500., 1000., 900., 800.,
200 700., 600., 500., 400., 300., 200., 100., 0.,
201 500., 1000., 1000., 1000., 1000., 1000., 1000., 1000.,
202 1000., 1000., 1000., 1000., 1000., 1000., 875., 750.,
203 625., 500., 375., 250., 125., 0., 0., 0.,
204 0., 500., 1000., 900., 800., 700., 600., 500.,
205 400., 300., 200., 100., 0., 500., 1000., 1000.])
206 red = ([ 0., 0., 0., 0., 500., 1000., 900., 800.,
207 700., 600., 500., 400., 300., 200., 100., 0.,
208 0., 0., 0., 0., 0., 0., 125., 250.,
209 375., 500., 500., 500., 500., 500., 500., 500.,
210 500., 500., 375., 250., 125., 0., 0., 0.,
211 0., 500., 1000., 900., 800., 700., 600., 500.,
212 400., 300., 200., 100., 0., 500., 1000., 1000.])
213 blue = ([ 0., 0., 0., 0., 500., 1000., 900., 800.,
214 700., 600., 500., 400., 300., 200., 100., 0.,
215 0., 0., 0., 0., 0., 0., 125., 250.,
216 375., 500., 625., 750., 875., 1000., 875., 750.,
217 625., 500., 375., 250., 125., 0., 0., 0.,
218 0., 500., 1000., 900., 800., 700., 600., 500.,
219 400., 300., 200., 100., 0., 500., 1000., 1000.])
221 self.ax.plot(green, color='green', label=_('Hands: %d\nProfit: (%s): %.2f') %(len(green), green[-1]))
222 self.ax.plot(blue, color='blue', label=_('Showdown') + ': $%.2f' %(blue[-1]))
223 self.ax.plot(red, color='red', label=_('Non-showdown') + ': $%.2f' %(red[-1]))
224 self.graphBox.add(self.canvas)
225 self.canvas.show()
226 self.canvas.draw()
228 #TODO: Do something useful like alert user
229 #print "No hands returned by graph query"
230 else:
231 self.ax.set_title((_("Profit graph for ring games")+names),fontsize=12)
233 #Draw plot
234 self.ax.plot(green, color='green', label=_('Hands: %d\nProfit: (%s): %.2f') %(len(green),graphops['dspin'], green[-1]))
235 if graphops['showdown'] == 'ON':
236 self.ax.plot(blue, color='blue', label=_('Showdown') + ' (%s): %.2f' %(graphops['dspin'], blue[-1]))
237 if graphops['nonshowdown'] == 'ON':
238 self.ax.plot(red, color='red', label=_('Non-showdown') + ' (%s): %.2f' %(graphops['dspin'], red[-1]))
240 if sys.version[0:3] == '2.5':
241 self.ax.legend(loc='upper left', shadow=True, prop=FontProperties(size='smaller'))
242 else:
243 self.ax.legend(loc='upper left', fancybox=True, shadow=True, prop=FontProperties(size='smaller'))
245 self.graphBox.add(self.canvas)
246 self.canvas.show()
247 self.canvas.draw()
248 #self.exportButton.set_sensitive(True)
249 except:
250 err = traceback.extract_tb(sys.exc_info()[2])[-1]
251 print _("Error:")+" "+err[2]+"("+str(err[1])+"): "+str(sys.exc_info()[1])
253 #end of def showClicked
256 def getRingProfitGraph(self, names, sites, limits, games, currencies, units):
257 # tmp = self.sql.query['getRingProfitAllHandsPlayerIdSite']
258 # print "DEBUG: getRingProfitGraph"
260 if units == '$':
261 tmp = self.sql.query['getRingProfitAllHandsPlayerIdSiteInDollars']
262 elif units == 'BB':
263 tmp = self.sql.query['getRingProfitAllHandsPlayerIdSiteInBB']
266 start_date, end_date = self.filters.getDates()
268 #Buggered if I can find a way to do this 'nicely' take a list of integers and longs
269 # and turn it into a tuple readale by sql.
270 # [5L] into (5) not (5,) and [5L, 2829L] into (5, 2829)
271 nametest = str(tuple(names))
272 sitetest = str(tuple(sites))
273 #nametest = nametest.replace("L", "")
275 q = []
276 for m in self.filters.display.items():
277 if m[0] == 'Games' and m[1]:
278 for n in games:
279 if games[n]:
280 q.append(n)
281 if len(q) > 0:
282 gametest = str(tuple(q))
283 gametest = gametest.replace("L", "")
284 gametest = gametest.replace(",)",")")
285 gametest = gametest.replace("u'","'")
286 gametest = "and gt.category in %s" % gametest
287 else:
288 gametest = "and gt.category IS NULL"
289 tmp = tmp.replace("<game_test>", gametest)
291 q = []
292 for n in currencies:
293 if currencies[n]:
294 q.append(n)
295 currencytest = str(tuple(q))
296 currencytest = currencytest.replace(",)",")")
297 currencytest = currencytest.replace("u'","'")
298 currencytest = "AND gt.currency in %s" % currencytest
299 tmp = tmp.replace("<currency_test>", currencytest)
301 lims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'fl']
302 potlims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'pl']
303 nolims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'nl']
304 capnolims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'cn']
305 limittest = "and ( (gt.limitType = 'fl' and gt.bigBlind in "
306 # and ( (limit and bb in()) or (nolimit and bb in ()) )
307 if lims:
308 blindtest = str(tuple(lims))
309 blindtest = blindtest.replace("L", "")
310 blindtest = blindtest.replace(",)",")")
311 limittest = limittest + blindtest + ' ) '
312 else:
313 limittest = limittest + '(-1) ) '
314 limittest = limittest + " or (gt.limitType = 'pl' and gt.bigBlind in "
315 if potlims:
316 blindtest = str(tuple(potlims))
317 blindtest = blindtest.replace("L", "")
318 blindtest = blindtest.replace(",)",")")
319 limittest = limittest + blindtest + ' ) '
320 else:
321 limittest = limittest + '(-1) ) '
322 limittest = limittest + " or (gt.limitType = 'nl' and gt.bigBlind in "
323 if nolims:
324 blindtest = str(tuple(nolims))
325 blindtest = blindtest.replace("L", "")
326 blindtest = blindtest.replace(",)",")")
327 limittest = limittest + blindtest + ' ) '
328 else:
329 limittest = limittest + '(-1) ) '
330 limittest = limittest + " or (gt.limitType = 'cn' and gt.bigBlind in "
331 if capnolims:
332 blindtest = str(tuple(capnolims))
333 blindtest = blindtest.replace("L", "")
334 blindtest = blindtest.replace(",)",")")
335 limittest = limittest + blindtest + ' ) )'
336 else:
337 limittest = limittest + '(-1) ) )'
339 if type == 'ring':
340 limittest = limittest + " and gt.type = 'ring' "
341 elif type == 'tour':
342 limittest = limittest + " and gt.type = 'tour' "
344 #Must be a nicer way to deal with tuples of size 1 ie. (2,) - which makes sql barf
345 tmp = tmp.replace("<player_test>", nametest)
346 tmp = tmp.replace("<site_test>", sitetest)
347 tmp = tmp.replace("<startdate_test>", start_date)
348 tmp = tmp.replace("<enddate_test>", end_date)
349 tmp = tmp.replace("<limit_test>", limittest)
350 tmp = tmp.replace(",)", ")")
352 #print "DEBUG: sql query:"
353 #print tmp
354 self.db.cursor.execute(tmp)
355 #returns (HandId,Winnings,Costs,Profit)
356 winnings = self.db.cursor.fetchall()
357 self.db.rollback()
359 if len(winnings) == 0:
360 return (None, None, None)
362 green = map(lambda x:float(x[1]), winnings)
363 blue = map(lambda x: float(x[1]) if x[2] == True else 0.0, winnings)
364 red = map(lambda x: float(x[1]) if x[2] == False else 0.0, winnings)
365 greenline = cumsum(green)
366 blueline = cumsum(blue)
367 redline = cumsum(red)
368 return (greenline/100, blueline/100, redline/100)
369 #end of def getRingProfitGraph
371 def exportGraph (self, widget, data):
372 if self.fig is None:
373 return # Might want to disable export button until something has been generated.
375 dia_chooser = gtk.FileChooserDialog(title=_("Please choose the directory you wish to export to:"),
376 action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
377 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OK,gtk.RESPONSE_OK))
378 dia_chooser.set_destroy_with_parent(True)
379 dia_chooser.set_transient_for(self.parent)
380 try:
381 dia_chooser.set_filename(self.exportFile) # use previously chosen export path as default
382 except:
383 pass
385 response = dia_chooser.run()
387 if response <> gtk.RESPONSE_OK:
388 print _('Closed, no graph exported')
389 dia_chooser.destroy()
390 return
392 # generate a unique filename for export
393 now = datetime.now()
394 now_formatted = now.strftime("%Y%m%d%H%M%S")
395 self.exportFile = dia_chooser.get_filename() + "/fpdb" + now_formatted + ".png"
396 dia_chooser.destroy()
398 #print "DEBUG: self.exportFile = %s" %(self.exportFile)
399 self.fig.savefig(self.exportFile, format="png")
401 #display info box to confirm graph created
402 diainfo = gtk.MessageDialog(parent=self.parent,
403 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
404 type=gtk.MESSAGE_INFO,
405 buttons=gtk.BUTTONS_OK,
406 message_format=_("Graph created"))
407 diainfo.format_secondary_text(self.exportFile)
408 diainfo.run()
409 diainfo.destroy()
411 #end of def exportGraph