If you only have games of a single limit type (fixed, pot, or no limit), but of more...
[fpdb-dooglus.git] / pyfpdb / GuiGraphViewer.py
blob443d65aa4242b40fa61a02f19bc68396799c4a81
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 #Copyright 2008-2010 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
50 install numpy and matplotlib if you want to use graphs.""")
51 print _("""This is of no consequence for other parts of the program, e.g. import
52 and HUD are NOT affected by this problem.""")
53 print "ImportError: %s" % inst.args
55 class GuiGraphViewer (threading.Thread):
57 def __init__(self, querylist, config, parent, debug=True):
58 """Constructor for GraphViewer"""
59 self.sql = querylist
60 self.conf = config
61 self.debug = debug
62 self.parent = parent
63 #print "start of GraphViewer constructor"
64 self.db = Database.Database(self.conf, sql=self.sql)
67 filters_display = { "Heroes" : True,
68 "Sites" : True,
69 "Games" : True,
70 "Limits" : True,
71 "LimitSep" : True,
72 "LimitType" : True,
73 "Type" : False,
74 "UseType" : 'ring',
75 "Seats" : False,
76 "SeatSep" : False,
77 "Dates" : True,
78 "GraphOps" : True,
79 "Groups" : False,
80 "Button1" : True,
81 "Button2" : True
84 self.filters = Filters.Filters(self.db, self.conf, self.sql, display = filters_display)
85 self.filters.registerButton1Name(_("Refresh _Graph"))
86 self.filters.registerButton1Callback(self.generateGraph)
87 self.filters.registerButton2Name(_("_Export to File"))
88 self.filters.registerButton2Callback(self.exportGraph)
90 self.mainHBox = gtk.HBox(False, 0)
91 self.mainHBox.show()
93 self.leftPanelBox = self.filters.get_vbox()
95 self.hpane = gtk.HPaned()
96 self.hpane.pack1(self.leftPanelBox)
97 self.mainHBox.add(self.hpane)
98 # hierarchy: self.mainHBox / self.hpane / self.graphBox / self.canvas / self.fig / self.ax
100 self.graphBox = gtk.VBox(False, 0)
101 self.graphBox.show()
102 self.hpane.pack2(self.graphBox)
103 self.hpane.show()
105 self.fig = None
106 #self.exportButton.set_sensitive(False)
107 self.canvas = None
110 self.db.rollback()
112 def get_vbox(self):
113 """returns the vbox of this thread"""
114 return self.mainHBox
115 #end def get_vbox
117 def clearGraphData(self):
119 try:
120 try:
121 if self.canvas:
122 self.graphBox.remove(self.canvas)
123 except:
124 pass
126 if self.fig != None:
127 self.fig.clear()
128 self.fig = Figure(figsize=(5,4), dpi=100)
129 if self.canvas is not None:
130 self.canvas.destroy()
132 self.canvas = FigureCanvas(self.fig) # a gtk.DrawingArea
133 except:
134 err = traceback.extract_tb(sys.exc_info()[2])[-1]
135 print _("***Error: ")+err[2]+"("+str(err[1])+"): "+str(sys.exc_info()[1])
136 raise
138 def generateGraph(self, widget, data):
139 try:
140 self.clearGraphData()
142 sitenos = []
143 playerids = []
145 sites = self.filters.getSites()
146 heroes = self.filters.getHeroes()
147 siteids = self.filters.getSiteIds()
148 limits = self.filters.getLimits()
149 games = self.filters.getGames()
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, 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: $%.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, 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 lims = [int(x) for x in limits if x.isdigit()]
292 potlims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'pl']
293 nolims = [int(x[0:-2]) for x in limits if len(x) > 2 and x[-2:] == 'nl']
294 limittest = "and ( (gt.limitType = 'fl' and gt.bigBlind in "
295 # and ( (limit and bb in()) or (nolimit and bb in ()) )
296 if lims:
297 blindtest = str(tuple(lims))
298 blindtest = blindtest.replace("L", "")
299 blindtest = blindtest.replace(",)",")")
300 limittest = limittest + blindtest + ' ) '
301 else:
302 limittest = limittest + '(-1) ) '
303 limittest = limittest + " or (gt.limitType = 'pl' and gt.bigBlind in "
304 if potlims:
305 blindtest = str(tuple(potlims))
306 blindtest = blindtest.replace("L", "")
307 blindtest = blindtest.replace(",)",")")
308 limittest = limittest + blindtest + ' ) '
309 else:
310 limittest = limittest + '(-1) ) '
311 limittest = limittest + " or (gt.limitType = 'nl' and gt.bigBlind in "
312 if nolims:
313 blindtest = str(tuple(nolims))
314 blindtest = blindtest.replace("L", "")
315 blindtest = blindtest.replace(",)",")")
316 limittest = limittest + blindtest + ' ) )'
317 else:
318 limittest = limittest + '(-1) ) )'
320 if type == 'ring':
321 limittest = limittest + " and gt.type = 'ring' "
322 elif type == 'tour':
323 limittest = limittest + " and gt.type = 'tour' "
325 #Must be a nicer way to deal with tuples of size 1 ie. (2,) - which makes sql barf
326 tmp = tmp.replace("<player_test>", nametest)
327 tmp = tmp.replace("<site_test>", sitetest)
328 tmp = tmp.replace("<startdate_test>", start_date)
329 tmp = tmp.replace("<enddate_test>", end_date)
330 tmp = tmp.replace("<limit_test>", limittest)
331 tmp = tmp.replace(",)", ")")
333 #print "DEBUG: sql query:"
334 #print tmp
335 self.db.cursor.execute(tmp)
336 #returns (HandId,Winnings,Costs,Profit)
337 winnings = self.db.cursor.fetchall()
338 self.db.rollback()
340 if len(winnings) == 0:
341 return (None, None, None)
343 green = map(lambda x:float(x[1]), winnings)
344 blue = map(lambda x: float(x[1]) if x[2] == True else 0.0, winnings)
345 red = map(lambda x: float(x[1]) if x[2] == False else 0.0, winnings)
346 greenline = cumsum(green)
347 blueline = cumsum(blue)
348 redline = cumsum(red)
349 return (greenline/100, blueline/100, redline/100)
350 #end of def getRingProfitGraph
352 def exportGraph (self, widget, data):
353 if self.fig is None:
354 return # Might want to disable export button until something has been generated.
356 dia_chooser = gtk.FileChooserDialog(title=_("Please choose the directory you wish to export to:"),
357 action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
358 buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OK,gtk.RESPONSE_OK))
359 dia_chooser.set_destroy_with_parent(True)
360 dia_chooser.set_transient_for(self.parent)
361 try:
362 dia_chooser.set_filename(self.exportFile) # use previously chosen export path as default
363 except:
364 pass
366 response = dia_chooser.run()
368 if response <> gtk.RESPONSE_OK:
369 print _('Closed, no graph exported')
370 dia_chooser.destroy()
371 return
373 # generate a unique filename for export
374 now = datetime.now()
375 now_formatted = now.strftime("%Y%m%d%H%M%S")
376 self.exportFile = dia_chooser.get_filename() + "/fpdb" + now_formatted + ".png"
377 dia_chooser.destroy()
379 #print "DEBUG: self.exportFile = %s" %(self.exportFile)
380 self.fig.savefig(self.exportFile, format="png")
382 #display info box to confirm graph created
383 diainfo = gtk.MessageDialog(parent=self.parent,
384 flags=gtk.DIALOG_DESTROY_WITH_PARENT,
385 type=gtk.MESSAGE_INFO,
386 buttons=gtk.BUTTONS_OK,
387 message_format=_("Graph created"))
388 diainfo.format_secondary_text(self.exportFile)
389 diainfo.run()
390 diainfo.destroy()
392 #end of def exportGraph