File: chart.py

package info (click to toggle)
python-jtoolkit 0.7.8-2
  • links: PTS
  • area: main
  • in suites: etch, etch-m68k
  • size: 1,436 kB
  • ctags: 2,536
  • sloc: python: 15,143; makefile: 20
file content (530 lines) | stat: -rwxr-xr-x 19,564 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""A simple wrapper class for the matplotlib chart module."""

# Copyright 2002, 2003 St James Software
# 
# This file is part of jToolkit.
#
# jToolkit is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
# 
# jToolkit 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 General Public License
# along with jToolkit; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

import cStringIO, tempfile, os, math
from jToolkit.widgets import widgets
from jToolkit.data import dates
from jToolkit import errors
import random, os, time
import sys

sys.stderr = sys.stdout   #Solve a problem with the font manager
if not hasattr(sys, "argv"):
  # matplotlib expects an argv...
  sys.argv = []
matplotlib_import_error = None
matplotlib_import_traceback = None

# TODO: improve error handling here...
try:
  # TODO: change to from matplotlib import matlab...
  os.environ['MATPLOTLIBDATA'] = os.path.join(sys.prefix, 'share', 'matplotlib')
  import matplotlib
  matplotlib.use('Agg')
  import matplotlib.pylab as matlab
  from matplotlib import ticker
  from matplotlib import dates as matdates
except Exception, e:
  # not importing these functions will cause charts to fail...
  errorhandler = errors.ConsoleErrorHandler()
  matplotlib_import_error = str(e)
  matplotlib_import_traceback = errorhandler.traceback_str()
  # print "error importing matplotlib. jToolkit.widgets.chart functions will fail later."
  raise

#We need this lock for gdchart, as it's not thread-safe
#from threading import Lock
#gdchartLock = Lock()

#Graph type constants
LINE_CHART = 0
BAR_CHART = 1

class jMajorDateLocator(matdates.DayLocator):
  def __call__(self):
    self.verify_intervals()
    vmin, vmax = self.viewInterval.get_bounds()

    ret = [vmin]
    ret.extend(matdates.DayLocator.__call__(self))
    ret.append(vmax)
    return ret

class jMinorLinearLocator(ticker.LinearLocator):
  """The purpose of this class is to create a LinearLocator that does not return vmin or vmax"""
  def __init__(self, numticks=None, presets=None):
    if numticks is not None:
      numticks += 2     # We strip these out in __call__
    ticker.LinearLocator.__init__(self, numticks, presets)

  def __call__(self):
    self.verify_intervals()
    vmin, vmax = self.viewInterval.get_bounds()

    ret = ticker.LinearLocator.__call__(self)[1:-1]
    return ret
    
class jMinuteLocator(matdates.MinuteLocator):
  """The purpose of this class is to ignore the change of day tick, so it doesn't overlap and look ugly"""
  def __call__(self):
    dates = matdates.MinuteLocator.__call__(self)
    days = matdates.DayLocator()
    days.set_view_interval(self.viewInterval)
    days.set_data_interval(self.dataInterval)
    daychanges = days()
    realdates = []
    for date in dates:
      if date not in daychanges:
        realdates.append(date)
    return realdates

class jSecondLocator(matdates.SecondLocator):
  """The purpose of this class is to ignore the change of day tick, so it doesn't overlap and look ugly"""
  def __call__(self):
    dates = matdates.SecondLocator.__call__(self)
    days = matdates.DayLocator()
    days.set_view_interval(self.viewInterval)
    days.set_data_interval(self.dataInterval)
    daychanges = days()
    realdates = []
    for date in dates:
      if date not in daychanges:
        realdates.append(date)
    return realdates

class Chart(widgets.ContentWidget):
  def __init__(self, charttable, newattribs={}):
    self.charttable = charttable
    if not newattribs.has_key('chartType'):
      newattribs['chartType'] = LINE_CHART
    if not newattribs.has_key('dpi'):
      newattribs['dpi'] = 80

    self.content_type = 'image/png'
    widgets.ContentWidget.__init__(self, "", [], newattribs)
    self.getdata()
    self.sizeimage()
    self.figure = matlab.new_figure_manager(1,(self.im_width,self.im_height),self.attribs['dpi'])

  def getdata(self):
    """Subclasses of this should implement this uniquely"""
    self.xdata = []
    self.ydata = []

  def getimage(self):
    """Subclasses of this should implement this uniquely"""
    return ''

  def sizeimage(self):  
    """Subclasses of this should implement this uniquely"""
    self.im_width = 8.75
    self.im_height = 5.6

  def getimagefromtempfile(self):
    #Save temporary file
#    tempfilename = os.path.join(tempfile.gettempdir(),'temp.png')
    retrieved = 0
    while not retrieved:
      try:
        tempfilename = str(random.randint(100000,999999)) + '.png'
        if hasattr(self.figure,'figure'):
          self.figure.figure.print_figure(tempfilename,self.attribs['dpi'])
        else: # matplotlib 0.50
          self.figure.canvas.print_figure(tempfilename,self.attribs['dpi'])

        #Read and report
        f = open(tempfilename,'rb')
        img = f.read()
        f.close()
        retrieved = 1
        os.remove(tempfilename)
      except:
        (etype, evalue, trace) = sys.exc_info()
#        print "Temp file error:",etype,evalue
#        import traceback
#        traceback.print_tb(trace)
    return img

  def gethtml(self):
#    gdchartLock.acquire()
    try:
      self.drawimage()
    except:
      raise
      print "Unexpected Error:"
      print errors.ConsoleErrorHandler().traceback_str()
#      gdchartLock.release()
      # return None
#    gdchartLock.release()
    img = self.getimagefromtempfile()
    return img

standardColours = [(0.0, 0.0, 1.0), # blue
                   (0.0, 0.5, 0.0), # green
                   (1.0, 0.0, 0.0), # red
                   (0.0, 0.75, 0.75),  # cyan
                   (0.75, 0, 0.75),  # magenta
                   (0.75, 0.75, 0),  # yellow
                   (0.0, 0.0, 0.0),  # black
                   '#4682B4',   # Steelblue
                   '#7FFF00',   # Chartreuse
                   '#FF7F50',   # Coral
                   '#808000',   # Olive
                   '#FF4500',   # Orangered
                  ]


#This class now assumes a text storage format for x values.  See below for subclass which handles dates
class LineChart(Chart):
  def __init__(self, charttable, xcolumn, ycolumns, ylabels=None, newattribs={}):
    if not newattribs.has_key('datalimits'):
      newattribs['datalimits'] = {}
    if not newattribs.has_key('numticks'):
      newattribs['numticks'] = 10
    if not newattribs.has_key('chartcolours'):
      newattribs['chartcolours'] = standardColours
    self.ylabels = ylabels
    self.normalisedValues = len(newattribs['datalimits']) > 0
    self.xcolumn = xcolumn
    self.ycolumns = ycolumns
    self.xMajorLocator = None
    self.xMajorFormatter = None
    self.xMinorLocator = None
    self.xMinorFormatter = None
    Chart.__init__(self, charttable, newattribs)

  def sizeimage(self):  
    self.im_width = self.attribs['width']/self.attribs['dpi']
    if self.attribs.has_key('height'):
      self.im_height = self.attribs['height']/self.attribs['dpi']
    else:
      self.im_height = 5.6

  def mapfromNone(self, value):
    if value == None or value == '':
      return 0
    else:
      return value

  def getdata(self):
    chartdata = self.charttable.gettablerows(self.attribs.get('filter',None))
    xlabels = [str(row[self.xcolumn]) for row in chartdata]
    self.ydata = [[self.mapfromNone(row[ycolumn]) for row in chartdata] for ycolumn in self.ycolumns]
    self.legendlabels = [str(ycolumn) for ycolumn in self.ycolumns]

    #x axis tick labels should be uniformly distributed in this case
    startVal = self.attribs.get('startval', 0)
    endVal = self.attribs.get('endval', len(self.xdata))
    self.xvalues = matlab.arange(startVal, endVal, float(endVal - startVal)/len(xlabels))
    self.xMajorLocator = ticker.LinearLocator(self.attribs.get('numticks',10))
    self.xMajorFormatter = ticker.FixedFormatter(xlabels)

  def normaliseValue(self, val, min, max, ymin, ymax):
    """Adjust value between min and max to be between ymin and ymax"""
    temp = (val - min)/(max - min)
    return temp*(ymax - ymin) + ymin

  def normaliseData(self, ymin, ymax):
    """If limits are provided for y values, normalise to between those limits"""
    newData = []
    for i, dataset in enumerate(self.ydata):
      #Don't factor limited data into ymin, ymax
      if self.legendlabels is not None and len(self.legendlabels) > i and self.attribs['datalimits'].get(self.legendlabels[i],None) is not None:
        min, max = self.attribs['datalimits'][self.legendlabels[i]]
        newSet = [self.normaliseValue(val,min,max,ymin,ymax) for val in dataset]
        newData.append(newSet)
      else:
        newData.append(dataset[:])

    self.ydata = newData        

  def getAxisMap(self, rangelimits):
    # Override this function for better management of axes
    # This version returns one set of axes
    minEU = None
    maxEU = None
    for plotname in rangelimits.keys():
      ymin, ymax = rangelimits[plotname]
      if maxEU is None or ymax > maxEU:
        maxEU = ymax
      if minEU is None or ymin > minEU:
        minEU = ymin

    axismap = [((minEU, maxEU), rangelimits.keys())]
    return axismap

  def drawimage(self):
    #Set the min/max of each axis
    plotnametodata = {}
    for i, dataset in enumerate(self.ydata):
      #Don't factor limited data into ymin, ymax
      datalimits = self.attribs['datalimits'].get(self.legendlabels[i],None)
      # Remember to check that the limits themselves have not been set to None
      if self.legendlabels is not None and len(self.legendlabels) > i and datalimits is not None and datalimits[0] is not None and datalimits[1] is not None and datalimits[0] != datalimits[1]:
        ymax, ymin = datalimits
      else:
        ymin = None
        ymax = None
        for value in dataset:
          if ymin is None or value < ymin:
            ymin = value
          if ymax is None or value > ymax:
            ymax = value
        self.attribs['datalimits'][self.legendlabels[i]] = [ymax, ymin]
      plotnametodata[self.legendlabels[i]] = dataset

    # Create the mapping the right way round
    tagtolimitmap = {}
    for plotname in self.attribs['datalimits'].keys():
      ymax, ymin = self.attribs['datalimits'][plotname]
      tagtolimitmap[plotname] = (ymin, ymax)
    #Set the size of the subplot big enough to handle times
    #[left, bottom, width, height] out of 1
    axismap = {}
    axismap = self.getAxisMap(tagtolimitmap)
    axes = []
    subplotTotal = len(axismap)
    if (subplotTotal != 0):
      plotSize = .85 / subplotTotal
    for i, (axis, tags) in enumerate(axismap):
      axes.append(self.figure.canvas.figure.add_subplot(subplotTotal,1,i+1,axisbg='w'))
      axes[-1].set_position([.08,i*plotSize+.15,.9,plotSize-.02])

    self.figure.canvas.figure.set_figsize_inches(self.im_width, self.im_height)

    #Plot each dataset
    plots = []
    for i, (axis, tags) in enumerate(axismap):
      for plotname in tags:
        if not self.attribs.get('plotdate',0):
          plots.append(axes[i].plot(self.xvalues, plotnametodata[plotname]))
        else:
          plots.append(axes[i].plot_date(self.xvalues,plotnametodata[plotname],fmt="-"))

    #Set the min/max of each axis
    for i, (axis, tags) in enumerate(axismap):
      ymin, ymax = axis
      if ymin == ymax:
        ymax += 1
        ymin -= 1
      axes[i].set_ylim([math.floor(ymin),math.ceil(ymax)])

    # We can set the colour of the lines here
    # with the variable plots
    chartColours = self.attribs.get('chartcolours',standardColours)
    plotNum = 0
    for lines in plots:
      for line in lines:
        line.set_color(chartColours[plotNum % len(chartColours)])
      plotNum += 1

    for ax in axes:
      ax.set_xlim([self.attribs.get('startval',None), self.attribs.get('endval',None)])

    if (len(axes) > 0):
      if self.xMajorLocator:
        for ax in axes:
          ax.xaxis.set_major_locator(self.xMajorLocator)
      if self.xMajorFormatter:
        axes[0].xaxis.set_major_formatter(self.xMajorFormatter)
      if self.xMinorLocator:
        for ax in axes:
          ax.xaxis.set_minor_locator(self.xMinorLocator)
      if self.xMinorFormatter:
        axes[0].xaxis.set_minor_formatter(self.xMinorFormatter)

    for ax in axes:
      ax.set_xlim([self.attribs.get('startval',None), self.attribs.get('endval',None)])

    for ax in axes[1:]:
      labels = ax.get_xticklabels()
      labels.extend([tick.label1 for tick in ax.xaxis.get_minor_ticks()])
      for label in labels:
        label.set_alpha(0)
        label.set_color('w')

    if (len(axes) > 0):
      labels = axes[0].get_xticklabels()
      labels.extend([tick.label1 for tick in axes[0].xaxis.get_minor_ticks()])
      for label in labels:
        label.set_rotation('vertical')

    #Draw a legend, but only if there are any plots to draw
    if self.legendlabels:
      for i, (axis, tags) in enumerate(axismap):
        axes[i].legend(tags,2)
        leg = axes[i].get_legend()
        leg.get_frame().set_fill(False)

    # ax.autoscale_view()
    
NUM_TIME_LABELS = 10

def pottime2date(tm):
  """Converts an object from a time object to a date object safely"""
  if not type(tm).__name__ == "time":
    return tm

  return dates.WinPyTimeToDate(tm)

class DateLineChart(LineChart):
  def __init__(self, charttable, xcolumn, ycolumns, ylabels=None, newattribs={}):
    newattribs['plotDate'] = 1
    LineChart.__init__(self, charttable, xcolumn, ycolumns, ylabels=ylabels, newattribs=newattribs)

  def getdata(self):
    filter = self.attribs.get('filter',None)
    chartdata = self.charttable.gettablerows(filter)

    startDate = self.attribs.get('startdate',None)
    endDate = self.attribs.get('enddate',None)
    if startDate == None:
      if chartdata:
        startDate = matdates.date2num(chartdata[0][self.xcolumn])
      else:
        startDate = matdates.date2num(dates.currentdate())
    else:
      startDate = matdates.date2num(startDate)
    if endDate == None:
      if chartdata:
        endDate = matdates.date2num(chartdata[-1][self.xcolumn])
      else:
        endDate = matdates.date2num(dates.currentdate() - dates.days(1))
    else:
      endDate = matdates.date2num(endDate)
    self.attribs['startval'] = startDate
    self.attribs['endval'] = endDate

    if chartdata == []:   #No data
      self.xvalues = [startDate, endDate]
      self.ydata = [[0 for xvalue in self.xvalues] for ycolumn in self.ycolumns]
    else:
      self.xvalues = [matdates.date2num(pottime2date(row[self.xcolumn])) for row in chartdata]
      self.ydata = [[self.mapfromNone(row[ycolumn]) for row in chartdata] for ycolumn in self.ycolumns]
    self.legendlabels = [str(ycolumn) for ycolumn in self.ycolumns]

    gradationUnits = self.attribs.get('gradationunits',None)
    numOfGradations = self.attribs.get('numofgradations',None)
    gradationSize = self.attribs.get('gradationsize',None)

    if gradationUnits is None or numOfGradations is None or gradationSize is None:
      self.xMajorLocator = jMajorDateLocator(time.timezone/3600)
      # self.xMajorLocator = matdates.DayLocator(100)
      self.xMajorFormatter = matdates.DateFormatter(self.attribs.get('xMajorFormat','%y-%m-%d'))
      self.xMinorLocator = jMinorLinearLocator(10)
      self.xMinorFormatter = matdates.DateFormatter(self.attribs.get('xMinorFormat','%H:%M:%S'))
    elif gradationUnits == 'days':
      self.xMajorLocator = matdates.MonthLocator()
      self.xMajorFormatter = matdates.DateFormatter('%y-%m-%d')
      self.xMinorLocator = matdates.DayLocator(interval=gradationSize)
      self.xMinorFormatter = matdates.DateFormatter('%m-%d')
    elif gradationUnits == 'hours':
      self.xMajorLocator = matdates.DayLocator()
      self.xMajorFormatter = matdates.DateFormatter('%y-%m-%d')
      self.xMinorLocator = matdates.HourLocator(byhour=range(1,24),interval=gradationSize)
      self.xMinorFormatter = matdates.DateFormatter('%H:%M')
    else:
      self.xMajorLocator = matdates.DayLocator()
      self.xMajorFormatter = matdates.DateFormatter('%y-%m-%d')
      if gradationUnits == 'minutes':
        self.xMinorLocator = jMinuteLocator(interval=gradationSize)
      else:
        self.xMinorLocator = jSecondLocator(interval=gradationSize)
      self.xMinorFormatter = matdates.DateFormatter('%H:%M:%S')

class CurrentValueLegendChart(Chart):
  """This class creates a bar chart which acts as a legend and a current value reporter"""
  def __init__(self, charttable, xcolumns, colours, newattribs={}):
    self.xcolumns = xcolumns
    newattribs['chartType'] = BAR_CHART
    Chart.__init__(self, charttable, newattribs)

    #Turn colours into an array exactly len(self.xcolumns) long
    repeatcolours = len(self.xcolumns) / len(colours)
    endcolours = len(self.xcolumns) % len(colours)
    self.colours = colours*repeatcolours + colours[:endcolours]

  def getdata(self):
    self.xdata = [str(xcolumn) for xcolumn in self.xcolumns]
    chartdata = self.charttable.gettablerows(self.attribs.get('filter',None))
    finalrow = chartdata[len(chartdata)-1]
    self.ydata = [[finalrow[xcolumn] for xcolumn in self.xcolumns]]

  def drawimage(self):
    #Find the longest text on the x axis
    maxtextlen = 0
    for text in self.xdata:
      if len(text) > maxtextlen:
        maxtextlen = len(text)

    #Convert it to a proportion of the image
    bottomProportion = .1 + maxtextlen*.013
    heightProportion = .99 - bottomProportion

    #Set the size of the subplot big enough to handle times
    #[left, bottom, width, height] out of 1
    self.figure.add_axes([.125,bottomProportion,.825,heightProportion],'w')

    #Set the min/max of each axis
    ymin = sys.maxint
    ymax = -sys.maxint+1
    for value in self.ydata[0]:
      if value < ymin:
        ymin = value
      if value > ymax:
        ymax = value
          
    self.figure.get_current_axis().set_xlim([0,len(self.xdata)+1])
    self.figure.get_current_axis().set_ylim([math.floor(ymin),math.ceil(ymax)])
    self.figure.get_current_axis().set_xticks(matlab.arange(len(self.xdata))+0.25)
    self.figure.get_current_axis().set_xticklabels(self.xdata,rotation='vertical')

    originY = None
    if ymin < 0 and ymax > 0:
      originY = 0
    self.figure.get_current_axis().bar(matlab.arange(len(self.xdata)),self.ydata[0],0.5,color=self.colours,originY=originY)

  def sizeimage(self):
    self.im_width = 3
    self.im_height = 4  #This should take tagname length into account


def test():
  """tests using some values that were giving problems"""
  import datetime
  startDate = datetime.datetime(2004, 12, 8, 14, 9, 34, 6000)
  endDate = datetime.datetime(2004, 12, 8, 15, 9, 34, 6000)
  newattribs = {"startDate": startDate, "endDate": endDate, 'width': 775, 'height': 550, 'dpi': 80}
  ylabels = ['MinEU', 'MaxEU']
  xcolumn = 'logtime'
  ycolumns = ['asd']
  class charttabletest:
    def gettablerows(self, filter):
      return [{'logtime':startDate, 'asd':20}, {'logtime':endDate, 'asd':40}]
  charttable = charttabletest()
  chart = DateLineChart(charttable, xcolumn, ycolumns, ylabels, newattribs)
  open("test.png", "wb").write(chart.gethtml())

if __name__ == "__main__":
  test()