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