1
2
3
4
5
6
7
8
9
10
11
12
13 """
14 GChartWrapper - Google Chart API Wrapper
15
16 The wrapper can render the URL of the Google chart based on your parameters.
17 With the chart you can render an HTML img tag to insert into webpages on the fly,
18 show it directly in a webbrowser, or save the chart PNG to disk. New versions
19 can generate PIL PngImage instances.
20
21 Example
22
23 >>> G = GChart('lc',['simpleisbetterthancomplexcomplexisbetterthancomplicated'])
24 >>> G.title('The Zen of Python','00cc00',36)
25 >>> G.color('00cc00')
26 >>> G
27 '''http://chart.apis.google.com/chart?
28 chd=s:simpleisbetterthancomplexcomplexisbetterthancomplicated
29 &chco=00cc00
30 &chts=00cc00,36
31 &chs=300x150
32 &cht=lc
33 &chtt=The+Zen+of+Python'''
34 >>> G.image() # PIL instance
35 <PngImagePlugin.PngImageFile instance at 0xb79fe2ac>
36 >>> G.show() # Webbrowser open
37 True
38 >>> G.save('tmp.png') # Save to disk
39 'tmp.png'
40
41 See tests.py for unit test and other examples
42 """
43 __all__ = ['Sparkline', 'Map', 'HorizontalBarStack', 'VerticalBarStack', 'QRCode',
44 'Line', 'GChart', 'HorizontalBarGroup', 'Scatter', 'Pie3D', 'Pie', 'Meter',
45 'Radar', 'VerticalBarGroup', 'LineXY', 'Venn', 'PieC','Pin','Text','Note','Bubble']
46 __version__ = 0.7
47 from GChartWrapper.constants import *
48 from GChartWrapper.constants import _print
49 from GChartWrapper.encoding import Encoder
50 from webbrowser import open as webopen
51 from copy import copy
52
53 try:
54 from sha import new as new_sha
55 except ImportError:
56 from hashlib import sha1
58 return sha1(bytes(astr,'utf-8'))
59
65
67 args = list(args)
68 for i in indexes:
69 args[i] = lookup_color(args[i])
70 return args
71
73 """
74 Abstract class for all dictionary operations
75 """
77 self.data = dict(*args, **kwargs)
78 - def __repr__(self): return '<GChartWrapper.%s>'%self.__class__.__name__
79 - def __cmp__(self, dict): return cmp(self.data, dict)
80 - def __len__(self): return len(self.data)
82 if key in self.data:
83 return self.data[key]
84 raise KeyError(key)
85 - def __setitem__(self, key, item): self.data[key] = item
89 for key in self.keys(): yield key
95 if kwargs: self.data.update(kwargs)
96 - def pop(self, key, *args): return self.data.pop(key, *args)
98
100 """
101 Axes attribute dictionary storage
102
103 Use this class via GChart(...).axes
104 Methods are taken one at a time, like so:
105
106 >>> G.axes.type('xy')
107 >>> G.axes.label('Label1') # X Axis
108 >>> G.axes.label('Label2') # Y Axis
109 """
111 self.parent = parent
112 self.labels,self.positions,self.ranges,self.styles = [],[],[],[]
113 self.data = {}
114 Dict.__init__(self)
115
116 - def type(self, atype):
117 """
118 Define the type of axes you wish to use
119 atype must be one of x,t,y,r
120 call the rest of the axes functions in the corresponding order that you declare the type
121 APIPARAM: chxt
122 """
123 for char in atype:
124 assert char in 'xtyr', 'Invalid axes type: %s'%char
125 if not ',' in atype:
126 atype = ','.join(atype)
127 self.data['chxt'] = atype
128 return self.parent
129
131 """
132 Label each axes one at a time
133 args are of the form <label 1>,...,<label n>
134 APIPARAM: chxl
135 """
136 label = '|'.join(map(str,args))
137 id = len(self.labels)
138 self.labels.append( str('%d:|%s'%(id,label)).replace('None','') )
139 return self.parent
140
142 """
143 Set the label position of each axis, one at a time
144 args are of the form <label position 1>,...,<label position n>
145 APIPARAM: chxp
146 """
147 position = ','.join(map(str,args))
148 id = len(self.positions)
149 self.positions.append( str('%d,%s'%(id,position)).replace('None','') )
150 return self.parent
151
153 """
154 Set the range of each axis, one at a time
155 args are of the form <start of range>,<end of range>
156 APIPARAM: chxr
157 """
158 self.ranges.append('%d,%s,%s'%(len(self.ranges), args[0], args[1]))
159 return self.parent
160
162 """
163 Add style to your axis, one at a time
164 args are of the form <axis color>,<font size>,<alignment>,<drawing control>,<tick mark color>
165 APIPARAM: chxs
166 """
167 id = str(len(self.styles))
168 args = color_args(args, 0)
169 self.styles.append(','.join([id]+list(map(str,args))))
170 return self.parent
171
173 """Render the axes data into the dict data"""
174 if self.labels:
175 self.data['chxl'] = '|'.join(self.labels)
176 if self.styles:
177 self.data['chxs'] = '|'.join(self.styles)
178 if self.positions:
179 self.data['chxp'] = '|'.join(self.positions)
180 if self.ranges:
181 self.data['chxr'] = '|'.join(self.ranges)
182 return self.data
183
185 """Main chart class
186
187 Chart type must be valid for cht parameter
188 Dataset can be any python iterable and be multi dimensional
189 Kwargs will be put into chart API params if valid"""
190 - def __init__(self, ctype=None, dataset=[], **kwargs):
191 self.lines,self.fills,self.markers,self.scales = [],[],[],[]
192 self._geo,self._ld = '',''
193 self._dataset = dataset
194 self.data = {}
195 Dict.__init__(self)
196 if ctype:
197 self.check_type(ctype)
198 self.data['cht'] = ctype
199 self._encoding = kwargs.pop('encoding', None)
200 self._scale = kwargs.pop('scale', None)
201 self.apiurl = kwargs.pop('apiurl', APIURL)
202 for k,v in kwargs.items():
203 assert k in APIPARAMS, 'Invalid chart parameter: %s'%k
204 self.data[k] = v
205 self.axes = Axes(self)
206
207
208
209
210 - def map(self, geo, country_codes):
211 """
212 Creates a map of the defined geography with the given country/state codes
213 Geography choices are africa, asia, europe, middle_east, south_america, and world
214 ISO country codes can be found at http://code.google.com/apis/chart/isocodes.html
215 US state codes can be found at http://code.google.com/apis/chart/statecodes.html
216 APIPARAMS: chtm & chld
217 """
218 assert geo in GEO, 'Geograpic area %s not recognized'%geo
219 self._geo = geo
220 self._ld = country_codes
221 return self
222
224 """
225 Just used in QRCode for the moment
226 args are error_correction,margin_size
227 APIPARAM: chld
228 """
229 assert args[0].lower() in 'lmqh', 'Unknown EC level %s'%level
230 self.data['chld'] = '%s|%s'%args
231 return self
232
233 - def bar(self, *args):
234 """
235 For bar charts, specify bar thickness and spacing with the args
236 args are <bar width>,<space between bars>,<space between groups>
237 bar width can be relative or absolute, see the official doc
238 APIPARAM: chbh
239 """
240 self.data['chbh'] = ','.join(map(str,args))
241 return self
242
244 """
245 Specifies the encoding to be used for the Encoder
246 Must be one of 'simple','text', or 'extended'
247 """
248 self._encoding = encoding
249 return self
250
252 """
253 Output encoding to use for QRCode encoding
254 Must be one of 'Shift_JIS','UTF-8', or 'ISO-8859-1'
255 APIPARAM: choe
256 """
257 assert encoding in ('Shift_JIS','UTF-8','ISO-8859-1'),\
258 'Unknown encoding %s'%encoding
259 self.data['choe'] = encoding
260 return self
261
263 """
264 Scales the data down to the given size
265 args must be in the form of <min>,<max>
266 will only work with text encoding
267 APIPARAM: chds
268 """
269 self._scale = ['%s,%s'%args]
270 return self
271
273 """
274 Update the chart's dataset, can be two dimensional or contain string data
275 """
276 self._dataset = data
277 return self
278
280 """
281 Defines markers one at a time for your graph
282 args are of the form <marker type>,<color>,<data set index>,<data point>,<size>,<priority>
283 see the official developers doc for the complete spec
284 APIPARAM: chm
285 """
286 if len(args[0]) == 1:
287 assert args[0] in MARKERS, 'Invalid marker type: %s'%args[0]
288 assert len(args) <= 6, 'Incorrect arguments %s'%str(args)
289 args = color_args(args, 1)
290 self.markers.append(','.join(map(str,args)) )
291 return self
292
293 - def line(self, *args):
294 """
295 Called one at a time for each dataset
296 args are of the form <data set n line thickness>,<length of line segment>,<length of blank segment>
297 APIPARAM: chls
298 """
299 self.lines.append(','.join(['%.1f'%x for x in map(float,args)]))
300 return self
301
302 - def fill(self, *args):
303 """
304 Apply a solid fill to your chart
305 args are of the form <fill type>,<fill style>,...
306 fill type must be one of c,bg,a
307 fill style must be one of s,lg,ls
308 the rest of the args refer to the particular style, refer to the official doc
309 APIPARAM: chf
310 """
311 assert args[0] in ('c','bg','a'), 'Fill type must be bg/c/a not %s'%args[0]
312 assert args[1] in ('s','lg','ls'), 'Fill style must be s/lg/ls not %s'%args[1]
313 if len(args) == 3:
314 args = color_args(args, 2)
315 else:
316 args = color_args(args, 3,5)
317 self.fills.append(','.join(map(str,args)))
318 return self
319
320 - def grid(self, *args):
321 """
322 Apply a grid to your chart
323 args are of the form <x axis step size>,<y axis step size>,<length of line segment>,<length of blank segment>
324 APIPARAM: chg
325 """
326 grids = map(str,map(float,args))
327 self.data['chg'] = ','.join(grids).replace('None','')
328 return self
329
331 """
332 Add a color for each dataset
333 args are of the form <color 1>,...<color n>
334 APIPARAM: chco
335 """
336 args = color_args(args, *range(len(args)))
337 self.data['chco'] = ','.join(args)
338 return self
339
340 - def type(self, type):
341 """
342 Set the chart type, either Google API type or regular name
343 APIPARAM: cht
344 """
345 self.data['cht'] = self.check_type(str(type))
346 return self
347
349 """
350 Add a simple label to your chart
351 call each time for each dataset
352 APIPARAM: chl
353 """
354 if self.data['cht'] == 'qr':
355 self.data['chl'] = ''.join(map(str,args))
356 else:
357 self.data['chl'] = '|'.join(map(str,args))
358 return self
359
361 """
362 Add a legend to your chart
363 call each time for each dataset
364 APIPARAM: chdl
365 """
366 self.data['chdl'] = '|'.join(args)
367 return self
368
370 """
371 Define a position for your legend to occupy
372 pos must be one of b,t,r,l
373 APIPARAM: chdlp
374 """
375 assert pos in 'btrl', 'Unknown legend position: %s'%pos
376 self.data['chdlp'] = str(pos)
377 return self
378
379 - def title(self, title, *args):
380 """
381 Add a title to your chart
382 args are optional style params of the form <color>,<font size>
383 APIPARAMS: chtt,chts
384 """
385 self.data['chtt'] = title
386 if args:
387 args = color_args(args, 0)
388 self.data['chts'] = ','.join(map(str,args))
389 return self
390
391 - def size(self,*args):
392 """
393 Set the size of the chart, args are width,height and can be tuple
394 APIPARAM: chs
395 """
396 if len(args) == 2:
397 x,y = map(int,args)
398 else:
399 x,y = map(int,args[0])
400 self.check_size(x,y)
401 self.data['chs'] = '%dx%d'%(x,y)
402 return self
403
405 """
406 Set the margins of your chart
407 args are of the form <left margin>,<right margin>,<top margin>,<bottom margin>[,<legend width>,<legend height>]
408 the legend args are optional
409 APIPARAM: chma
410 """
411 if len(args) == 4:
412 self.data['chma'] = ','.join(map(str,args))
413 elif len(args) == 6:
414 self.data['chma'] = ','.join(map(str,args[:4]))+'|'+','.join(map(str,args[4:]))
415 else:
416 raise ValueError('Margin arguments must be either 4 or 6 items')
417
419 """
420 Renders the chart context and axes into the dict data
421 """
422 self.data.update(self.axes.render())
423 encoder = Encoder(self._encoding)
424 if not 'chs' in self.data:
425 self.data['chs'] = '300x150'
426 else:
427 size = self.data['chs'].split('x')
428 assert len(size) == 2, 'Invalid size, must be in the format WxH'
429 self.check_size(*map(int,size))
430 assert 'cht' in self.data, 'No chart type defined, use type method'
431 self.data['cht'] = self.check_type(self.data['cht'])
432 if ('any' in dir(self._dataset) and self._dataset.any()) or self._dataset:
433 self.data['chd'] = encoder.encode(self._dataset)
434 elif not 'choe' in self.data:
435 assert 'chd' in self.data, 'You must have a dataset, or use chd'
436 if self._scale:
437 assert self.data['chd'].startswith('t:'), 'You must use text encoding with chds'
438 self.data['chds'] = ','.join(self._scale)
439 if self._geo and self._ld:
440 self.data['chtm'] = self._geo
441 self.data['chld'] = self._ld
442 if self.lines:
443 self.data['chls'] = '|'.join(self.lines)
444 if self.markers:
445 self.data['chm'] = '|'.join(self.markers)
446 if self.fills:
447 self.data['chf'] = '|'.join(self.fills)
448
449
450
451
453 """
454 Make sure the chart size fits the standards
455 """
456 assert x <= 1000, 'Width larger than 1,000'
457 assert y <= 1000, 'Height larger than 1,000'
458 assert x*y <= 300000, 'Resolution larger than 300,000'
459
461 """Check to see if the type is either in TYPES or fits type name
462
463 Returns proper type
464 """
465 if type in TYPES:
466 return type
467 tdict = dict(zip(TYPES,TYPES))
468 tdict['line'] = 'lc'
469 tdict['bar'] = 'bvs'
470 tdict['pie'] = 'p'
471 tdict['venn'] = 'v'
472 tdict['scater'] = 's'
473 assert type in tdict, 'Invalid chart type: %s'%type
474 return tdict[type]
475
476
477
478
480 """
481 Gets the name of the chart, if it exists
482 """
483 return self.data.get('chtt','')
484
486 """
487 Returns the decoded dataset from chd param
488 """
489
490 return Encoder(self._encoding).decode(self.data['chd'])
491
493 return ('%s=%s'%(k,QUOTE(v)) for k,v in self.data.items() if v)
494
496 """
497 Returns the rendered URL of the chart
498 """
499 self.render()
500 return self.apiurl + '&'.join(self._parts()).replace(' ','+')
501
503 """
504 Uses str, AND enforces replacing spaces w/ pluses
505 """
506 return self.__str__()
507
508 - def show(self, *args, **kwargs):
509 """
510 Shows the chart URL in a webbrowser
511
512 Other arguments passed to webbrowser.open
513 """
514 return webopen(str(self), *args, **kwargs)
515
516 - def save(self, fname=None):
517 """
518 Download the chart from the URL into a filename as a PNG
519
520 The filename defaults to the chart title (chtt) if any
521 """
522 if not fname:
523 fname = self.getname()
524 assert fname != None, 'You must specify a filename to save to'
525 if not fname.endswith('.png'):
526 fname += '.png'
527 try:
528 urlretrieve(str(self), fname)
529 except:
530 raise IOError('Problem saving chart to file: %s'%fname)
531 return fname
532
533 - def img(self, **kwargs):
534 """
535 Returns an XHTML <img/> tag of the chart
536
537 kwargs can be other img tag attributes, which are strictly enforced
538 uses strict escaping on the url, necessary for proper XHTML
539 """
540 safe = 'src="%s" ' % self.url().replace('&','&').replace('<', '<')\
541 .replace('>', '>').replace('"', '"').replace( "'", ''')
542 for item in kwargs.items():
543 if not item[0] in IMGATTRS:
544 raise AttributeError('Invalid img tag attribute: %s'%item[0])
545 safe += '%s="%s" '%item
546 return '<img %s/>'%safe
547
549 """
550 Grabs readable PNG file pointer
551 """
552 return urlopen(self.__str__())
553
555 """
556 Returns a PngImageFile instance of the chart
557
558 You must have PIL installed for this to work
559 """
560 try:
561 import Image
562 except ImportError:
563 raise ImportError('You must install PIL to fetch image objects')
564 try:
565 from cStringIO import StringIO
566 except ImportError:
567 from StringIO import StringIO
568 return Image.open(StringIO(self.urlopen().read()))
569
571 """
572 Writes out PNG image data in chunks to file pointer fp
573
574 fp must support w or wb
575 """
576 urlfp = self.urlopen().fp
577 while 1:
578 try:
579 fp.write(urlfp.next())
580 except StopIteration:
581 return
582
584 """
585 Returns the SHA1 hexdigest of the chart URL param parts
586
587 good for unittesting...
588 """
589 self.render()
590 items = [(k,self.data[k]) for k in sorted(self.data)]
591 return new_sha(str(items)).hexdigest()
592
593
594
600
602 - def __init__(self, content='', **kwargs):
603 kwargs['choe'] = 'UTF-8'
604 if type(content) in (type(''),):
605 kwargs['chl'] = QUOTE(content)
606 else:
607 kwargs['chl'] = QUOTE(content[0])
608 GChart.__init__(self, 'qr', None, **kwargs)
609
613
617
621
625
629
633
637
641
645
649
653
657
661
665
666
667
668
670 - def render(self): pass
671 - def __init__(self, *args):
672 GChart.__init__(self)
673 self.data['chst'] = 'd_text_outline'
674 args = list(map(str, color_args(args, 0, 3)))
675 assert args[2] in 'lrh', 'Invalid text alignment'
676 assert args[4] in '_b', 'Invalid font style'
677 self.data['chld'] = '|'.join(args)\
678 .replace('\r\n','|').replace('\r','|').replace('\n','|').replace(' ','+')
679
683 GChart.__init__(self)
684 assert type in PIN_TYPES, 'Invalid type'
685 if type == "pin_letter":
686 args = color_args(args, 1,2)
687 elif type == 'pin_icon':
688 args = color_args(args, 1)
689 assert args[0] in PIN_ICONS, 'Invalid icon name'
690 elif type == 'xpin_letter':
691 args = color_args(args, 2,3,4)
692 assert args[0] in PIN_SHAPES, 'Invalid pin shape'
693 if not args[0].startswith('pin_'):
694 args[0] = 'pin_%s'%args[0]
695 elif type == 'xpin_icon':
696 args = color_args(args, 2,3)
697 assert args[0] in PIN_SHAPES, 'Invalid pin shape'
698 if not args[0].startswith('pin_'):
699 args[0] = 'pin_%s'%args[0]
700 assert args[1] in PIN_ICONS, 'Invalid icon name'
701 elif type == 'spin':
702 args = color_args(args, 2)
703 self.data['chst'] = 'd_map_%s'%type
704 self.data['chld'] = '|'.join(map(str, args))\
705 .replace('\r\n','|').replace('\r','|').replace('\n','|').replace(' ','+')
707 image = copy(self)
708 chsts = self.data['chst'].split('_')
709 chsts[-1] = 'shadow'
710 image.data['chst'] = '_'.join(chsts)
711 return image
712
716 GChart.__init__(self)
717 assert args[0] in NOTE_TYPES,'Invalid note type'
718 assert args[1] in NOTE_IMAGES,'Invalid note image'
719 if args[0].find('note')>-1:
720 self.data['chst'] = 'd_f%s'%args[0]
721 args = color_args(args, 3)
722 else:
723 self.data['chst'] = 'd_%s'%args[0]
724 assert args[2] in NOTE_WEATHERS,'Invalid weather'
725 args = args[1:]
726 self.data['chld'] = '|'.join(map(str, args))\
727 .replace('\r\n','|').replace('\r','|').replace('\n','|').replace(' ','+')
728
732 GChart.__init__(self)
733 assert type in BUBBLE_TYPES, 'Invalid type'
734 if type in ('icon_text_small','icon_text_big'):
735 args = color_args(args, 3,4)
736 assert args[0] in BUBBLE_SICONS,'Invalid icon type'
737 elif type == 'icon_texts_big':
738 args = color_args(args, 2,3)
739 assert args[0] in BUBBLE_LICONS,'Invalid icon type'
740 elif type == 'texts_big':
741 args = color_args(args, 1,2)
742 self.data['chst'] = 'd_bubble_%s'%type
743 self.data['chld'] = '|'.join(map(str, args))\
744 .replace('\r\n','|').replace('\r','|').replace('\n','|').replace(' ','+')
746 image = copy(self)
747 image.data['chst'] = '%s_shadow'%self.data['chst']
748 return image
749
750 if __name__=='__main__':
751 from tests import test
752 test()
753