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