Package GChartWrapper :: Module GChart'
[hide private]
[frames] | no frames]

Source Code for Module GChartWrapper.GChart'

  1  # -*- coding: utf-8 -*- 
  2  ################################################################################ 
  3  #  GChartWrapper - v0.8 
  4  #  Copyright (C) 2009  Justin Quick <justquick@gmail.com> 
  5  # 
  6  #  This program is free software; you can redistribute it and/or modify 
  7  #  it under the terms of the GNU General Public License version 3 as published 
  8  #  by the Free Software Foundation. 
  9  # 
 10  #  Thanks to anyone who does anything for this project. 
 11  #  If you have even the smallest revision, please email me at above address. 
 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')) 
51 52 -def lookup_color(color):
53 """ 54 Returns the hex color for any valid css color name 55 56 >>> lookup_color('aliceblue') 57 'F0F8FF' 58 """ 59 if color is None: return 60 color = color.lower() 61 if color in COLOR_MAP: 62 return COLOR_MAP[color] 63 return color
64
65 -def color_args(args, *indexes):
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
81 82 -class Axes(dict):
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 """
97 - def __init__(self, parent):
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
137 - def position(self, index, *args):
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
175 - def render(self):
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
184 -class GChart(dict):
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
208 - def fromurl(cls, qs):
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 # Callables 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
237 - def level_data(self, *args):
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
257 - def encoding(self, arg):
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
265 - def output_encoding(self, encoding):
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
276 - def scale(self, *args):
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
298 - def marker(self, *args):
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
365 - def color(self, *args):
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
383 - def label(self, *args):
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
395 - def legend(self, *args):
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
404 - def legend_pos(self, pos):
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
438 - def margin(self, *args):
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
459 - def orientation(self, angle):
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
469 - def render(self):
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 # Checks 503 ###################
504 - def check_size(self,x,y):
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
512 - def check_type(self, type):
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 # Convience Functions 534 #####################
535 - def getname(self):
536 """ 537 Gets the name of the chart, if it exists 538 """ 539 return self.get('chtt','')
540
541 - def getdata(self):
542 """ 543 Returns the decoded dataset from chd param 544 """ 545 #XXX: Why again? not even sure decode works well 546 return Encoder(self._encoding).decode(self['chd'])
547
548 - def _parts(self):
549 return ('%s=%s'%(k,smart_str(v)) for k,v in self.items() if v)
550
551 - def __str__(self):
552 return self.url
553
554 - def __repr__(self):
555 return '<GChartWrapper.%s %s>'%(self.__class__.__name__,self)
556 557 @property
558 - def url(self):
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('&','&amp;').replace('<', '&lt;')\ 599 .replace('>', '&gt;').replace('"', '&quot;').replace( "'", '&#39;') 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
606 - def urlopen(self):
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
618 - def image(self):
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
637 - def write(self, fp):
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
650 - def checksum(self):
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
659 # Now a whole mess of convenience classes 660 # *for those of us who dont speak API* 661 -class QRCode(GChart):
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
670 -class _AbstractGChart(GChart):
671 o,t = {},None
672 - def __init__(self, dataset, **kwargs):
673 kwargs.update(self.o) 674 GChart.__init__(self, self.t, dataset, **kwargs)
675
676 -class Meter(_AbstractGChart): o,t = {'encoding':'text'},'gom'
677 -class Line(_AbstractGChart): t = 'lc'
678 -class LineXY(_AbstractGChart): t = 'lxy'
679 -class HorizontalBarStack(_AbstractGChart): t = 'bhs'
680 -class VerticalBarStack(_AbstractGChart): t = 'bvs'
681 -class HorizontalBarGroup(_AbstractGChart): t = 'bhg'
682 -class VerticalBarGroup(_AbstractGChart): t = 'bvg'
683 -class Pie(_AbstractGChart): t = 'p'
684 -class Pie3D(_AbstractGChart): t = 'p3'
685 -class Venn(_AbstractGChart): t = 'v'
686 -class Scatter(_AbstractGChart): t = 's'
687 -class Sparkline(_AbstractGChart): t = 'ls'
688 -class Radar(_AbstractGChart): t = 'r'
689 -class RadarSpline(_AbstractGChart): t = 'rs'
690 -class Map(_AbstractGChart): t = 't'
691 -class PieC(_AbstractGChart): t = 'pc'
692
693 ######################################## 694 # Now for something completely different 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
707 -class Pin(GChart):
708 - def render(self): pass
709 - def __init__(self, ptype, *args):
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(' ','+')
733 - def shadow(self):
734 image = copy(self) 735 chsts = self['chst'].split('_') 736 chsts[-1] = 'shadow' 737 image.data['chst'] = '_'.join(chsts) 738 return image
739
740 -class Note(GChart):
741 - def render(self): pass
742 - def __init__(self, *args):
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
756 -class Bubble(GChart):
757 - def render(self): pass
758 - def __init__(self, btype, *args):
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(' ','+')
772 - def shadow(self):
773 image = copy(self) 774 image.data['chst'] = '%s_shadow'%self['chst'] 775 return image
776