1 #!/usr/bin/python2.4 2 # 3 # Copyright 2008 Google Inc. 4 # 5 # Licensed under the Apache License, Version 2.0 (the "License"); 6 # you may not use this file except in compliance with the License. 7 # You may obtain a copy of the License at 8 # 9 # http://www.apache.org/licenses/LICENSE-2.0 10 # 11 # Unless required by applicable law or agreed to in writing, software 12 # distributed under the License is distributed on an "AS IS" BASIS, 13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 # See the License for the specific language governing permissions and 15 # limitations under the License. 16 17 """Utility functions for working with the Google Chart API. 18 19 Not intended for end users, use the methods in __init__ instead.""" 20 21 import cgi 22 import string 23 import urllib 24 25 26 # TODO: Find a better representation 27 LONG_NAMES = dict( 28 client_id='chc', 29 size='chs', 30 chart_type='cht', 31 axis_type='chxt', 32 axis_label='chxl', 33 axis_position='chxp', 34 axis_range='chxr', 35 axis_style='chxs', 36 data='chd', 37 label='chl', 38 y_label='chly', 39 data_label='chld', 40 data_series_label='chdl', 41 color='chco', 42 extra='chp', 43 right_label='chlr', 44 label_position='chlp', 45 y_label_position='chlyp', 46 right_label_position='chlrp', 47 grid='chg', 48 axis='chx', 49 # This undocumented parameter specifies the length of the tick marks for an 50 # axis. Negative values will extend tick marks into the main graph area. 51 axis_tick_marks='chxtc', 52 line_style='chls', 53 marker='chm', 54 fill='chf', 55 bar_size='chbh', 56 bar_height='chbh', 57 label_color='chlc', 58 signature='sig', 59 output_format='chof', 60 title='chtt', 61 title_style='chts', 62 callback='callback', 63 ) 64 65 """ Used for parameters which involve joining multiple values.""" 66 JOIN_DELIMS = dict( 67 data=',', 68 color=',', 69 line_style='|', 70 marker='|', 71 axis_type=',', 72 axis_range='|', 73 axis_label='|', 74 axis_position='|', 75 axis_tick_marks='|', 76 data_series_label='|', 77 label='|', 78 bar_size=',', 79 bar_height=',', 80 ) 81 82 83 class SimpleDataEncoder: 84 85 """Encode data using simple encoding. Out-of-range data will 86 be dropped (encoded as '_'). 87 """ 88 89 def __init__(self): 90 self.prefix = 's:' 91 self.code = string.ascii_uppercase + string.ascii_lowercase + string.digits 92 self.min = 0 93 self.max = len(self.code) - 1 94 95 def Encode(self, data): 96 return ''.join(self._EncodeItem(i) for i in data) 97 98 def _EncodeItem(self, x): 99 if x is None: 100 return '_' 101 x = int(round(x)) 102 if x < self.min or x > self.max: 103 return '_' 104 return self.code[int(x)] 105 106 107 class EnhancedDataEncoder: 108 109 """Encode data using enhanced encoding. Out-of-range data will 110 be dropped (encoded as '_'). 111 """ 112 113 def __init__(self): 114 self.prefix = 'e:' 115 chars = string.ascii_uppercase + string.ascii_lowercase + string.digits \ 116 + '-.' 117 self.code = [x + y for x in chars for y in chars] 118 self.min = 0 119 self.max = len(self.code) - 1 120 121 def Encode(self, data): 122 return ''.join(self._EncodeItem(i) for i in data) 123 124 def _EncodeItem(self, x): 125 if x is None: 126 return '__' 127 x = int(round(x)) 128 if x < self.min or x > self.max: 129 return '__' 130 return self.code[int(x)] 131 132 133 def EncodeUrl(base, params, escape_url, use_html_entities): 134 """Escape params, combine and append them to base to generate a full URL.""" 135 real_params = [] 136 for key, value in params.iteritems(): 137 if escape_url: 138 value = urllib.quote(value) 139 if value: 140 real_params.append('%s=%s' % (key, value)) 141 if real_params: 142 url = '%s?%s' % (base, '&'.join(real_params)) 143 else: 144 url = base 145 if use_html_entities: 146 url = cgi.escape(url, quote=True) 147 return url 148 149 150 def ShortenParameterNames(params): 151 """Shorten long parameter names (like size) to short names (like chs).""" 152 out = {} 153 for name, value in params.iteritems(): 154 short_name = LONG_NAMES.get(name, name) 155 if short_name in out: 156 # params can't have duplicate keys, so the caller must have specified 157 # a parameter using both long & short names, like 158 # {'size': '300x400', 'chs': '800x900'}. We don't know which to use. 159 raise KeyError('Both long and short version of parameter %s (%s) ' 160 'found. It is unclear which one to use.' % (name, short_name)) 161 out[short_name] = value 162 return out 163 164 165 def StrJoin(delim, data): 166 """String-ize & join data.""" 167 return delim.join(str(x) for x in data) 168 169 170 def JoinLists(**args): 171 """Take a dictionary of {long_name:values}, and join the values. 172 173 For each long_name, join the values into a string according to 174 JOIN_DELIMS. If values is empty or None, replace with an empty string. 175 176 Returns: 177 A dictionary {long_name:joined_value} entries. 178 """ 179 out = {} 180 for key, val in args.items(): 181 if val: 182 out[key] = StrJoin(JOIN_DELIMS[key], val) 183 else: 184 out[key] = '' 185 return out 186 187 188 def EncodeData(chart, series, y_min, y_max, encoder): 189 """Format the given data series in plain or extended format. 190 191 Use the chart's encoder to determine the format. The formatted data will 192 be scaled to fit within the range of values supported by the chosen 193 encoding. 194 195 Args: 196 chart: The chart. 197 series: A list of the the data series to format; each list element is 198 a list of data points. 199 y_min: Minimum data value. May be None if y_max is also None 200 y_max: Maximum data value. May be None if y_min is also None 201 Returns: 202 A dictionary with one key, 'data', whose value is the fully encoded series. 203 """ 204 assert (y_min is None) == (y_max is None) 205 if y_min is not None: 206 def _ScaleAndEncode(series): 207 series = ScaleData(series, y_min, y_max, encoder.min, encoder.max) 208 return encoder.Encode(series) 209 encoded_series = [_ScaleAndEncode(s) for s in series] 210 else: 211 encoded_series = [encoder.Encode(s) for s in series] 212 result = JoinLists(**{'data': encoded_series}) 213 result['data'] = encoder.prefix + result['data'] 214 return result 215 216 217 def ScaleData(data, old_min, old_max, new_min, new_max): 218 """Scale the input data so that the range old_min-old_max maps to 219 new_min-new_max. 220 """ 221 def ScalePoint(x): 222 if x is None: 223 return None 224 return scale * x + translate 225 226 if old_min == old_max: 227 scale = 1 228 else: 229 scale = (new_max - new_min) / float(old_max - old_min) 230 translate = new_min - scale * old_min 231 return map(ScalePoint, data) 232