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