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"""This module contains various formatters which can help format a chart 18object. To use these, add them to your chart's list of formatters. For 19example: 20 chart.formatters.append(InlineLegend) 21 chart.formatters.append(LabelSeparator(right=8)) 22 23Feel free to write your own formatter. Formatters are just callables that 24modify the chart in some (hopefully useful) way. For example, the AutoColor 25formatter makes sure each DataSeries has a color applied to it. The formatter 26should take the chart to format as its only argument. 27 28(The formatters work on a deepcopy of the user's chart, so modifications 29shouldn't leak back into the user's original chart) 30""" 31 32def AutoLegend(chart): 33 """Automatically fill out the legend based on series labels. This will only 34 fill out the legend if is at least one series with a label. 35 """ 36 chart._show_legend = False 37 labels = [] 38 for series in chart.data: 39 if series.label is None: 40 labels.append('') 41 else: 42 labels.append(series.label) 43 chart._show_legend = True 44 if chart._show_legend: 45 chart._legend_labels = labels 46 47 48class AutoColor(object): 49 """Automatically add colors to any series without colors. 50 51 Object attributes: 52 colors: The list of colors (hex strings) to cycle through. You can modify 53 this list if you don't like the default colors. 54 """ 55 def __init__(self): 56 # TODO: Add a few more default colors. 57 # TODO: Add a default styles too, so if you don't specify color or 58 # style, you get a unique set of colors & styles for your data. 59 self.colors = ['0000ff', 'ff0000', '00dd00', '000000'] 60 61 def __call__(self, chart): 62 index = -1 63 for series in chart.data: 64 if series.style.color is None: 65 index += 1 66 if index >= len(self.colors): 67 index = 0 68 series.style.color = self.colors[index] 69 70 71class AutoScale(object): 72 """If you don't set min/max on the dependent axes, this fills them in 73 automatically by calculating min/max dynamically from the data. 74 75 You can set just min or just max and this formatter will fill in the other 76 value for you automatically. For example, if you only set min then this will 77 set max automatically, but leave min untouched. 78 79 Charts can have multiple dependent axes (chart.left & chart.right, for 80 example.) If you set min/max on some axes but not others, then this formatter 81 copies your min/max to the un-set axes. For example, if you set up min/max on 82 only the right axis then your values will be automatically copied to the left 83 axis. (if you use different min/max values for different axes, the 84 precendence is undefined. So don't do that.) 85 """ 86 87 def __init__(self, buffer=0.05): 88 """Create a new AutoScale formatter. 89 90 Args: 91 buffer: percentage of extra space to allocate around the chart's axes. 92 """ 93 self.buffer = buffer 94 95 def __call__(self, chart): 96 """Format the chart by setting the min/max values on its dependent axis.""" 97 if not chart.data: 98 return # Nothing to do. 99 min_value, max_value = chart.GetMinMaxValues() 100 if None in (min_value, max_value): 101 return # No data. Nothing to do. 102 103 # Honor user's choice, if they've picked min/max. 104 for axis in chart.GetDependentAxes(): 105 if axis.min is not None: 106 min_value = axis.min 107 if axis.max is not None: 108 max_value = axis.max 109 110 buffer = (max_value - min_value) * self.buffer # Stay away from edge. 111 112 for axis in chart.GetDependentAxes(): 113 if axis.min is None: 114 axis.min = min_value - buffer 115 if axis.max is None: 116 axis.max = max_value + buffer 117 118 119class LabelSeparator(object): 120 121 """Adjust the label positions to avoid having them overlap. This happens for 122 any axis with minimum_label_spacing set. 123 """ 124 125 def __init__(self, left=None, right=None, bottom=None): 126 self.left = left 127 self.right = right 128 self.bottom = bottom 129 130 def __call__(self, chart): 131 self.AdjustLabels(chart.left, self.left) 132 self.AdjustLabels(chart.right, self.right) 133 self.AdjustLabels(chart.bottom, self.bottom) 134 135 def AdjustLabels(self, axis, minimum_label_spacing): 136 if minimum_label_spacing is None: 137 return 138 if len(axis.labels) <= 1: # Nothing to adjust 139 return 140 if axis.max is not None and axis.min is not None: 141 # Find the spacing required to fit all labels evenly. 142 # Don't try to push them farther apart than that. 143 maximum_possible_spacing = (axis.max - axis.min) / (len(axis.labels) - 1) 144 if minimum_label_spacing > maximum_possible_spacing: 145 minimum_label_spacing = maximum_possible_spacing 146 147 labels = [list(x) for x in zip(axis.label_positions, axis.labels)] 148 labels = sorted(labels, reverse=True) 149 150 # First pass from the top, moving colliding labels downward 151 for i in range(1, len(labels)): 152 if labels[i - 1][0] - labels[i][0] < minimum_label_spacing: 153 new_position = labels[i - 1][0] - minimum_label_spacing 154 if axis.min is not None and new_position < axis.min: 155 new_position = axis.min 156 labels[i][0] = new_position 157 158 # Second pass from the bottom, moving colliding labels upward 159 for i in range(len(labels) - 2, -1, -1): 160 if labels[i][0] - labels[i + 1][0] < minimum_label_spacing: 161 new_position = labels[i + 1][0] + minimum_label_spacing 162 if axis.max is not None and new_position > axis.max: 163 new_position = axis.max 164 labels[i][0] = new_position 165 166 # Separate positions and labels 167 label_positions, labels = zip(*labels) 168 axis.labels = labels 169 axis.label_positions = label_positions 170 171 172def InlineLegend(chart): 173 """Provide a legend for line charts by attaching labels to the right 174 end of each line. Supresses the regular legend. 175 """ 176 show = False 177 labels = [] 178 label_positions = [] 179 for series in chart.data: 180 if series.label is None: 181 labels.append('') 182 else: 183 labels.append(series.label) 184 show = True 185 label_positions.append(series.data[-1]) 186 187 if show: 188 chart.right.min = chart.left.min 189 chart.right.max = chart.left.max 190 chart.right.labels = labels 191 chart.right.label_positions = label_positions 192 chart._show_legend = False # Supress the regular legend. 193