Jason R. Coombs avatar Jason R. Coombs committed 90384d9

Moved project to Unix line endings. They're better and most Windows editors can handle it.

Comments (0)

Files changed (20)

-Copyright (c) 2008-2009 Jason R. Coombs
-
-Permission is hereby granted, free of charge, to any person obtaining
-a copy of this software and associated documentation files (the
-"Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+Copyright (c) 2008-2009 Jason R. Coombs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
-
-The MIT License
-
-Copyright © 2008 Jason R. Coombs
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
+
+
+The MIT License
+
+Copyright © 2008 Jason R. Coombs
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
-.. -*- restructuredtext -*-
-
-``svg.charts`` - Package for generating SVG Charts in Python
-============================================================
-
-.. contents::
-
-Status and License
-------------------
-
-``svg.charts`` is a pure-python library for generating charts and graphs
-in SVG, originally based on the SVG::Graph Ruby package by Sean E. Russel.
-
-``svg.charts`` supercedes ``svg_charts`` 1.1 and 1.2.
-
-``svg.charts`` is written by Jason R. Coombs.  It is licensed under an
-`MIT-style permissive license
-<http://svg-charts.hg.sourceforge.net/hgweb/svg-charts/py-svg/raw-file/tip/docs/license.txt>`_.
-
-You can install it with ``easy_install svg.charts``, or from the
-`mercurial repository source <http://svg-charts.hg.sourceforge.net:8000/hgroot/svg-charts/svg-charts#egg=svg.charts-dev>`_ with
-``easy_install svg.charts==dev``.
-
-Acknowledgements
-----------------
-
-``svg.charts`` depends heavily on lxml and cssutils. Thanks to the
-contributors of those projects for stable, performant, standards-based
-packages.
-
-Sean E. Russel for creating the SVG::Graph Ruby package from which this
-Python port was originally derived.
-
-Leo Lapworth for creating the SVG::TT::Graph package which the Ruby
-port was based on.
-
-Stephen Morgan for creating the TT template and SVG.
-
-Getting Started
----------------
-
-``svg.charts`` has some examples (taken directly from the reference implementation)
-in `tests/samples.py <http://svg-charts.hg.sourceforge.net/hgweb/svg-charts/py-svg/raw-file/tip/tests/samples.py>`_.
-These examples show sample usage of the various chart types. They should provide a
-good starting point for learning the usage of the library.
-
-An example of using ``svg.charts`` in a `CherryPy
-<http://www.cherrypy.org/>`_ web app can be found in `jaraco.site.charts
-<https://bitbucket.org/jaraco/jaraco.site/src/tip/jaraco/site/charts.py>`_.
-If the site is working, you can see the `rendered output here
-<http://www.jaraco.com/charts/plot>`_.
-
-Upgrade Notes
--------------
-
-Upgrading from 1.x to 2.0
-
-I suggest removing SVG 1.0 from the python installation.  This involves removing the SVG directory (or svg_chart*) from site-packages.
-
-Change import statements to import from the new namespace.
-
-from SVG import Bar
-Bar.VerticalBar(...)
-becomes
-from svg.charts.bar import VerticalBar
-VerticalBar(...)
-
-More To-Dos
------------
-
--  Documentation! This package desperately needs some high-level,
-   tutorial-style how-tos, and not just links to example code.
--  Implement javascript-based animation (See JellyGraph for a Silverlight example of what simple animation can do for a charting library).
-
-Reporting Bugs and Getting Help
--------------------------------
-
-This project is `hosted at sourceforge
-<https://sourceforge.net/projects/svg-charts/>`_. Please use that site for
-reporting bugs and requesting help. Patches are also welcome.
-
-Changes
--------
-
-2.0.8
-~~~~~
-
-* Updated to latest cssutils with Python 3 support. Thanks Christof!
-* Fixed a few remaining issues with Python 3 compatibility.
-
-2.0.7
-~~~~~
-
-* Fixed bug in rendering of Pie Chart styles.
-* Improved testing framework. Now samples are at least generated as part
-  of the test suite.
-* Fixed bug in javascript when label ids had spaces. See #3139197.
-* Fixed build issue where package data wasn't included due to 2to3
-  technique. Now using distribute technique and installation on Python
-  3 requires distribute.
-
-2.0.6
-~~~~~
-
-* Fixed bug where x axis labels would not be rendered properly if the
-  largest value was the same as the largest visible x value on the
-  chart.
-
-2.0.5
-~~~~~
-
-* Altered the way CSS files are loaded, so they can be more easily
-  customized by subclasses (and less dependent on the class names).
-
-2.0.4
-~~~~~
-
-* A small attempt to improve the documentation - added links to examples
-  that already exist.
-
-2.0.3
-~~~~~
-
-* Fix IndexError in ``svg.charts.plot.Plot.field_size`` when there are
-  only two values returned by float_range (in the case there are only
-  two different 'y' values in the data) and scale_y_integers == True.
-  Credit to `Jean Schurger <http://schurger.org/>`_ for the patch.
-* Fixed problem in setup.py installing on Unix OS (case sensitivity of
-  readme.txt). Credit to Luke Miller and Jean Schurger for supplying
-  a patch for this issue.
-
-2.0.2
-~~~~~
-
-* Updated cssutils dependency to 0.9.6 (currently in beta) to require the CSS profiles support.
-* Completed an SVG CSS profile according to the SVG 1.1 spec.
-
-2.0.1
-~~~~~
-
-* Added preliminary SVG CSS profile, suitable for stock CSS properties.
-
-2.0
-~~~~~
-
-* First major divergence from the Ruby reference implementation
-* Now implemented as a namespace package (svg.charts instead of svg_charts)
-* Changed XML processor to lxml
-* Enabled extensible css support using cssutils, greatly reducing static CSS
-* Renamed modules and methods to be more consistent with PEP-8 naming convention
-
-1.2
-~~~
-
-* Bug fixes
-
-1.1
-~~~
-
-* First public release
+.. -*- restructuredtext -*-
+
+``svg.charts`` - Package for generating SVG Charts in Python
+============================================================
+
+.. contents::
+
+Status and License
+------------------
+
+``svg.charts`` is a pure-python library for generating charts and graphs
+in SVG, originally based on the SVG::Graph Ruby package by Sean E. Russel.
+
+``svg.charts`` supercedes ``svg_charts`` 1.1 and 1.2.
+
+``svg.charts`` is written by Jason R. Coombs.  It is licensed under an
+`MIT-style permissive license
+<http://svg-charts.hg.sourceforge.net/hgweb/svg-charts/py-svg/raw-file/tip/docs/license.txt>`_.
+
+You can install it with ``easy_install svg.charts``, or from the
+`mercurial repository source <http://svg-charts.hg.sourceforge.net:8000/hgroot/svg-charts/svg-charts#egg=svg.charts-dev>`_ with
+``easy_install svg.charts==dev``.
+
+Acknowledgements
+----------------
+
+``svg.charts`` depends heavily on lxml and cssutils. Thanks to the
+contributors of those projects for stable, performant, standards-based
+packages.
+
+Sean E. Russel for creating the SVG::Graph Ruby package from which this
+Python port was originally derived.
+
+Leo Lapworth for creating the SVG::TT::Graph package which the Ruby
+port was based on.
+
+Stephen Morgan for creating the TT template and SVG.
+
+Getting Started
+---------------
+
+``svg.charts`` has some examples (taken directly from the reference implementation)
+in `tests/samples.py <http://svg-charts.hg.sourceforge.net/hgweb/svg-charts/py-svg/raw-file/tip/tests/samples.py>`_.
+These examples show sample usage of the various chart types. They should provide a
+good starting point for learning the usage of the library.
+
+An example of using ``svg.charts`` in a `CherryPy
+<http://www.cherrypy.org/>`_ web app can be found in `jaraco.site.charts
+<https://bitbucket.org/jaraco/jaraco.site/src/tip/jaraco/site/charts.py>`_.
+If the site is working, you can see the `rendered output here
+<http://www.jaraco.com/charts/plot>`_.
+
+Upgrade Notes
+-------------
+
+Upgrading from 1.x to 2.0
+
+I suggest removing SVG 1.0 from the python installation.  This involves removing the SVG directory (or svg_chart*) from site-packages.
+
+Change import statements to import from the new namespace.
+
+from SVG import Bar
+Bar.VerticalBar(...)
+becomes
+from svg.charts.bar import VerticalBar
+VerticalBar(...)
+
+More To-Dos
+-----------
+
+-  Documentation! This package desperately needs some high-level,
+   tutorial-style how-tos, and not just links to example code.
+-  Implement javascript-based animation (See JellyGraph for a Silverlight example of what simple animation can do for a charting library).
+
+Reporting Bugs and Getting Help
+-------------------------------
+
+This project is `hosted at sourceforge
+<https://sourceforge.net/projects/svg-charts/>`_. Please use that site for
+reporting bugs and requesting help. Patches are also welcome.
+
+Changes
+-------
+
+2.0.8
+~~~~~
+
+* Updated to latest cssutils with Python 3 support. Thanks Christof!
+* Fixed a few remaining issues with Python 3 compatibility.
+
+2.0.7
+~~~~~
+
+* Fixed bug in rendering of Pie Chart styles.
+* Improved testing framework. Now samples are at least generated as part
+  of the test suite.
+* Fixed bug in javascript when label ids had spaces. See #3139197.
+* Fixed build issue where package data wasn't included due to 2to3
+  technique. Now using distribute technique and installation on Python
+  3 requires distribute.
+
+2.0.6
+~~~~~
+
+* Fixed bug where x axis labels would not be rendered properly if the
+  largest value was the same as the largest visible x value on the
+  chart.
+
+2.0.5
+~~~~~
+
+* Altered the way CSS files are loaded, so they can be more easily
+  customized by subclasses (and less dependent on the class names).
+
+2.0.4
+~~~~~
+
+* A small attempt to improve the documentation - added links to examples
+  that already exist.
+
+2.0.3
+~~~~~
+
+* Fix IndexError in ``svg.charts.plot.Plot.field_size`` when there are
+  only two values returned by float_range (in the case there are only
+  two different 'y' values in the data) and scale_y_integers == True.
+  Credit to `Jean Schurger <http://schurger.org/>`_ for the patch.
+* Fixed problem in setup.py installing on Unix OS (case sensitivity of
+  readme.txt). Credit to Luke Miller and Jean Schurger for supplying
+  a patch for this issue.
+
+2.0.2
+~~~~~
+
+* Updated cssutils dependency to 0.9.6 (currently in beta) to require the CSS profiles support.
+* Completed an SVG CSS profile according to the SVG 1.1 spec.
+
+2.0.1
+~~~~~
+
+* Added preliminary SVG CSS profile, suitable for stock CSS properties.
+
+2.0
+~~~~~
+
+* First major divergence from the Ruby reference implementation
+* Now implemented as a namespace package (svg.charts instead of svg_charts)
+* Changed XML processor to lxml
+* Enabled extensible css support using cssutils, greatly reducing static CSS
+* Renamed modules and methods to be more consistent with PEP-8 naming convention
+
+1.2
+~~~
+
+* Bug fixes
+
+1.1
+~~~
+
+* First public release
-[egg_info]
-
-[nosetests]
-with-doctest=1
-
-[pytest]
-addopts = --doctest-modules
-norecursedirs = build
+[egg_info]
+
+[nosetests]
+with-doctest=1
+
+[pytest]
+addopts = --doctest-modules
+norecursedirs = build
-#!python
-
-import os
-import sys
-from setuptools import find_packages
-
-from distutils.cmd import Command
-
-class DisabledTestCommand(Command):
-	user_options = []
-	def __init__(self, dist):
-		raise RuntimeError("test command not supported on svg.charts. Use setup.py nosetests instead")
-
-_this_dir = os.path.dirname(__file__)
-_readme = os.path.join(_this_dir, 'readme.txt')
-_long_description = open(_readme).read().strip()
-
-# it seems that dateutil 2.0 only works under Python 3
-dateutil_req = (
-	['python-dateutil>=1.4,<2.0dev'] if sys.version_info < (3,0)
-	else ['python-dateutil>=2.0'] )
-
-setup_params = dict(
-	name = "svg.charts",
-	use_hg_version=True,
-	description = "Python SVG Charting Library",
-	long_description = _long_description,
-	author = "Jason R. Coombs",
-	author_email = "jaraco@jaraco.com",
-	url = "http://svg-charts.sourceforge.net",
-	packages = find_packages(),
-	zip_safe=True,
-	namespace_packages=['svg'],
-	include_package_data = True,
-	install_requires=[
-		'cssutils>=0.9.8a3',
-		'lxml>=2.0',
-	] + dateutil_req,
-	license = "MIT",
-	classifiers = [
-		"Development Status :: 5 - Production/Stable",
-		"Intended Audience :: Developers",
-		"Intended Audience :: Science/Research",
-		"Programming Language :: Python :: 2.6",
-		"Programming Language :: Python :: 2.7",
-		"Programming Language :: Python :: 3",
-		"License :: OSI Approved :: MIT License",
-	],
-	entry_points = {
-	},
-	# Don't use setup.py test - nose doesn't support it
-	# see http://code.google.com/p/python-nose/issues/detail?id=219
-	cmdclass=dict(
-		test=DisabledTestCommand,
-	),
-	setup_requires=[
-		'hgtools',
-	],
-	use_2to3 = True,
-)
-
-if __name__ == '__main__':
-	from setuptools import setup
-	setup(**setup_params)
+#!python
+
+import os
+import sys
+from setuptools import find_packages
+
+from distutils.cmd import Command
+
+class DisabledTestCommand(Command):
+	user_options = []
+	def __init__(self, dist):
+		raise RuntimeError("test command not supported on svg.charts. Use setup.py nosetests instead")
+
+_this_dir = os.path.dirname(__file__)
+_readme = os.path.join(_this_dir, 'readme.txt')
+_long_description = open(_readme).read().strip()
+
+# it seems that dateutil 2.0 only works under Python 3
+dateutil_req = (
+	['python-dateutil>=1.4,<2.0dev'] if sys.version_info < (3,0)
+	else ['python-dateutil>=2.0'] )
+
+setup_params = dict(
+	name = "svg.charts",
+	use_hg_version=True,
+	description = "Python SVG Charting Library",
+	long_description = _long_description,
+	author = "Jason R. Coombs",
+	author_email = "jaraco@jaraco.com",
+	url = "http://svg-charts.sourceforge.net",
+	packages = find_packages(),
+	zip_safe=True,
+	namespace_packages=['svg'],
+	include_package_data = True,
+	install_requires=[
+		'cssutils>=0.9.8a3',
+		'lxml>=2.0',
+	] + dateutil_req,
+	license = "MIT",
+	classifiers = [
+		"Development Status :: 5 - Production/Stable",
+		"Intended Audience :: Developers",
+		"Intended Audience :: Science/Research",
+		"Programming Language :: Python :: 2.6",
+		"Programming Language :: Python :: 2.7",
+		"Programming Language :: Python :: 3",
+		"License :: OSI Approved :: MIT License",
+	],
+	entry_points = {
+	},
+	# Don't use setup.py test - nose doesn't support it
+	# see http://code.google.com/p/python-nose/issues/detail?id=219
+	cmdclass=dict(
+		test=DisabledTestCommand,
+	),
+	setup_requires=[
+		'hgtools',
+	],
+	use_2to3 = True,
+)
+
+if __name__ == '__main__':
+	from setuptools import setup
+	setup(**setup_params)

svg/charts/__init__.py

-#!python
-# -*- coding: UTF-8 -*-
-
-"""
-svg.charts package.
-"""
-
-__all__ = ('graph', 'plot', 'time_series', 'bar', 'pie', 'schedule', 'util')
+#!python
+# -*- coding: UTF-8 -*-
+
+"""
+svg.charts package.
+"""
+
+__all__ = ('graph', 'plot', 'time_series', 'bar', 'pie', 'schedule', 'util')

svg/charts/bar.py

-#!python
-from itertools import chain
-from lxml import etree
-from svg.charts.graph import Graph
-
-__all__ = ('VerticalBar', 'HorizontalBar')
-
-class Bar(Graph):
-	"A superclass for bar-style graphs.  Do not instantiate directly."
-
-	# gap between bars
-	bar_gap = True
-	# how to stack adjacent dataset series
-	# overlap - overlap bars with transparent colors
-	# top - stack bars on top of one another
-	# side - stack bars side-by-side
-	stack = 'overlap'
-	
-	scale_divisions = None
-
-	stylesheet_names = Graph.stylesheet_names + ['bar.css']
-
-	def __init__(self, fields, *args, **kargs):
-		self.fields = fields
-		super(Bar, self).__init__(*args, **kargs)
-
-	# adapted from Plot
-	def get_data_values(self):
-		min_value, max_value, scale_division = self.data_range()
-		result = tuple(float_range(min_value, max_value + scale_division, scale_division))
-		if self.scale_integers:
-			result = map(int, result)
-		return result
-	
-	# adapted from plot (very much like calling data_range('y'))
-	def data_range(self):
-		min_value = self.data_min()
-		max_value = self.data_max()
-		range = max_value - min_value
-
-		data_pad = range / 20.0 or 10
-		scale_range = (max_value + data_pad) - min_value
-		
-		scale_division = self.scale_divisions or (scale_range / 10.0)
-		
-		if self.scale_integers:
-			scale_division = round(scale_division) or 1
-			
-		return min_value, max_value, scale_division
-
-	def get_field_labels(self):
-		return self.fields
-
-	def get_data_labels(self):
-		return map(str, self.get_data_values())
-
-	def data_max(self):
-		return max(chain(*map(lambda set: set['data'], self.data)))
-		# above is same as
-		# return max(map(lambda set: max(set['data']), self.data))
-		
-	def data_min(self):
-		if not getattr(self, 'min_scale_value') is None: return self.min_scale_value
-		min_value = min(chain(*map(lambda set: set['data'], self.data)))
-		min_value = min(min_value, 0)
-		return min_value
-
-	def get_bar_gap(self, field_size):
-		bar_gap = 10 # default gap
-		if field_size < 10:
-			# adjust for narrow fields
-			bar_gap = field_size / 2
-		# the following zero's out the gap if bar_gap is False
-		bar_gap = int(self.bar_gap) * bar_gap
-		return bar_gap
-
-def float_range(start = 0, stop = None, step = 1):
-	"Much like the built-in function range, but accepts floats"
-	while start < stop:
-		yield float(start)
-		start += step
-
-
-class VerticalBar(Bar):
-	"""    # === Create presentation quality SVG bar graphs easily
-    #
-    # = Synopsis
-    #
-    #   require 'SVG/Graph/Bar'
-    #
-    #   fields = %w(Jan Feb Mar);
-    #   data_sales_02 = [12, 45, 21]
-    #
-    #   graph = SVG::Graph::Bar.new(
-    #     :height => 500,
-    #     :width => 300,
-    #     :fields => fields
-    #  )
-    #
-    #   graph.add_data(
-    #     :data => data_sales_02,
-    #     :title => 'Sales 2002'
-    #  )
-    #
-    #   print "Content-type: image/svg+xml\r\n\r\n"
-    #   print graph.burn
-    #
-    # = Description
-    #
-    # This object aims to allow you to easily create high quality
-    # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
-    # style sheet or supply your own. Either way there are many options which
-    # can be configured to give you control over how the graph is generated -
-    # with or without a key, data elements at each point, title, subtitle etc.
-    #
-    # = Notes
-    #
-    # The default stylesheet handles upto 12 data sets, if you
-    # use more you must create your own stylesheet and add the
-    # additional settings for the extra data sets. You will know
-    # if you go over 12 data sets as they will have no style and
-    # be in black.
-    #
-    # = Examples
-    #
-    # * http://germane-software.com/repositories/public/SVG/test/test.rb
-    #
-    # = See also
-    #
-    # * SVG::Graph::Graph
-    # * SVG::Graph::BarHorizontal
-    # * SVG::Graph::Line
-    # * SVG::Graph::Pie
-    # * SVG::Graph::Plot
-    # * SVG::Graph::TimeSeries
-"""
-	top_align = top_font = 1
-
-	def get_x_labels(self):
-		return self.get_field_labels()
-
-	def get_y_labels(self):
-		return self.get_data_labels()
-
-	def x_label_offset(self, width):
-		return width / 2.0
-
-	def draw_data(self):
-		min_value = self.data_min()
-		unit_size = (float(self.graph_height) - self.font_size*2*self.top_font)
-		unit_size /= (max(self.get_data_values()) - min(self.get_data_values()))
-
-		bar_gap = self.get_bar_gap(self.get_field_width())		
-
-		bar_width = self.get_field_width() - bar_gap
-		if self.stack == 'side':
-			bar_width /= len(self.data)
-		
-		x_mod = (self.graph_width - bar_gap)/2
-		if self.stack == 'side':
-			x_mod -= bar_width/2
-
-		bottom = self.graph_height
-		
-		for field_count, field in enumerate(self.fields):
-			for dataset_count, dataset in enumerate(self.data):
-				# cases (assume 0 = +ve):
-				#   value  min  length
-				#    +ve   +ve  value - min
-				#    +ve   -ve  value - 0
-				#    -ve   -ve  value.abs - 0
-				value = dataset['data'][field_count]
-				
-				left = self.get_field_width() * field_count
-				
-				length = (abs(value) - max(min_value, 0)) * unit_size
-				# top is 0 if value is negative
-				top = bottom - ((max(value,0) - min_value) * unit_size)
-				if self.stack == 'side':
-					left += bar_width * dataset_count
-
-				rect = etree.SubElement(self.graph, 'rect', {
-					'x': str(left),
-					'y': str(top),
-					'width': str(bar_width),
-					'height': str(length),
-					'class': 'fill%s' % (dataset_count+1),
-				})
-				
-				self.make_datapoint_text(left + bar_width/2.0, top-6, value)
-
-class HorizontalBar(Bar):
-	rotate_y_labels = True
-	show_x_guidelines = True
-	show_y_guidelines = False
-	right_align = right_font = True
-	
-
-	def get_x_labels(self):
-		return self.get_data_labels()
-	
-	def get_y_labels(self):
-		return self.get_field_labels()
-
-	def y_label_offset(self, height):
-		return height / -2.0
-	
-	def draw_data(self):
-		min_value = self.data_min()
-
-		unit_size = float(self.graph_width)
-		unit_size -= self.font_size*2*self.right_font
-		unit_size /= max(self.get_data_values()) - min(self.get_data_values())
-		
-		bar_gap = self.get_bar_gap(self.get_field_height())
-
-		bar_height = self.get_field_height() - bar_gap
-		if self.stack == 'side':
-			bar_height /= len(self.data)
-
-		y_mod = (bar_height / 2) + (self.font_size / 2)
-
-		for field_count, field in enumerate(self.fields):
-			for dataset_count, dataset in enumerate(self.data):
-				value = dataset['data'][field_count]
-		
-				top = self.graph_height - (self.get_field_height() * (field_count+1))
-				if self.stack == 'side':
-					top += (bar_height * dataset_count)
-				# cases (assume 0 = +ve):
-				#   value  min  length          left
-				#    +ve   +ve  value.abs - min minvalue.abs
-				#    +ve   -ve  value.abs - 0   minvalue.abs
-				#    -ve   -ve  value.abs - 0   minvalue.abs + value
-				length = (abs(value) - max(min_value, 0)) * unit_size
-				# left is 0 if value is negative
-				left = (abs(min_value) + min(value, 0)) * unit_size
-
-				rect = etree.SubElement(self.graph, 'rect', {
-					'x': str(left),
-					'y': str(top),
-					'width': str(length),
-					'height': str(bar_height),
-					'class': 'fill%s' % (dataset_count+1),
-				})
-				
-				self.make_datapoint_text(left+length+5, top+y_mod, value,
-										 "text-anchor: start; ")
+#!python
+from itertools import chain
+from lxml import etree
+from svg.charts.graph import Graph
+
+__all__ = ('VerticalBar', 'HorizontalBar')
+
+class Bar(Graph):
+	"A superclass for bar-style graphs.  Do not instantiate directly."
+
+	# gap between bars
+	bar_gap = True
+	# how to stack adjacent dataset series
+	# overlap - overlap bars with transparent colors
+	# top - stack bars on top of one another
+	# side - stack bars side-by-side
+	stack = 'overlap'
+	
+	scale_divisions = None
+
+	stylesheet_names = Graph.stylesheet_names + ['bar.css']
+
+	def __init__(self, fields, *args, **kargs):
+		self.fields = fields
+		super(Bar, self).__init__(*args, **kargs)
+
+	# adapted from Plot
+	def get_data_values(self):
+		min_value, max_value, scale_division = self.data_range()
+		result = tuple(float_range(min_value, max_value + scale_division, scale_division))
+		if self.scale_integers:
+			result = map(int, result)
+		return result
+	
+	# adapted from plot (very much like calling data_range('y'))
+	def data_range(self):
+		min_value = self.data_min()
+		max_value = self.data_max()
+		range = max_value - min_value
+
+		data_pad = range / 20.0 or 10
+		scale_range = (max_value + data_pad) - min_value
+		
+		scale_division = self.scale_divisions or (scale_range / 10.0)
+		
+		if self.scale_integers:
+			scale_division = round(scale_division) or 1
+			
+		return min_value, max_value, scale_division
+
+	def get_field_labels(self):
+		return self.fields
+
+	def get_data_labels(self):
+		return map(str, self.get_data_values())
+
+	def data_max(self):
+		return max(chain(*map(lambda set: set['data'], self.data)))
+		# above is same as
+		# return max(map(lambda set: max(set['data']), self.data))
+		
+	def data_min(self):
+		if not getattr(self, 'min_scale_value') is None: return self.min_scale_value
+		min_value = min(chain(*map(lambda set: set['data'], self.data)))
+		min_value = min(min_value, 0)
+		return min_value
+
+	def get_bar_gap(self, field_size):
+		bar_gap = 10 # default gap
+		if field_size < 10:
+			# adjust for narrow fields
+			bar_gap = field_size / 2
+		# the following zero's out the gap if bar_gap is False
+		bar_gap = int(self.bar_gap) * bar_gap
+		return bar_gap
+
+def float_range(start = 0, stop = None, step = 1):
+	"Much like the built-in function range, but accepts floats"
+	while start < stop:
+		yield float(start)
+		start += step
+
+
+class VerticalBar(Bar):
+	"""    # === Create presentation quality SVG bar graphs easily
+    #
+    # = Synopsis
+    #
+    #   require 'SVG/Graph/Bar'
+    #
+    #   fields = %w(Jan Feb Mar);
+    #   data_sales_02 = [12, 45, 21]
+    #
+    #   graph = SVG::Graph::Bar.new(
+    #     :height => 500,
+    #     :width => 300,
+    #     :fields => fields
+    #  )
+    #
+    #   graph.add_data(
+    #     :data => data_sales_02,
+    #     :title => 'Sales 2002'
+    #  )
+    #
+    #   print "Content-type: image/svg+xml\r\n\r\n"
+    #   print graph.burn
+    #
+    # = Description
+    #
+    # This object aims to allow you to easily create high quality
+    # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
+    # style sheet or supply your own. Either way there are many options which
+    # can be configured to give you control over how the graph is generated -
+    # with or without a key, data elements at each point, title, subtitle etc.
+    #
+    # = Notes
+    #
+    # The default stylesheet handles upto 12 data sets, if you
+    # use more you must create your own stylesheet and add the
+    # additional settings for the extra data sets. You will know
+    # if you go over 12 data sets as they will have no style and
+    # be in black.
+    #
+    # = Examples
+    #
+    # * http://germane-software.com/repositories/public/SVG/test/test.rb
+    #
+    # = See also
+    #
+    # * SVG::Graph::Graph
+    # * SVG::Graph::BarHorizontal
+    # * SVG::Graph::Line
+    # * SVG::Graph::Pie
+    # * SVG::Graph::Plot
+    # * SVG::Graph::TimeSeries
+"""
+	top_align = top_font = 1
+
+	def get_x_labels(self):
+		return self.get_field_labels()
+
+	def get_y_labels(self):
+		return self.get_data_labels()
+
+	def x_label_offset(self, width):
+		return width / 2.0
+
+	def draw_data(self):
+		min_value = self.data_min()
+		unit_size = (float(self.graph_height) - self.font_size*2*self.top_font)
+		unit_size /= (max(self.get_data_values()) - min(self.get_data_values()))
+
+		bar_gap = self.get_bar_gap(self.get_field_width())		
+
+		bar_width = self.get_field_width() - bar_gap
+		if self.stack == 'side':
+			bar_width /= len(self.data)
+		
+		x_mod = (self.graph_width - bar_gap)/2
+		if self.stack == 'side':
+			x_mod -= bar_width/2
+
+		bottom = self.graph_height
+		
+		for field_count, field in enumerate(self.fields):
+			for dataset_count, dataset in enumerate(self.data):
+				# cases (assume 0 = +ve):
+				#   value  min  length
+				#    +ve   +ve  value - min
+				#    +ve   -ve  value - 0
+				#    -ve   -ve  value.abs - 0
+				value = dataset['data'][field_count]
+				
+				left = self.get_field_width() * field_count
+				
+				length = (abs(value) - max(min_value, 0)) * unit_size
+				# top is 0 if value is negative
+				top = bottom - ((max(value,0) - min_value) * unit_size)
+				if self.stack == 'side':
+					left += bar_width * dataset_count
+
+				rect = etree.SubElement(self.graph, 'rect', {
+					'x': str(left),
+					'y': str(top),
+					'width': str(bar_width),
+					'height': str(length),
+					'class': 'fill%s' % (dataset_count+1),
+				})
+				
+				self.make_datapoint_text(left + bar_width/2.0, top-6, value)
+
+class HorizontalBar(Bar):
+	rotate_y_labels = True
+	show_x_guidelines = True
+	show_y_guidelines = False
+	right_align = right_font = True
+	
+
+	def get_x_labels(self):
+		return self.get_data_labels()
+	
+	def get_y_labels(self):
+		return self.get_field_labels()
+
+	def y_label_offset(self, height):
+		return height / -2.0
+	
+	def draw_data(self):
+		min_value = self.data_min()
+
+		unit_size = float(self.graph_width)
+		unit_size -= self.font_size*2*self.right_font
+		unit_size /= max(self.get_data_values()) - min(self.get_data_values())
+		
+		bar_gap = self.get_bar_gap(self.get_field_height())
+
+		bar_height = self.get_field_height() - bar_gap
+		if self.stack == 'side':
+			bar_height /= len(self.data)
+
+		y_mod = (bar_height / 2) + (self.font_size / 2)
+
+		for field_count, field in enumerate(self.fields):
+			for dataset_count, dataset in enumerate(self.data):
+				value = dataset['data'][field_count]
+		
+				top = self.graph_height - (self.get_field_height() * (field_count+1))
+				if self.stack == 'side':
+					top += (bar_height * dataset_count)
+				# cases (assume 0 = +ve):
+				#   value  min  length          left
+				#    +ve   +ve  value.abs - min minvalue.abs
+				#    +ve   -ve  value.abs - 0   minvalue.abs
+				#    -ve   -ve  value.abs - 0   minvalue.abs + value
+				length = (abs(value) - max(min_value, 0)) * unit_size
+				# left is 0 if value is negative
+				left = (abs(min_value) + min(value, 0)) * unit_size
+
+				rect = etree.SubElement(self.graph, 'rect', {
+					'x': str(left),
+					'y': str(top),
+					'width': str(length),
+					'height': str(bar_height),
+					'class': 'fill%s' % (dataset_count+1),
+				})
+				
+				self.make_datapoint_text(left+length+5, top+y_mod, value,
+										 "text-anchor: start; ")

svg/charts/css.py

-import cssutils
-
-SVG = 'SVG 1.1' # http://www.w3.org/TR/SVG11/styling.html
-
-macros = {
-	'paint': 'none|currentColor|{color}',
-	'unitidentifier': 'em|ex|px|pt|pc|cm|mm|in|%',
-	'length': '{positivenum}({unitidentifier})?',
-	'dasharray': '{positivenum}(\s*,\s*{positivenum})*',
-	# a number greater-than or equal to one
-	'number-ge-one': '{[1-9][0-9]*(\.[0-9]+)?',
-	}
-properties = {
-	# Clipping, Masking, and Compositing
-	'clip-path': '{uri}|none|inherit',
-	'clip-rule': 'nonzero|evenodd|inherit',
-	'mask': '{uri}|none|inherit',
-	'opacity': '{num}|inherit',
-
-	# Filter Effects
-	'enable-background': 'accumulate|new(\s+{num}){0,4}|inherit',
-	'filter': '{uri}|none|inherit',
-	'flood-color': 'currentColor|{color}|inherit',
-	'flood-opacity': '{num}|inherit',
-	'lighting-color': 'currentColor|{color}|inherit',
-
-	# Gradient Properties
-	'stop-color': 'currentColor|{color}|inherit',
-	'stop-opacity': '{num}|inherit',
-
-	# Interactivity Properties
-	'pointer-events': 'visiblePainted|visibleFill|visibleStroke|visible|painted|fill|stroke|all|none|inherit',
-
-	# Color and Pointing Properties
-	'color-interpolation': 'auto|sRGB|linearRGB|inherit',
-	'color-interpolation-filters': 'auto|sRGB|linearRGB|inherit',
-	'color-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit',
-	'shape-rendering': 'auto|optimizeSpeed|crispEdges|geometricPrecision|inherit',
-	'text-rendering': 'auto|optimizeSpeed|optimizeLegibility|geometricPrecision|inherit',
-	'fill': '{paint}',
-	'fill-opacity': '{num}|inherit',
-	'fill-rule': 'nonzero|evenodd|inherit',
-	'image-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit',
-	'marker': 'none|inherit|{uri}',
-	'marker-end': 'none|inherit|{uri}',
-	'marker-mid': 'none|inherit|{uri}',
-	'marker-start': 'none|inherit|{uri}',
-	'shape-rendering': 'auto|optimizeSpeed|crispEdges|geometricPrecision|inherit',
-	'stroke': '{paint}',
-	'stroke-dasharray': 'none|{dasharray}|inherit',
-	'stroke-dashoffset': '{length}|inherit',
-	'stroke-linecap': 'butt|round|square|inherit',
-	'stroke-linejoin': 'miter|round|bevel|inherit',
-	'stroke-miterlimit': '{number-ge-one}|inherit',
-	'stroke-opacity': '{num}|inherit',
-	'stroke-width': '{length}|inherit',
-	'text-rendering': 'auto|optimizeSpeed|optimizeLegibility|geometricPrecision|inherit',
-
-	# Text Properties
-	'alignment-baseline': 'auto|baseline|before-edge|text-before-edge|middle|central|after-edge|text-after-edge|ideographic|alphabetic|hanging|mathematical|inherit',
-	'baseline-shift': 'baseline|sub|super|{percentage}|{length}|inherit',
-	'dominant-baseline': 'auto|use-script|no-change|reset-size|ideographic|alphabetic|hanging||mathematical|central|middle|text-after-edge|text-before-edge|inherit',
-	'glyph-orientation-horizontal': '{angle}|inherit',
-	'glyph-orientation-vertical': 'auto|{angle}|inherit',
-	'kerning': 'auto|{length}|inherit',
-	'text-anchor': 'start|middle|end|inherit',
-	'writing-mode': 'lr-tb|rl-tb|tb-rl|lr|rl|tb|inherit',
-	}
-
-cssutils.profile.addProfile(SVG, properties, macros)
-
+import cssutils
+
+SVG = 'SVG 1.1' # http://www.w3.org/TR/SVG11/styling.html
+
+macros = {
+	'paint': 'none|currentColor|{color}',
+	'unitidentifier': 'em|ex|px|pt|pc|cm|mm|in|%',
+	'length': '{positivenum}({unitidentifier})?',
+	'dasharray': '{positivenum}(\s*,\s*{positivenum})*',
+	# a number greater-than or equal to one
+	'number-ge-one': '{[1-9][0-9]*(\.[0-9]+)?',
+	}
+properties = {
+	# Clipping, Masking, and Compositing
+	'clip-path': '{uri}|none|inherit',
+	'clip-rule': 'nonzero|evenodd|inherit',
+	'mask': '{uri}|none|inherit',
+	'opacity': '{num}|inherit',
+
+	# Filter Effects
+	'enable-background': 'accumulate|new(\s+{num}){0,4}|inherit',
+	'filter': '{uri}|none|inherit',
+	'flood-color': 'currentColor|{color}|inherit',
+	'flood-opacity': '{num}|inherit',
+	'lighting-color': 'currentColor|{color}|inherit',
+
+	# Gradient Properties
+	'stop-color': 'currentColor|{color}|inherit',
+	'stop-opacity': '{num}|inherit',
+
+	# Interactivity Properties
+	'pointer-events': 'visiblePainted|visibleFill|visibleStroke|visible|painted|fill|stroke|all|none|inherit',
+
+	# Color and Pointing Properties
+	'color-interpolation': 'auto|sRGB|linearRGB|inherit',
+	'color-interpolation-filters': 'auto|sRGB|linearRGB|inherit',
+	'color-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit',
+	'shape-rendering': 'auto|optimizeSpeed|crispEdges|geometricPrecision|inherit',
+	'text-rendering': 'auto|optimizeSpeed|optimizeLegibility|geometricPrecision|inherit',
+	'fill': '{paint}',
+	'fill-opacity': '{num}|inherit',
+	'fill-rule': 'nonzero|evenodd|inherit',
+	'image-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit',
+	'marker': 'none|inherit|{uri}',
+	'marker-end': 'none|inherit|{uri}',
+	'marker-mid': 'none|inherit|{uri}',
+	'marker-start': 'none|inherit|{uri}',
+	'shape-rendering': 'auto|optimizeSpeed|crispEdges|geometricPrecision|inherit',
+	'stroke': '{paint}',
+	'stroke-dasharray': 'none|{dasharray}|inherit',
+	'stroke-dashoffset': '{length}|inherit',
+	'stroke-linecap': 'butt|round|square|inherit',
+	'stroke-linejoin': 'miter|round|bevel|inherit',
+	'stroke-miterlimit': '{number-ge-one}|inherit',
+	'stroke-opacity': '{num}|inherit',
+	'stroke-width': '{length}|inherit',
+	'text-rendering': 'auto|optimizeSpeed|optimizeLegibility|geometricPrecision|inherit',
+
+	# Text Properties
+	'alignment-baseline': 'auto|baseline|before-edge|text-before-edge|middle|central|after-edge|text-after-edge|ideographic|alphabetic|hanging|mathematical|inherit',
+	'baseline-shift': 'baseline|sub|super|{percentage}|{length}|inherit',
+	'dominant-baseline': 'auto|use-script|no-change|reset-size|ideographic|alphabetic|hanging||mathematical|central|middle|text-after-edge|text-before-edge|inherit',
+	'glyph-orientation-horizontal': '{angle}|inherit',
+	'glyph-orientation-vertical': 'auto|{angle}|inherit',
+	'kerning': 'auto|{length}|inherit',
+	'text-anchor': 'start|middle|end|inherit',
+	'writing-mode': 'lr-tb|rl-tb|tb-rl|lr|rl|tb|inherit',
+	}
+
+cssutils.profile.addProfile(SVG, properties, macros)
+
 cssutils.profile.defaultProfiles = [SVG, cssutils.profile.CSS_LEVEL_2]

svg/charts/graph.py

-#!python
-# -*- coding: UTF-8 -*-
-
-"""
-svg.charts.graph
-
-The base module for `svg.charts` classes.
-"""
-
-from operator import itemgetter
-from itertools import islice
-import pkg_resources
-import functools
-
-import cssutils
-from lxml import etree
-
-from svg.charts import css # causes the SVG profile to be loaded
-
-try:
-	import zlib
-except ImportError:
-	zlib = None
-
-def sort_multiple(arrays):
-	"sort multiple lists (of equal size) using the first list for the sort keys"
-	tuples = zip(*arrays)
-	tuples.sort()
-	return zip(*tuples)
-
-class Graph(object):
-	"""
-	Base object for generating SVG Graphs
-	
-	Synopsis
-
-		This class is only used as a superclass of specialized charts.  Do not
-		attempt to use this class directly, unless creating a new chart type.
-
-		For examples of how to subclass this class, see the existing specific
-		subclasses, such as svn.charts.Pie.
-
-	* svg.charts.bar
-	* svg.charts.line
-	* svg.charts.pie
-	* svg.charts.plot
-	* svg.charts.time_series
-
-	"""
-	width=                500
-	height=               300
-	show_x_guidelines=    False
-	show_y_guidelines=    True
-	show_data_values=     True
-	min_scale_value=      None
-	show_x_labels=        True
-	stagger_x_labels=     False
-	rotate_x_labels=      False
-	step_x_labels=        1
-	step_include_first_x_label= True
-	show_y_labels=        True
-	rotate_y_labels=      False
-	stagger_y_labels=     False
-	step_include_first_y_label= True
-	step_y_labels=        1
-	scale_integers=       False
-	show_x_title=         False
-	x_title=              'X Field names'
-	show_y_title=         False
-	y_title_text_direction= 'bt' # 'bt' for bottom to top; 'tb' for top to bottom
-	y_title=              'Y Scale'
-	show_graph_title=     False
-	graph_title=          'Graph Title'
-	show_graph_subtitle=  False
-	graph_subtitle=       'Graph Subtitle'
-	key=                  True
-	key_position=         'right' # 'bottom' or 'right',
-	
-	font_size=            12
-	title_font_size=      16
-	subtitle_font_size=   14
-	x_label_font_size=    12
-	x_title_font_size=    14
-	y_label_font_size=    12
-	y_title_font_size=    14
-	key_font_size=        10
-	
-	css_inline=           False
-	add_popups=           False
-
-	top_align = top_font = right_align = right_font = 0
-	
-	compress = False
-	
-	stylesheet_names = ['graph.css']
-
-	def __init__(self, config = {}):
-		"""Initialize the graph object with the graph settings."""
-		if self.__class__ is Graph:
-			raise NotImplementedError("Graph is an abstract base class")
-		self.load_config(config)
-		self.clear_data()
-		self.style = {}
-
-	def load_config(self, config):
-		self.__dict__.update(config)
-		
-	def add_data(self, conf):
-		"""
-		Add data to the graph object. May be called several times to add
-		additional data sets.
-		
-		>>> data_sales_02 = [12, 45, 21] # doctest: +SKIP
-		>>> graph.add_data({ # doctest: +SKIP
-		...  'data': data_sales_02,
-		...  'title': 'Sales 2002'
-		... }) # doctest: +SKIP
-		"""
-		self.validate_data(conf)
-		self.process_data(conf)
-		self.data.append(conf)
-
-	def validate_data(self, conf):
-		try:
-			assert(isinstance(conf['data'], (tuple, list)))
-		except TypeError, e:
-			raise TypeError, "conf should be dictionary with 'data' and other items"
-		except AssertionError:
-			if not hasattr(conf['data'], '__iter__'):
-				raise TypeError, "conf['data'] should be tuple or list or iterable"
-
-	def process_data(self, data): pass
-	
-	def clear_data(self):
-		"""
-		This method removes all data from the object so that you can
-		reuse it to create a new graph but with the same config options.
-		
-		>>> graph.clear_data() # doctest: +SKIP
-		"""
-		self.data = []
-		
-	def burn(self):
-		"""
-		Process the template with the data and
-		config which has been set and return the resulting SVG.
-		
-		Raises ValueError when no data set has
-		been added to the graph object.
-		"""
-		if not self.data: raise ValueError("No data available")
-		
-		if hasattr(self, 'calculations'): self.calculations()
-		
-		self.start_svg()
-		self.calculate_graph_dimensions()
-		self.foreground = etree.Element("g")
-		self.draw_graph()
-		self.draw_titles()
-		self.draw_legend()
-		self.draw_data()
-		self.graph.append(self.foreground)
-		self.render_inline_styles()
-		
-		return self._burn_compressed()
-
-	def _burn_compressed(self):
-		if self.compress and not zlib:
-			self.root.addprevious(etree.Comment('Python zlib not available for SVGZ'))
-		
-		data = etree.tostring(self.root, pretty_print=True, xml_declaration=True, encoding='utf-8')
-		
-		if self.compress and zlib:
-			data = zlib.compress(data)
-
-		return data
-	
-	KEY_BOX_SIZE = 12
-	
-	def calculate_left_margin(self):
-		"""
-		Calculates the margin to the left of the plot area, setting
-		border_left.
-		"""
-		bl = 7
-		# Check for Y labels
-		if self.rotate_y_labels:
-			max_y_label_height_px = self.y_label_font_size
-		else:
-			label_lengths = map(len, self.get_y_labels())
-			max_y_label_len = max(label_lengths)
-			max_y_label_height_px = 0.6 * max_y_label_len * self.y_label_font_size
-		if self.show_y_labels: bl += max_y_label_height_px
-		if self.stagger_y_labels: bl += max_y_label_height_px + 10
-		if self.show_y_title: bl += self.y_title_font_size + 5
-		self.border_left = bl
-		
-	def max_y_label_width_px(self):
-		"""
-		Calculate the width of the widest Y label.  This will be the
-		character height if the Y labels are rotated.
-		"""
-		if self.rotate_y_labels:
-			return self.font_size
-		
-	def calculate_right_margin(self):
-		"""
-		Calculate the margin in pixels to the right of the plot area,
-		setting border_right.
-		"""
-		br = 7
-		if self.key and self.key_position == 'right':
-			max_key_len = max(map(len, self.keys()))
-			br += max_key_len * self.key_font_size * 0.6
-			br += self.KEY_BOX_SIZE
-			br += 10		# Some padding around the box
-		self.border_right = br
-		
-	def calculate_top_margin(self):
-		"""
-		Calculate the margin in pixels above the plot area, setting
-		border_top.
-		"""
-		self.border_top = 5
-		if self.show_graph_title: self.border_top += self.title_font_size
-		self.border_top += 5
-		if self.show_graph_subtitle: self.border_top += self.subtitle_font_size
-		
-	def add_popup(self, x, y, label):
-		"""
-		Add pop-up information to a point on the graph.
-		"""
-		txt_width = len(label) * self.font_size * 0.6 + 10
-		tx = x + [5,-5][int(x+txt_width > self.width)]
-		anchor = ['start', 'end'][x+txt_width > self.width]
-		style = 'fill: #000; text-anchor: %s;' % anchor
-		id = 'label-%s' % label
-		t = etree.SubElement(self.foreground, 'text', {
-			'x': str(tx),
-			'y': str(y - self.font_size),
-			'visibility': 'hidden',
-			'style': style,
-			'text': label,
-			'id': id
-			})
-
-		# add the circle element to the foreground
-		visibility = "document.getElementById('%s').setAttribute('visibility', %%s)" % id
-		t = etree.SubElement(self.foreground, 'circle', {
-			'cx': str(x),
-			'cy': str(y),
-			'r': str(10),
-			'style': 'opacity: 0;',
-			'onmouseover': visibility % 'visible',
-			'onmouseout': visibility % 'hidden',
-			})
-
-	def calculate_bottom_margin(self):
-		"""
-		Calculate the margin in pixels below the plot area, setting
-		border_bottom.
-		"""
-		bb = 7
-		if self.key and self.key_position == 'bottom':
-			bb += len(self.data) * (self.font_size + 5)
-			bb += 10
-		if self.show_x_labels:
-			max_x_label_height_px = self.x_label_font_size
-			if self.rotate_x_labels:
-				label_lengths = map(len, self.get_x_labels())
-				max_x_label_len = reduce(max, label_lengths)
-				max_x_label_height_px *= 0.6 * max_x_label_len
-			bb += max_x_label_height_px
-			if self.stagger_x_labels: bb += max_x_label_height_px + 10
-		if self.show_x_title: bb += self.x_title_font_size + 5
-		self.border_bottom = bb
-		
-	def draw_graph(self):
-		"""
-		The central logic for drawing the graph.
-
-		Sets self.graph (the 'g' element in the SVG root)
-		"""
-		transform = 'translate (%s %s)' % (self.border_left, self.border_top)
-		self.graph = etree.SubElement(self.root, 'g', transform=transform)
-		
-		etree.SubElement(self.graph, 'rect', {
-			'x': '0',
-			'y': '0',
-			'width': str(self.graph_width),
-			'height': str(self.graph_height),
-			'class': 'graphBackground'
-			})
-		
-		#Axis
-		etree.SubElement(self.graph, 'path', {
-			'd': 'M 0 0 v%s' % self.graph_height,
-			'class': 'axis',
-			'id': 'xAxis'
-		})
-		etree.SubElement(self.graph, 'path', {
-			'd': 'M 0 %s h%s' % (self.graph_height, self.graph_width),
-			'class': 'axis',
-			'id': 'yAxis'
-		})
-		
-		self.draw_x_labels()
-		self.draw_y_labels()
-	
-	def x_label_offset(self, width):
-		"""
-		Return an offset for drawing the x label. Currently returns 0.
-		"""
-		# consider width/2 for centering the labels
-		return 0
-
-	def make_datapoint_text(self, x, y, value, style=None):
-		"""
-		Add text for a datapoint
-		"""
-		if not self.show_data_values:
-			# do nothing
-			return
-		# first lay down the text in a wide white stroke to
-		#  differentiate it from the background
-		e = etree.SubElement(self.foreground, 'text', {
-			'x': str(x),
-			'y': str(y),
-			'class': 'dataPointLabel',
-			'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(),
-		})
-		e.text = str(value)
-		# then lay down the text in the specified style
-		e = etree.SubElement(self.foreground, 'text', {
-			'x': str(x),
-			'y': str(y),
-			'class': 'dataPointLabel'})
-		e.text = str(value)
-		if style: e.set('style', style)
-
-	def draw_x_labels(self):
-		"Draw the X axis labels"
-		if self.show_x_labels:
-			labels = self.get_x_labels()
-			count = len(labels)
-			
-			labels = enumerate(iter(labels))
-			start = int(not self.step_include_first_x_label)
-			labels = islice(labels, start, None, self.step_x_labels)
-			map(self.draw_x_label, labels)
-			self.draw_x_guidelines(self.field_width(), count)
-	
-	def draw_x_label(self, label):
-		label_width = self.field_width()
-		index, label = label
-		text = etree.SubElement(self.graph, 'text', {'class': 'xAxisLabels'})
-		text.text = label
-		
-		x = index * label_width + self.x_label_offset(label_width)
-		y = self.graph_height + self.x_label_font_size + 3
-		t = 0 - (self.font_size / 2)
-		
-		if self.stagger_x_labels and  (index % 2):
-			stagger = self.x_label_font_size + 5
-			y += stagger
-			graph_height = self.graph_height
-			path = etree.SubElement(self.graph, 'path', {
-				'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(),
-				'class': 'staggerGuideLine'
-			})
-			
-		text.set('x', str(x))
-		text.set('y', str(y))
-		
-		if self.rotate_x_labels:
-			transform = 'rotate(90 %d %d) translate(0 -%d)' % \
-				(x, y-self.x_label_font_size, self.x_label_font_size/4)
-			text.set('transform', transform)
-			text.set('style', 'text-anchor: start')
-		else:
-			text.set('style', 'text-anchor: middle')
-			
-	def y_label_offset(self, height):
-		"""
-		Return an offset for drawing the y label. Currently returns 0.
-		"""
-		# Consider height/2 to center within the field.
-		return 0
-	
-	def get_field_width(self):
-		return float(self.graph_width - self.font_size*2*self.right_font) / \
-			(len(self.get_x_labels()) - self.right_align)
-	field_width = get_field_width
-	
-	def get_field_height(self):
-		return float(self.graph_height - self.font_size*2*self.top_font) / \
-			(len(self.get_y_labels()) - self.top_align)
-	field_height = get_field_height
-
-	def draw_y_labels(self):
-		"Draw the Y axis labels"
-		if not self.show_y_labels:
-			# do nothing
-			return
-
-		labels = self.get_y_labels()
-		count = len(labels)
-		
-		labels = enumerate(iter(labels))
-		start = int(not self.step_include_first_y_label)
-		labels = islice(labels, start, None, self.step_y_labels)
-		map(self.draw_y_label, labels)
-		self.draw_y_guidelines(self.field_height(), count)
-
-	def get_y_offset(self):
-		result = self.graph_height + self.y_label_offset(self.field_height())
-		if not self.rotate_y_labels: result += self.font_size/1.2
-		return result
-	y_offset = property(get_y_offset)
-	
-	def draw_y_label(self, label):
-		label_height = self.field_height()
-		index, label = label
-		text = etree.SubElement(self.graph, 'text', {'class': 'yAxisLabels'})
-		text.text = label
-		
-		y = self.y_offset - (label_height * index)
-		x = {True: 0, False:-3}[self.rotate_y_labels]
-		
-		if self.stagger_y_labels and  (index % 2):
-			stagger = self.y_label_font_size + 5
-			x -= stagger
-			path = etree.SubElement(self.graph, 'path', {
-				'd': 'M%(x)f %(y)f h%(stagger)d' % vars(),
-				'class': 'staggerGuideLine'
-			})
-			
-		text.set('x', str(x))
-		text.set('y', str(y))
-		
-		if self.rotate_y_labels:
-			transform = 'translate(-%d 0) rotate (90 %d %d)' % \
-				(self.font_size, x, y)
-			text.set('transform', transform)
-			text.set('style', 'text-anchor: middle')
-		else:
-			text.set('y', str(y - self.y_label_font_size/2))
-			text.set('style', 'text-anchor: end')
-		
-	def draw_x_guidelines(self, label_height, count):
-		"Draw the X-axis guidelines"
-		if not self.show_x_guidelines: return
-		# skip the first one
-		for count in range(1,count):
-			start = label_height*count
-			stop = self.graph_height
-			path = etree.SubElement(self.graph, 'path', {
-				'd': 'M %(start)s 0 v%(stop)s' % vars(),
-				'class': 'guideLines'})
-
-	def draw_y_guidelines(self, label_height, count):
-		"Draw the Y-axis guidelines"
-		if not self.show_y_guidelines: return
-		for count in range(1, count):
-			start = self.graph_height - label_height*count
-			stop = self.graph_width
-			path = etree.SubElement(self.graph, 'path', {
-				'd': 'M 0 %(start)s h%(stop)s' % vars(),
-				'class': 'guideLines'})
-
-	def draw_titles(self):
-		"Draws the graph title and subtitle"
-		if self.show_graph_title: self.draw_graph_title()
-		if self.show_graph_subtitle: self.draw_graph_subtitle()
-		if self.show_x_title: self.draw_x_title()
-		if self.show_y_title: self.draw_y_title()
-
-	def draw_graph_title(self):
-		text = etree.SubElement(self.root, 'text', {
-			'x': str(self.width / 2),
-			'y': str(self.title_font_size),
-			'class': 'mainTitle'})
-		text.text = self.graph_title
-
-	def draw_graph_subtitle(self):
-		y_subtitle_options = [subtitle_font_size, title_font_size+10]
-		y_subtitle = y_subtitle_options[self.show_graph_title]
-		text = etree.SubElement(self.root, 'text', {
-			'x': str(self.width/2),
-			'y': str(y_subtitle),
-			'class': 'subTitle',
-			})
-		text.text = self.graph_title
-
-	def draw_x_title(self):
-		y = self.graph_height + self.border_top + self.x_title_font_size
-		if self.show_x_labels:
-			y_size = self.x_label_font_size+5
-			if self.stagger_x_labels: y_size*=2
-			y += y_size
-		x = self.width / 2
-		
-		text = etree.SubElement(self.root, 'text', {
-			'x': str(x),
-			'y': str(y),
-			'class': 'xAxisTitle',
-			})
-		text.text = self.x_title
-
-	def draw_y_title(self):
-		x = self.y_title_font_size
-		if self.y_title_text_direction=='bt':
-				x += 3
-				rotate = -90
-		else:
-				x -= 3
-				rotate = 90
-		y = self.height / 2
-		text = etree.SubElement(self.root, 'text', {
-				'x': str(x),
-				'y': str(y),
-				'class': 'yAxisTitle',
-				})
-		text.text = self.y_title
-		text.set('transform', 'rotate(%(rotate)d, %(x)s, %(y)s)' % vars())
-
-	def keys(self):
-		return map(itemgetter('title'), self.data)
-	
-	def draw_legend(self):
-		if not self.key:
-			# do nothing
-			return
-
-		group = etree.SubElement(self.root, 'g')
-		
-		for key_count, key_name in enumerate(self.keys()):
-			y_offset = (self.KEY_BOX_SIZE * key_count) + (key_count * 5)
-			etree.SubElement(group, 'rect', {
-				'x': '0',
-				'y': str(y_offset),
-				'width': str(self.KEY_BOX_SIZE),
-				'height': str(self.KEY_BOX_SIZE),
-				'class': 'key%s' % (key_count + 1),
-			})
-			text = etree.SubElement(group, 'text', {
-				'x': str(self.KEY_BOX_SIZE + 5),
-				'y': str(y_offset + self.KEY_BOX_SIZE),
-				'class': 'keyText'})
-			text.text = key_name
-		
-		if self.key_position == 'right':
-			x_offset = self.graph_width + self.border_left + 10
-			y_offset = self.border_top + 20
-		if self.key_position == 'bottom':
-			x_offset, y_offset = self.calculate_offsets_bottom()
-		group.set('transform', 'translate(%(x_offset)d %(y_offset)d)' % vars())
-
-	def calculate_offsets_bottom(self):
-		x_offset = self.border_left + 20
-		y_offset = self.border_top + self.graph_height + 5
-		if self.show_x_labels:
-			max_x_label_height_px = x_label_font_size
-			if self.rotate_x_labels:
-				longest_label_length = max(map(len, self.get_x_labels()))
-				# note: I think 0.6 is the ratio of width to height of characters
-				max_x_label_height_px *= longest_label_length * 0.6
-			y_offset += max_x_label_height_px
-			if self.stagger_x_labels:
-				y_offset += max_x_label_height_px + 5
-		if self.show_x_title:
-			y_offset += x_title_font_size + 5
-		return x_offset, y_offset
-			
-	def render_inline_styles(self):
-		"Hard-code the styles into the SVG XML if style sheets are not used."
-		if not self.css_inline:
-			# do nothing
-			return
-
-		styles = self.parse_css()
-		for node in xpath.Evaluate('//*[@class]', self.root):
-			cl = node.getAttribute('class')
-			style = styles[cl]
-			if node.hasAttribute('style'):
-				style += node.getAttribute('style')
-			node.setAttribute('style', style)
-
-	def parse_css(self):
-		"""
-		Take a .css file (classes only please) and parse it into a dictionary
-		of class/style pairs.
-		"""
-		# todo: save the prefs for use later
-		#orig_prefs = cssutils.ser.prefs
-		cssutils.ser.prefs.useMinified()
-		get_pair = lambda r: (r.selectorText, r.style.cssText)
-		result = dict(map(get_pair, self.get_stylesheet()))
-		return result
-
-	def add_defs(self, defs):
-		"""
-		Override and place code to add defs here. TODO: what are defs?
-		"""
-	
-	def start_svg(self):
-		"Base SVG Document Creation"
-		SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
-		SVG = '{%s}' % SVG_NAMESPACE
-		NSMAP = {
-			None: SVG_NAMESPACE,
-			'xlink': 'http://www.w3.org/1999/xlink',
-			'a3': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/',
-			}
-		self.root = etree.Element(SVG+"svg", attrib={
-			'width': str(self.width),
-			'height': str(self.height),
-			'viewBox': '0 0 %s %s' % (self.width, self.height),
-			'{http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/}scriptImplementation': 'Adobe',
-			}, nsmap=NSMAP)
-		if hasattr(self, 'style_sheet_href'):
-			pi = etree.ProcessingInstruction(
-				'xml-stylesheet',
-				'href="%s" type="text/css"' % self.style_sheet_href
-				)
-			self.root.addprevious(pi)
-		
-		comment_strings = (
-			' Created with SVG.Graph ',
-			' SVG.Graph by Jason R. Coombs ',
-			' Based on SVG::Graph by Sean E. Russel ',
-			' Based on Perl SVG:TT:Graph by Leo Lapworth & Stephan Morgan ',
-			' '+'/'*66,
-			)
-		map(self.root.append, map(etree.Comment, comment_strings))
-
-		defs = etree.SubElement(self.root, 'defs')
-		self.add_defs(defs)
-		
-		if not hasattr(self, 'style_sheet_href') and not self.css_inline:
-			self.root.append(etree.Comment(' include default stylesheet if none specified '))
-			style = etree.SubElement(defs, 'style', type='text/css')
-			# TODO: the text was previously escaped in a CDATA declaration... how
-			#  to do that with etree?
-			style.text = self.get_stylesheet().cssText
-		
-		self.root.append(etree.Comment('SVG Background'))
-		rect = etree.SubElement(self.root, 'rect', {
-			'width': str(self.width),
-			'height': str(self.height),
-			'x': '0',
-			'y': '0',
-			'class': 'svgBackground'})
-		
-	def calculate_graph_dimensions(self):
-		self.calculate_left_margin()
-		self.calculate_right_margin()
-		self.calculate_bottom_margin()
-		self.calculate_top_margin()
-		self.graph_width = self.width - self.border_left - self.border_right
-		self.graph_height = self.height - self.border_top - self.border_bottom
-
-	@staticmethod
-	def load_resource_stylesheet(name, subs=dict()):
-		css_stream = pkg_resources.resource_stream('svg.charts', name)
-		css_string = css_stream.read().decode('utf-8')
-		css_string = css_string % subs
-		sheet = cssutils.parseString(css_string)
-		return sheet
-
-	def get_stylesheet_resources(self):
-		"Get the stylesheets for this instance"
-		# allow css to include class variables
-		class_vars = class_dict(self)
-		loader = functools.partial(self.load_resource_stylesheet,
-			subs=class_vars)
-		sheets = map(loader, self.stylesheet_names)
-		return sheets
-
-	def get_stylesheet(self):
-		cssutils.log.setLevel(30) # disable INFO log messages
-		def merge_sheets(s1, s2):
-			map(s1.add, s2)
-			return s1
-		return reduce(merge_sheets, self.get_stylesheet_resources())
-
-class class_dict(object):
-	"Emulates a dictionary, but retrieves class attributes"
-	def __init__(self, obj):
-		self.__obj__ = obj
-
-	def __getitem__(self, item):
-		return getattr(self.__obj__, item)
-
-	def keys(self):
-		# dir returns a good guess of what attributes might be available
-		return dir(self.__obj__)
+#!python
+# -*- coding: UTF-8 -*-
+
+"""
+svg.charts.graph
+
+The base module for `svg.charts` classes.
+"""
+
+from operator import itemgetter
+from itertools import islice
+import pkg_resources
+import functools
+
+import cssutils
+from lxml import etree
+
+from svg.charts import css # causes the SVG profile to be loaded
+
+try:
+	import zlib
+except ImportError:
+	zlib = None
+
+def sort_multiple(arrays):
+	"sort multiple lists (of equal size) using the first list for the sort keys"
+	tuples = zip(*arrays)
+	tuples.sort()
+	return zip(*tuples)
+
+class Graph(object):
+	"""
+	Base object for generating SVG Graphs
+	
+	Synopsis
+
+		This class is only used as a superclass of specialized charts.  Do not
+		attempt to use this class directly, unless creating a new chart type.
+
+		For examples of how to subclass this class, see the existing specific
+		subclasses, such as svn.charts.Pie.
+
+	* svg.charts.bar
+	* svg.charts.line
+	* svg.charts.pie
+	* svg.charts.plot
+	* svg.charts.time_series
+
+	"""
+	width=                500
+	height=               300
+	show_x_guidelines=    False
+	show_y_guidelines=    True
+	show_data_values=     True
+	min_scale_value=      None
+	show_x_labels=        True
+	stagger_x_labels=     False
+	rotate_x_labels=      False
+	step_x_labels=        1
+	step_include_first_x_label= True
+	show_y_labels=        True
+	rotate_y_labels=      False
+	stagger_y_labels=     False
+	step_include_first_y_label= True
+	step_y_labels=        1
+	scale_integers=       False
+	show_x_title=         False
+	x_title=              'X Field names'
+	show_y_title=         False
+	y_title_text_direction= 'bt' # 'bt' for bottom to top; 'tb' for top to bottom
+	y_title=              'Y Scale'
+	show_graph_title=     False
+	graph_title=          'Graph Title'
+	show_graph_subtitle=  False
+	graph_subtitle=       'Graph Subtitle'
+	key=                  True
+	key_position=         'right' # 'bottom' or 'right',
+	
+	font_size=            12
+	title_font_size=      16
+	subtitle_font_size=   14
+	x_label_font_size=    12
+	x_title_font_size=    14
+	y_label_font_size=    12
+	y_title_font_size=    14
+	key_font_size=        10
+	
+	css_inline=           False
+	add_popups=           False
+
+	top_align = top_font = right_align = right_font = 0
+	
+	compress = False
+	
+	stylesheet_names = ['graph.css']
+
+	def __init__(self, config = {}):
+		"""Initialize the graph object with the graph settings."""
+		if self.__class__ is Graph:
+			raise NotImplementedError("Graph is an abstract base class")
+		self.load_config(config)
+		self.clear_data()
+		self.style = {}
+
+	def load_config(self, config):
+		self.__dict__.update(config)
+		
+	def add_data(self, conf):
+		"""
+		Add data to the graph object. May be called several times to add
+		additional data sets.
+		
+		>>> data_sales_02 = [12, 45, 21] # doctest: +SKIP
+		>>> graph.add_data({ # doctest: +SKIP
+		...  'data': data_sales_02,
+		...  'title': 'Sales 2002'
+		... }) # doctest: +SKIP
+		"""
+		self.validate_data(conf)
+		self.process_data(conf)
+		self.data.append(conf)
+
+	def validate_data(self, conf):
+		try:
+			assert(isinstance(conf['data'], (tuple, list)))
+		except TypeError, e:
+			raise TypeError, "conf should be dictionary with 'data' and other items"
+		except AssertionError:
+			if not hasattr(conf['data'], '__iter__'):
+				raise TypeError, "conf['data'] should be tuple or list or iterable"
+
+	def process_data(self, data): pass
+	
+	def clear_data(self):
+		"""
+		This method removes all data from the object so that you can
+		reuse it to create a new graph but with the same config options.
+		
+		>>> graph.clear_data() # doctest: +SKIP
+		"""
+		self.data = []
+		
+	def burn(self):
+		"""
+		Process the template with the data and
+		config which has been set and return the resulting SVG.
+		
+		Raises ValueError when no data set has
+		been added to the graph object.
+		"""
+		if not self.data: raise ValueError("No data available")
+		
+		if hasattr(self, 'calculations'): self.calculations()
+		
+		self.start_svg()
+		self.calculate_graph_dimensions()
+		self.foreground = etree.Element("g")
+		self.draw_graph()
+		self.draw_titles()
+		self.draw_legend()
+		self.draw_data()
+		self.graph.append(self.foreground)
+		self.render_inline_styles()
+		
+		return self._burn_compressed()
+
+	def _burn_compressed(self):
+		if self.compress and not zlib:
+			self.root.addprevious(etree.Comment('Python zlib not available for SVGZ'))
+		
+		data = etree.tostring(self.root, pretty_print=True, xml_declaration=True, encoding='utf-8')
+		
+		if self.compress and zlib:
+			data = zlib.compress(data)
+
+		return data
+	
+	KEY_BOX_SIZE = 12
+	
+	def calculate_left_margin(self):
+		"""
+		Calculates the margin to the left of the plot area, setting
+		border_left.
+		"""
+		bl = 7
+		# Check for Y labels
+		if self.rotate_y_labels:
+			max_y_label_height_px = self.y_label_font_size
+		else:
+			label_lengths = map(len, self.get_y_labels())
+			max_y_label_len = max(label_lengths)
+			max_y_label_height_px = 0.6 * max_y_label_len * self.y_label_font_size
+		if self.show_y_labels: bl += max_y_label_height_px
+		if self.stagger_y_labels: bl += max_y_label_height_px + 10
+		if self.show_y_title: bl += self.y_title_font_size + 5
+		self.border_left = bl
+		
+	def max_y_label_width_px(self):
+		"""
+		Calculate the width of the widest Y label.  This will be the
+		character height if the Y labels are rotated.
+		"""
+		if self.rotate_y_labels:
+			return self.font_size
+		
+	def calculate_right_margin(self):
+		"""
+		Calculate the margin in pixels to the right of the plot area,
+		setting border_right.
+		"""
+		br = 7
+		if self.key and self.key_position == 'right':
+			max_key_len = max(map(len, self.keys()))
+			br += max_key_len * self.key_font_size * 0.6
+			br += self.KEY_BOX_SIZE
+			br += 10		# Some padding around the box
+		self.border_right = br
+		
+	def calculate_top_margin(self):
+		"""
+		Calculate the margin in pixels above the plot area, setting
+		border_top.
+		"""
+		self.border_top = 5
+		if self.show_graph_title: self.border_top += self.title_font_size
+		self.border_top += 5
+		if self.show_graph_subtitle: self.border_top += self.subtitle_font_size
+		
+	def add_popup(self, x, y, label):
+		"""
+		Add pop-up information to a point on the graph.
+		"""
+		txt_width = len(label) * self.font_size * 0.6 + 10
+		tx = x + [5,-5][int(x+txt_width > self.width)]
+		anchor = ['start', 'end'][x+txt_width > self.width]
+		style = 'fill: #000; text-anchor: %s;' % anchor
+		id = 'label-%s' % label
+		t = etree.SubElement(self.foreground, 'text', {
+			'x': str(tx),
+			'y': str(y - self.font_size),
+			'visibility': 'hidden',
+			'style': style,
+			'text': label,
+			'id': id
+			})
+
+		# add the circle element to the foreground
+		visibility = "document.getElementById('%s').setAttribute('visibility', %%s)" % id
+		t = etree.SubElement(self.foreground, 'circle', {
+			'cx': str(x),
+			'cy': str(y),
+			'r': str(10),
+			'style': 'opacity: 0;',
+			'onmouseover': visibility % 'visible',
+			'onmouseout': visibility % 'hidden',
+			})
+
+	def calculate_bottom_margin(self):
+		"""
+		Calculate the margin in pixels below the plot area, setting
+		border_bottom.
+		"""
+		bb = 7
+		if self.key and self.key_position == 'bottom':
+			bb += len(self.data) * (self.font_size + 5)
+			bb += 10
+		if self.show_x_labels:
+			max_x_label_height_px = self.x_label_font_size
+			if self.rotate_x_labels:
+				label_lengths = map(len, self.get_x_labels())
+				max_x_label_len = reduce(max, label_lengths)
+				max_x_label_height_px *= 0.6 * max_x_label_len
+			bb += max_x_label_height_px
+			if self.stagger_x_labels: bb += max_x_label_height_px + 10
+		if self.show_x_title: bb += self.x_title_font_size + 5
+		self.border_bottom = bb
+		
+	def draw_graph(self):
+		"""
+		The central logic for drawing the graph.
+
+		Sets self.graph (the 'g' element in the SVG root)
+		"""
+		transform = 'translate (%s %s)' % (self.border_left, self.border_top)
+		self.graph = etree.SubElement(self.root, 'g', transform=transform)
+		
+		etree.SubElement(self.graph, 'rect', {
+			'x': '0',
+			'y': '0',
+			'width': str(self.graph_width),
+			'height': str(self.graph_height),
+			'class': 'graphBackground'
+			})
+		
+		#Axis
+		etree.SubElement(self.graph, 'path', {
+			'd': 'M 0 0 v%s' % self.graph_height,
+			'class': 'axis',
+			'id': 'xAxis'
+		})
+		etree.SubElement(self.graph, 'path', {
+			'd': 'M 0 %s h%s' % (self.graph_height, self.graph_width),
+			'class': 'axis',
+			'id': 'yAxis'
+		})
+		
+		self.draw_x_labels()
+		self.draw_y_labels()
+	
+	def x_label_offset(self, width):
+		"""
+		Return an offset for drawing the x label. Currently returns 0.
+		"""
+		# consider width/2 for centering the labels
+		return 0
+
+	def make_datapoint_text(self, x, y, value, style=None):
+		"""
+		Add text for a datapoint
+		"""
+		if not self.show_data_values:
+			# do nothing
+			return
+		# first lay down the text in a wide white stroke to
+		#  differentiate it from the background
+		e = etree.SubElement(self.foreground, 'text', {
+			'x': str(x),
+			'y': str(y),
+			'class': 'dataPointLabel',
+			'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(),
+		})
+		e.text = str(value)
+		# then lay down the text in the specified style
+		e = etree.SubElement(self.foreground, 'text', {
+			'x': str(x),
+			'y': str(y),
+			'class': 'dataPointLabel'})
+		e.text = str(value)
+		if style: e.set('style', style)
+
+	def draw_x_labels(self):
+		"Draw the X axis labels"
+		if self.show_x_labels:
+			labels = self.get_x_labels()
+			count = len(labels)
+			
+			labels = enumerate(iter(labels))
+			start = int(not self.step_include_first_x_label)
+			labels = islice(labels, start, None, self.step_x_labels)
+			map(self.draw_x_label, labels)
+			self.draw_x_guidelines(self.field_width(), count)
+	
+	def draw_x_label(self, label):
+		label_width = self.field_width()
+		index, label = label
+		text = etree.SubElement(self.graph, 'text', {'class': 'xAxisLabels'})
+		text.text = label
+		
+		x = index * label_width + self.x_label_offset(label_width)
+		y = self.graph_height + self.x_label_font_size + 3
+		t = 0 - (self.font_size / 2)
+		
+		if self.stagger_x_labels and  (index % 2):
+			stagger = self.x_label_font_size + 5
+			y += stagger
+			graph_height = self.graph_height
+			path = etree.SubElement(self.graph, 'path', {
+				'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(),
+				'class': 'staggerGuideLine'
+			})
+			
+		text.set('x', str(x))
+		text.set('y', str(y))
+		
+		if self.rotate_x_labels:
+			transform = 'rotate(90 %d %d) translate(0 -%d)' % \
+				(x, y-self.x_label_font_size, self.x_label_font_size/4)
+			text.set('transform', transform)
+			text.set('style', 'text-anchor: start')
+		else:
+			text.set('style', 'text-anchor: middle')
+			
+	def y_label_offset(self, height):
+		"""
+		Return an offset for drawing the y label. Currently returns 0.
+		"""
+		# Consider height/2 to center within the field.
+		return 0
+	
+	def get_field_width(self):
+		return float(self.graph_width - self.font_size*2*self.right_font) / \
+			(len(self.get_x_labels()) - self.right_align)
+	field_width = get_field_width
+	
+	def get_field_height(self):
+		return float(self.graph_height - self.font_size*2*self.top_font) / \
+			(len(self.get_y_labels()) - self.top_align)
+	field_height = get_field_height
+
+	def draw_y_labels(self):
+		"Draw the Y axis labels"
+		if not self.show_y_labels:
+			# do nothing
+			return
+
+		labels = self.get_y_labels()
+		count = len(labels)