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.
19 _
= L10n
.get_translation()
29 from datetime
import datetime
38 calluse
= not 'matplotlib' in sys
.modules
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
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"""
61 #print "start of GraphViewer constructor"
62 self
.db
= Database
.Database(self
.conf
, sql
=self
.sql
)
65 filters_display
= { "Heroes" : 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)
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)
100 self
.hpane
.pack2(self
.graphBox
)
104 #self.exportButton.set_sensitive(False)
111 """returns the vbox of this thread"""
115 def clearGraphData(self
):
120 self
.graphBox
.remove(self
.canvas
)
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
132 err
= traceback
.extract_tb(sys
.exc_info()[2])[-1]
133 print _("Error:")+" "+err
[2]+"("+str(err
[1])+"): "+str(sys
.exc_info()[1])
136 def generateGraph(self
, widget
, data
):
138 self
.clearGraphData()
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()
151 for i
in ('show', 'none'):
154 # Which sites are selected?
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
165 #Should probably pop up here.
166 print _("No sites selected - defaulting to PokerStars")
171 print _("No player ids found")
176 print _("No limits found")
180 #Set graph properties
181 self
.ax
= self
.fig
.add_subplot(111)
183 #Get graph data from DB
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
)
226 #TODO: Do something useful like alert user
227 #print "No hands returned by graph query"
229 self
.ax
.set_title((_("Profit graph for ring games")+names
),fontsize
=12)
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'))
241 self
.ax
.legend(loc
='upper left', fancybox
=True, shadow
=True, prop
=FontProperties(size
='smaller'))
243 self
.graphBox
.add(self
.canvas
)
246 #self.exportButton.set_sensitive(True)
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"
259 tmp
= self
.sql
.query
['getRingProfitAllHandsPlayerIdSiteInDollars']
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", "")
274 for m
in self
.filters
.display
.items():
275 if m
[0] == 'Games' and m
[1]:
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
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 ()) )
296 blindtest
= str(tuple(lims
))
297 blindtest
= blindtest
.replace("L", "")
298 blindtest
= blindtest
.replace(",)",")")
299 limittest
= limittest
+ blindtest
+ ' ) '
301 limittest
= limittest
+ '(-1) ) '
302 limittest
= limittest
+ " or (gt.limitType = 'pl' and gt.bigBlind in "
304 blindtest
= str(tuple(potlims
))
305 blindtest
= blindtest
.replace("L", "")
306 blindtest
= blindtest
.replace(",)",")")
307 limittest
= limittest
+ blindtest
+ ' ) '
309 limittest
= limittest
+ '(-1) ) '
310 limittest
= limittest
+ " or (gt.limitType = 'nl' and gt.bigBlind in "
312 blindtest
= str(tuple(nolims
))
313 blindtest
= blindtest
.replace("L", "")
314 blindtest
= blindtest
.replace(",)",")")
315 limittest
= limittest
+ blindtest
+ ' ) '
317 limittest
= limittest
+ '(-1) ) '
318 limittest
= limittest
+ " or (gt.limitType = 'cn' and gt.bigBlind in "
320 blindtest
= str(tuple(capnolims
))
321 blindtest
= blindtest
.replace("L", "")
322 blindtest
= blindtest
.replace(",)",")")
323 limittest
= limittest
+ blindtest
+ ' ) )'
325 limittest
= limittest
+ '(-1) ) )'
328 limittest
= limittest
+ " and gt.type = 'ring' "
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:"
342 self
.db
.cursor
.execute(tmp
)
343 #returns (HandId,Winnings,Costs,Profit)
344 winnings
= self
.db
.cursor
.fetchall()
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
):
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
)
369 dia_chooser
.set_filename(self
.exportFile
) # use previously chosen export path as default
373 response
= dia_chooser
.run()
375 if response
<> gtk
.RESPONSE_OK
:
376 print _('Closed, no graph exported')
377 dia_chooser
.destroy()
380 # generate a unique filename for export
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
)
399 #end of def exportGraph