Don't omit the 'fl' for fixed limit games in the Limits filter.
[fpdb-dooglus.git] / pyfpdb / GuiGraphViewer.py
blob138b2e6a5c9a4956e79cb907c7dacc6e95866fc3
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 "Limits" : True,
69 "LimitSep" : True,
70 "LimitType" : True,
71 "Type" : False,
72 "UseType" : 'ring',
73 "Seats" : False,
74 "SeatSep" : False,
75 "Dates" : True,
76 "GraphOps" : True,
77 "Groups" : False,
78 "Button1" : True,
79 "Button2" : True
82 self.filters = Filters.Filters(self.db, self.conf, self.sql, display = filters_display)
83 self.filters.registerButton1Name(_("Refresh _Graph"))
84 self.filters.registerButton1Callback(self.generateGraph)
85 self.filters.registerButton2Name(_("_Export to File"))
86 self.filters.registerButton2Callback(self.exportGraph)
88 self.mainHBox = gtk.HBox(False, 0)
89 self.mainHBox.show()
91 self.leftPanelBox = self.filters.get_vbox()
93 self.hpane = gtk.HPaned()
94 self.hpane.pack1(self.leftPanelBox)
95 self.mainHBox.add(self.hpane)
96 # hierarchy: self.mainHBox / self.hpane / self.graphBox / self.canvas / self.fig / self.ax
98 self.graphBox = gtk.VBox(False, 0)
99 self.graphBox.show()
100 self.hpane.pack2(self.graphBox)
101 self.hpane.show()
103 self.fig = None
104 #self.exportButton.set_sensitive(False)
105 self.canvas = None
108 self.db.rollback()
110 def get_vbox(self):
111 """returns the vbox of this thread"""
112 return self.mainHBox
113 #end def get_vbox
115 def clearGraphData(self):
117 try:
118 try:
119 if self.canvas:
120 self.graphBox.remove(self.canvas)
121 except:
122 pass
124 if self.fig != None:
125 self.fig.clear()
126 self.fig = Figure(figsize=(5,4), dpi=100)
127 if self.canvas is not None:
128 self.canvas.destroy()
130 self.canvas = FigureCanvas(self.fig) # a gtk.DrawingArea
131 except:
132 err = traceback.extract_tb(sys.exc_info()[2])[-1]
133 print _("Error:")+" "+err[2]+"("+str(err[1])+"): "+str(sys.exc_info()[1])
134 raise
136 def generateGraph(self, widget, data):
137 try:
138 self.clearGraphData()
140 sitenos = []
141 playerids = []
143 sites = self.filters.getSites()
144 heroes = self.filters.getHeroes()
145 siteids = self.filters.getSiteIds()
146 limits = self.filters.getLimits()
147 games = self.filters.getGames()
148 graphops = self.filters.getGraphOps()
149 names = ""
151 for i in ('show', 'none'):
152 if i in limits:
153 limits.remove(i)
154 # Which sites are selected?
155 for site in sites:
156 if sites[site] == True:
157 sitenos.append(siteids[site])
158 _hname = Charset.to_utf8(heroes[site])
159 result = self.db.get_player_id(self.conf, site, _hname)
160 if result is not None:
161 playerids.append(int(result))
162 names = names + "\n"+_hname + " on "+site
164 if not sitenos:
165 #Should probably pop up here.
166 print _("No sites selected - defaulting to PokerStars")
167 self.db.rollback()
168 return
170 if not playerids:
171 print _("No player ids found")
172 self.db.rollback()
173 return
175 if not limits:
176 print _("No limits found")
177 self.db.rollback()
178 return
180 #Set graph properties
181 self.ax = self.fig.add_subplot(111)
183 #Get graph data from DB
184 starttime = time()
185 (green, blue, red) = self.getRingProfitGraph(playerids, sitenos, limits, games, graphops['dspin'])
186 print _("Graph generated in: %s") %(time() - starttime)
190 #Set axis labels and grid overlay properites
191 self.ax.set_xlabel(_("Hands"), fontsize = 12)
192 # SET LABEL FOR X AXIS
193 self.ax.set_ylabel(graphops['dspin'], fontsize = 12)
194 self.ax.grid(color='g', linestyle=':', linewidth=0.2)
195 if green == None or green == []:
196 self.ax.set_title(_("No Data for Player(s) Found"))
197 green = ([ 0., 0., 0., 0., 500., 1000., 900., 800.,
198 700., 600., 500., 400., 300., 200., 100., 0.,
199 500., 1000., 1000., 1000., 1000., 1000., 1000., 1000.,
200 1000., 1000., 1000., 1000., 1000., 1000., 875., 750.,
201 625., 500., 375., 250., 125., 0., 0., 0.,
202 0., 500., 1000., 900., 800., 700., 600., 500.,
203 400., 300., 200., 100., 0., 500., 1000., 1000.])
204 red = ([ 0., 0., 0., 0., 500., 1000., 900., 800.,
205 700., 600., 500., 400., 300., 200., 100., 0.,
206 0., 0., 0., 0., 0., 0., 125., 250.,
207 375., 500., 500., 500., 500., 500., 500., 500.,
208 500., 500., 375., 250., 125., 0., 0., 0.,
209 0., 500., 1000., 900., 800., 700., 600., 500.,
210 400., 300., 200., 100., 0., 500., 1000., 1000.])
211 blue = ([ 0., 0., 0., 0., 500., 1000., 900., 800.,
212 700., 600., 500., 400., 300., 200., 100., 0.,
213 0., 0., 0., 0., 0., 0., 125., 250.,
214 375., 500., 625., 750., 875., 1000., 875., 750.,
215 625., 500., 375., 250., 125., 0., 0., 0.,
216 0., 500., 1000., 900., 800., 700., 600., 500.,
217 400., 300., 200., 100., 0., 500., 1000., 1000.])
219 self.ax.plot(green, color='green', label=_('Hands: %d\nProfit: (%s): %.2f') %(len(green), green[-1]))
220 self.ax.plot(blue, color='blue', label=_('Showdown') + ': $%.2f' %(blue[-1]))
221 self.ax.plot(red, color='red', label=_('Non-showdown') + ': $%.2f' %(red[-1]))
222 self.graphBox.add(self.canvas)
223 self.canvas.show()
224 self.canvas.draw()
226 #TODO: Do something useful like alert user
227 #print "No hands returned by graph query"
228 else:
229 self.ax.set_title((_("Profit graph for ring games")+names),fontsize=12)
231 #Draw plot
232 self.ax.plot(green, color='green', label=_('Hands: %d\nProfit: (%s): %.2f') %(len(green),graphops['dspin'], green[-1]))
233 if graphops['showdown'] == 'ON':
234 self.ax.plot(blue, color='blue', label=_('Showdown') + ' (%s): %.2f' %(graphops['dspin'], blue[-1]))
235 if graphops['nonshowdown'] == 'ON':
236 self.ax.plot(red, color='red', label=_('Non-showdown') + ' (%s): %.2f' %(graphops['dspin'], red[-1]))
238 if sys.version[0:3] == '2.5':
239 self.ax.legend(loc='upper left', shadow=True, prop=FontProperties(size='smaller'))
240 else:
241 self.ax.legend(loc='upper left', fancybox=True, shadow=True, prop=FontProperties(size='smaller'))
243 self.graphBox.add(self.canvas)
244 self.canvas.show()
245 self.canvas.draw()
246 #self.exportButton.set_sensitive(True)
247 except:
248 err = traceback.extract_tb(sys.exc_info()[2])[-1]
249 print _("Error:")+" "+err[2]+"("+str(err[1])+"): "+str(sys.exc_info()[1])
251 #end of def showClicked
254 def getRingProfitGraph(self, names, sites, limits, games, units):
255 # tmp = self.sql.query['getRingProfitAllHandsPlayerIdSite']
256 # print "DEBUG: getRingProfitGraph"
258 if units == '$':
259 tmp = self.sql.query['getRingProfitAllHandsPlayerIdSiteInDollars']
260 elif units == 'BB':
261 tmp = self.sql.query['getRingProfitAllHandsPlayerIdSiteInBB']
264 start_date, end_date = self.filters.getDates()
266 #Buggered if I can find a way to do this 'nicely' take a list of integers and longs
267 # and turn it into a tuple readale by sql.
268 # [5L] into (5) not (5,) and [5L, 2829L] into (5, 2829)
269 nametest = str(tuple(names))
270 sitetest = str(tuple(sites))
271 #nametest = nametest.replace("L", "")
273 q = []
274 for m in self.filters.display.items():
275 if m[0] == 'Games' and m[1]:
276 for n in games:
277 if games[n]:
278 q.append(n)
279 if len(q) > 0:
280 gametest = str(tuple(q))
281 gametest = gametest.replace("L", "")
282 gametest = gametest.replace(",)",")")
283 gametest = gametest.replace("u'","'")
284 gametest = "and gt.category in %s" % gametest
285 else:
286 gametest = "and gt.category IS NULL"
287 tmp = tmp.replace("<game_test>", gametest)
289 lims = [int(x) for x in limits if x.isdigit()]
290 potlims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'pl']
291 nolims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'nl']
292 capnolims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'cn']
293 limittest = "and ( (gt.limitType = 'fl' and gt.bigBlind in "
294 # and ( (limit and bb in()) or (nolimit and bb in ()) )
295 if lims:
296 blindtest = str(tuple(lims))
297 blindtest = blindtest.replace("L", "")
298 blindtest = blindtest.replace(",)",")")
299 limittest = limittest + blindtest + ' ) '
300 else:
301 limittest = limittest + '(-1) ) '
302 limittest = limittest + " or (gt.limitType = 'pl' and gt.bigBlind in "
303 if potlims:
304 blindtest = str(tuple(potlims))
305 blindtest = blindtest.replace("L", "")
306 blindtest = blindtest.replace(",)",")")
307 limittest = limittest + blindtest + ' ) '
308 else:
309 limittest = limittest + '(-1) ) '
310 limittest = limittest + " or (gt.limitType = 'nl' and gt.bigBlind in "
311 if nolims:
312 blindtest = str(tuple(nolims))
313 blindtest = blindtest.replace("L", "")
314 blindtest = blindtest.replace(",)",")")
315 limittest = limittest + blindtest + ' ) '
316 else:
317 limittest = limittest + '(-1) ) '
318 limittest = limittest + " or (gt.limitType = 'cn' and gt.bigBlind in "
319 if capnolims:
320 blindtest = str(tuple(capnolims))
321 blindtest = blindtest.replace("L", "")
322 blindtest = blindtest.replace(",)",")")
323 limittest = limittest + blindtest + ' ) )'
324 else:
325 limittest = limittest + '(-1) ) )'
327 if type == 'ring':
328 limittest = limittest + " and gt.type = 'ring' "
329 elif type == 'tour':
330 limittest = limittest + " and gt.type = 'tour' "
332 #Must be a nicer way to deal with tuples of size 1 ie. (2,) - which makes sql barf
333 tmp = tmp.replace("<player_test>", nametest)
334 tmp = tmp.replace("<site_test>", sitetest)
335 tmp = tmp.replace("<startdate_test>", start_date)
336 tmp = tmp.replace("<enddate_test>", end_date)
337 tmp = tmp.replace("<limit_test>", limittest)
338 tmp = tmp.replace(",)", ")")
340 #print "DEBUG: sql query:"
341 #print tmp
342 self.db.cursor.execute(tmp)
343 #returns (HandId,Winnings,Costs,Profit)
344 winnings = self.db.cursor.fetchall()
345 self.db.rollback()
347 if len(winnings) == 0:
348 return (None, None, None)
350 green = map(lambda x:float(x[1]), winnings)
351 blue = map(lambda x: float(x[1]) if x[2] == True else 0.0, winnings)
352 red = map(lambda x: float(x[1]) if x[2] == False else 0.0, winnings)
353 greenline = cumsum(green)
354 blueline = cumsum(blue)
355 redline = cumsum(red)
356 return (greenline/100, blueline/100, redline/100)
357 #end of def getRingProfitGraph
359 def exportGraph (self, widget, data):
360 if self.fig is None:
361 return # Might want to disable export button until something has been generated.
363 dia_chooser = gtk.FileChooserDialog(title=_("Please choose the directory you wish to export to:"),
364 action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
365 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OK,gtk.RESPONSE_OK))
366 dia_chooser.set_destroy_with_parent(True)
367 dia_chooser.set_transient_for(self.parent)
368 try:
369 dia_chooser.set_filename(self.exportFile) # use previously chosen export path as default
370 except:
371 pass
373 response = dia_chooser.run()
375 if response <> gtk.RESPONSE_OK:
376 print _('Closed, no graph exported')
377 dia_chooser.destroy()
378 return
380 # generate a unique filename for export
381 now = datetime.now()
382 now_formatted = now.strftime("%Y%m%d%H%M%S")
383 self.exportFile = dia_chooser.get_filename() + "/fpdb" + now_formatted + ".png"
384 dia_chooser.destroy()
386 #print "DEBUG: self.exportFile = %s" %(self.exportFile)
387 self.fig.savefig(self.exportFile, format="png")
389 #display info box to confirm graph created
390 diainfo = gtk.MessageDialog(parent=self.parent,
391 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
392 type=gtk.MESSAGE_INFO,
393 buttons=gtk.BUTTONS_OK,
394 message_format=_("Graph created"))
395 diainfo.format_secondary_text(self.exportFile)
396 diainfo.run()
397 diainfo.destroy()
399 #end of def exportGraph