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 >>> str(G)
27 'http://chart.apis.google.com/chart?
28 chd=e:simpleisbetterthancomplexcomplexisbetterthancomplicated
29 &chs=300x150
30 &cht=lc
31 &chtt=The+Zen+of+Python'
32 >>> G.image() # PIL instance
33 <PngImagePlugin.PngImageFile instance at ...>
34 >>> 1#G.show() # Webbrowser open
35 True
36 >>> G.save('tmp.png') # Save to disk
37 'tmp.png'
38
39 See tests.py for unit test and other examples
40 """
41 from GChartWrapper.constants import *
42 from GChartWrapper.encoding import Encoder
43 from webbrowser import open as webopen
44 from copy import copy
45
46 try:
47 from sha import new as new_sha
48 except ImportError:
49 from hashlib import sha1
50 new_sha = lambda s: sha1(bytes(s,'utf-8'))
64
66 """
67 Color a list of arguments on particular indexes
68
69 >>> c = color_args([None,'blue'], 1)
70 >>> c.next()
71 None
72 >>> c.next()
73 '0000FF'
74 """
75 for i,arg in enumerate(args):
76 if i in indexes:
77 yield lookup_color(arg)
78 else:
79 yield arg
80
83 """
84 Axes attribute dictionary storage
85
86 Use this class via GChart(...).axes
87 Methods are taken one at a time, like so:
88
89 >>> G = GChart()
90 >>> G.axes.type('xy')
91 {}
92 >>> G.axes.label(1,'Label1') # X Axis
93 {}
94 >>> G.axes.label(2,'Label2') # Y Axis
95 {}
96 """
98 self.parent = parent
99 self.data = {'ticks':[],'labels':[],'positions':[],
100 'ranges':[],'styles':[]}
101 dict.__init__(self)
102
103 - def tick(self, index, length):
104 """
105 Add tick marks in order of axes by width
106 APIPARAM: chxtc <axis index>,<length of tick mark>
107 """
108 assert int(length) <= 25, 'Width cannot be more than 25'
109 self.data['ticks'].append('%s,%d'%(index,length))
110 return self.parent
111
112 - def type(self, atype):
113 """
114 Define the type of axes you wish to use
115 atype must be one of x,t,y,r
116 APIPARAM: chxt
117 """
118 for char in atype:
119 assert char in 'xtyr', 'Invalid axes type: %s'%char
120 if not ',' in atype:
121 atype = ','.join(atype)
122 self['chxt'] = atype
123 return self.parent
124 __call__ = type
125
126 - def label(self, index, *args):
127 """
128 Label each axes one at a time
129 args are of the form <label 1>,...,<label n>
130 APIPARAM: chxl
131 """
132 self.data['labels'].append(
133 str('%s:|%s'%(index, '|'.join(map(str,args)) )).replace('None','')
134 )
135 return self.parent
136
138 """
139 Set the label position of each axis, one at a time
140 args are of the form <label position 1>,...,<label position n>
141 APIPARAM: chxp
142 """
143 self.data['positions'].append(
144 str('%s,%s'%(index, ','.join(map(str,args)) )).replace('None','')
145 )
146 return self.parent
147
148 - def range(self, index, *args):
149 """
150 Set the range of each axis, one at a time
151 args are of the form <start of range>,<end of range>,<interval>
152 APIPARAM: chxr
153 """
154 self.data['ranges'].append('%s,%s'%(index,
155 ','.join(map(smart_str, args))))
156 return self.parent
157
158 - def style(self, index, *args):
159 """
160 Add style to your axis, one at a time
161 args are of the form::
162 <axis color>,
163 <font size>,
164 <alignment>,
165 <drawing control>,
166 <tick mark color>
167 APIPARAM: chxs
168 """
169 args = color_args(args, 0)
170 self.data['styles'].append(
171 ','.join([str(index)]+list(map(str,args)))
172 )
173 return self.parent
174
176 """Render the axes data into the dict data"""
177 for opt,values in self.data.items():
178 if opt == 'ticks':
179 self['chxtc'] = '|'.join(values)
180 else:
181 self['chx%s'%opt[0]] = '|'.join(values)
182 return self
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._series = kwargs.pop('series',None)
192 self.lines,self.fills,self.markers,self.scales = [],[],[],[]
193 self._geo,self._ld = '',''
194 self._dataset = dataset
195 dict.__init__(self)
196 if ctype:
197 self.check_type(ctype)
198 self['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[k] = v
205 self.axes = Axes(self)
206
207 @classmethod
209 """
210 Reverse a chart URL or dict into a GChart instance
211
212 >>> G = GChart.fromurl('http://chart.apis.google.com/chart?...')
213 >>> G
214 <GChartWrapper.GChart instance at...>
215 >>> G.image().save('chart.jpg','JPEG')
216 """
217 if isinstance(qs, dict):
218 return cls(**qs)
219 return cls(**dict(parse_qsl(qs[qs.index('?')+1:])))
220
221
222
223
224 - def map(self, geo, country_codes):
225 """
226 Creates a map of the defined geography with the given country/state codes
227 Geography choices are africa, asia, europe, middle_east, south_america, and world
228 ISO country codes can be found at http://code.google.com/apis/chart/isocodes.html
229 US state codes can be found at http://code.google.com/apis/chart/statecodes.html
230 APIPARAMS: chtm & chld
231 """
232 assert geo in GEO, 'Geograpic area %s not recognized'%geo
233 self._geo = geo
234 self._ld = country_codes
235 return self
236
238 """
239 Just used in QRCode for the moment
240 args are error_correction,margin_size
241 APIPARAM: chld
242 """
243 assert args[0].lower() in 'lmqh', 'Unknown EC level %s'%level
244 self['chld'] = '%s|%s'%args
245 return self
246
247 - def bar(self, *args):
248 """
249 For bar charts, specify bar thickness and spacing with the args
250 args are <bar width>,<space between bars>,<space between groups>
251 bar width can be relative or absolute, see the official doc
252 APIPARAM: chbh
253 """
254 self['chbh'] = ','.join(map(str,args))
255 return self
256
258 """
259 Specifies the encoding to be used for the Encoder
260 Must be one of 'simple','text', or 'extended'
261 """
262 self._encoding = arg
263 return self
264
266 """
267 Output encoding to use for QRCode encoding
268 Must be one of 'Shift_JIS','UTF-8', or 'ISO-8859-1'
269 APIPARAM: choe
270 """
271 assert encoding in ('Shift_JIS','UTF-8','ISO-8859-1'),\
272 'Unknown encoding %s'%encoding
273 self['choe'] = encoding
274 return self
275
277 """
278 Scales the data down to the given size
279 args must be of the form::
280 <data set 1 minimum value>,
281 <data set 1 maximum value>,
282 <data set n minimum value>,
283 <data set n maximum value>
284 will only work with text encoding!
285 APIPARAM: chds
286 """
287 self._scale = [','.join(map(smart_str, args))]
288 return self
289
290 - def dataset(self, data, series=''):
291 """
292 Update the chart's dataset, can be two dimensional or contain string data
293 """
294 self._dataset = data
295 self._series = series
296 return self
297
299 """
300 Defines markers one at a time for your graph
301 args are of the form::
302 <marker type>,
303 <color>,
304 <data set index>,
305 <data point>,
306 <size>,
307 <priority>
308 see the official developers doc for the complete spec
309 APIPARAM: chm
310 """
311 if len(args[0]) == 1:
312 assert args[0] in MARKERS, 'Invalid marker type: %s'%args[0]
313 assert len(args) <= 6, 'Incorrect arguments %s'%str(args)
314 args = color_args(args, 1)
315 self.markers.append(','.join(map(str,args)) )
316 return self
317
318 - def line(self, *args):
319 """
320 Called one at a time for each dataset
321 args are of the form::
322 <data set n line thickness>,
323 <length of line segment>,
324 <length of blank segment>
325 APIPARAM: chls
326 """
327 self.lines.append(','.join(['%.1f'%x for x in map(float,args)]))
328 return self
329
330 - def fill(self, *args):
331 """
332 Apply a solid fill to your chart
333 args are of the form <fill type>,<fill style>,...
334 fill type must be one of c,bg,a
335 fill style must be one of s,lg,ls
336 the rest of the args refer to the particular style
337 APIPARAM: chf
338 """
339 a,b = args[:2]
340 assert a in ('c','bg','a'), 'Fill type must be bg/c/a not %s'%a
341 assert b in ('s','lg','ls'), 'Fill style must be s/lg/ls not %s'%b
342 if len(args) == 3:
343 args = color_args(args, 2)
344 else:
345 args = color_args(args, 3,5)
346 self.fills.append(','.join(map(str,args)))
347 return self
348
349 - def grid(self, *args):
350 """
351 Apply a grid to your chart
352 args are of the form::
353 <x axis step size>,
354 <y axis step size>,
355 <length of line segment>,
356 <length of blank segment>
357 <x offset>,
358 <y offset>
359 APIPARAM: chg
360 """
361 grids = map(str,map(float,args))
362 self['chg'] = ','.join(grids).replace('None','')
363 return self
364
366 """
367 Add a color for each dataset
368 args are of the form <color 1>,...<color n>
369 APIPARAM: chco
370 """
371 args = color_args(args, *range(len(args)))
372 self['chco'] = ','.join(args)
373 return self
374
375 - def type(self, type):
376 """
377 Set the chart type, either Google API type or regular name
378 APIPARAM: cht
379 """
380 self['cht'] = self.check_type(str(type))
381 return self
382
384 """
385 Add a simple label to your chart
386 call each time for each dataset
387 APIPARAM: chl
388 """
389 if self['cht'] == 'qr':
390 self['chl'] = ''.join(map(str,args))
391 else:
392 self['chl'] = '|'.join(map(str,args))
393 return self
394
396 """
397 Add a legend to your chart
398 call each time for each dataset
399 APIPARAM: chdl
400 """
401 self['chdl'] = '|'.join(args)
402 return self
403
405 """
406 Define a position for your legend to occupy
407 APIPARAM: chdlp
408 """
409 assert pos in LEGEND_POSITIONS, 'Unknown legend position: %s'%pos
410 self['chdlp'] = str(pos)
411 return self
412
413 - def title(self, title, *args):
414 """
415 Add a title to your chart
416 args are optional style params of the form <color>,<font size>
417 APIPARAMS: chtt,chts
418 """
419 self['chtt'] = title
420 if args:
421 args = color_args(args, 0)
422 self['chts'] = ','.join(map(str,args))
423 return self
424
425 - def size(self,*args):
426 """
427 Set the size of the chart, args are width,height and can be tuple
428 APIPARAM: chs
429 """
430 if len(args) == 2:
431 x,y = map(int,args)
432 else:
433 x,y = map(int,args[0])
434 self.check_size(x,y)
435 self['chs'] = '%dx%d'%(x,y)
436 return self
437
439 """
440 Set the margins of your chart
441 args are of the form::
442 <left margin>,
443 <right margin>,
444 <top margin>,
445 <bottom margin>
446 [,<legend width>,<legend height>]
447 the legend args are optional
448 APIPARAM: chma
449 """
450 if len(args) == 4:
451 self['chma'] = ','.join(map(str,args))
452 elif len(args) == 6:
453 self['chma'] = ','.join(
454 map(str,args[:4]))+'|'+','.join(map(str,args[4:]))
455 else:
456 raise ValueError('Margin arguments must be either 4 or 6 items')
457 return self
458
460 """
461 Set the chart's orientation for pie charts
462 angle is <angle in radians>
463 APIPARAM: chp
464 """
465 self['chp'] = '%f'%angle
466 return self
467 position = orientation
468
470 """
471 Renders the chart context and axes into the dict data
472 """
473 self.update(self.axes.render())
474 encoder = Encoder(self._encoding, None, self._series)
475 if not 'chs' in self:
476 self['chs'] = '300x150'
477 else:
478 size = self['chs'].split('x')
479 assert len(size) == 2, 'Invalid size, must be in the format WxH'
480 self.check_size(*map(int,size))
481 assert 'cht' in self, 'No chart type defined, use type method'
482 self['cht'] = self.check_type(self['cht'])
483 if ('any' in dir(self._dataset) and self._dataset.any()) or self._dataset:
484 self['chd'] = encoder.encode(self._dataset)
485 elif not 'choe' in self:
486 assert 'chd' in self, 'You must have a dataset, or use chd'
487 if self._scale:
488 assert self['chd'].startswith('t'),\
489 'You must use text encoding with chds'
490 self['chds'] = ','.join(self._scale)
491 if self._geo and self._ld:
492 self['chtm'] = self._geo
493 self['chld'] = self._ld
494 if self.lines:
495 self['chls'] = '|'.join(self.lines)
496 if self.markers:
497 self['chm'] = '|'.join(self.markers)
498 if self.fills:
499 self['chf'] = '|'.join(self.fills)
500
501
502
503
505 """
506 Make sure the chart size fits the standards
507 """
508 assert x <= 1000, 'Width larger than 1,000'
509 assert y <= 1000, 'Height larger than 1,000'
510 assert x*y <= 300000, 'Resolution larger than 300,000'
511
513 """Check to see if the type is either in TYPES or fits type name
514
515 Returns proper type
516 """
517 if type in TYPES:
518 return type
519 tdict = dict(zip(TYPES,TYPES))
520 tdict.update({
521 'line': 'lc',
522 'bar': 'bvs',
523 'pie': 'p',
524 'venn': 'v',
525 'scater': 's',
526 'radar': 'r',
527 'meter': 'gom',
528 })
529 assert type in tdict, 'Invalid chart type: %s'%type
530 return tdict[type]
531
532
533
534
536 """
537 Gets the name of the chart, if it exists
538 """
539 return self.get('chtt','')
540
542 """
543 Returns the decoded dataset from chd param
544 """
545
546 return Encoder(self._encoding).decode(self['chd'])
547
549 return ('%s=%s'%(k,smart_str(v)) for k,v in self.items() if v)
550
553
555 return '<GChartWrapper.%s %s>'%(self.__class__.__name__,self)
556
557 @property
559 """
560 Returns the rendered URL of the chart
561 """
562 self.render()
563 return self.apiurl + '&'.join(self._parts()).replace(' ','+')
564
565
566 - def show(self, *args, **kwargs):
567 """
568 Shows the chart URL in a webbrowser
569
570 Other arguments passed to webbrowser.open
571 """
572 return webopen(str(self), *args, **kwargs)
573
574 - def save(self, fname=None):
575 """
576 Download the chart from the URL into a filename as a PNG
577
578 The filename defaults to the chart title (chtt) if any
579 """
580 if not fname:
581 fname = self.getname()
582 assert fname != None, 'You must specify a filename to save to'
583 if not fname.endswith('.png'):
584 fname += '.png'
585 try:
586 urlretrieve(self.url, fname)
587 except Exception:
588 raise IOError('Problem saving %s to file'%fname)
589 return fname
590
591 - def img(self, **kwargs):
592 """
593 Returns an XHTML <img/> tag of the chart
594
595 kwargs can be other img tag attributes, which are strictly enforced
596 uses strict escaping on the url, necessary for proper XHTML
597 """
598 safe = 'src="%s" ' % self.url.replace('&','&').replace('<', '<')\
599 .replace('>', '>').replace('"', '"').replace( "'", ''')
600 for item in kwargs.items():
601 if not item[0] in IMGATTRS:
602 raise AttributeError('Invalid img tag attribute: %s'%item[0])
603 safe += '%s="%s" '%item
604 return '<img %s/>'%safe
605
607 """
608 Grabs readable PNG file pointer
609 """
610 req = Request(str(self))
611 try:
612 return urlopen(req)
613 except HTTPError:
614 _print('The server couldn\'t fulfill the request.')
615 except URLError:
616 _print('We failed to reach a server.')
617
619 """
620 Returns a PngImageFile instance of the chart
621
622 You must have PIL installed for this to work
623 """
624 try:
625 try:
626 import Image
627 except ImportError:
628 from PIL import Image
629 except ImportError:
630 raise ImportError('You must install PIL to fetch image objects')
631 try:
632 from cStringIO import StringIO
633 except ImportError:
634 from StringIO import StringIO
635 return Image.open(StringIO(self.urlopen().read()))
636
638 """
639 Writes out PNG image data in chunks to file pointer fp
640
641 fp must support w or wb
642 """
643 urlfp = self.urlopen().fp
644 while 1:
645 try:
646 fp.write(urlfp.next())
647 except StopIteration:
648 return
649
651 """
652 Returns the unique SHA1 hexdigest of the chart URL param parts
653
654 good for unittesting...
655 """
656 self.render()
657 return new_sha(''.join(sorted(self._parts()))).hexdigest()
658
662 - def __init__(self, content='', **kwargs):
663 kwargs['choe'] = 'UTF-8'
664 if isinstance(content, str):
665 kwargs['chl'] = quote(content).replace('%0A','\n')
666 else:
667 kwargs['chl'] = quote(content[0]).replace('%0A','\n')
668 GChart.__init__(self, 'qr', None, **kwargs)
669
675
676 -class Meter(_AbstractGChart): o,t = {'encoding':'text'},'gom'
677 -class Line(_AbstractGChart): t = 'lc'
678 -class LineXY(_AbstractGChart): t = 'lxy'
683 -class Pie(_AbstractGChart): t = 'p'
684 -class Pie3D(_AbstractGChart): t = 'p3'
685 -class Venn(_AbstractGChart): t = 'v'
688 -class Radar(_AbstractGChart): t = 'r'
690 -class Map(_AbstractGChart): t = 't'
691 -class PieC(_AbstractGChart): t = 'pc'
692
693
694
695
696 -class Text(GChart):
697 - def render(self): pass
698 - def __init__(self, *args):
699 GChart.__init__(self)
700 self['chst'] = 'd_text_outline'
701 args = list(map(str, color_args(args, 0, 3)))
702 assert args[2] in 'lrh', 'Invalid text alignment'
703 assert args[4] in '_b', 'Invalid font style'
704 self['chld'] = '|'.join(args).replace('\r\n','|')\
705 .replace('\r','|').replace('\n','|').replace(' ','+')
706
710 GChart.__init__(self)
711 assert ptype in PIN_TYPES, 'Invalid type'
712 if ptype == "pin_letter":
713 args = color_args(args, 1,2)
714 elif ptype == 'pin_icon':
715 args = list(color_args(args, 1))
716 assert args[0] in PIN_ICONS, 'Invalid icon name'
717 elif ptype == 'xpin_letter':
718 args = list(color_args(args, 2,3,4))
719 assert args[0] in PIN_SHAPES, 'Invalid pin shape'
720 if not args[0].startswith('pin_'):
721 args[0] = 'pin_%s'%args[0]
722 elif ptype == 'xpin_icon':
723 args = list(color_args(args, 2,3))
724 assert args[0] in PIN_SHAPES, 'Invalid pin shape'
725 if not args[0].startswith('pin_'):
726 args[0] = 'pin_%s'%args[0]
727 assert args[1] in PIN_ICONS, 'Invalid icon name'
728 elif ptype == 'spin':
729 args = color_args(args, 2)
730 self['chst'] = 'd_map_%s'%ptype
731 self['chld'] = '|'.join(map(str, args)).replace('\r\n','|')\
732 .replace('\r','|').replace('\n','|').replace(' ','+')
734 image = copy(self)
735 chsts = self['chst'].split('_')
736 chsts[-1] = 'shadow'
737 image.data['chst'] = '_'.join(chsts)
738 return image
739
743 GChart.__init__(self)
744 assert args[0] in NOTE_TYPES,'Invalid note type'
745 assert args[1] in NOTE_IMAGES,'Invalid note image'
746 if args[0].find('note')>-1:
747 self['chst'] = 'd_f%s'%args[0]
748 args = list(color_args(args, 3))
749 else:
750 self['chst'] = 'd_%s'%args[0]
751 assert args[2] in NOTE_WEATHERS,'Invalid weather'
752 args = args[1:]
753 self['chld'] = '|'.join(map(str, args)).replace('\r\n','|')\
754 .replace('\r','|').replace('\n','|').replace(' ','+')
755
759 GChart.__init__(self)
760 assert btype in BUBBLE_TYPES, 'Invalid type'
761 if btype in ('icon_text_small','icon_text_big'):
762 args = list(color_args(args, 3,4))
763 assert args[0] in BUBBLE_SICONS,'Invalid icon type'
764 elif btype == 'icon_texts_big':
765 args = list(color_args(args, 2,3))
766 assert args[0] in BUBBLE_LICONS,'Invalid icon type'
767 elif btype == 'texts_big':
768 args = color_args(args, 1,2)
769 self['chst'] = 'd_bubble_%s'%btype
770 self['chld'] = '|'.join(map(str, args)).replace('\r\n','|')\
771 .replace('\r','|').replace('\n','|').replace(' ','+')
773 image = copy(self)
774 image.data['chst'] = '%s_shadow'%self['chst']
775 return image
776