Home | History | Annotate | Download | only in google_chart_api
      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