1# Copyright 2009 Google Inc. All Rights Reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#      http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15"""Some code for  creating Google Chart URI's."""
16
17__author__ = 'tstromberg@google.com (Thomas Stromberg)'
18
19import itertools
20import math
21import re
22import urllib
23
24# external dependencies (from nb_third_party)
25from graphy import common
26from graphy.backends import google_chart_api
27
28CHART_URI = 'http://chart.apis.google.com/chart'
29BASE_COLORS = ('ff9900', '1a00ff', 'ff00e6', '80ff00', '00e6ff', 'fae30a',
30               'BE81F7', '9f5734', '000000', 'ff0000', '3090c0', '477248f',
31               'ababab', '7b9f34', '00ff00', '0000ff', '9900ff', '405090',
32               '051290', 'f3e000', '9030f0', 'f03060', 'e0a030', '4598cd')
33CHART_WIDTH = 720
34CHART_HEIGHT = 415
35
36
37def DarkenHexColorCode(color, shade=1):
38  """Given a color in hex format (for HTML), darken it X shades."""
39  rgb_values = [int(x, 16) for x in re.findall('\w\w', color)]
40  new_color = []
41  for value in rgb_values:
42    value -= shade*32
43    if value <= 0:
44      new_color.append('00')
45    elif value <= 16:
46      # Google Chart API requires that color values be 0-padded.
47      new_color.append('0' + hex(value)[2:])
48    else:
49      new_color.append(hex(value)[2:])
50
51  return ''.join(new_color)
52
53
54def _GoodTicks(max_value, tick_size=2.5, num_ticks=10.0):
55  """Find a good round tick size to use in graphs."""
56  try_tick = tick_size
57  while try_tick < max_value:
58    if (max_value / try_tick) > num_ticks:
59      try_tick *= 2
60    else:
61      return int(round(try_tick))
62  # Fallback
63  print "Could not find good tick size for %s (size=%s, num=%s)" % (max_value, tick_size, num_ticks)
64  simple_value = int(max_value  / num_ticks)
65  if simple_value > 0:
66    return simple_value
67  else:
68    return 1
69
70def _BarGraphHeight(bar_count):
71  # TODO(tstromberg): Fix hardcoding.
72  proposed_height = 52 + (bar_count*13)
73  if proposed_height > CHART_HEIGHT:
74    return CHART_HEIGHT
75  else:
76    return proposed_height
77
78
79def PerRunDurationBarGraph(run_data, scale=None):
80  """Output a Google Chart API URL showing per-run durations."""
81  chart = google_chart_api.BarChart()
82  chart.vertical = False
83  chart.bottom.label_gridlines = True
84  chart.bottom.label_positions = chart.bottom.labels
85
86  max_run_avg = -1
87  runs = {}
88  for (ns, run_averages) in run_data:
89    chart.left.labels.append(ns)
90    for run_num, run_avg in enumerate(run_averages):
91      if run_num not in runs:
92        runs[run_num] = []
93
94      runs[run_num].append(run_avg)
95      if run_avg > max_run_avg:
96        max_run_avg = run_avg
97
98  if max_run_avg < 0:
99    print "No decent data to graph: %s" % run_data
100    return None
101
102  if not scale:
103    scale = int(math.ceil(max_run_avg / 5) * 5)
104
105  if len(runs) == 1:
106    bar_count = len(runs[0])
107    chart.AddBars(runs[0])
108  else:
109    bar_count = 0
110    for run_num in sorted(runs):
111      bar_count += len(runs[run_num])
112      chart.AddBars(runs[run_num], label='Run %s' % (run_num+1),
113                    color=DarkenHexColorCode('4684ee', run_num*3))
114
115  tick = _GoodTicks(scale, num_ticks=15.0)
116  labels = range(0, scale, tick) + [scale]
117  chart.bottom.min = 0
118  chart.display.enhanced_encoding = True
119  bottom_axis = chart.AddAxis('x', common.Axis())
120  bottom_axis.labels = ['Duration in ms.']
121  bottom_axis.label_positions = [int((max_run_avg/2.0)*.9)]
122  chart.bottom.labels = labels
123  chart.bottom.max = labels[-1]
124  return chart.display.Url(CHART_WIDTH, _BarGraphHeight(bar_count))
125
126
127def MinimumDurationBarGraph(fastest_data, scale=None):
128  """Output a Google Chart API URL showing minimum-run durations."""
129  chart = google_chart_api.BarChart()
130  chart.vertical = False
131  chart.bottom.label_gridlines = True
132  chart.bottom.label_positions = chart.bottom.labels
133  chart.AddBars([x[1] for x in fastest_data])
134  chart.left.labels = [x[0].name for x in fastest_data]
135
136  slowest_time = fastest_data[-1][1]
137  if not scale:
138    scale = int(math.ceil(slowest_time / 5) * 5)
139
140  tick = _GoodTicks(scale, num_ticks=15.0)
141  labels = range(0, scale, tick) + [scale]
142  chart.bottom.min = 0
143  chart.bottom.max = scale
144  chart.display.enhanced_encoding = True
145  bottom_axis = chart.AddAxis('x', common.Axis())
146  bottom_axis.labels = ['Duration in ms.']
147  bottom_axis.label_positions = [int((scale/2.0)*.9)]
148  chart.bottom.labels = labels
149  return chart.display.Url(CHART_WIDTH, _BarGraphHeight(len(chart.left.labels)))
150
151
152def _MakeCumulativeDistribution(run_data, x_chunk=1.5, percent_chunk=3.5):
153  """Given run data, generate a cumulative distribution (X in Xms).
154
155  Args:
156    run_data: a tuple of nameserver and query durations
157    x_chunk: How much value should be chunked together on the x-axis
158    percent_chunk: How much percentage should be chunked together on y-axis.
159
160  Returns:
161    A list of tuples of tuples: [(ns_name, ((percentage, time),))]
162
163  We chunk the data together to intelligently minimize the number of points
164  that need to be passed to the Google Chart API later (URL limitation!)
165  """
166  # TODO(tstromberg): Use a more efficient algorithm. Pop values out each iter?
167  dist = []
168  for (ns, results) in run_data:
169    if not results:
170      continue
171
172    host_dist = [(0, 0)]
173    max_result = max(results)
174    chunk_max = min(results)
175    # Why such a low value? To make sure the delta for the first coordinate is
176    # always >percent_chunk. We always want to store the first coordinate.
177    last_percent = -99
178
179    while chunk_max < max_result:
180      values = [x for x in results if x <= chunk_max]
181      percent = float(len(values)) / float(len(results)) * 100
182
183      if (percent - last_percent) > percent_chunk:
184        host_dist.append((percent, max(values)))
185        last_percent = percent
186
187      # TODO(tstromberg): Think about using multipliers to degrade precision.
188      chunk_max += x_chunk
189
190    # Make sure the final coordinate is exact.
191    host_dist.append((100, max_result))
192    dist.append((ns, host_dist))
193  return dist
194
195
196def _MaximumRunDuration(run_data):
197  """For a set of run data, return the longest duration.
198
199  Args:
200    run_data: a tuple of nameserver and query durations
201
202  Returns:
203    longest duration found in runs_data (float)
204  """
205  times = [x[1] for x in run_data]
206  return max(itertools.chain(*times))
207
208
209def _SortDistribution(a, b):
210  """Sort distribution graph by nameserver name."""
211  sys_pos_cmp = cmp(b[0].system_position, a[0].system_position)
212  if sys_pos_cmp:
213    return sys_pos_cmp
214
215  preferred_cmp = cmp(b[0].is_preferred, a[0].is_preferred)
216  if preferred_cmp:
217    return preferred_cmp
218
219  return cmp(a[0].name, b[0].name)
220
221
222def DistributionLineGraph(run_data, scale=None, sort_by=None):
223  """Return a Google Chart API URL showing duration distribution per ns."""
224
225  # TODO(tstromberg): Rewrite this method using graphy. Graphy does not
226  # support setting explicit x values for line graphs, which makes things
227  # difficult.
228  distribution = _MakeCumulativeDistribution(run_data)
229  datasets = []
230  labels = []
231  # TODO(tstromberg): Find a way to make colors consistent between runs.
232  colors = BASE_COLORS[0:len(distribution)]
233
234  if not sort_by:
235    sort_by = _SortDistribution
236
237  max_value = _MaximumRunDuration(run_data)
238  if not scale:
239    scale = max_value
240  elif scale < max_value:
241    max_value = scale
242
243  scale = max_value / 100.0
244
245  for (ns, xy_pairs) in sorted(distribution, cmp=sort_by):
246    if len(ns.name) > 1:
247      labels.append(urllib.quote_plus(ns.name))
248    else:
249      labels.append(urllib.quote_plus(ns.ip))
250    x = []
251    y = []
252    for (percentage, duration) in xy_pairs:
253      scaled_duration = int(round(duration/scale))
254      x.append(scaled_duration)
255      y.append(int(round(percentage)))
256      # Only append one point passed the scale max.
257      if scaled_duration >= 100:
258        break
259
260    # TODO(tstromberg): Use google_chart_api.util.EnhancedEncoder
261    datasets.append(','.join(map(str, x)))
262    datasets.append(','.join(map(str, y)))
263
264  # TODO(tstromberg): See if we can get the % sign in the labels!
265  uri = (('%(uri)s?cht=lxy&chs=%(x)sx%(y)s&chxt=x,y&chg=10,20'
266          '&chxr=0,0,%(max)s|1,0,100&chd=t:%(datasets)s&chco=%(colors)s'
267          '&chxt=x,y,x,y&chxl=2:||Duration+in+ms||3:||%%25|'
268          '&chdl=%(labels)s') %
269         {'uri': CHART_URI, 'datasets': '|'.join(map(str, datasets)),
270          'max': int(round(max_value)), 'x': CHART_WIDTH, 'y': CHART_HEIGHT,
271          'colors': ','.join(colors), 'labels': '|'.join(labels)})
272  return uri
273