14a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair#!/usr/bin/python2.4
24a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair#
34a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# Copyright 2008 Google Inc.
44a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair#
54a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# Licensed under the Apache License, Version 2.0 (the "License");
64a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# you may not use this file except in compliance with the License.
74a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# You may obtain a copy of the License at
84a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair#
94a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair#      http://www.apache.org/licenses/LICENSE-2.0
104a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair#
114a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# Unless required by applicable law or agreed to in writing, software
124a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# distributed under the License is distributed on an "AS IS" BASIS,
134a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
144a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# See the License for the specific language governing permissions and
154a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# limitations under the License.
164a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
174a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair"""Code common to all chart types."""
184a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
194a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairimport copy
204a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairimport warnings
214a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
224a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom graphy import formatters
234a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairfrom graphy import util
244a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
254a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
264a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairclass Marker(object):
274a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
284a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Represents an abstract marker, without position.  You can attach these to
294a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  a DataSeries.
304a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
314a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Object attributes:
324a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    shape: One of the shape codes (Marker.arrow, Marker.diamond, etc.)
334a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    color: color (as hex string, f.ex. '0000ff' for blue)
344a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    size:  size of the marker
354a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """
364a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # TODO: Write an example using markers.
374a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
384a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # Shapes:
394a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  arrow = 'a'
404a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  cross = 'c'
414a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  diamond = 'd'
424a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  circle = 'o'
434a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  square = 's'
444a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  x = 'x'
454a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
464a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # Note: The Google Chart API also knows some other markers ('v', 'V', 'r',
474a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # 'b') that I think would fit better into a grid API.
484a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # TODO: Make such a grid API
494a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
504a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def __init__(self, shape, color, size):
514a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Construct a Marker.  See class docstring for details on args."""
524a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # TODO: Shapes 'r' and 'b' would be much easier to use if they had a
534a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # special-purpose API (instead of trying to fake it with markers)
544a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.shape = shape
554a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.color = color
564a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.size = size
574a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
584a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
594a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairclass _BasicStyle(object):
604a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Basic style object.  Used internally."""
614a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
624a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def __init__(self, color):
634a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.color = color
644a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
654a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
664a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairclass DataSeries(object):
674a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
684a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Represents one data series for a chart (both data & presentation
694a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  information).
704a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
714a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Object attributes:
724a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    points:  List of numbers representing y-values (x-values are not specified
734a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             because the Google Chart API expects even x-value spacing).
744a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    label:   String with the series' label in the legend.  The chart will only
754a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             have a legend if at least one series has a label.  If some series
764a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             do not have a label then they will have an empty description in
774a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             the legend.  This is currently a limitation in the Google Chart
784a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             API.
794a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    style:   A chart-type-specific style object.  (LineStyle for LineChart,
804a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             BarsStyle for BarChart, etc.)
814a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    markers: List of (x, m) tuples where m is a Marker object and x is the
824a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             x-axis value to place it at.
834a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
844a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             The "fill" markers ('r' & 'b') are a little weird because they
854a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             aren't a point on a line.  For these, you can fake it by
864a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             passing slightly weird data (I'd like a better API for them at
874a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             some point):
884a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair               For 'b', you attach the marker to the starting series, and set x
894a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair               to the index of the ending line.  Size is ignored, I think.
904a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
914a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair               For 'r', you can attach to any line, specify the starting
924a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair               y-value for x and the ending y-value for size.  Y, in this case,
934a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair               is becase 0.0 (bottom) and 1.0 (top).
944a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    color:   DEPRECATED
954a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """
964a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
974a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # TODO: Should we require the points list to be non-empty ?
984a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # TODO: Do markers belong here?  They are really only used for LineCharts
994a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def __init__(self, points, label=None, style=None, markers=None, color=None):
1004a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Construct a DataSeries.  See class docstring for details on args."""
1014a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    if label is not None and util._IsColor(label):
1024a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      warnings.warn('Your code may be broken! Label is a hex triplet.  Maybe '
1034a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    'it is a color? The old argument order (color & style '
1044a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    'before label) is deprecated.', DeprecationWarning,
1054a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    stacklevel=2)
1064a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    if color is not None:
1074a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      warnings.warn('Passing color is deprecated.  Pass a style object '
1084a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    'instead.', DeprecationWarning, stacklevel=2)
1094a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      # Attempt to fix it for them.  If they also passed a style, honor it.
1104a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      if style is None:
1114a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair        style = _BasicStyle(color)
1124a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    if style is not None and isinstance(style, basestring):
1134a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      warnings.warn('Your code is broken! Style is a string, not an object. '
1144a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    'Maybe you are passing a color?  Passing color is '
1154a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    'deprecated; pass a style object instead.',
1164a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    DeprecationWarning, stacklevel=2)
1174a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    if style is None:
1184a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      style = _BasicStyle(None)
1194a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.data = points
1204a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.style = style
1214a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.markers = markers or []
1224a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.label = label
1234a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1244a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _GetColor(self):
1254a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    warnings.warn('DataSeries.color is deprecated, use '
1264a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                  'DataSeries.style.color instead.', DeprecationWarning,
1274a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                  stacklevel=2)
1284a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self.style.color
1294a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1304a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _SetColor(self, color):
1314a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    warnings.warn('DataSeries.color is deprecated, use '
1324a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                  'DataSeries.style.color instead.', DeprecationWarning,
1334a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                  stacklevel=2)
1344a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.style.color = color
1354a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1364a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  color = property(_GetColor, _SetColor)
1374a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1384a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _GetStyle(self):
1394a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self._style;
1404a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1414a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _SetStyle(self, style):
1424a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    if style is not None and callable(style):
1434a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      warnings.warn('Your code may be broken ! LineStyle.solid and similar '
1444a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    'are no longer constants, but class methods that '
1454a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    'create LineStyle instances. Change your code to call '
1464a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    'LineStyle.solid() instead of passing it as a value.',
1474a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    DeprecationWarning, stacklevel=2)
1484a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      self._style = style()
1494a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    else:
1504a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      self._style = style
1514a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1524a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  style = property(_GetStyle, _SetStyle)
1534a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1544a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1554a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairclass AxisPosition(object):
1564a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Represents all the available axis positions.
1574a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1584a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  The available positions are as follows:
1594a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    AxisPosition.TOP
1604a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    AxisPosition.BOTTOM
1614a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    AxisPosition.LEFT
1624a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    AxisPosition.RIGHT
1634a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """
1644a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  LEFT = 'y'
1654a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  RIGHT = 'r'
1664a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  BOTTOM = 'x'
1674a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  TOP = 't'
1684a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1694a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1704a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairclass Axis(object):
1714a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1724a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Represents one axis.
1734a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1744a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Object setings:
1754a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    min:    Minimum value for the bottom or left end of the axis
1764a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    max:    Max value.
1774a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    labels: List of labels to show along the axis.
1784a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    label_positions: List of positions to show the labels at.  Uses the scale
1794a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                     set by min & max, so if you set min = 0 and max = 10, then
1804a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                     label positions [0, 5, 10] would be at the bottom,
1814a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                     middle, and top of the axis, respectively.
1824a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    grid_spacing:  Amount of space between gridlines (in min/max scale).
1834a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                   A value of 0 disables gridlines.
1844a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    label_gridlines: If True, draw a line extending from each label
1854a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                   on the axis all the way across the chart.
1864a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """
1874a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1884a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def __init__(self, axis_min=None, axis_max=None):
1894a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Construct a new Axis.
1904a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
1914a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Args:
1924a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      axis_min: smallest value on the axis
1934a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      axis_max: largest value on the axis
1944a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
1954a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.min = axis_min
1964a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.max = axis_max
1974a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.labels = []
1984a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.label_positions = []
1994a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.grid_spacing = 0
2004a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.label_gridlines = False
2014a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2024a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# TODO: Add other chart types.  Order of preference:
2034a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# - scatter plots
2044a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair# - us/world maps
2054a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2064a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclairclass BaseChart(object):
2074a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """Base chart object with standard behavior for all other charts.
2084a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2094a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  Object attributes:
2104a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    data: List of DataSeries objects. Chart subtypes provide convenience
2114a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair          functions (like AddLine, AddBars, AddSegment) to add more series
2124a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair          later.
2134a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    left/right/bottom/top: Axis objects for the 4 different axes.
2144a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    formatters: A list of callables which will be used to format this chart for
2154a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                display.  TODO: Need better documentation for how these
2164a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                work.
2174a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    auto_scale, auto_color, auto_legend:
2184a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      These aliases let users access the default formatters without poking
2194a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      around in self.formatters.  If the user removes them from
2204a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      self.formatters then they will no longer be enabled, even though they'll
2214a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      still be accessible through the aliases.  Similarly, re-assigning the
2224a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      aliases has no effect on the contents of self.formatters.
2234a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    display: This variable is reserved for backends to populate with a display
2244a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             object.  The intention is that the display object would be used to
2254a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             render this chart.  The details of what gets put here depends on
2264a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair             the specific backend you are using.
2274a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  """
2284a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2294a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # Canonical ordering of position keys
2304a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  _POSITION_CODES = 'yrxt'
2314a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2324a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # TODO: Add more inline args to __init__ (esp. labels).
2334a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  # TODO: Support multiple series in the constructor, if given.
2344a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def __init__(self):
2354a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Construct a BaseChart object."""
2364a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.data = []
2374a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2384a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self._axes = {}
2394a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    for code in self._POSITION_CODES:
2404a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      self._axes[code] = [Axis()]
2414a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self._legend_labels = []  # AutoLegend fills this out
2424a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self._show_legend = False  # AutoLegend fills this out
2434a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2444a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # Aliases for default formatters
2454a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.auto_color = formatters.AutoColor()
2464a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.auto_scale = formatters.AutoScale()
2474a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.auto_legend = formatters.AutoLegend
2484a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.formatters = [self.auto_color, self.auto_scale, self.auto_legend]
2494a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # display is used to convert the chart into something displayable (like a
2504a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # url or img tag).
2514a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.display = None
2524a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2534a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def AddFormatter(self, formatter):
2544a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Add a new formatter to the chart (convenience method)."""
2554a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.formatters.append(formatter)
2564a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2574a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def AddSeries(self, points, color=None, style=None, markers=None,
2584a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                label=None):
2594a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """DEPRECATED
2604a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2614a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Add a new series of data to the chart; return the DataSeries object."""
2624a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    warnings.warn('AddSeries is deprecated.  Instead, call AddLine for '
2634a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                  'LineCharts, AddBars for BarCharts, AddSegment for '
2644a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                  'PieCharts ', DeprecationWarning, stacklevel=2)
2654a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    series = DataSeries(points, color=color, style=style, markers=markers,
2664a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                        label=label)
2674a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.data.append(series)
2684a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return series
2694a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2704a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def GetDependentAxes(self):
2714a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Return any dependent axes ('left' and 'right' by default for LineCharts,
2724a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    although bar charts would use 'bottom' and 'top').
2734a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
2744a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self._axes[AxisPosition.LEFT] + self._axes[AxisPosition.RIGHT]
2754a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2764a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def GetIndependentAxes(self):
2774a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Return any independent axes (normally top & bottom, although horizontal
2784a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    bar charts use left & right by default).
2794a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
2804a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self._axes[AxisPosition.TOP] + self._axes[AxisPosition.BOTTOM]
2814a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2824a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def GetDependentAxis(self):
2834a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Return this chart's main dependent axis (often 'left', but
2844a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    horizontal bar-charts use 'bottom').
2854a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
2864a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self.left
2874a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2884a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def GetIndependentAxis(self):
2894a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Return this chart's main independent axis (often 'bottom', but
2904a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    horizontal bar-charts use 'left').
2914a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
2924a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self.bottom
2934a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2944a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _Clone(self):
2954a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Make a deep copy this chart.
2964a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
2974a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Formatters & display will be missing from the copy, due to limitations in
2984a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    deepcopy.
2994a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
3004a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    orig_values = {}
3014a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # Things which deepcopy will likely choke on if it tries to copy.
3024a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    uncopyables = ['formatters', 'display', 'auto_color', 'auto_scale',
3034a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                   'auto_legend']
3044a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    for name in uncopyables:
3054a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      orig_values[name] = getattr(self, name)
3064a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      setattr(self, name, None)
3074a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    clone = copy.deepcopy(self)
3084a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    for name, orig_value in orig_values.iteritems():
3094a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      setattr(self, name, orig_value)
3104a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return clone
3114a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3124a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def GetFormattedChart(self):
3134a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Get a copy of the chart with formatting applied."""
3144a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # Formatters need to mutate the chart, but we don't want to change it out
3154a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # from under the user.  So, we work on a copy of the chart.
3164a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    scratchpad = self._Clone()
3174a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    for formatter in self.formatters:
3184a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      formatter(scratchpad)
3194a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return scratchpad
3204a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3214a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def GetMinMaxValues(self):
3224a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Get the largest & smallest values in this chart, returned as
3234a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    (min_value, max_value).  Takes into account complciations like stacked data
3244a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    series.
3254a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3264a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    For example, with non-stacked series, a chart with [1, 2, 3] and [4, 5, 6]
3274a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    would return (1, 6).  If the same chart was stacking the data series, it
3284a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    would return (5, 9).
3294a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
3304a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    MinPoint = lambda data: min(x for x in data if x is not None)
3314a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    MaxPoint = lambda data: max(x for x in data if x is not None)
3324a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    mins  = [MinPoint(series.data) for series in self.data if series.data]
3334a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    maxes = [MaxPoint(series.data) for series in self.data if series.data]
3344a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    if not mins or not maxes:
3354a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      return None, None # No data, just bail.
3364a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return min(mins), max(maxes)
3374a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3384a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def AddAxis(self, position, axis):
3394a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Add an axis to this chart in the given position.
3404a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3414a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Args:
3424a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      position: an AxisPosition object specifying the axis's position
3434a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      axis: The axis to add, an Axis object
3444a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Returns:
3454a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      the value of the axis parameter
3464a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
3474a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self._axes.setdefault(position, []).append(axis)
3484a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return axis
3494a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3504a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def GetAxis(self, position):
3514a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Get or create the first available axis in the given position.
3524a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3534a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    This is a helper method for the left, right, top, and bottom properties.
3544a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    If the specified axis does not exist, it will be created.
3554a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3564a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Args:
3574a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      position: the position to search for
3584a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Returns:
3594a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      The first axis in the given position
3604a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
3614a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # Not using setdefault here just in case, to avoid calling the Axis()
3624a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    # constructor needlessly
3634a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    if position in self._axes:
3644a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      return self._axes[position][0]
3654a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    else:
3664a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      axis = Axis()
3674a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      self._axes[position] = [axis]
3684a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      return axis
3694a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3704a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def SetAxis(self, position, axis):
3714a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Set the first axis in the given position to the given value.
3724a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3734a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    This is a helper method for the left, right, top, and bottom properties.
3744a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3754a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Args:
3764a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      position: an AxisPosition object specifying the axis's position
3774a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      axis: The axis to set, an Axis object
3784a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    Returns:
3794a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      the value of the axis parameter
3804a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
3814a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self._axes.setdefault(position, [None])[0] = axis
3824a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return axis
3834a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3844a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _GetAxes(self):
3854a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """Return a generator of (position_code, Axis) tuples for this chart's axes.
3864a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3874a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    The axes will be sorted by position using the canonical ordering sequence,
3884a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    _POSITION_CODES.
3894a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    """
3904a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    for code in self._POSITION_CODES:
3914a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair      for axis in self._axes.get(code, []):
3924a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair        yield (code, axis)
3934a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3944a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _GetBottom(self):
3954a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self.GetAxis(AxisPosition.BOTTOM)
3964a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
3974a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _SetBottom(self, value):
3984a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.SetAxis(AxisPosition.BOTTOM, value)
3994a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4004a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  bottom = property(_GetBottom, _SetBottom,
4014a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                    doc="""Get or set the bottom axis""")
4024a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4034a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _GetLeft(self):
4044a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self.GetAxis(AxisPosition.LEFT)
4054a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4064a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _SetLeft(self, value):
4074a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.SetAxis(AxisPosition.LEFT, value)
4084a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4094a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  left = property(_GetLeft, _SetLeft,
4104a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                  doc="""Get or set the left axis""")
4114a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4124a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _GetRight(self):
4134a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self.GetAxis(AxisPosition.RIGHT)
4144a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4154a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _SetRight(self, value):
4164a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.SetAxis(AxisPosition.RIGHT, value)
4174a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4184a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  right = property(_GetRight, _SetRight,
4194a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                   doc="""Get or set the right axis""")
4204a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4214a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _GetTop(self):
4224a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    return self.GetAxis(AxisPosition.TOP)
4234a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4244a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  def _SetTop(self, value):
4254a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair    self.SetAxis(AxisPosition.TOP, value)
4264a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair
4274a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair  top = property(_GetTop, _SetTop,
4284a4f2fe02baf385f6c24fc98c6e17bf6ac5e0724Dan Sinclair                 doc="""Get or set the top axis""")
429