pycha-0.7.0/ 0000775 0001750 0001750 00000000000 12130340466 012217 5 ustar lgs lgs 0000000 0000000 pycha-0.7.0/setup.cfg 0000664 0001750 0001750 00000000073 12130340466 014040 0 ustar lgs lgs 0000000 0000000 [egg_info]
tag_build =
tag_date = 0
tag_svn_revision = 0
pycha-0.7.0/MANIFEST.in 0000664 0001750 0001750 00000000247 12130334357 013762 0 ustar lgs lgs 0000000 0000000 recursive-include chavier *.py
recursive-include examples *.py
recursive-include pycha *.py
recursive-include tests *.py
include *.txt
include COPYING
include AUTHORS
pycha-0.7.0/COPYING 0000664 0001750 0001750 00000016727 12130334357 013271 0 ustar lgs lgs 0000000 0000000 GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
pycha-0.7.0/pycha.egg-info/ 0000775 0001750 0001750 00000000000 12130340466 015015 5 ustar lgs lgs 0000000 0000000 pycha-0.7.0/pycha.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 12130340366 021062 0 ustar lgs lgs 0000000 0000000
pycha-0.7.0/pycha.egg-info/SOURCES.txt 0000664 0001750 0001750 00000001633 12130340366 016703 0 ustar lgs lgs 0000000 0000000 AUTHORS
CHANGES.txt
COPYING
MANIFEST.in
README.txt
setup.py
chavier/__init__.py
chavier/app.py
chavier/dialogs.py
chavier/gui.py
examples/barchart.py
examples/errorbarchart.py
examples/interval.py
examples/linechart.py
examples/lines.py
examples/piechart.py
examples/pychadownloads.py
examples/ringchart.py
examples/scatterchart.py
examples/stackedbarchart.py
examples/svg.py
examples/test.py
examples/color/colorschemes.py
pycha/__init__.py
pycha/bar.py
pycha/chart.py
pycha/color.py
pycha/line.py
pycha/pie.py
pycha/polygonal.py
pycha/radial.py
pycha/ring.py
pycha/scatter.py
pycha/stackedbar.py
pycha/utils.py
pycha.egg-info/PKG-INFO
pycha.egg-info/SOURCES.txt
pycha.egg-info/dependency_links.txt
pycha.egg-info/entry_points.txt
pycha.egg-info/top_level.txt
pycha.egg-info/zip-safe
tests/__init__.py
tests/bar.py
tests/chart.py
tests/color.py
tests/line.py
tests/pie.py
tests/runner.py
tests/stackedbar.py
tests/utils.py pycha-0.7.0/pycha.egg-info/top_level.txt 0000664 0001750 0001750 00000000016 12130340366 017543 0 ustar lgs lgs 0000000 0000000 pycha
chavier
pycha-0.7.0/pycha.egg-info/PKG-INFO 0000664 0001750 0001750 00000013771 12130340366 016122 0 ustar lgs lgs 0000000 0000000 Metadata-Version: 1.0
Name: pycha
Version: 0.7.0
Summary: A library for making charts with Python
Home-page: http://bitbucket.org/lgs/pycha/
Author: Lorenzo Gil Sanchez
Author-email: lorenzo.gil.sanchez@gmail.com
License: LGPL 3
Description: .. contents::
=====
PyCha
=====
Pycha is a very simple Python package for drawing charts using the great
`Cairo `_ library. Its goals are:
* Lightweight
* Simple to use
* Nice looking with default values
* Customization
It won't try to draw any possible chart on earth but draw the most common ones
nicely. There are some other options you may want to look at like
`pyCairoChart `_.
Pycha is based on `Plotr `_ which is based on
`PlotKit `_. Both libraries are written in
JavaScript and are great for client web programming. I needed the same for the
server side so that's the reason I ported Plotr to Python. Now we can deliver
charts to people with JavaScript disabled or embed them in PDF reports.
Pycha is distributed under the terms of the `GNU Lesser General Public License
`_.
Documentation
-------------
You can find Pycha's documentation at http://packages.python.org/pycha
Development
-----------
You can get the last bleeding edge version of pycha by getting a clone of
the Mercurial repository::
hg clone https://bitbucket.org/lgs/pycha
Don't forget to check the
`Release Notes `_
for each version to learn the new features and incompatible changes.
Contact
-------
There is a mailing list about PyCha at http://groups.google.com/group/pycha
You can join it to ask questions about its use or simply to talk about its
development. Your ideas and feedback are greatly appreciated!
Changes
=======
0.7.0 (2012-04-07)
------------------
- Radial Chart by Roberto Garcia Carvajal
- Polygonal Chart by Roberto Garcia Carvajal
- Ring Chart by Roberto Garcia Carvajal
- Minor cleanups in the code
0.6.0 (2010-12-31)
------------------
- Buildout support
- Documentation revamped
- Debug improvements
- Autopadding
- Make the unicode strings used in labels safer
0.5.3 (2010-03-29)
------------------
- New title color option
- Fix crash in chavier application
- New horizontal axis lines. Options to turn it (and vertical ones) on and off
- Improve precision in axis ticks
- Add some examples and update old ones
0.5.2 (2009-09-26)
------------------
- Add a MANIFEST.in to explictly include all files in the source distribution
0.5.1 (2009-09-19)
------------------
- Several bug fixes (Lorenzo)
- Draw circles instead of lines for scatter chart symbols (Lorenzo)
- Error bars (Yang Zhang)
- Improve tick labels (Simon)
- Add labels with yvals next to the bars (Simon (Vsevolod) Ilyushchenko)
- Change the project website (Lorenzo)
0.5.0 (2009-03-22)
------------------
- Bar chart fixes (Adam)
- Support for custon fonts in the ticks (Ged)
- Support for an 'interval' option (Nicolas)
- New color scheme system (Lorenzo)
- Stacked bar charts support (Lorenzo)
0.4.2 (2009-02-15)
------------------
- Much better documentation (Adam)
- Fixes integer division when computing xscale (Laurent)
- Fix for a broken example (Lorenzo)
- Use labelFontSize when rendering the axis (Adam Przywecki)
- Code cleanups. Now it should pass pyflakes and pep8 in most files (Lorenzo)
- Support for running the test suite with python setup.py test (Lorenzo)
- Support for SVG (and PDF, Postscript, Win32, Quartz) by changing the way
we compute the surface dimensions (Lorenzo)
0.4.1 (2008-10-29)
------------------
- Fix a colon in the README.txt file (Lorenzo)
- Add a test_suite option to setup.py so we can run the tests before deployment
(Lorenzo)
0.4.0 (2008-10-28)
------------------
- Improved test suite (Lorenzo, Nicolas)
- Many bugs fixed (Lorenzo, Stephane Wirtel)
- Support for negative values in the datasets (Nicolas, Lorenzo)
- Chavier, a simple pygtk application for playing with Pycha charts (Lorenzo)
- Allow the legend to be placed relative to the right and bottom of the canvas
(Nicolas Evrard)
- Easier debugging by adding __str__ methods to aux classes (rectangle, point,
area, ...) (Lorenzo)
- Do not overlap Y axis label when ticks label are not rotated (John Eikenberry)
0.3.0 (2008-03-22)
------------------
- Scattered charts (Tamas Nepusz )
- Chart titles (John Eikenberry )
- Axis labels and rotated ticks (John)
- Chart background and surface background (John)
- Automatically augment the light in large color schemes (John)
- Lots of bug fixes (John and Lorenzo)
0.2.0 (2007-10-25)
------------------
- Test suite
- Python 2.4 compatibility (patch by Miguel Hernandez)
- API docs
- Small fixes
0.1.0 (2007-10-17)
------------------
- Initial release
Keywords: chart cairo
Platform: UNKNOWN
pycha-0.7.0/pycha.egg-info/entry_points.txt 0000664 0001750 0001750 00000000052 12130340366 020307 0 ustar lgs lgs 0000000 0000000 [gui_scripts]
chavier = chavier.app:main
pycha-0.7.0/pycha.egg-info/zip-safe 0000664 0001750 0001750 00000000001 12130334552 016444 0 ustar lgs lgs 0000000 0000000
pycha-0.7.0/README.txt 0000664 0001750 0001750 00000003136 12130334357 013722 0 ustar lgs lgs 0000000 0000000 .. contents::
=====
PyCha
=====
Pycha is a very simple Python package for drawing charts using the great
`Cairo `_ library. Its goals are:
* Lightweight
* Simple to use
* Nice looking with default values
* Customization
It won't try to draw any possible chart on earth but draw the most common ones
nicely. There are some other options you may want to look at like
`pyCairoChart `_.
Pycha is based on `Plotr `_ which is based on
`PlotKit `_. Both libraries are written in
JavaScript and are great for client web programming. I needed the same for the
server side so that's the reason I ported Plotr to Python. Now we can deliver
charts to people with JavaScript disabled or embed them in PDF reports.
Pycha is distributed under the terms of the `GNU Lesser General Public License
`_.
Documentation
-------------
You can find Pycha's documentation at http://packages.python.org/pycha
Development
-----------
You can get the last bleeding edge version of pycha by getting a clone of
the Mercurial repository::
hg clone https://bitbucket.org/lgs/pycha
Don't forget to check the
`Release Notes `_
for each version to learn the new features and incompatible changes.
Contact
-------
There is a mailing list about PyCha at http://groups.google.com/group/pycha
You can join it to ask questions about its use or simply to talk about its
development. Your ideas and feedback are greatly appreciated!
pycha-0.7.0/setup.py 0000664 0001750 0001750 00000003067 12130334357 013741 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import os
from setuptools import setup
from pycha import version
def read(*rnames):
return open(os.path.join(os.path.dirname(__file__), *rnames)).read()
setup(
name="pycha",
version=version,
author="Lorenzo Gil Sanchez",
author_email="lorenzo.gil.sanchez@gmail.com",
description="A library for making charts with Python",
long_description=(
read('README.txt')
+ '\n\n' +
read('CHANGES.txt')
),
license="LGPL 3",
keywords="chart cairo",
packages=['pycha', 'chavier'],
url='http://bitbucket.org/lgs/pycha/',
# if would be nice if pycairo would have an egg (sigh)
# install_requires = [
# 'pycairo',
# ],
zip_safe=True,
entry_points={
'gui_scripts': [
'chavier = chavier.app:main',
]
},
test_suite="tests",
)
pycha-0.7.0/chavier/ 0000775 0001750 0001750 00000000000 12130340466 013640 5 ustar lgs lgs 0000000 0000000 pycha-0.7.0/chavier/dialogs.py 0000664 0001750 0001750 00000015600 12130334357 015640 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of Chavier.
#
# Chavier is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Chavier is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Chavier. If not, see .
import random
import webbrowser
import pygtk
pygtk.require('2.0')
import gtk
class TextInputDialog(gtk.Dialog):
def __init__(self, toplevel_window, suggested_name):
flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT
buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
super(TextInputDialog, self).__init__(u'Enter a name for the dataset',
toplevel_window, flags, buttons)
self.set_default_size(300, -1)
hbox = gtk.HBox(spacing=6)
hbox.set_border_width(12)
label = gtk.Label(u'Name')
hbox.pack_start(label, False, False)
self.entry = gtk.Entry()
self.entry.set_text(suggested_name)
self.entry.set_activates_default(True)
hbox.pack_start(self.entry, True, True)
self.vbox.pack_start(hbox, False, False)
self.vbox.show_all()
self.set_default_response(gtk.RESPONSE_ACCEPT)
def get_name(self):
return self.entry.get_text()
class PointDialog(gtk.Dialog):
def __init__(self, toplevel_window, initial_x, initial_y):
flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT
buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
super(PointDialog, self).__init__(u'Enter the point values',
toplevel_window, flags, buttons)
initials = {u'x': str(initial_x), u'y': str(initial_y)}
self.entries = {}
for coordinate in (u'x', u'y'):
hbox = gtk.HBox(spacing=6)
hbox.set_border_width(12)
label = gtk.Label(coordinate)
hbox.pack_start(label, False, False)
entry = gtk.Entry()
entry.set_activates_default(True)
entry.set_text(initials[coordinate])
hbox.pack_start(entry, True, True)
self.entries[coordinate] = entry
self.vbox.pack_start(hbox, False, False)
self.vbox.show_all()
self.set_default_response(gtk.RESPONSE_ACCEPT)
def get_point(self):
return (float(self.entries[u'x'].get_text()),
float(self.entries[u'y'].get_text()))
class OptionDialog(gtk.Dialog):
def __init__(self, toplevel_window, label, value, value_type):
flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT
buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
super(OptionDialog, self).__init__(u'Enter the option value',
toplevel_window, flags, buttons)
hbox = gtk.HBox(spacing=6)
hbox.set_border_width(12)
label = gtk.Label(label)
hbox.pack_start(label, False, False)
self.entry = gtk.Entry()
self.entry.set_text(value or '')
self.entry.set_activates_default(True)
hbox.pack_start(self.entry, True, True)
self.vbox.pack_start(hbox, False, False)
self.vbox.show_all()
self.set_default_response(gtk.RESPONSE_ACCEPT)
def get_value(self):
return self.entry.get_text()
class RandomGeneratorDialog(gtk.Dialog):
def __init__(self, toplevel_window):
flags = gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT
buttons = (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
super(RandomGeneratorDialog, self).__init__(u'Points generation',
toplevel_window,
flags, buttons)
self.size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
self.number = self._create_spin_button('Number of points to generate',
0, 1, 5, 1, 1000, 10)
self.min = self._create_spin_button('Minimum y value',
2, 0.5, 1, -1000, 1000, 0)
self.max = self._create_spin_button('Maximum y value',
2, 0.5, 1, 0, 1000, 10)
self.vbox.show_all()
self.set_default_response(gtk.RESPONSE_ACCEPT)
def _create_spin_button(self, label_text, digits, step, page,
min_value, max_value, value):
hbox = gtk.HBox(spacing=6)
hbox.set_border_width(12)
label = gtk.Label(label_text)
label.set_alignment(1.0, 0.5)
self.size_group.add_widget(label)
hbox.pack_start(label, False, False)
spin_button = gtk.SpinButton(digits=digits)
spin_button.set_increments(step, page)
spin_button.set_range(min_value, max_value)
spin_button.set_value(value)
spin_button.set_activates_default(True)
hbox.pack_start(spin_button, True, True)
self.vbox.pack_start(hbox, False, False)
return spin_button
def generate_points(self):
n = self.number.get_value_as_int()
min_value = self.min.get_value()
max_value = self.max.get_value()
return [(x, random.uniform(min_value, max_value))
for x in range(n)]
class AboutDialog(gtk.AboutDialog):
def __init__(self, toplevel_window):
super(AboutDialog, self).__init__()
self.set_transient_for(toplevel_window)
self.set_name('Chavier')
self.set_version('0.1')
self.set_comments('A Chart Viewer for the Pycha library')
self.set_copyright('Copyleft 2008 Lorenzo Gil Sanchez')
#self.set_license('LGPL')
author = 'Lorenzo Gil Sanchez '
self.set_authors([author])
self.set_program_name('Chavier')
self.set_website('http://www.lorenzogil.com/projects/pycha')
self.set_website_label('Project website')
def url_handler(dialog, link, data=None):
webbrowser.open(link)
gtk.about_dialog_set_url_hook(url_handler)
def warning(window, msg):
dialog = gtk.MessageDialog(window,
gtk.DIALOG_MODAL|gtk.DIALOG_DESTROY_WITH_PARENT,
gtk.MESSAGE_WARNING, gtk.BUTTONS_OK, msg)
dialog.run()
dialog.destroy()
pycha-0.7.0/chavier/gui.py 0000664 0001750 0001750 00000046673 12130334357 015020 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of Chavier.
#
# Chavier is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Chavier is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Chavier. If not, see .
import pygtk
pygtk.require('2.0')
import gtk
from chavier.dialogs import (
TextInputDialog, PointDialog, OptionDialog, RandomGeneratorDialog,
AboutDialog, warning,
)
class GUI(object):
def __init__(self, app):
self.app = app
self.chart = None
self.surface = None
self.main_window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.main_window.connect('delete_event', self.delete_event)
self.main_window.connect('destroy', self.destroy)
self.main_window.set_default_size(640, 480)
self.main_window.set_title(u'Chavier')
vbox = gtk.VBox()
self.main_window.add(vbox)
vbox.show()
menubar, toolbar = self._create_ui_manager()
vbox.pack_start(menubar, False, False)
menubar.show()
vbox.pack_start(toolbar, False, False)
toolbar.show()
hpaned = gtk.HPaned()
vbox.pack_start(hpaned, True, True)
hpaned.show()
vpaned = gtk.VPaned()
hpaned.add1(vpaned)
vpaned.show()
block1 = self._create_sidebar_block(u'Data sets',
self._datasets_notebook_creator)
self._create_dataset("Dataset 1")
block1.set_size_request(-1, 200)
vpaned.add1(block1)
block1.show()
block2 = self._create_sidebar_block(u'Options',
self._options_treeview_creator)
vpaned.add2(block2)
block2.show()
self.drawing_area = gtk.DrawingArea()
self.drawing_area.connect('expose_event',
self.drawing_area_expose_event)
self.drawing_area.connect('size_allocate',
self.drawing_area_size_allocate_event)
hpaned.add2(self.drawing_area)
self.drawing_area.show()
self.main_window.show()
def _create_ui_manager(self):
self.uimanager = gtk.UIManager()
accel_group = self.uimanager.get_accel_group()
self.main_window.add_accel_group(accel_group)
action_group = gtk.ActionGroup('default')
action_group.add_actions([
('file', None, '_File', None, 'File', None),
('quit', gtk.STOCK_QUIT, None, None, 'Quit the program',
self.quit),
('edit', None, '_Edit', None, 'Edit', None),
('add_dataset', gtk.STOCK_ADD, '_Add dataset',
'plus', 'Add another dataset', self.add_dataset),
('remove_dataset', gtk.STOCK_REMOVE, '_Remove dataset',
'minus', 'Remove the current dataset',
self.remove_dataset),
('edit_dataset', gtk.STOCK_EDIT, '_Edit dataset name',
'e', 'Edit the name of the current dataset',
self.edit_dataset),
('add_point', gtk.STOCK_ADD, 'Add _point', 'plus',
'Add another point to the current dataset', self.add_point),
('remove_point', gtk.STOCK_REMOVE, 'Remove p_oint',
'minus',
'Remove the current point of the current dataset',
self.remove_point),
('edit_point', gtk.STOCK_EDIT, 'Edit po_int', 'e',
'Edit the current point of the current dataset',
self.edit_point),
('edit_option', gtk.STOCK_EDIT, 'Edit op_tion', None,
'Edit the current option',
self.edit_option),
('view', None, '_View', None, 'View', None),
('refresh', gtk.STOCK_REFRESH, None, 'r',
'Update the chart', self.refresh),
('tools', None, '_Tools', None, 'Tools', None),
('random-points', gtk.STOCK_EXECUTE, '_Generate random points',
'g', 'Generate random points',
self.generate_random_points),
('dump-chart-state', gtk.STOCK_CONVERT, '_Dump chart state',
'd', 'Dump internal chart variables',
self.dump_chart_state),
('help', None, '_Help', None, 'Help', None),
('about', gtk.STOCK_ABOUT, None, None, 'About this program',
self.about),
])
action_group.add_radio_actions([
('verticalbar', None, '_Vertical bars', None,
'Use vertical bars chart', self.app.VERTICAL_BAR_TYPE),
('horizontalbar', None, '_Horizontal bars', None,
'Use horizontal bars chart', self.app.HORIZONTAL_BAR_TYPE),
('line', None, '_Line', None,
'Use lines chart', self.app.LINE_TYPE),
('pie', None, '_Pie', None,
'Use pie chart', self.app.PIE_TYPE),
('radial', None, '_Radial', None,
'Use radial chart', self.app.RADIAL_TYPE),
('polygonal', None, '_Polygonal', None,
'Use polygonal chart', self.app.POLYGONAL_TYPE),
('scatter', None, '_Scatter', None,
'Use scatter chart', self.app.SCATTER_TYPE),
('stackedverticalbar', None, '_Stacked Vertical bars', None,
'Use stacked vertical bars chart',
self.app.STACKED_VERTICAL_BAR_TYPE),
('stackedhorizontalbar', None, '_Stacked Horizontal bars', None,
'Use stacked horizontal bars chart',
self.app.STACKED_HORIZONTAL_BAR_TYPE),
], self.app.VERTICAL_BAR_TYPE, self.on_chart_type_change)
self.uimanager.insert_action_group(action_group, -1)
ui = """
"""
self.uimanager.add_ui_from_string(ui)
self.uimanager.ensure_update()
menubar = self.uimanager.get_widget('/MenuBar')
toolbar = self.uimanager.get_widget('/ToolBar')
return menubar, toolbar
def _create_sidebar_block(self, title, child_widget_creator):
box = gtk.VBox(spacing=6)
box.set_border_width(6)
label = gtk.Label()
label.set_markup(u'%s' % title)
label.set_alignment(0.0, 0.5)
box.pack_start(label, False, False)
label.show()
child_widget = child_widget_creator()
box.pack_start(child_widget, True, True)
child_widget.show()
return box
def _datasets_notebook_creator(self):
self.datasets_notebook = gtk.Notebook()
self.datasets_notebook.set_scrollable(True)
return self.datasets_notebook
def _dataset_treeview_creator(self):
store = gtk.ListStore(float, float)
treeview = gtk.TreeView(store)
column1 = gtk.TreeViewColumn('x', gtk.CellRendererText(), text=0)
treeview.append_column(column1)
column2 = gtk.TreeViewColumn('y', gtk.CellRendererText(), text=1)
treeview.append_column(column2)
treeview.connect('row-activated', self.dataset_treeview_row_activated)
scrolled_window = gtk.ScrolledWindow()
scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
scrolled_window.add(treeview)
treeview.show()
return scrolled_window
def _options_treeview_creator(self):
self.options_store = gtk.TreeStore(str, str, object)
options = self.app.get_default_options()
self._fill_options_store(options, None, self.app.OPTIONS_TYPES)
self.options_treeview = gtk.TreeView(self.options_store)
column1 = gtk.TreeViewColumn('Name', gtk.CellRendererText(), text=0)
self.options_treeview.append_column(column1)
column2 = gtk.TreeViewColumn('Value', gtk.CellRendererText(), text=1)
self.options_treeview.append_column(column2)
self.options_treeview.expand_all()
self.options_treeview.connect('row-activated',
self.options_treeview_row_activated)
scrolled_window = gtk.ScrolledWindow()
scrolled_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
scrolled_window.add(self.options_treeview)
self.options_treeview.show()
return scrolled_window
def _fill_options_store(self, options, parent_node, types):
for name, value in options.items():
value_type = types[name]
if isinstance(value, dict):
current_parent = self.options_store.append(parent_node,
(name, None, None))
self._fill_options_store(value, current_parent, value_type)
else:
if value is not None:
value = str(value)
self.options_store.append(parent_node,
(name, value, value_type))
def _get_current_dataset_tab(self):
current_tab = self.datasets_notebook.get_current_page()
if current_tab != -1:
return self.datasets_notebook.get_nth_page(current_tab)
def _create_dataset(self, name):
scrolled_window = self._dataset_treeview_creator()
scrolled_window.show()
label = gtk.Label(name)
self.datasets_notebook.append_page(scrolled_window, label)
def _get_datasets(self):
datasets = []
n_pages = self.datasets_notebook.get_n_pages()
for i in range(n_pages):
tab = self.datasets_notebook.get_nth_page(i)
label = self.datasets_notebook.get_tab_label(tab)
name = label.get_label()
treeview = tab.get_children()[0]
model = treeview.get_model()
points = [(x, y) for x, y in model]
if len(points) > 0:
datasets.append((name, points))
return datasets
def _get_chart_type(self):
action_group = self.uimanager.get_action_groups()[0]
action = action_group.get_action('verticalbar')
return action.get_current_value()
def _get_options(self, iter):
options = {}
while iter is not None:
name, value, value_type = self.options_store.get(iter, 0, 1, 2)
if value_type is None:
child = self.options_store.iter_children(iter)
options[name] = self._get_options(child)
else:
if value is not None:
converter = str_converters[value_type]
value = converter(value)
options[name] = value
iter = self.options_store.iter_next(iter)
return options
def _edit_point_internal(self, model, iter):
x, y = model.get(iter, 0, 1)
dialog = PointDialog(self.main_window, x, y)
response = dialog.run()
if response == gtk.RESPONSE_ACCEPT:
x, y = dialog.get_point()
model.set(iter, 0, x, 1, y)
self.refresh()
dialog.destroy()
def _edit_option_internal(self, model, iter):
name, value, value_type = model.get(iter, 0, 1, 2)
parents = []
parent = model.iter_parent(iter)
while parent is not None:
parents.append(model.get_value(parent, 0))
parent = model.iter_parent(parent)
parents.reverse()
parents.append(name)
label = u'.'.join(parents)
dialog = OptionDialog(self.main_window, label, value, value_type)
response = dialog.run()
if response == gtk.RESPONSE_ACCEPT:
new_value = dialog.get_value()
if new_value == "":
new_value = None
model.set_value(iter, 1, new_value)
self.refresh()
dialog.destroy()
def delete_event(self, widget, event, data=None):
return False
def destroy(self, widget, data=None):
gtk.main_quit()
def drawing_area_expose_event(self, widget, event, data=None):
if self.chart is None:
return
cr = widget.window.cairo_create()
cr.rectangle(event.area.x, event.area.y,
event.area.width, event.area.height)
cr.clip()
cr.set_source_surface(self.chart.surface, 0, 0)
cr.paint()
def drawing_area_size_allocate_event(self, widget, event, data=None):
if self.chart is not None:
self.refresh()
def on_chart_type_change(self, action, current, data=None):
if self.chart is not None:
self.refresh()
def dataset_treeview_row_activated(self, treeview, path, view_column):
model = treeview.get_model()
iter = model.get_iter(path)
self._edit_point_internal(model, iter)
def options_treeview_row_activated(self, treeview, path, view_column):
model = treeview.get_model()
iter = model.get_iter(path)
self._edit_option_internal(model, iter)
def quit(self, action):
self.main_window.destroy()
def add_dataset(self, action):
n_pages = self.datasets_notebook.get_n_pages()
suggested_name = u'Dataset %d' % (n_pages + 1)
dialog = TextInputDialog(self.main_window, suggested_name)
response = dialog.run()
if response == gtk.RESPONSE_ACCEPT:
name = dialog.get_name()
self._create_dataset(name)
self.datasets_notebook.set_current_page(n_pages)
dialog.destroy()
def remove_dataset(self, action):
current_tab = self.datasets_notebook.get_current_page()
assert current_tab != -1
self.datasets_notebook.remove_page(current_tab)
def edit_dataset(self, action):
tab = self._get_current_dataset_tab()
assert tab is not None
label = self.datasets_notebook.get_tab_label(tab)
name = label.get_label()
dialog = TextInputDialog(self.main_window, name)
response = dialog.run()
if response == gtk.RESPONSE_ACCEPT:
name = dialog.get_name()
label.set_label(name)
dialog.destroy()
def add_point(self, action):
tab = self._get_current_dataset_tab()
assert tab is not None
treeview = tab.get_children()[0]
model = treeview.get_model()
dialog = PointDialog(self.main_window, len(model) * 1.0, 0.0)
response = dialog.run()
if response == gtk.RESPONSE_ACCEPT:
x, y = dialog.get_point()
model.append((x, y))
self.refresh()
dialog.destroy()
def remove_point(self, action):
tab = self._get_current_dataset_tab()
assert tab is not None
treeview = tab.get_children()[0]
selection = treeview.get_selection()
model, selected = selection.get_selected()
if selected is None:
warning(self.main_window, "You must select the point to remove")
return
model.remove(selected)
self.refresh()
def edit_point(self, action):
tab = self._get_current_dataset_tab()
assert tab is not None
treeview = tab.get_children()[0]
selection = treeview.get_selection()
model, selected = selection.get_selected()
if selected is None:
warning(self.main_window, "You must select the point to edit")
return
self._edit_point_internal(model, selected)
def edit_option(self, action):
selection = self.options_treeview.get_selection()
model, selected = selection.get_selected()
if selected is None:
warning(self.main_window, "You must select the option to edit")
return
self._edit_option_internal(model, selected)
def refresh(self, action=None):
datasets = self._get_datasets()
if datasets:
root = self.options_store.get_iter_first()
options = self._get_options(root)
chart_type = self._get_chart_type()
alloc = self.drawing_area.get_allocation()
self.chart = self.app.get_chart(datasets, options, chart_type,
alloc.width, alloc.height)
self.drawing_area.queue_draw()
else:
self.chart = None
def generate_random_points(self, action=None):
tab = self._get_current_dataset_tab()
assert tab is not None
treeview = tab.get_children()[0]
model = treeview.get_model()
dialog = RandomGeneratorDialog(self.main_window)
response = dialog.run()
if response == gtk.RESPONSE_ACCEPT:
points = dialog.generate_points()
for point in points:
model.append(point)
self.refresh()
dialog.destroy()
def dump_chart_state(self, action=None):
if self.chart is None:
return
alloc = self.drawing_area.get_allocation()
print 'CHART STATE'
print '-' * 70
print 'surface: %d x %d' % (alloc.width, alloc.height)
print 'area :', self.chart.area
print
print 'minxval:', self.chart.minxval
print 'maxxval:', self.chart.maxxval
print 'xrange :', self.chart.xrange
print
print 'minyval:', self.chart.minyval
print 'maxyval:', self.chart.maxyval
print 'yrange :', self.chart.yrange
def about(self, action=None):
dialog = AboutDialog(self.main_window)
dialog.run()
dialog.destroy()
def run(self):
gtk.main()
def str2bool(str):
if str.lower() == "true":
return True
else:
return False
str_converters = {
str: str,
int: int,
float: float,
unicode: unicode,
bool: str2bool,
}
pycha-0.7.0/chavier/app.py 0000664 0001750 0001750 00000010671 12130334357 015001 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of Chavier.
#
# Chavier is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Chavier is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Chavier. If not, see .
import cairo
from pycha.chart import DEFAULT_OPTIONS
from pycha.bar import HorizontalBarChart, VerticalBarChart
from pycha.line import LineChart
from pycha.pie import PieChart
from pycha.radial import RadialChart
from pycha.polygonal import PolygonalChart
from pycha.scatter import ScatterplotChart
from pycha.stackedbar import StackedVerticalBarChart, StackedHorizontalBarChart
from chavier.gui import GUI
class App(object):
CHART_TYPES = (
VerticalBarChart,
HorizontalBarChart,
LineChart,
PieChart,
RadialChart,
PolygonalChart,
ScatterplotChart,
StackedVerticalBarChart,
StackedHorizontalBarChart,
)
(VERTICAL_BAR_TYPE,
HORIZONTAL_BAR_TYPE,
LINE_TYPE,
PIE_TYPE,
RADIAL_TYPE,
POLYGONAL_TYPE,
SCATTER_TYPE,
STACKED_VERTICAL_BAR_TYPE,
STACKED_HORIZONTAL_BAR_TYPE) = range(len(CHART_TYPES))
OPTIONS_TYPES = dict(
axis=dict(
lineWidth=float,
lineColor=str,
tickSize=float,
labelColor=str,
labelFont=str,
labelFontSize=int,
tickFont=str,
tickFontSize=int,
x=dict(
hide=bool,
ticks=list,
tickCount=int,
tickPrecision=int,
range=list,
rotate=float,
label=unicode,
interval=int,
showLines=bool,
),
y=dict(
hide=bool,
ticks=list,
tickCount=int,
tickPrecision=int,
range=list,
rotate=float,
label=unicode,
interval=int,
showLines=bool,
),
),
background=dict(
hide=bool,
baseColor=str,
chartColor=str,
lineColor=str,
lineWidth=float,
),
legend=dict(
opacity=float,
borderColor=str,
borderWidth=int,
hide=bool,
position=dict(
top=int,
left=int,
bottom=int,
right=int,
)
),
padding=dict(
left=int,
right=int,
top=int,
bottom=int,
),
stroke=dict(
color=str,
hide=bool,
shadow=bool,
width=int,
),
yvals=dict(
show=bool,
inside=bool,
fontSize=int,
fontColor=str,
skipSmallValues=bool,
snapToOrigin=bool,
renderer=str,
),
fillOpacity=float,
shouldFill=bool,
barWidthFillFraction=float,
pieRadius=float,
colorScheme=dict(
name=str,
args=dict(
initialColor=str,
colors=list,
),
),
title=unicode,
titleColor=str,
titleFont=str,
titleFontSize=int,
encoding=str,
)
def __init__(self):
self.gui = GUI(self)
def run(self):
self.gui.run()
def get_default_options(self):
return DEFAULT_OPTIONS
def get_chart(self, datasets, options, chart_type, width, height):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
chart_factory = self.CHART_TYPES[chart_type]
chart = chart_factory(surface, options)
chart.addDataset(datasets)
chart.render()
return chart
def main():
app = App()
app.run()
return 0
if __name__ == '__main__':
main()
pycha-0.7.0/chavier/__init__.py 0000664 0001750 0001750 00000000000 12130334357 015741 0 ustar lgs lgs 0000000 0000000 pycha-0.7.0/tests/ 0000775 0001750 0001750 00000000000 12130340466 013361 5 ustar lgs lgs 0000000 0000000 pycha-0.7.0/tests/utils.py 0000664 0001750 0001750 00000003216 12130334357 015077 0 ustar lgs lgs 0000000 0000000 # -*- encoding: utf-8 -*-
# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
# 2010 by Yaco S.L.
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import unittest
import pycha.utils
class UtilsTests(unittest.TestCase):
def test_clamp(self):
self.assertEqual(pycha.utils.clamp(0, 1, 2), 1)
self.assertEqual(pycha.utils.clamp(0, 1, -1), 0)
self.assertEqual(pycha.utils.clamp(0, 1, 0.5), 0.5)
self.assertEqual(pycha.utils.clamp(0, 1, 1), 1)
self.assertEqual(pycha.utils.clamp(0, 1, 0), 0)
def test_safe_unicode(self):
self.assertEqual(pycha.utils.safe_unicode(u'unicode'), u'unicode')
self.assertEqual(pycha.utils.safe_unicode('ascii'), u'ascii')
self.assertEqual(pycha.utils.safe_unicode('non ascii ñ', 'utf-8'),
u'non ascii ñ')
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(UtilsTests),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
pycha-0.7.0/tests/chart.py 0000664 0001750 0001750 00000026035 12130334357 015044 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import unittest
import cairo
import pycha.chart
class FunctionsTests(unittest.TestCase):
def test_uniqueIndices(self):
arr = (range(10), range(5), range(20), range(30))
self.assertEqual(pycha.chart.uniqueIndices(arr), range(30))
arr = (range(30), range(20), range(5), range(10))
self.assertEqual(pycha.chart.uniqueIndices(arr), range(30))
arr = (range(4), )
self.assertEqual(pycha.chart.uniqueIndices(arr), range(4))
arr = (range(0), )
self.assertEqual(pycha.chart.uniqueIndices(arr), [])
class AreaTests(unittest.TestCase):
def test_area(self):
area = pycha.chart.Area(10, 20, 100, 300)
self.assertEqual(area.x, 10)
self.assertEqual(area.y, 20)
self.assertEqual(area.w, 100)
self.assertEqual(area.h, 300)
msg = ""
self.assertEqual(str(area), msg)
class OptionTests(unittest.TestCase):
def test_options(self):
opt = pycha.chart.Option(a=1, b=2, c=3)
self.assertEqual(opt.a, opt['a'])
self.assertEqual(opt.b, 2)
self.assertEqual(opt['c'], 3)
opt = pycha.chart.Option({'a': 1, 'b': 2, 'c': 3})
self.assertEqual(opt.a, opt['a'])
self.assertEqual(opt.b, 2)
self.assertEqual(opt['c'], 3)
def test_merge(self):
opt = pycha.chart.Option(a=1, b=2,
c=pycha.chart.Option(d=4, e=5))
self.assertEqual(opt.c.d, 4)
opt.merge(dict(c=pycha.chart.Option(d=7, e=8, f=9)))
self.assertEqual(opt.c.d, 7)
# new attributes not present in original option are not merged
self.assertRaises(AttributeError, getattr, opt.c, 'f')
opt.merge(pycha.chart.Option(a=10, b=20))
self.assertEqual(opt.a, 10)
self.assertEqual(opt.b, 20)
class ChartTests(unittest.TestCase):
def test_init(self):
ch = pycha.chart.Chart(None)
self.assertEqual(ch.resetFlag, False)
self.assertEqual(ch.datasets, [])
self.assertNotEqual(ch.layout, None)
self.assertEqual(ch.minxval, None)
self.assertEqual(ch.maxxval, None)
self.assertEqual(ch.minyval, None)
self.assertEqual(ch.maxyval, None)
self.assertEqual(ch.xscale, 1.0)
self.assertEqual(ch.yscale, 1.0)
self.assertEqual(ch.xrange, None)
self.assertEqual(ch.yrange, None)
self.assertEqual(ch.xticks, [])
self.assertEqual(ch.yticks, [])
self.assertEqual(ch.options, pycha.chart.DEFAULT_OPTIONS)
self.assertEqual(ch.origin, 0.0)
def test_datasets(self):
ch = pycha.chart.Chart(None)
d1 = ('dataset1', ([0, 0], [1, 2], [2, 1.5]))
d2 = ('dataset2', ([0, 1], [1, 2], [2, 2.4]))
d3 = ('dataset3', ([0, 4], [1, 3], [2, 0.5]))
ch.addDataset((d1, d2, d3))
self.assertEqual(ch._getDatasetsKeys(),
['dataset1', 'dataset2', 'dataset3'])
self.assertEqual(ch._getDatasetsValues(),
[d1[1], d2[1], d3[1]])
def test_options(self):
ch = pycha.chart.Chart(None)
opt = pycha.chart.Option(shouldFill=False)
ch.setOptions(opt)
self.assertEqual(ch.options.shouldFill, False)
opt = {'pieRadius': 0.8}
ch.setOptions(opt)
self.assertEqual(ch.options.pieRadius, 0.8)
def test_reset(self):
ch = pycha.chart.Chart(None, options={'shouldFill': False})
self.assertEqual(ch.resetFlag, False)
self.assertEqual(ch.options.shouldFill, False)
dataset = (('dataset1', ([0, 1], [1, 1])), )
ch.addDataset(dataset)
self.assertEqual(ch._getDatasetsKeys(), ['dataset1'])
ch.reset()
defaultFill = pycha.chart.DEFAULT_OPTIONS.shouldFill
self.assertEqual(ch.options.shouldFill, defaultFill)
self.assertEqual(ch.datasets, [])
self.assertEqual(ch.resetFlag, True)
def test_colorscheme(self):
options = {'colorScheme': {'name': 'gradient',
'args': {'initialColor': '#000000'}}}
ch = pycha.chart.Chart(None, options)
dataset = (('dataset1', ([0, 1], [1, 1])), )
ch.addDataset(dataset)
ch._setColorscheme()
self.assert_(isinstance(ch.colorScheme, dict))
self.assertEqual(ch.colorScheme, {'dataset1': (0.0, 0.0, 0.0)})
options = {'colorScheme': {'name': 'foo'}}
ch = pycha.chart.Chart(None, options)
ch.addDataset(dataset)
self.assertRaises(ValueError, ch._setColorscheme)
def test_updateXY(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
opt = {'padding': dict(left=10, right=10, top=10, bottom=10)}
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.chart.Chart(surface, opt)
ch.addDataset(dataset)
ch._updateXY()
self.assertEqual(ch.minxval, 0.0)
self.assertEqual(ch.maxxval, 3)
self.assertEqual(ch.xrange, 3)
self.assertEqual(ch.xscale, 1/3.0)
self.assertEqual(ch.minyval, 0)
self.assertEqual(ch.maxyval, 4)
self.assertEqual(ch.yrange, 4)
self.assertEqual(ch.yscale, 1/4.0)
# TODO: test with different options (axis.range, ...)
def test_updateTicks(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
opt = {'padding': dict(left=10, right=10, top=10, bottom=10)}
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.chart.Chart(surface, opt)
ch.addDataset(dataset)
ch._updateXY()
ch._updateTicks()
xticks = [(0.0, 0), (1/3.0, 1), (2/3.0, 2)]
for i in range(len(xticks)):
self.assertAlmostEqual(ch.xticks[i][0], xticks[i][0], 4)
self.assertAlmostEqual(ch.xticks[i][1], xticks[i][1], 4)
yticks = [(1 - 0.1 * i, 0.4*i)
for i in range(ch.options.axis.y.tickCount + 1)]
self.assertEqual(len(ch.yticks), len(yticks))
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 4)
self.assertAlmostEqual(ch.yticks[i][1], yticks[i][1], 4)
def test_updateExplicitTicks(self):
"""Test for bug #7"""
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
yticks = [dict(v=i, label=str(i)) for i in range(0, 3)]
opt = {'axis': {'y': {'ticks': yticks}}}
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
)
ch = pycha.chart.Chart(surface, opt)
ch.addDataset(dataset)
ch._updateXY()
ch._updateTicks()
self.assertAlmostEqual(ch.yticks[0][0], 1.0, 4)
self.assertAlmostEqual(ch.yticks[1][0], 2/3.0, 4)
self.assertAlmostEqual(ch.yticks[2][0], 1/3.0, 4)
def test_updateTicksPrecission(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
opt = {'axis': {'y': {'tickCount': 10, 'tickPrecission': 1}}}
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.chart.Chart(surface, opt)
ch.addDataset(dataset)
ch._updateXY()
ch._updateTicks()
xticks = [(0.0, 0), (1/3.0, 1), (2/3.0, 2)]
for i in range(len(xticks)):
self.assertAlmostEqual(ch.xticks[i][0], xticks[i][0], 4)
self.assertAlmostEqual(ch.xticks[i][1], xticks[i][1], 4)
yticks = ((1, 0), (0.9, 0.4), (0.8, 0.8), (0.7, 1.2), (0.6, 1.6),
(0.5, 2.0), (0.4, 2.4), (0.3, 2.8), (0.2, 3.2), (0.1, 3.6),
(0.0, 4.0))
self.assertEqual(len(ch.yticks), len(yticks))
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 1)
self.assertAlmostEqual(ch.yticks[i][1], yticks[i][1], 1)
# decrease precission to 0
opt = {'axis': {'y': {'tickCount': 10, 'tickPrecision': 0}}}
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.chart.Chart(surface, opt)
ch.addDataset(dataset)
ch._updateXY()
ch._updateTicks()
yticks = ((1, 0), (0.75, 1), (0.5, 2), (0.25, 3), (0.0, 4))
self.assertEqual(len(ch.yticks), len(yticks))
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 1, i)
self.assertEqual(ch.yticks[i][1], yticks[i][1], i)
def test_abstractChart(self):
ch = pycha.chart.Chart(None)
self.assertRaises(NotImplementedError, ch._updateChart)
self.assertRaises(NotImplementedError, ch._renderChart, None)
def test_range(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
opt = {'axis': {'x': {'range': (1, 10)}, 'y': {'range': (1.0, 10.0)}}}
ch = pycha.chart.Chart(surface, opt)
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
)
ch.addDataset(dataset)
ch._updateXY()
self.assertAlmostEqual(ch.xrange, 9, 4)
self.assertAlmostEqual(ch.yrange, 9, 4)
self.assertAlmostEqual(ch.xscale, 0.1111, 4)
self.assertAlmostEqual(ch.yscale, 0.1111, 4)
def test_interval(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
opt = {'axis': {'y': {'interval': 2.5}}}
ch = pycha.chart.Chart(surface, opt)
dataset = (
('dataset1', ([0, 1], [1, 4], [2, 10])),
)
ch.addDataset(dataset)
ch._updateXY()
ch._updateTicks()
yticks = ((0.75, 2.5), (0.5, 5.0),
(0.25, 7.5), (0.0, 10.0))
self.assertEqual(len(yticks), len(ch.yticks))
for i, (pos, label) in enumerate(yticks):
tick = ch.yticks[i]
self.assertAlmostEqual(tick[0], pos, 2)
self.assertAlmostEqual(tick[1], label, 2)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(FunctionsTests),
unittest.makeSuite(AreaTests),
unittest.makeSuite(OptionTests),
unittest.makeSuite(ChartTests),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
pycha-0.7.0/tests/runner.py 0000664 0001750 0001750 00000002155 12130334357 015251 0 ustar lgs lgs 0000000 0000000 # Copyright (c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import unittest
import bar
import chart
import color
import line
import pie
import utils
def test_suite():
return unittest.TestSuite((
bar.test_suite(),
chart.test_suite(),
color.test_suite(),
line.test_suite(),
pie.test_suite(),
utils.test_suite(),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
pycha-0.7.0/tests/pie.py 0000664 0001750 0001750 00000014445 12130334357 014522 0 ustar lgs lgs 0000000 0000000 # Copyright (c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import math
import unittest
import cairo
import pycha.pie
class SliceTests(unittest.TestCase):
def test_init(self):
slice = pycha.pie.Slice('test', 3/5.0, 0, 4, 1/4.0)
self.assertEqual(slice.name, 'test')
self.assertEqual(slice.fraction, 3/5.0)
self.assertEqual(slice.xval, 0)
self.assertEqual(slice.yval, 4)
self.assertEqual(slice.startAngle, math.pi / 2)
self.assertEqual(slice.endAngle, 1.7 * math.pi)
def test_isBigEnough(self):
slice = pycha.pie.Slice('test 1', 3/5.0, 0, 4, 1/4.0)
self.assertEqual(slice.isBigEnough(), True)
slice = pycha.pie.Slice('test 2', 1/10000.0, 0, 4, 1/4.0)
self.assertEqual(slice.isBigEnough(), False)
def test_normalisedAngle(self):
# First quadrant
slice = pycha.pie.Slice('test 1', 1/6.0, 0, 4, 0)
self.assertAlmostEqual(slice.getNormalisedAngle(), 1/6.0 * math.pi, 4)
# Second quadrant
slice = pycha.pie.Slice('test 1', 1/6.0, 0, 4, 1/4.0)
self.assertAlmostEqual(slice.getNormalisedAngle(), 2/3.0 * math.pi, 4)
# Third quadrant
slice = pycha.pie.Slice('test 1', 1/6.0, 0, 4, 1/2.0)
self.assertAlmostEqual(slice.getNormalisedAngle(), 7/6.0 * math.pi, 4)
# Fouth quadrant
slice = pycha.pie.Slice('test 1', 1/6.0, 0, 4, 3/4.0)
self.assertAlmostEqual(slice.getNormalisedAngle(), 10/6.0 * math.pi, 4)
# Bigger than a circle
slice = pycha.pie.Slice('test 1', 2/3.0, 0, 4, 3/4.0)
self.assertAlmostEqual(slice.getNormalisedAngle(), 1/6.0 * math.pi, 4)
# Negative angle
slice = pycha.pie.Slice('test 1', -1/6.0, 0, 4, 0)
self.assertAlmostEqual(slice.getNormalisedAngle(), 11/6.0 * math.pi, 4)
class PieTests(unittest.TestCase):
def test_init(self):
ch = pycha.pie.PieChart(None)
self.assertEqual(ch.slices, [])
self.assertEqual(ch.centerx, 0)
self.assertEqual(ch.centery, 0)
def test_updateChart(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 10],)),
('dataset2', ([0, 20],)),
('dataset3', ([0, 70],)),
)
opt = {'padding': {'left': 0, 'right': 0, 'top': 0, 'bottom': 0},
'pieRadius': 0.5}
ch = pycha.pie.PieChart(surface, opt)
ch.addDataset(dataset)
ch.render()
self.assertEqual(ch.centerx, 250)
self.assertEqual(ch.centery, 250)
slices = (
pycha.pie.Slice('dataset1', 0.1, 0, 10, 0),
pycha.pie.Slice('dataset2', 0.2, 1, 20, 0.1),
pycha.pie.Slice('dataset3', 0.7, 2, 70, 0.3),
)
for i, slice in enumerate(slices):
s1, s2 = ch.slices[i], slice
self.assertEqual(s1.name, s2.name)
self.assertAlmostEqual(s1.fraction, s2.fraction, 4)
self.assertAlmostEqual(s1.startAngle, s2.startAngle, 4)
self.assertAlmostEqual(s1.endAngle, s2.endAngle, 4)
self.assertEqual(s1.xval, s2.xval)
self.assertEqual(s1.yval, s2.yval)
def test_updateTicks(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 10],)),
('dataset2', ([0, 20],)),
('dataset3', ([0, 70],)),
)
opt = {'padding': {'left': 0, 'right': 0, 'top': 0, 'bottom': 0},
'pieRadius': 0.5}
ch = pycha.pie.PieChart(surface, opt)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
ch._updateTicks()
self.assertEqual(ch.xticks, [(0, 'dataset1 (10.0%)'),
(1, 'dataset2 (20.0%)'),
(2, 'dataset3 (70.0%)')])
ticks = [{'v': 0, 'label': 'First dataset'},
{'v': 1, 'label': 'Second dataset'},
{'v': 2, 'label': 'Third dataset'}]
opt = {'axis': {'x': {'ticks': ticks},},}
ch = pycha.pie.PieChart(surface, opt)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
ch._updateTicks()
self.assertEqual(ch.xticks, [(0, 'First dataset (10.0%)'),
(1, 'Second dataset (20.0%)'),
(2, 'Third dataset (70.0%)')])
def test_issue5(self):
"""See http://bitbucket.org/lgs/pycha/issue/5/"""
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 30],)),
('dataset2', ([0, 0],)), # Empty set!!
('dataset3', ([0, 70],)),
)
ch = pycha.pie.PieChart(surface, {})
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
# there is no slice for the empty set
slices = (
pycha.pie.Slice('dataset1', 0.3, 0, 30, 0),
pycha.pie.Slice('dataset3', 0.7, 2, 70, 0.3),
)
for i, slice in enumerate(slices):
s1, s2 = ch.slices[i], slice
self.assertEqual(s1.name, s2.name)
self.assertAlmostEqual(s1.fraction, s2.fraction, 4)
self.assertAlmostEqual(s1.startAngle, s2.startAngle, 4)
self.assertAlmostEqual(s1.endAngle, s2.endAngle, 4)
self.assertEqual(s1.xval, s2.xval)
self.assertEqual(s1.yval, s2.yval)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(SliceTests),
unittest.makeSuite(PieTests),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
pycha-0.7.0/tests/color.py 0000664 0001750 0001750 00000012777 12130334357 015071 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
# 2009 by Yaco S.L.
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import unittest
import pycha.color
class SimpleColorScheme(pycha.color.ColorScheme):
pass
class ColorTests(unittest.TestCase):
def test_hex2rgb(self):
color = pycha.color.hex2rgb('#ff0000')
self.assert_(isinstance(color, tuple))
self.assertAlmostEqual(1, color[0])
self.assertAlmostEqual(0, color[1])
self.assertAlmostEqual(0, color[2])
color2 = pycha.color.hex2rgb(color)
self.assertEqual(color, color2)
color = pycha.color.hex2rgb('#000fff000', digits=3)
self.assert_(isinstance(color, tuple))
self.assertEqual(0, color[0])
self.assertEqual(1, color[1])
self.assertEqual(0, color[2])
color = pycha.color.hex2rgb('#00000000ffff', digits=4)
self.assert_(isinstance(color, tuple))
self.assertEqual(0, color[0])
self.assertEqual(0, color[1])
self.assertEqual(1, color[2])
def test_rgb2hsv_and_hsv2rgb(self):
for rgb, hsv in (((1.0, 0.0, 0.0), (0.0, 1.0, 1.0)),
((1.0, 0.5, 0.0), (30.0, 1.0, 1.0)),
((1.0, 1.0, 0.0), (60.0, 1.0, 1.0)),
((0.5, 1.0, 0.0), (90.0, 1.0, 1.0)),
((0.0, 1.0, 0.0), (120.0, 1.0, 1.0)),
((0.0, 1.0, 0.5), (150.0, 1.0, 1.0)),
((0.0, 1.0, 1.0), (180.0, 1.0, 1.0)),
((0.0, 0.5, 1.0), (210.0, 1.0, 1.0)),
((0.0, 0.0, 1.0), (240.0, 1.0, 1.0)),
((0.5, 0.0, 1.0), (270.0, 1.0, 1.0)),
((1.0, 0.0, 1.0), (300.0, 1.0, 1.0)),
((1.0, 0.0, 0.5), (330.0, 1.0, 1.0)),
((0.375, 0.5, 0.25), (90.0, 0.5, 0.5)),
((0.21875, 0.25, 0.1875), (90.0, 0.25, 0.25))):
self._assertColors(pycha.color.rgb2hsv(*rgb), hsv, 5)
self._assertColors(pycha.color.hsv2rgb(*hsv), rgb, 5)
def test_lighten(self):
r, g, b = (1.0, 1.0, 0.0)
r2, g2, b2 = pycha.color.lighten(r, g, b, 0.1)
self.assertEqual((r2, g2, b2), (1.0, 1.0, 0.1))
r3, g3, b3 = pycha.color.lighten(r2, g2, b2, 0.5)
self.assertEqual((r3, g3, b3), (1.0, 1.0, 0.6))
def _assertColors(self, c1, c2, precission):
for i in range(3):
self.assertAlmostEqual(c1[i], c2[i], precission)
def test_basicColors(self):
colors = ('red', 'green', 'blue', 'grey', 'black', 'darkcyan')
for color in colors:
self.assert_(color in pycha.color.basicColors)
def test_ColorSchemeRegistry(self):
self.assertEquals(SimpleColorScheme,
pycha.color.ColorScheme.getColorScheme('simple'))
self.assertEquals(None,
pycha.color.ColorScheme.getColorScheme('foo'))
def test_FixedColorScheme(self):
keys = range(3)
colors = ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0))
scheme = pycha.color.FixedColorScheme(keys, colors)
self._assertColors(scheme[0], (1.0, 0.0, 0.0), 1)
self._assertColors(scheme[1], (0.0, 1.0, 0.0), 3)
self._assertColors(scheme[2], (0.0, 0.0, 1.0), 3)
def test_GradientColorScheme(self):
keys = range(5)
scheme = pycha.color.GradientColorScheme(keys, "#000000")
self._assertColors(scheme[0], (0.0, 0.0, 0.0), 3)
self._assertColors(scheme[1], (0.1, 0.1, 0.1), 3)
self._assertColors(scheme[2], (0.2, 0.2, 0.2), 3)
self._assertColors(scheme[3], (0.3, 0.3, 0.3), 3)
self._assertColors(scheme[4], (0.4, 0.4, 0.4), 3)
def test_autoLighting(self):
"""This test ensures that the colors don't get to white too fast.
See bug #8.
"""
# we have a lot of keys
n = 50
keys = range(n)
color = '#ff0000'
scheme = pycha.color.GradientColorScheme(keys, color)
# ensure that the last color is not completely white
color = scheme[n-1]
# the red component was already 1
self.assertAlmostEqual(color[0], 1.0, 4)
self.assertNotAlmostEqual(color[1], 1.0, 4)
self.assertNotAlmostEqual(color[2], 1.0, 4)
def test_RainbowColorScheme(self):
keys = range(5)
scheme = pycha.color.GradientColorScheme(keys, "#ff0000")
self._assertColors(scheme[0], (1.0, 0.0, 0.0), 3)
self._assertColors(scheme[1], (1.0, 0.1, 0.1), 3)
self._assertColors(scheme[2], (1.0, 0.2, 0.2), 3)
self._assertColors(scheme[3], (1.0, 0.3, 0.3), 3)
self._assertColors(scheme[4], (1.0, 0.4, 0.4), 3)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(ColorTests),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
pycha-0.7.0/tests/bar.py 0000664 0001750 0001750 00000033662 12130334357 014513 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import unittest
import cairo
import pycha.bar
class RectTests(unittest.TestCase):
def test_rect(self):
r = pycha.bar.Rect(2, 3, 20, 40, 2.5, 3.4, 'test')
self.assertEqual(r.x, 2)
self.assertEqual(r.y, 3)
self.assertEqual(r.w, 20)
self.assertEqual(r.h, 40)
self.assertEqual(r.xval, 2.5)
self.assertEqual(r.yval, 3.4)
self.assertEqual(r.name, 'test')
class BarTests(unittest.TestCase):
def test_init(self):
ch = pycha.bar.BarChart(None)
self.assertEqual(ch.bars, [])
self.assertEqual(ch.minxdelta, 0)
def test_updateChart(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
# An evil dataset with just one point. See bug #9
dataset = (
('dataset1', ([0, 0], )),
)
ch = pycha.bar.BarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.xscale, 1.0)
self.assertEqual(ch.minxval, 0)
self.assertEqual(ch.minxdelta, 1.0)
self.assertAlmostEqual(ch.barWidthForSet, 0.75, 4)
self.assertAlmostEqual(ch.barMargin, 0.125, 4)
def test_customRangeWithOnePoint(self):
"""Weird results with a custom range and just one point. See bug #20"""
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], )),
)
options = {
'axis': {
'x': {
'range': (0.0, 4.0),
},
},
}
ch = pycha.bar.BarChart(surface, options)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.xscale, 0.2)
self.assertEqual(ch.minxval, 0)
self.assertEqual(ch.minxdelta, 1.0)
self.assertAlmostEqual(ch.barWidthForSet, 0.15, 2)
self.assertAlmostEqual(ch.barMargin, 0.025, 3)
class VerticalBarTests(unittest.TestCase):
def test_updateChart(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 3], [1, 4], [2, 2], [3, 5], [4, 3.5])),
('dataset2', ([0, 2], [1, 3], [2, 1], [3, 5], [4, 2.5])),
)
ch = pycha.bar.VerticalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.minxval, 0)
self.assertEqual(ch.maxxval, 4)
self.assertEqual(ch.xrange, 4)
self.assertAlmostEqual(ch.xscale, 0.20, 4)
self.assertEqual(ch.minyval, 0)
self.assertEqual(ch.maxyval, 5)
self.assertEqual(ch.yrange, 5)
self.assertAlmostEqual(ch.yscale, 0.20, 4)
self.assertEqual(ch.minxdelta, 1)
self.assertAlmostEqual(ch.barWidthForSet, 0.075, 4)
self.assertAlmostEqual(ch.barMargin, 0.025, 4)
R = pycha.bar.Rect
bars = (
R(0.025, 0.400, 0.075, 0.600, 0, 3, 'dataset1'),
R(0.225, 0.200, 0.075, 0.800, 1, 4, 'dataset1'),
R(0.425, 0.600, 0.075, 0.400, 2, 2, 'dataset1'),
R(0.625, 0.000, 0.075, 1.000, 3, 5, 'dataset1'),
R(0.825, 0.300, 0.075, 0.700, 4, 3.5, 'dataset1'),
R(0.100, 0.600, 0.075, 0.400, 0, 2, 'dataset2'),
R(0.300, 0.400, 0.075, 0.600, 1, 3, 'dataset2'),
R(0.500, 0.800, 0.075, 0.200, 2, 1, 'dataset2'),
R(0.700, 0.000, 0.075, 1.000, 3, 5, 'dataset2'),
R(0.900, 0.500, 0.075, 0.500, 4, 2.5, 'dataset2'),
)
for i, bar in enumerate(bars):
b1, b2 = ch.bars[i], bar
self.assertAlmostEqual(b1.x, b2.x, 4)
self.assertAlmostEqual(b1.y, b2.y, 4)
self.assertAlmostEqual(b1.w, b2.w, 4)
self.assertAlmostEqual(b1.h, b2.h, 4)
self.assertEqual(b1.xval, b2.xval)
self.assertEqual(b1.yval, b2.yval)
self.assertEqual(b1.name, b2.name)
def test_updateChartWithNegatives(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, -3], [1, -1], [2, 3], [3, 5])),
)
ch = pycha.bar.VerticalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.minxval, 0)
self.assertEqual(ch.maxxval, 3)
self.assertEqual(ch.xrange, 3)
self.assertAlmostEqual(ch.xscale, 0.25, 4)
self.assertEqual(ch.minyval, -3)
self.assertEqual(ch.maxyval, 5)
self.assertEqual(ch.yrange, 8)
self.assertAlmostEqual(ch.yscale, 0.125, 4)
self.assertAlmostEqual(ch.origin, 0.375)
self.assertEqual(ch.minxdelta, 1)
self.assertAlmostEqual(ch.barWidthForSet, 0.1875, 4)
self.assertAlmostEqual(ch.barMargin, 0.03125, 4)
R = pycha.bar.Rect
bars = (
R(0.03125, 0.625, 0.1875, 0.375, 0, -3, 'dataset1'),
R(0.28125, 0.625, 0.1875, 0.125, 1, -1, 'dataset1'),
R(0.53125, 0.250, 0.1875, 0.375, 2, 3, 'dataset1'),
R(0.78125, 0.000, 0.1875, 0.625, 3, 5, 'dataset1'),
)
for i, bar in enumerate(bars):
b1, b2 = ch.bars[i], bar
self.assertAlmostEqual(b1.x, b2.x, 4)
self.assertAlmostEqual(b1.y, b2.y, 4)
self.assertAlmostEqual(b1.w, b2.w, 4)
self.assertAlmostEqual(b1.h, b2.h, 4)
self.assertEqual(b1.xval, b2.xval)
self.assertEqual(b1.yval, b2.yval)
self.assertEqual(b1.name, b2.name)
def test_updateTicks(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.bar.VerticalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
ch._updateTicks()
xticks = [(0.125, 0), (0.375, 1), (0.625, 2)]
for i in range(len(xticks)):
self.assertAlmostEqual(ch.xticks[i][0], xticks[i][0], 4)
self.assertAlmostEqual(ch.xticks[i][1], xticks[i][1], 4)
yticks = [
(1.0, 0.0), (0.9, 0.4), (0.8, 0.8), (0.7, 1.2), (0.6, 1.6),
(0.5, 2.0), (0.4, 2.4), (0.3, 2.8), (0.2, 3.2), (0.1, 3.6),
(0.0, 4.0),
]
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 4)
self.assertAlmostEqual(ch.yticks[i][1], yticks[i][1], 4)
def test_udpateTicksWithNegatives(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, -2], [1, 1], [2, 3])),
)
ch = pycha.bar.VerticalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
ch._updateTicks()
xticks = [(0.1667, 0), (0.5000, 1), (0.8333, 2)]
for i in range(len(xticks)):
self.assertAlmostEqual(ch.xticks[i][0], xticks[i][0], 4)
self.assertAlmostEqual(ch.xticks[i][1], xticks[i][1], 4)
yticks = [
(1.0, -2.0), (0.9, -1.5), (0.8, -1.0), (0.7, -0.5), (0.6, 0.0),
(0.5, 0.5), (0.4, 1.0), (0.3, 1.5), (0.2, 2.0), (0.1, 2.5),
(0.0, 3.0),
]
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 4)
self.assertAlmostEqual(ch.yticks[i][1], yticks[i][1], 4)
def test_shadowRectangle(self):
ch = pycha.bar.VerticalBarChart(None)
shadow = ch._getShadowRectangle(10, 20, 400, 300)
self.assertEqual(shadow, (8, 18, 404, 302))
class HorizontalBarTests(unittest.TestCase):
def test_updateChart(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.bar.HorizontalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.xrange, 3)
self.assertAlmostEqual(ch.xscale, 0.25, 4)
self.assertAlmostEqual(ch.yscale, 0.25, 4)
self.assertEqual(ch.minxdelta, 1)
self.assertAlmostEqual(ch.barWidthForSet, 0.09375, 4)
self.assertAlmostEqual(ch.barMargin, 0.03125, 4)
bars = (
pycha.bar.Rect(0, 0.03125, 0.25, 0.09375, 0, 1, 'dataset1'),
pycha.bar.Rect(0, 0.28125, 0.25, 0.09375, 1, 1, 'dataset1'),
pycha.bar.Rect(0, 0.53125, 0.75, 0.09375, 2, 3, 'dataset1'),
pycha.bar.Rect(0, 0.125, 0.5, 0.09375, 0, 2, 'dataset2'),
pycha.bar.Rect(0, 0.375, 0.0, 0.09375, 1, 0, 'dataset2'),
pycha.bar.Rect(0, 0.875, 1.0, 0.09375, 3, 4, 'dataset2'),
)
for i, bar in enumerate(bars):
b1, b2 = ch.bars[i], bar
self.assertAlmostEqual(b1.x, b2.x, 4)
self.assertAlmostEqual(b1.y, b2.y, 4)
self.assertAlmostEqual(b1.w, b2.w, 4)
self.assertAlmostEqual(b1.h, b2.h, 4)
self.assertEqual(b1.xval, b2.xval)
self.assertEqual(b1.yval, b2.yval)
self.assertEqual(b1.name, b2.name)
def test_updateChartWithNegatives(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, -3], [1, -1], [2, 3], [3, 5])),
)
ch = pycha.bar.HorizontalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.minxval, 0)
self.assertEqual(ch.maxxval, 3)
self.assertEqual(ch.xrange, 3)
self.assertAlmostEqual(ch.xscale, 0.25, 4)
self.assertEqual(ch.minyval, -3)
self.assertEqual(ch.maxyval, 5)
self.assertEqual(ch.yrange, 8)
self.assertAlmostEqual(ch.yscale, 0.125, 4)
self.assertAlmostEqual(ch.origin, 0.375)
self.assertEqual(ch.minxdelta, 1)
self.assertAlmostEqual(ch.barWidthForSet, 0.1875, 4)
self.assertAlmostEqual(ch.barMargin, 0.03125, 4)
R = pycha.bar.Rect
bars = (
R(0.000, 0.03125, 0.375, 0.1875, 0, -3, 'dataset1'),
R(0.250, 0.28125, 0.125, 0.1875, 1, -1, 'dataset1'),
R(0.375, 0.53125, 0.375, 0.1875, 2, 3, 'dataset1'),
R(0.375, 0.78125, 0.625, 0.1875, 3, 5, 'dataset1'),
)
for i, bar in enumerate(bars):
b1, b2 = ch.bars[i], bar
self.assertAlmostEqual(b1.x, b2.x, 4)
self.assertAlmostEqual(b1.y, b2.y, 4)
self.assertAlmostEqual(b1.w, b2.w, 4)
self.assertAlmostEqual(b1.h, b2.h, 4)
self.assertEqual(b1.xval, b2.xval)
self.assertEqual(b1.yval, b2.yval)
self.assertEqual(b1.name, b2.name)
def test_updateTicks(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.bar.HorizontalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
ch._updateTicks()
xticks = [
(0.0, 0.0), (0.1, 0.4), (0.2, 0.8), (0.3, 1.2), (0.4, 1.6),
(0.5, 2.0), (0.6, 2.4), (0.7, 2.8), (0.8, 3.2), (0.9, 3.6),
(1.0, 4.0),
]
for i in range(len(xticks)):
self.assertAlmostEqual(ch.xticks[i][0], xticks[i][0], 4)
self.assertAlmostEqual(ch.xticks[i][1], xticks[i][1], 4)
yticks = [(0.125, 0), (0.375, 1), (0.625, 2)]
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 4)
self.assertAlmostEqual(ch.yticks[i][1], yticks[i][1], 4)
def test_udpateTicksWithNegatives(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, -2], [1, 1], [2, 3])),
)
ch = pycha.bar.HorizontalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
ch._updateTicks()
xticks = [
(0.0, -2.0), (0.1, -1.5), (0.2, -1.0), (0.3, -0.5), (0.4, 0.0),
(0.5, 0.5), (0.6, 1.0), (0.7, 1.5), (0.8, 2.0), (0.9, 2.5),
(1.0, 3.0),
]
for i in range(len(xticks)):
self.assertAlmostEqual(ch.xticks[i][0], xticks[i][0], 4)
self.assertAlmostEqual(ch.xticks[i][1], xticks[i][1], 4)
yticks = [(0.1667, 0), (0.5000, 1), (0.8333, 2)]
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 4)
self.assertAlmostEqual(ch.yticks[i][1], yticks[i][1], 4)
def test_shadowRectangle(self):
ch = pycha.bar.HorizontalBarChart(None)
shadow = ch._getShadowRectangle(10, 20, 400, 300)
self.assertEqual(shadow, (10, 18, 402, 304))
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(RectTests),
unittest.makeSuite(BarTests),
unittest.makeSuite(VerticalBarTests),
unittest.makeSuite(HorizontalBarTests),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
pycha-0.7.0/tests/line.py 0000664 0001750 0001750 00000010362 12130334357 014666 0 ustar lgs lgs 0000000 0000000 # Copyright (c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import unittest
import cairo
import pycha.line
class PointTests(unittest.TestCase):
def test_point(self):
point = pycha.line.Point(2, 3, 1.0, 2.0, "test")
self.assertEqual(point.x, 2)
self.assertEqual(point.y, 3)
self.assertEqual(point.xval, 1.0)
self.assertEqual(point.yval, 2.0)
self.assertEqual(point.name, "test")
class LineTests(unittest.TestCase):
def test_init(self):
ch = pycha.line.LineChart(None)
self.assertEqual(ch.points, [])
def test_updateChart(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.line.LineChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.minxval, 0)
self.assertEqual(ch.maxxval, 3)
self.assertEqual(ch.xrange, 3)
self.assertAlmostEqual(ch.xscale, 1/3.0, 4)
self.assertEqual(ch.minyval, 0)
self.assertEqual(ch.maxyval, 4)
self.assertEqual(ch.yrange, 4)
self.assertAlmostEqual(ch.yscale, 0.25, 4)
points = (
pycha.line.Point(0, 0.75, 0, 1, 'dataset1'),
pycha.line.Point(1/3.0, 0.75, 1, 1, 'dataset1'),
pycha.line.Point(2/3.0, 0.25, 2, 3, 'dataset1'),
pycha.line.Point(0, 0.5, 0, 2, 'dataset2'),
pycha.line.Point(1/3.0, 1, 1, 0, 'dataset2'),
pycha.line.Point(1, 0, 3, 4, 'dataset2'),
)
for i, point in enumerate(points):
p1, p2 = ch.points[i], point
self.assertEqual(p1.x, p2.x)
self.assertEqual(p1.y, p2.y)
self.assertAlmostEqual(p1.xval, p2.xval, 4)
self.assertAlmostEqual(p1.yval, p2.yval, 4)
self.assertEqual(p1.name, p2.name)
def test_updateChartWithNegatives(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], [1, -2], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, -4])),
)
ch = pycha.line.LineChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.minxval, 0)
self.assertEqual(ch.maxxval, 3)
self.assertEqual(ch.xrange, 3)
self.assertAlmostEqual(ch.xscale, 1/3.0, 4)
self.assertEqual(ch.minyval, -4)
self.assertEqual(ch.maxyval, 3)
self.assertEqual(ch.yrange, 7)
self.assertAlmostEqual(ch.yscale, 1/7.0, 4)
points = (
pycha.line.Point(0, 0.2857, 0, 1, 'dataset1'),
pycha.line.Point(1/3.0, 0.7143, 1, -2, 'dataset1'),
pycha.line.Point(2/3.0, 0.0, 2, 3, 'dataset1'),
pycha.line.Point(0, 0.1429, 0, 2, 'dataset2'),
pycha.line.Point(1/3.0, 0.4286, 1, 0, 'dataset2'),
pycha.line.Point(1, 1.0, 3, -4, 'dataset2'),
)
for i, point in enumerate(points):
p1, p2 = ch.points[i], point
self.assertAlmostEqual(p1.x, p2.x, 4)
self.assertAlmostEqual(p1.y, p2.y, 4)
self.assertAlmostEqual(p1.xval, p2.xval, 4)
self.assertAlmostEqual(p1.yval, p2.yval, 4)
self.assertEqual(p1.name, p2.name)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(PointTests),
unittest.makeSuite(LineTests),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
pycha-0.7.0/tests/stackedbar.py 0000664 0001750 0001750 00000017263 12130334357 016051 0 ustar lgs lgs 0000000 0000000 # Copyright (c) 2009-2010 by Yaco S.L.
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import unittest
import cairo
import pycha.stackedbar
class StackedBarTests(unittest.TestCase):
def test_init(self):
ch = pycha.stackedbar.StackedBarChart(None)
self.assertEqual(ch.barWidth, 0.0)
def test_updateXY(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ((0, 1), (1, 2))),
('dataset2', ((0, 3), (1, 1))),
)
ch = pycha.stackedbar.StackedBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
self.assertEqual(ch.yrange, 4.0)
self.assertAlmostEqual(ch.yscale, 0.25)
def test_updateChart(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ((0, 1), (1, 2))),
('dataset2', ((0, 3), (1, 1))),
)
ch = pycha.stackedbar.StackedBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.minxdelta, 1)
self.assertAlmostEqual(ch.barWidth, 0.375, 3)
self.assertAlmostEqual(ch.barMargin, 0.0625, 4)
class StackedVerticalBarTests(unittest.TestCase):
def test_updateChart(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 3], [1, 4], [2, 2], [3, 5], [4, 3.5])),
('dataset2', ([0, 2], [1, 3], [2, 1], [3, 5], [4, 2.5])),
)
ch = pycha.stackedbar.StackedVerticalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.minxval, 0)
self.assertEqual(ch.maxxval, 4)
self.assertEqual(ch.xrange, 4)
self.assertAlmostEqual(ch.xscale, 0.20, 4)
self.assertEqual(ch.minyval, 0)
self.assertEqual(ch.maxyval, 5)
self.assertEqual(ch.yrange, 10)
self.assertAlmostEqual(ch.yscale, 0.10, 4)
self.assertEqual(ch.minxdelta, 1)
self.assertAlmostEqual(ch.barWidth, 0.150, 4)
self.assertAlmostEqual(ch.barMargin, 0.025, 4)
R = pycha.bar.Rect
bars = (
R(0.025, 0.700, 0.150, 0.300, 0, 3, 'dataset1'),
R(0.225, 0.600, 0.150, 0.400, 1, 4, 'dataset1'),
R(0.425, 0.800, 0.150, 0.200, 2, 2, 'dataset1'),
R(0.625, 0.500, 0.150, 0.500, 3, 5, 'dataset1'),
R(0.825, 0.650, 0.150, 0.350, 4, 3.5, 'dataset1'),
R(0.025, 0.500, 0.150, 0.200, 0, 2, 'dataset2'),
R(0.225, 0.300, 0.150, 0.300, 1, 3, 'dataset2'),
R(0.425, 0.700, 0.150, 0.100, 2, 1, 'dataset2'),
R(0.625, 0.000, 0.150, 0.500, 3, 5, 'dataset2'),
R(0.825, 0.400, 0.150, 0.250, 4, 2.5, 'dataset2'),
)
for i, bar in enumerate(bars):
b1, b2 = ch.bars[i], bar
self.assertAlmostEqual(b1.x, b2.x, 4)
self.assertAlmostEqual(b1.y, b2.y, 4)
self.assertAlmostEqual(b1.w, b2.w, 4)
self.assertAlmostEqual(b1.h, b2.h, 4)
self.assertEqual(b1.xval, b2.xval)
self.assertEqual(b1.yval, b2.yval)
self.assertEqual(b1.name, b2.name)
def test_updateTicks(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [3, 4])),
)
ch = pycha.stackedbar.StackedVerticalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
ch._updateTicks()
xticks = [(0.125, 0), (0.375, 1), (0.625, 2)]
for i in range(len(xticks)):
self.assertAlmostEqual(ch.xticks[i][0], xticks[i][0], 4)
self.assertAlmostEqual(ch.xticks[i][1], xticks[i][1], 4)
yticks = [
(1.0, 0.0), (0.9, 0.7), (0.8, 1.4), (0.7, 2.1), (0.6, 2.8),
(0.5, 3.5), (0.4, 4.2), (0.3, 4.9), (0.2, 5.6),
(0.1, 6.3), (0.0, 7.0),
]
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 4)
self.assertAlmostEqual(ch.yticks[i][1], yticks[i][1], 4)
class StackedHorizontalBarTests(unittest.TestCase):
def test_updateChart(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [2, 4])),
)
ch = pycha.stackedbar.StackedHorizontalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
self.assertEqual(ch.xrange, 2)
self.assertAlmostEqual(ch.xscale, 0.3333, 4)
self.assertAlmostEqual(ch.yscale, 0.1429, 4)
self.assertEqual(ch.minxdelta, 1)
self.assertAlmostEqual(ch.barWidth, 0.25, 4)
self.assertAlmostEqual(ch.barMargin, 0.0417, 4)
bars = (
pycha.bar.Rect(0, 0.0417, 0.1429, 0.25, 0, 1, 'dataset1'),
pycha.bar.Rect(0, 0.3750, 0.1429, 0.25, 1, 1, 'dataset1'),
pycha.bar.Rect(0, 0.7083, 0.4286, 0.25, 2, 3, 'dataset1'),
pycha.bar.Rect(0.1429, 0.0417, 0.2857, 0.25, 0, 2, 'dataset2'),
pycha.bar.Rect(0.1429, 0.3750, 0.0000, 0.25, 1, 0, 'dataset2'),
pycha.bar.Rect(0.4286, 0.7083, 0.5714, 0.25, 2, 4, 'dataset2'),
)
for i, bar in enumerate(bars):
b1, b2 = ch.bars[i], bar
self.assertAlmostEqual(b1.x, b2.x, 4)
self.assertAlmostEqual(b1.y, b2.y, 4)
self.assertAlmostEqual(b1.w, b2.w, 4)
self.assertAlmostEqual(b1.h, b2.h, 4)
self.assertEqual(b1.xval, b2.xval)
self.assertEqual(b1.yval, b2.yval)
self.assertEqual(b1.name, b2.name)
def test_updateTicks(self):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 500)
dataset = (
('dataset1', ([0, 1], [1, 1], [2, 3])),
('dataset2', ([0, 2], [1, 0], [2, 4])),
)
ch = pycha.stackedbar.StackedHorizontalBarChart(surface)
ch.addDataset(dataset)
ch._updateXY()
ch._updateChart()
ch._updateTicks()
xticks = [
(0.0, 0.0), (0.1, 0.7), (0.2, 1.4), (0.3, 2.1),
(0.4, 2.8), (0.5, 3.5), (0.6, 4.2), (0.7, 4.9),
(0.8, 5.6), (0.9, 6.3), (1.0, 7.0),
]
for i in range(len(xticks)):
self.assertAlmostEqual(ch.xticks[i][0], xticks[i][0], 4)
self.assertAlmostEqual(ch.xticks[i][1], xticks[i][1], 4)
yticks = [(0.1667, 0), (0.5, 1), (0.8333, 2)]
for i in range(len(yticks)):
self.assertAlmostEqual(ch.yticks[i][0], yticks[i][0], 4)
self.assertAlmostEqual(ch.yticks[i][1], yticks[i][1], 4)
def test_suite():
return unittest.TestSuite((
unittest.makeSuite(StackedBarTests),
unittest.makeSuite(StackedVerticalBarTests),
unittest.makeSuite(StackedHorizontalBarTests),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
pycha-0.7.0/tests/__init__.py 0000664 0001750 0001750 00000000000 12130334357 015462 0 ustar lgs lgs 0000000 0000000 pycha-0.7.0/CHANGES.txt 0000664 0001750 0001750 00000005736 12130337453 014045 0 ustar lgs lgs 0000000 0000000 Changes
=======
0.7.0 (2012-04-07)
------------------
- Radial Chart by Roberto Garcia Carvajal
- Polygonal Chart by Roberto Garcia Carvajal
- Ring Chart by Roberto Garcia Carvajal
- Minor cleanups in the code
0.6.0 (2010-12-31)
------------------
- Buildout support
- Documentation revamped
- Debug improvements
- Autopadding
- Make the unicode strings used in labels safer
0.5.3 (2010-03-29)
------------------
- New title color option
- Fix crash in chavier application
- New horizontal axis lines. Options to turn it (and vertical ones) on and off
- Improve precision in axis ticks
- Add some examples and update old ones
0.5.2 (2009-09-26)
------------------
- Add a MANIFEST.in to explictly include all files in the source distribution
0.5.1 (2009-09-19)
------------------
- Several bug fixes (Lorenzo)
- Draw circles instead of lines for scatter chart symbols (Lorenzo)
- Error bars (Yang Zhang)
- Improve tick labels (Simon)
- Add labels with yvals next to the bars (Simon (Vsevolod) Ilyushchenko)
- Change the project website (Lorenzo)
0.5.0 (2009-03-22)
------------------
- Bar chart fixes (Adam)
- Support for custon fonts in the ticks (Ged)
- Support for an 'interval' option (Nicolas)
- New color scheme system (Lorenzo)
- Stacked bar charts support (Lorenzo)
0.4.2 (2009-02-15)
------------------
- Much better documentation (Adam)
- Fixes integer division when computing xscale (Laurent)
- Fix for a broken example (Lorenzo)
- Use labelFontSize when rendering the axis (Adam Przywecki)
- Code cleanups. Now it should pass pyflakes and pep8 in most files (Lorenzo)
- Support for running the test suite with python setup.py test (Lorenzo)
- Support for SVG (and PDF, Postscript, Win32, Quartz) by changing the way
we compute the surface dimensions (Lorenzo)
0.4.1 (2008-10-29)
------------------
- Fix a colon in the README.txt file (Lorenzo)
- Add a test_suite option to setup.py so we can run the tests before deployment
(Lorenzo)
0.4.0 (2008-10-28)
------------------
- Improved test suite (Lorenzo, Nicolas)
- Many bugs fixed (Lorenzo, Stephane Wirtel)
- Support for negative values in the datasets (Nicolas, Lorenzo)
- Chavier, a simple pygtk application for playing with Pycha charts (Lorenzo)
- Allow the legend to be placed relative to the right and bottom of the canvas
(Nicolas Evrard)
- Easier debugging by adding __str__ methods to aux classes (rectangle, point,
area, ...) (Lorenzo)
- Do not overlap Y axis label when ticks label are not rotated (John Eikenberry)
0.3.0 (2008-03-22)
------------------
- Scattered charts (Tamas Nepusz )
- Chart titles (John Eikenberry )
- Axis labels and rotated ticks (John)
- Chart background and surface background (John)
- Automatically augment the light in large color schemes (John)
- Lots of bug fixes (John and Lorenzo)
0.2.0 (2007-10-25)
------------------
- Test suite
- Python 2.4 compatibility (patch by Miguel Hernandez)
- API docs
- Small fixes
0.1.0 (2007-10-17)
------------------
- Initial release
pycha-0.7.0/examples/ 0000775 0001750 0001750 00000000000 12130340466 014035 5 ustar lgs lgs 0000000 0000000 pycha-0.7.0/examples/scatterchart.py 0000664 0001750 0001750 00000003725 12130334357 017107 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import random
import sys
import cairo
import pycha.scatter
def scatterplotChart(output):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 400)
top = 50
dataSet = (
('points 1', [(i, random.random() * float(top)) for i in range(top)]),
('points 2', [(i, random.random() * float(top)) for i in range(top)]),
('points 3', [(i, random.random() * float(top)) for i in range(top)]),
('points 4', [(i, random.random() * float(top)) for i in range(top)]),
('points 5', [(i, random.random() * float(top)) for i in range(top)]),
)
options = {
'background': {
'color': '#eeeeff',
'lineColor': '#444444',
},
'colorScheme': {
'name': 'rainbow',
'args': {
'initialColor': 'blue',
},
},
'legend': {
'hide': True,
},
'title': u'Scatter plot',
}
chart = pycha.scatter.ScatterplotChart(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png(output)
if __name__ == '__main__':
if len(sys.argv) > 1:
output = sys.argv[1]
else:
output = 'scatterchart.png'
scatterplotChart(output)
pycha-0.7.0/examples/test.py 0000664 0001750 0001750 00000004640 12130334357 015374 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import cairo
from pycha.pie import PieChart
from pycha.bar import VerticalBarChart
from pycha.line import LineChart
def testPie():
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 400, 400)
chart = PieChart(surface)
dataSet = (
('myFirstDataset', [[0, 3]]),
('mySecondDataset', [[0, 1.4]]),
('myThirdDataset', [[0, 0.46]]),
('myFourthDataset', [[0, 0.3]]),
)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png("testpie.png")
def testBar():
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 500, 300)
options = {
'legend': {
'position': {
'left': 330
}
},
}
chart = VerticalBarChart(surface, options)
dataSet = (
('myFirstDataset', [[0, 1], [1, 1], [2, 1.414], [3, 1.73]]),
('mySecondDataset', [[0, 0.3], [1, 2.67], [2, 1.34], [3, 1.73]]),
('myThirdDataset', [[0, 0.46], [1, 1.45], [2, 2.5], [3, 1.2]]),
('myFourthDataset', [[0, 0.86], [1, 0.83], [2, 3], [3, 1.73]]),
)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png("testbar.png")
def testLine():
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 600, 500)
chart = LineChart(surface)
dataSet = (
('myFirstDataset', [[0, 3], [1, 2], [2, 1.414], [3, 2.3]]),
('mySecondDataset', [[0, 1.4], [1, 2.67], [2, 1.34], [3, 1.2]]),
('myThirdDataset', [[0, 0.46], [1, 1.45], [2, 1.0], [3, 1.6]]),
('myFourthDataset', [[0, 0.3], [1, 0.83], [2, 0.7], [3, 0.2]]),
)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png("testline.png")
testPie()
testBar()
testLine()
pycha-0.7.0/examples/stackedbarchart.py 0000664 0001750 0001750 00000003451 12130334357 017541 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2009-2010 by Yaco S.L.
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import sys
import cairo
import pycha.stackedbar
def stackedBarChart(output, chartFactory):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 400)
dataSet = (
('internal', [(0, 8), (1, 10), (2, 5), (3, 6)]),
('external', [(0, 5), (1, 2), (2, 4), (3, 8)]),
)
options = {
'background': {
'chartColor': '#ffeeff',
'baseColor': '#ffffff',
'lineColor': '#444444',
},
'colorScheme': {
'name': 'gradient',
'args': {
'initialColor': 'red',
},
},
'legend': {
'hide': True,
},
'title': 'Sample Chart'
}
chart = chartFactory(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png(output)
if __name__ == '__main__':
if len(sys.argv) > 1:
output = sys.argv[1]
else:
output = 'stackedbarchart.png'
stackedBarChart('v' + output, pycha.stackedbar.StackedVerticalBarChart)
stackedBarChart('h' + output, pycha.stackedbar.StackedHorizontalBarChart)
pycha-0.7.0/examples/linechart.py 0000664 0001750 0001750 00000003423 12130334357 016364 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import sys
import cairo
import pycha.line
from lines import lines
def lineChart(output):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 400)
dataSet = (
('lines', [(i, l[1]) for i, l in enumerate(lines)]),
)
options = {
'axis': {
'x': {
'ticks': [dict(v=i, label=l[0]) for i, l in enumerate(lines)],
},
'y': {
'tickCount': 4,
}
},
'background': {
'color': '#eeeeff',
'lineColor': '#444444'
},
'colorScheme': {
'name': 'gradient',
'args': {
'initialColor': 'blue',
},
},
'legend': {
'hide': True,
},
}
chart = pycha.line.LineChart(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png(output)
if __name__ == '__main__':
if len(sys.argv) > 1:
output = sys.argv[1]
else:
output = 'linechart.png'
lineChart(output)
pycha-0.7.0/examples/interval.py 0000664 0001750 0001750 00000003113 12130334357 016233 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import cairo
import pycha.line
def intervalExample():
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 400)
dataSet = (
('dataset 1', [(0, 10), (1, 20), (2, 45), (3, 33)]),
('dataset 2', [(0, 14), (1, 18), (2, 32), (3, 21)]),
)
options = {
'axis': {
'x': {
'interval': 0.5,
},
'y': {
'interval': 5,
},
},
'legend': {
'hide': True,
},
'title': 'Interval example',
'background': {
'baseColor': '#f0f0f0',
},
'shouldFill': False,
}
chart = pycha.line.LineChart(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png("interval.png")
if __name__ == '__main__':
intervalExample()
pycha-0.7.0/examples/pychadownloads.py 0000664 0001750 0001750 00000004506 12130334357 017435 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import cairo
import pycha.stackedbar
def barChart(output):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 400)
dataSet = (
('main release', [(0, 263), (1, 641), (2, 969), (3, 278), (4, 989)]),
('1st bug fix', [(0, 0), (1, 0), (2, 0), (3, 787), (4, 234)]),
('2nd bug fix', [(0, 0), (1, 0), (2, 0), (3, 309), (4, 1581)]),
('3nd bug fix', [(0, 0), (1, 0), (2, 0), (3, 0), (4, 1824)]),
)
options = {
'axis': {
'x': {
'ticks': [dict(v=0, label='0.1 (Oct 07)'),
dict(v=1, label='0.2 (Oct 07)'),
dict(v=2, label='0.3 (Mar 08)'),
dict(v=3, label='0.4 (Oct 08)'),
dict(v=4, label='0.5 (Mar 09)')],
'label': 'Releases',
},
'y': {
'label': 'Downloads',
}
},
'background': {
'chartColor': '#ffeeff',
'baseColor': '#ffffff',
'lineColor': '#444444'
},
'colorScheme': {
'name': 'gradient',
'args': {
'initialColor': 'green',
},
},
'legend': {
'position': {
'top': 20,
'left': 80,
}
},
'title': 'Pycha Downloads'
}
chart = pycha.stackedbar.StackedVerticalBarChart(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png(output)
if __name__ == '__main__':
barChart('pychadownloads.png')
pycha-0.7.0/examples/piechart.py 0000664 0001750 0001750 00000002727 12130334357 016220 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import sys
import cairo
import pycha.pie
from lines import lines
def pieChart(output):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 800)
dataSet = [(line[0], [[0, line[1]]]) for line in lines]
options = {
'axis': {
'x': {
'ticks': [dict(v=i, label=d[0]) for i, d in enumerate(lines)],
}
},
'legend': {
'hide': True,
},
'title': 'Pie Chart',
}
chart = pycha.pie.PieChart(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png(output)
if __name__ == '__main__':
if len(sys.argv) > 1:
output = sys.argv[1]
else:
output = 'piechart.png'
pieChart(output)
pycha-0.7.0/examples/errorbarchart.py 0000664 0001750 0001750 00000003251 12130334357 017252 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import sys
import cairo
import pycha.bar
def barChart(output, chartFactory):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 400)
# note that this dataset is composed by triplets, where the
# third item is the error
dataSet = (
('data 1', [(0, 30, 5), (1, 40, 7), (2, 25, 3), (3, 50, 10)]),
)
options = {
'background': {
'chartColor': '#ffeeff',
'baseColor': '#ffffff',
'lineColor': '#444444',
},
'legend': {
'hide': True,
},
'title': 'Error bars'
}
chart = chartFactory(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png(output)
if __name__ == '__main__':
if len(sys.argv) > 1:
output = sys.argv[1]
else:
output = 'errorbars.png'
barChart('v' + output, pycha.bar.VerticalBarChart)
barChart('h' + output, pycha.bar.HorizontalBarChart)
pycha-0.7.0/examples/lines.py 0000664 0001750 0001750 00000001677 12130334357 015536 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
# common data for several examples
lines = (
('bar.py', 319),
('chart.py', 875),
('color.py', 204),
('line.py', 130),
('pie.py', 352),
('scatter.py', 38),
('stackedbar.py', 121),
)
pycha-0.7.0/examples/barchart.py 0000664 0001750 0001750 00000004210 12130334357 016174 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import sys
import cairo
import pycha.bar
from lines import lines
def barChart(output, chartFactory):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 400)
dataSet = (
('lines', [(i, l[1]) for i, l in enumerate(lines)]),
)
options = {
'axis': {
'x': {
'ticks': [dict(v=i, label=l[0]) for i, l in enumerate(lines)],
'label': 'Files',
'rotate': 25,
},
'y': {
'tickCount': 4,
'rotate': 25,
'label': 'Lines'
}
},
'background': {
'chartColor': '#ffeeff',
'baseColor': '#ffffff',
'lineColor': '#444444'
},
'colorScheme': {
'name': 'gradient',
'args': {
'initialColor': 'red',
},
},
'legend': {
'hide': True,
},
'padding': {
'left': 0,
'bottom': 0,
},
'title': 'Sample Chart'
}
chart = chartFactory(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png(output)
if __name__ == '__main__':
if len(sys.argv) > 1:
output = sys.argv[1]
else:
output = 'barchart.png'
barChart('v' + output, pycha.bar.VerticalBarChart)
barChart('h' + output, pycha.bar.HorizontalBarChart)
pycha-0.7.0/examples/svg.py 0000664 0001750 0001750 00000002673 12130334357 015220 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import cairo
import pycha.bar
def testBar():
surface = cairo.SVGSurface("testsvg.svg", 500, 300)
options = {
'legend': {
'position': {
'left': 330
}
},
}
chart = pycha.bar.VerticalBarChart(surface, options)
dataSet = (
('myFirstDataset', [[0, 1], [1, 1], [2, 1.414], [3, 1.73]]),
('mySecondDataset', [[0, 0.3], [1, 2.67], [2, 1.34], [3, 1.73]]),
('myThirdDataset', [[0, 0.46], [1, 1.45], [2, 2.5], [3, 1.2]]),
('myFourthDataset', [[0, 0.86], [1, 0.83], [2, 3], [3, 1.73]]),
)
chart.addDataset(dataSet)
chart.render()
surface.flush()
if __name__ == '__main__':
testBar()
pycha-0.7.0/examples/ringchart.py 0000664 0001750 0001750 00000003403 12130334671 016371 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import sys
import cairo
import pycha.ring
lines = (
('bar.py', 219, 201, 31),
('chart.py', 975, 450, 341),
('color.py', 104, 300, 200),
('line.py', 230, 100, 450),
('pie.py', 452, 100, 304),
('scatter.py', 138, 500, 200),
('stackedbar.py', 21, 110, 200),
)
def ringChart(output):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 800, 800)
dataSet = [(line[0], [[0, line[1]], [1, line[2]], [2, line[3]]])
for line in lines]
options = {
'axis': {
'x': {
'ticks': [dict(v=i, label=d) for i, d in
enumerate(['2010', '2011', '2012'])],
}
},
'legend': {
'hide': False,
},
'title': 'Ring Chart',
}
chart = pycha.ring.RingChart(surface, options)
chart.addDataset(dataSet)
chart.render()
surface.write_to_png(output)
if __name__ == '__main__':
if len(sys.argv) > 1:
output = sys.argv[1]
else:
output = 'ringchart.png'
ringChart(output)
pycha-0.7.0/examples/color/ 0000775 0001750 0001750 00000000000 12130340466 015153 5 ustar lgs lgs 0000000 0000000 pycha-0.7.0/examples/color/colorschemes.py 0000664 0001750 0001750 00000003524 12130334357 020221 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
# 2009 by Yaco S.L.
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import cairo
import pycha.pie
def pieChart(colorScheme):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 400, 400)
options = {
'background': {
'hide': True,
},
'colorScheme': colorScheme,
'title': colorScheme['name'],
}
chart = pycha.pie.PieChart(surface, options)
dataSet = (
('dataset 1', ((0, 10), )),
('dataset 2', ((0, 15), )),
('dataset 3', ((0, 20), )),
('dataset 4', ((0, 25), )),
('dataset 5', ((0, 30), )),
('dataset 6', ((0, 20), )),
('dataset 7', ((0, 40), )),
)
chart.addDataset(dataSet)
chart.render()
output = colorScheme['name'] + '.png'
surface.write_to_png(output)
if __name__ == '__main__':
pieChart({'name': 'gradient', 'args': {'initialColor': 'red'}})
colors = ('#ff0000', '#00ff00', '#0000ff',
'#00ffff', '#000000', '#ff00ff',
'#ffff00')
pieChart({'name': 'fixed', 'args': {'colors': colors}})
pieChart({'name': 'rainbow', 'args': {'initialColor': 'red'}})
pycha-0.7.0/pycha/ 0000775 0001750 0001750 00000000000 12130340466 013323 5 ustar lgs lgs 0000000 0000000 pycha-0.7.0/pycha/utils.py 0000664 0001750 0001750 00000002544 12130334357 015044 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
# 2009-2010 by Yaco S.L.
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
def clamp(minValue, maxValue, value):
"""Make sure value is between minValue and maxValue"""
if value < minValue:
return minValue
if value > maxValue:
return maxValue
return value
def safe_unicode(obj, encoding=None):
"""Return a unicode value from the argument"""
if isinstance(obj, unicode):
return obj
elif isinstance(obj, str):
if encoding is None:
return unicode(obj)
else:
return unicode(obj, encoding)
else:
# it may be an int or a float
return unicode(obj)
pycha-0.7.0/pycha/ring.py 0000664 0001750 0001750 00000030567 12130334671 014650 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Roberto Garcia Carvajal
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import math
import cairo
from pycha.chart import Chart, Option, Layout, Area, get_text_extents
from pycha.color import hex2rgb
class RingChart(Chart):
def __init__(self, surface=None, options={}, debug=False):
super(RingChart, self).__init__(surface, options, debug)
self.slices = {}
self.centerx = 0
self.centery = 0
self.layout = RingLayout(self.slices)
self.rings = []
self.nrings = 0
self.dataset_names = []
self.dataset_order = {}
def _updateChart(self):
"""Evaluates measures for pie charts"""
self.rings = [i
for i in set(
[data[0]
for dataset in self.datasets
for data in dataset[1]])]
self.nrings = len(self.rings)
self.dataset_names = [i for i in
set([data[0] for data in self.datasets])]
self.dataset_order = {val: i
for i, val in enumerate(self.dataset_names)}
slices = {i: list() for i in self.rings}
for dataset in self.datasets:
dataset_name = dataset[0]
for data in dataset[1]:
dataset_order = data[0]
dataset_value = data[1]
slices[dataset_order].append(
dict(name=dataset_name, value=dataset_value))
s = dict()
for i in self.rings:
s[i] = float(sum(slice['value'] for slice in slices[i]))
for i in self.rings:
fraction = angle = 0.0
self.slices[i] = list()
for slice in slices[i]:
if slice['value'] > 0:
angle += fraction
fraction = slice['value'] / s[i]
self.slices[i].append(
Slice(slice['name'], fraction, i, slice['value'],
angle))
def _updateTicks(self):
"""Evaluates pie ticks"""
self.xticks = []
lookups = [key for key in self.slices.keys()]
if self.options.axis.x.ticks:
ticks = [tick['v'] for tick in self.options.axis.x.ticks]
if frozenset(lookups) != frozenset(ticks):
#TODO: Is there better option than ValueError?
raise ValueError(u"Incompatible ticks")
for tick in self.options.axis.x.ticks:
if not isinstance(tick, Option):
tick = Option(tick)
label = tick.label or str(tick.v)
self.xticks.append((tick.v, label))
else:
for i in lookups:
self.xticks.append((i, u"%s" % i))
def _renderLines(self, cx):
"""Aux function for _renderBackground"""
# there are no lines in a Pie Chart
def _renderChart(self, cx):
"""Renders a pie chart"""
self.centerx = self.layout.chart.x + self.layout.chart.w * 0.5
self.centery = self.layout.chart.y + self.layout.chart.h * 0.5
cx.set_line_join(cairo.LINE_JOIN_ROUND)
if self.options.stroke.shadow and False:
cx.save()
cx.set_source_rgba(0, 0, 0, 0.15)
cx.new_path()
cx.move_to(self.centerx, self.centery)
cx.arc(self.centerx + 1, self.centery + 2,
self.layout.radius + 1, 0, math.pi * 2)
cx.line_to(self.centerx, self.centery)
cx.close_path()
cx.fill()
cx.restore()
cx.save()
self.rings.reverse()
radius = self.layout.radius
radius_dec = radius / (self.nrings + 1)
for i in self.rings:
slices = self.slices[i]
for slice in slices:
if slice.isBigEnough():
cx.set_source_rgb(*self.colorScheme[slice.name])
if self.options.shouldFill:
slice.draw(cx, self.centerx, self.centery,
radius)
cx.fill()
if not self.options.stroke.hide:
slice.draw(cx, self.centerx, self.centery,
radius)
cx.set_line_width(self.options.stroke.width)
cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
cx.stroke()
radius = radius - radius_dec
cx.new_path()
cx.move_to(self.centerx, self.centery)
cx.arc(self.centerx, self.centery, radius, 0, 360)
cx.close_path()
cx.set_line_width(self.options.stroke.width)
cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
cx.fill()
cx.stroke()
cx.restore()
if self.debug:
cx.set_source_rgba(1, 0, 0, 0.5)
px = max(cx.device_to_user_distance(1, 1))
for x, y in self.layout._lines:
cx.arc(x, y, 5 * px, 0, 2 * math.pi)
cx.fill()
cx.new_path()
cx.move_to(self.centerx, self.centery)
cx.line_to(x, y)
cx.stroke()
def _renderAxis(self, cx):
"""Renders the axis for pie charts"""
if self.options.axis.x.hide or not self.xticks:
return
self.xlabels = []
if self.debug:
px = max(cx.device_to_user_distance(1, 1))
cx.set_source_rgba(0, 0, 1, 0.5)
for x, y, w, h in self.layout.ticks:
cx.rectangle(x, y, w, h)
cx.stroke()
cx.arc(x + w / 2.0, y + h / 2.0, 5 * px, 0, 2 * math.pi)
cx.fill()
cx.arc(x, y, 2 * px, 0, 2 * math.pi)
cx.fill()
cx.select_font_face(self.options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.axis.tickFontSize)
cx.set_source_rgb(*hex2rgb(self.options.axis.labelColor))
radius = self.layout.radius
radius_inc = radius / (self.nrings + 1)
current_radius = self.centery + radius_inc + radius_inc / 2
for i, tick in enumerate(self.xticks):
label = tick[1]
cx.move_to(self.centerx, current_radius)
cx.show_text(label)
current_radius += radius_inc
class Slice(object):
def __init__(self, name, fraction, xval, yval, angle):
self.name = name
self.fraction = fraction
self.xval = xval
self.yval = yval
self.startAngle = 2 * angle * math.pi
self.endAngle = 2 * (angle + fraction) * math.pi
def __str__(self):
return ("" %
(self.startAngle, self.endAngle, self.fraction))
def isBigEnough(self):
return abs(self.startAngle - self.endAngle) > 0.001
def draw(self, cx, centerx, centery, radius):
cx.new_path()
cx.move_to(centerx, centery)
cx.arc(centerx, centery, radius, -self.endAngle, -self.startAngle)
cx.close_path()
def getNormalisedAngle(self):
normalisedAngle = (self.startAngle + self.endAngle) / 2
if normalisedAngle > math.pi * 2:
normalisedAngle -= math.pi * 2
elif normalisedAngle < 0:
normalisedAngle += math.pi * 2
return normalisedAngle
class RingLayout(Layout):
"""Set of chart areas for ring charts"""
def __init__(self, slices):
self.slices = slices
self.title = Area()
self.chart = Area()
self.ticks = []
self.radius = 0
self._areas = (
(self.title, (1, 126 / 255.0, 0)), # orange
(self.chart, (75 / 255.0, 75 / 255.0, 1.0)), # blue
)
self._lines = []
def update(self, cx, options, width, height, xticks, yticks):
self.title.x = options.padding.left
self.title.y = options.padding.top
self.title.w = width - (options.padding.left + options.padding.right)
self.title.h = get_text_extents(cx,
options.title,
options.titleFont,
options.titleFontSize,
options.encoding)[1]
self.chart.x = self.title.x
self.chart.y = self.title.y + self.title.h
self.chart.w = self.title.w
self.chart.h = height - self.title.h - (options.padding.top
+ options.padding.bottom)
self.radius = min(self.chart.w / 2.0, self.chart.h / 2.0)
def _get_min_radius(self, angle, centerx, centery, width, height):
min_radius = None
# precompute some common values
tan = math.tan(angle)
half_width = width / 2.0
half_height = height / 2.0
offset_x = half_width * tan
offset_y = half_height / tan
def intersect_horizontal_line(y):
return centerx + (centery - y) / tan
def intersect_vertical_line(x):
return centery - tan * (x - centerx)
# computes the intersection between the rect that has
# that angle with the X axis and the bounding chart box
if 0.25 * math.pi <= angle < 0.75 * math.pi:
# intersects with the top rect
y = self.chart.y
x = intersect_horizontal_line(y)
self._lines.append((x, y))
x1 = x - half_width - offset_y
self.ticks.append((x1, self.chart.y, width, height))
min_radius = abs((y + height) - centery)
elif 0.75 * math.pi <= angle < 1.25 * math.pi:
# intersects with the left rect
x = self.chart.x
y = intersect_vertical_line(x)
self._lines.append((x, y))
y1 = y - half_height - offset_x
self.ticks.append((x, y1, width, height))
min_radius = abs(centerx - (x + width))
elif 1.25 * math.pi <= angle < 1.75 * math.pi:
# intersects with the bottom rect
y = self.chart.y + self.chart.h
x = intersect_horizontal_line(y)
self._lines.append((x, y))
x1 = x - half_width + offset_y
self.ticks.append((x1, y - height, width, height))
min_radius = abs((y - height) - centery)
else:
# intersects with the right rect
x = self.chart.x + self.chart.w
y = intersect_vertical_line(x)
self._lines.append((x, y))
y1 = y - half_height + offset_x
self.ticks.append((x - width, y1, width, height))
min_radius = abs((x - width) - centerx)
return min_radius
def _get_tick_position(self, radius, angle, tick, centerx, centery):
text_width, text_height = tick[2:4]
half_width = text_width / 2.0
half_height = text_height / 2.0
if 0 <= angle < 0.5 * math.pi:
# first quadrant
k1 = j1 = k2 = 1
j2 = -1
elif 0.5 * math.pi <= angle < math.pi:
# second quadrant
k1 = k2 = -1
j1 = j2 = 1
elif math.pi <= angle < 1.5 * math.pi:
# third quadrant
k1 = j1 = k2 = -1
j2 = 1
elif 1.5 * math.pi <= angle < 2 * math.pi:
# fourth quadrant
k1 = k2 = 1
j1 = j2 = -1
cx = radius * math.cos(angle) + k1 * half_width
cy = radius * math.sin(angle) + j1 * half_height
radius2 = math.sqrt(cx * cx + cy * cy)
tan = math.tan(angle)
x = math.sqrt((radius2 * radius2) / (1 + tan * tan))
y = tan * x
x = centerx + k2 * x
y = centery + j2 * y
return x - half_width, y - half_height, text_width, text_height
pycha-0.7.0/pycha/chart.py 0000664 0001750 0001750 00000074545 12130334357 015017 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import copy
import inspect
import math
import cairo
from pycha.color import ColorScheme, hex2rgb, DEFAULT_COLOR
from pycha.utils import safe_unicode
class Chart(object):
def __init__(self, surface, options={}, debug=False):
# this flag is useful to reuse this chart for drawing different data
# or use different options
self.resetFlag = False
# initialize storage
self.datasets = []
# computed values used in several methods
self.layout = Layout()
self.minxval = None
self.maxxval = None
self.minyval = None
self.maxyval = None
self.xscale = 1.0
self.yscale = 1.0
self.xrange = None
self.yrange = None
self.origin = 0.0
self.xticks = []
self.yticks = []
# set the default options
self.options = copy.deepcopy(DEFAULT_OPTIONS)
if options:
self.options.merge(options)
# initialize the surface
self._initSurface(surface)
self.colorScheme = None
# debug mode to draw aditional hints
self.debug = debug
def addDataset(self, dataset):
"""Adds an object containing chart data to the storage hash"""
self.datasets += dataset
def _getDatasetsKeys(self):
"""Return the name of each data set"""
return [d[0] for d in self.datasets]
def _getDatasetsValues(self):
"""Return the data (value) of each data set"""
return [d[1] for d in self.datasets]
def setOptions(self, options={}):
"""Sets options of this chart"""
self.options.merge(options)
def getSurfaceSize(self):
cx = cairo.Context(self.surface)
x, y, w, h = cx.clip_extents()
return w, h
def reset(self):
"""Resets options and datasets.
In the next render the surface will be cleaned before any drawing.
"""
self.resetFlag = True
self.options = copy.deepcopy(DEFAULT_OPTIONS)
self.datasets = []
def render(self, surface=None, options={}):
"""Renders the chart with the specified options.
The optional parameters can be used to render a chart in a different
surface with new options.
"""
self._update(options)
if surface:
self._initSurface(surface)
cx = cairo.Context(self.surface)
# calculate area data
surface_width, surface_height = self.getSurfaceSize()
self.layout.update(cx, self.options, surface_width, surface_height,
self.xticks, self.yticks)
self._renderBackground(cx)
if self.debug:
self.layout.render(cx)
self._renderChart(cx)
self._renderAxis(cx)
self._renderTitle(cx)
self._renderLegend(cx)
def clean(self):
"""Clears the surface with a white background."""
cx = cairo.Context(self.surface)
cx.save()
cx.set_source_rgb(1, 1, 1)
cx.paint()
cx.restore()
def _setColorscheme(self):
"""Sets the colorScheme used for the chart using the
options.colorScheme option
"""
name = self.options.colorScheme.name
keys = self._getDatasetsKeys()
colorSchemeClass = ColorScheme.getColorScheme(name, None)
if colorSchemeClass is None:
raise ValueError('Color scheme "%s" is invalid!' % name)
# Remove invalid args before calling the constructor
kwargs = dict(self.options.colorScheme.args)
validArgs = inspect.getargspec(colorSchemeClass.__init__)[0]
kwargs = dict([(k, v) for k, v in kwargs.items() if k in validArgs])
self.colorScheme = colorSchemeClass(keys, **kwargs)
def _initSurface(self, surface):
self.surface = surface
if self.resetFlag:
self.resetFlag = False
self.clean()
def _update(self, options={}):
"""Update all the information needed to render the chart"""
self.setOptions(options)
self._setColorscheme()
self._updateXY()
self._updateChart()
self._updateTicks()
def _updateXY(self):
"""Calculates all kinds of metrics for the x and y axis"""
x_range_is_defined = self.options.axis.x.range is not None
y_range_is_defined = self.options.axis.y.range is not None
if not x_range_is_defined or not y_range_is_defined:
stores = self._getDatasetsValues()
# gather data for the x axis
if x_range_is_defined:
self.minxval, self.maxxval = self.options.axis.x.range
else:
xdata = [pair[0] for pair in reduce(lambda a, b: a+b, stores)]
self.minxval = float(min(xdata))
self.maxxval = float(max(xdata))
if self.minxval * self.maxxval > 0 and self.minxval > 0:
self.minxval = 0.0
self.xrange = self.maxxval - self.minxval
if self.xrange == 0:
self.xscale = 1.0
else:
self.xscale = 1.0 / self.xrange
# gather data for the y axis
if y_range_is_defined:
self.minyval, self.maxyval = self.options.axis.y.range
else:
ydata = [pair[1] for pair in reduce(lambda a, b: a+b, stores)]
self.minyval = float(min(ydata))
self.maxyval = float(max(ydata))
if self.minyval * self.maxyval > 0 and self.minyval > 0:
self.minyval = 0.0
self.yrange = self.maxyval - self.minyval
if self.yrange == 0:
self.yscale = 1.0
else:
self.yscale = 1.0 / self.yrange
if self.minyval * self.maxyval < 0: # different signs
self.origin = abs(self.minyval) * self.yscale
else:
self.origin = 0.0
def _updateChart(self):
raise NotImplementedError
def _updateTicks(self):
"""Evaluates ticks for x and y axis.
You should call _updateXY before because that method computes the
values of xscale, minxval, yscale, and other attributes needed for
this method.
"""
stores = self._getDatasetsValues()
# evaluate xTicks
self.xticks = []
if self.options.axis.x.ticks:
for tick in self.options.axis.x.ticks:
if not isinstance(tick, Option):
tick = Option(tick)
if tick.label is None:
label = str(tick.v)
else:
label = tick.label
pos = self.xscale * (tick.v - self.minxval)
if 0.0 <= pos <= 1.0:
self.xticks.append((pos, label))
elif self.options.axis.x.interval > 0:
interval = self.options.axis.x.interval
label = (divmod(self.minxval, interval)[0] + 1) * interval
pos = self.xscale * (label - self.minxval)
prec = self.options.axis.x.tickPrecision
while 0.0 <= pos <= 1.0:
pretty_label = round(label, prec)
if prec == 0:
pretty_label = int(pretty_label)
self.xticks.append((pos, pretty_label))
label += interval
pos = self.xscale * (label - self.minxval)
elif self.options.axis.x.tickCount > 0:
uniqx = range(len(uniqueIndices(stores)) + 1)
roughSeparation = self.xrange / self.options.axis.x.tickCount
i = j = 0
while i < len(uniqx) and j < self.options.axis.x.tickCount:
if (uniqx[i] - self.minxval) >= (j * roughSeparation):
pos = self.xscale * (uniqx[i] - self.minxval)
if 0.0 <= pos <= 1.0:
self.xticks.append((pos, uniqx[i]))
j += 1
i += 1
# evaluate yTicks
self.yticks = []
if self.options.axis.y.ticks:
for tick in self.options.axis.y.ticks:
if not isinstance(tick, Option):
tick = Option(tick)
if tick.label is None:
label = str(tick.v)
else:
label = tick.label
pos = 1.0 - (self.yscale * (tick.v - self.minyval))
if 0.0 <= pos <= 1.0:
self.yticks.append((pos, label))
elif self.options.axis.y.interval > 0:
interval = self.options.axis.y.interval
label = (divmod(self.minyval, interval)[0] + 1) * interval
pos = 1.0 - (self.yscale * (label - self.minyval))
prec = self.options.axis.y.tickPrecision
while 0.0 <= pos <= 1.0:
pretty_label = round(label, prec)
if prec == 0:
pretty_label = int(pretty_label)
self.yticks.append((pos, pretty_label))
label += interval
pos = 1.0 - (self.yscale * (label - self.minyval))
elif self.options.axis.y.tickCount > 0:
prec = self.options.axis.y.tickPrecision
num = self.yrange / self.options.axis.y.tickCount
if (num < 1 and prec == 0):
roughSeparation = 1
else:
roughSeparation = round(num, prec)
for i in range(self.options.axis.y.tickCount + 1):
yval = self.minyval + (i * roughSeparation)
pos = 1.0 - ((yval - self.minyval) * self.yscale)
if 0.0 <= pos <= 1.0:
pretty_label = round(yval, prec)
if prec == 0:
pretty_label = int(pretty_label)
self.yticks.append((pos, pretty_label))
def _renderBackground(self, cx):
"""Renders the background area of the chart"""
if self.options.background.hide:
return
cx.save()
if self.options.background.baseColor:
cx.set_source_rgb(*hex2rgb(self.options.background.baseColor))
cx.paint()
if self.options.background.chartColor:
cx.set_source_rgb(*hex2rgb(self.options.background.chartColor))
surface_width, surface_height = self.getSurfaceSize()
cx.rectangle(self.options.padding.left, self.options.padding.top,
surface_width - (self.options.padding.left
+ self.options.padding.right),
surface_height - (self.options.padding.top
+ self.options.padding.bottom))
cx.fill()
if self.options.background.lineColor:
cx.set_source_rgb(*hex2rgb(self.options.background.lineColor))
cx.set_line_width(self.options.axis.lineWidth)
self._renderLines(cx)
cx.restore()
def _renderLines(self, cx):
"""Aux function for _renderBackground"""
if self.options.axis.y.showLines and self.yticks:
for tick in self.yticks:
self._renderLine(cx, tick, False)
if self.options.axis.x.showLines and self.xticks:
for tick in self.xticks:
self._renderLine(cx, tick, True)
def _renderLine(self, cx, tick, horiz):
"""Aux function for _renderLines"""
x1, x2, y1, y2 = (0, 0, 0, 0)
if horiz:
x1 = x2 = tick[0] * self.layout.chart.w + self.layout.chart.x
y1 = self.layout.chart.y
y2 = y1 + self.layout.chart.h
else:
x1 = self.layout.chart.x
x2 = x1 + self.layout.chart.w
y1 = y2 = tick[0] * self.layout.chart.h + self.layout.chart.y
cx.new_path()
cx.move_to(x1, y1)
cx.line_to(x2, y2)
cx.close_path()
cx.stroke()
def _renderChart(self, cx):
raise NotImplementedError
def _renderTick(self, cx, tick, x, y, x2, y2, rotate, text_position):
"""Aux method for _renderXTick and _renderYTick"""
if callable(tick):
return
cx.new_path()
cx.move_to(x, y)
cx.line_to(x2, y2)
cx.close_path()
cx.stroke()
cx.select_font_face(self.options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.axis.tickFontSize)
label = safe_unicode(tick[1], self.options.encoding)
xb, yb, width, height, xa, ya = cx.text_extents(label)
x, y = text_position
if rotate:
cx.save()
cx.translate(x, y)
cx.rotate(math.radians(rotate))
x = -width / 2.0
y = -height / 2.0
cx.move_to(x - xb, y - yb)
cx.show_text(label)
if self.debug:
cx.rectangle(x, y, width, height)
cx.stroke()
cx.restore()
else:
x -= width / 2.0
y -= height / 2.0
cx.move_to(x - xb, y - yb)
cx.show_text(label)
if self.debug:
cx.rectangle(x, y, width, height)
cx.stroke()
return label
def _renderYTick(self, cx, tick):
"""Aux method for _renderAxis"""
x = self.layout.y_ticks.x + self.layout.y_ticks.w
y = self.layout.y_ticks.y + tick[0] * self.layout.y_ticks.h
text_position = ((self.layout.y_tick_labels.x
+ self.layout.y_tick_labels.w / 2.0), y)
return self._renderTick(cx, tick,
x, y,
x - self.options.axis.tickSize, y,
self.options.axis.y.rotate,
text_position)
def _renderXTick(self, cx, tick):
"""Aux method for _renderAxis"""
x = self.layout.x_ticks.x + tick[0] * self.layout.x_ticks.w
y = self.layout.x_ticks.y
text_position = (x, (self.layout.x_tick_labels.y
+ self.layout.x_tick_labels.h / 2.0))
return self._renderTick(cx, tick,
x, y,
x, y + self.options.axis.tickSize,
self.options.axis.x.rotate,
text_position)
def _renderAxisLabel(self, cx, label, x, y, vertical=False):
cx.save()
cx.select_font_face(self.options.axis.labelFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cx.set_font_size(self.options.axis.labelFontSize)
cx.set_source_rgb(*hex2rgb(self.options.axis.labelColor))
xb, yb, width, height, xa, ya = cx.text_extents(label)
if vertical:
y = y + width / 2.0
cx.move_to(x - xb, y - yb)
cx.translate(x, y)
cx.rotate(-math.radians(90))
cx.move_to(-xb, -yb)
cx.show_text(label)
if self.debug:
cx.rectangle(0, 0, width, height)
cx.stroke()
else:
x = x - width / 2.0
cx.move_to(x - xb, y - yb)
cx.show_text(label)
if self.debug:
cx.rectangle(x, y, width, height)
cx.stroke()
cx.restore()
def _renderYAxisLabel(self, cx, label_text):
label = safe_unicode(label_text, self.options.encoding)
x = self.layout.y_label.x
y = self.layout.y_label.y + self.layout.y_label.h / 2.0
self._renderAxisLabel(cx, label, x, y, True)
def _renderYAxis(self, cx):
"""Draws the vertical line represeting the Y axis"""
cx.new_path()
cx.move_to(self.layout.chart.x, self.layout.chart.y)
cx.line_to(self.layout.chart.x,
self.layout.chart.y + self.layout.chart.h)
cx.close_path()
cx.stroke()
def _renderXAxisLabel(self, cx, label_text):
label = safe_unicode(label_text, self.options.encoding)
x = self.layout.x_label.x + self.layout.x_label.w / 2.0
y = self.layout.x_label.y
self._renderAxisLabel(cx, label, x, y, False)
def _renderXAxis(self, cx):
"""Draws the horizontal line representing the X axis"""
cx.new_path()
y = self.layout.chart.y + (1.0 - self.origin) * self.layout.chart.h
cx.move_to(self.layout.chart.x, y)
cx.line_to(self.layout.chart.x + self.layout.chart.w, y)
cx.close_path()
cx.stroke()
def _renderAxis(self, cx):
"""Renders axis"""
if self.options.axis.x.hide and self.options.axis.y.hide:
return
cx.save()
cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
cx.set_line_width(self.options.axis.lineWidth)
if not self.options.axis.y.hide:
if self.yticks:
for tick in self.yticks:
self._renderYTick(cx, tick)
if self.options.axis.y.label:
self._renderYAxisLabel(cx, self.options.axis.y.label)
self._renderYAxis(cx)
if not self.options.axis.x.hide:
if self.xticks:
for tick in self.xticks:
self._renderXTick(cx, tick)
if self.options.axis.x.label:
self._renderXAxisLabel(cx, self.options.axis.x.label)
self._renderXAxis(cx)
cx.restore()
def _renderTitle(self, cx):
if self.options.title:
cx.save()
cx.select_font_face(self.options.titleFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_BOLD)
cx.set_font_size(self.options.titleFontSize)
cx.set_source_rgb(*hex2rgb(self.options.titleColor))
title = safe_unicode(self.options.title, self.options.encoding)
extents = cx.text_extents(title)
title_width = extents[2]
x = (self.layout.title.x
+ self.layout.title.w / 2.0
- title_width / 2.0)
y = self.layout.title.y - extents[1]
cx.move_to(x, y)
cx.show_text(title)
cx.restore()
def _renderLegend(self, cx):
"""This function adds a legend to the chart"""
if self.options.legend.hide:
return
surface_width, surface_height = self.getSurfaceSize()
# Compute legend dimensions
padding = 4
bullet = 15
width = 0
height = padding
keys = self._getDatasetsKeys()
cx.select_font_face(self.options.legend.legendFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.legend.legendFontSize)
for key in keys:
key = safe_unicode(key, self.options.encoding)
extents = cx.text_extents(key)
width = max(extents[2], width)
height += max(extents[3], bullet) + padding
width = padding + bullet + padding + width + padding
# Compute legend position
legend = self.options.legend
if legend.position.right is not None:
legend.position.left = (surface_width
- legend.position.right
- width)
if legend.position.bottom is not None:
legend.position.top = (surface_height
- legend.position.bottom
- height)
# Draw the legend
cx.save()
cx.rectangle(self.options.legend.position.left,
self.options.legend.position.top,
width, height)
cx.set_source_rgba(1, 1, 1, self.options.legend.opacity)
cx.fill_preserve()
cx.set_line_width(self.options.legend.borderWidth)
cx.set_source_rgb(*hex2rgb(self.options.legend.borderColor))
cx.stroke()
def drawKey(key, x, y, text_height):
cx.rectangle(x, y, bullet, bullet)
cx.set_source_rgb(*self.colorScheme[key])
cx.fill_preserve()
cx.set_source_rgb(0, 0, 0)
cx.stroke()
cx.move_to(x + bullet + padding,
y + bullet / 2.0 + text_height / 2.0)
cx.show_text(key)
cx.set_line_width(1)
x = self.options.legend.position.left + padding
y = self.options.legend.position.top + padding
for key in keys:
extents = cx.text_extents(key)
drawKey(key, x, y, extents[3])
y += max(extents[3], bullet) + padding
cx.restore()
def uniqueIndices(arr):
"""Return a list with the indexes of the biggest element of arr"""
return range(max([len(a) for a in arr]))
class Area(object):
"""Simple rectangle to hold an area coordinates and dimensions"""
def __init__(self, x=0.0, y=0.0, w=0.0, h=0.0):
self.x, self.y, self.w, self.h = x, y, w, h
def __str__(self):
msg = ""
return msg % (self.x, self.y, self.w, self.h)
def get_text_extents(cx, text, font, font_size, encoding):
if text:
cx.save()
cx.select_font_face(font,
cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD)
cx.set_font_size(font_size)
safe_text = safe_unicode(text, encoding)
extents = cx.text_extents(safe_text)
cx.restore()
return extents[2:4]
return (0.0, 0.0)
class Layout(object):
"""Set of chart areas"""
def __init__(self):
self.title = Area()
self.x_label = Area()
self.y_label = Area()
self.x_tick_labels = Area()
self.y_tick_labels = Area()
self.x_ticks = Area()
self.y_ticks = Area()
self.chart = Area()
self._areas = (
(self.title, (1, 126/255.0, 0)), # orange
(self.y_label, (41/255.0, 91/255.0, 41/255.0)), # grey
(self.x_label, (41/255.0, 91/255.0, 41/255.0)), # grey
(self.y_tick_labels, (0, 115/255.0, 0)), # green
(self.x_tick_labels, (0, 115/255.0, 0)), # green
(self.y_ticks, (229/255.0, 241/255.0, 18/255.0)), # yellow
(self.x_ticks, (229/255.0, 241/255.0, 18/255.0)), # yellow
(self.chart, (75/255.0, 75/255.0, 1.0)), # blue
)
def update(self, cx, options, width, height, xticks, yticks):
self.title.x = options.padding.left
self.title.y = options.padding.top
self.title.w = width - (options.padding.left + options.padding.right)
self.title.h = get_text_extents(cx,
options.title,
options.titleFont,
options.titleFontSize,
options.encoding)[1]
x_axis_label_height = get_text_extents(cx,
options.axis.x.label,
options.axis.labelFont,
options.axis.labelFontSize,
options.encoding)[1]
y_axis_label_width = get_text_extents(cx,
options.axis.y.label,
options.axis.labelFont,
options.axis.labelFontSize,
options.encoding)[1]
x_axis_tick_labels_height = self._getAxisTickLabelsSize(cx, options,
options.axis.x,
xticks)[1]
y_axis_tick_labels_width = self._getAxisTickLabelsSize(cx, options,
options.axis.y,
yticks)[0]
self.y_label.x = options.padding.left
self.y_label.y = options.padding.top + self.title.h
self.y_label.w = y_axis_label_width
self.y_label.h = height - (options.padding.bottom
+ options.padding.top
+ x_axis_label_height
+ x_axis_tick_labels_height
+ options.axis.tickSize
+ self.title.h)
self.x_label.x = (options.padding.left
+ y_axis_label_width
+ y_axis_tick_labels_width
+ options.axis.tickSize)
self.x_label.y = height - (options.padding.bottom
+ x_axis_label_height)
self.x_label.w = width - (options.padding.left
+ options.padding.right
+ options.axis.tickSize
+ y_axis_label_width
+ y_axis_tick_labels_width)
self.x_label.h = x_axis_label_height
self.y_tick_labels.x = self.y_label.x + self.y_label.w
self.y_tick_labels.y = self.y_label.y
self.y_tick_labels.w = y_axis_tick_labels_width
self.y_tick_labels.h = self.y_label.h
self.x_tick_labels.x = self.x_label.x
self.x_tick_labels.y = self.x_label.y - x_axis_tick_labels_height
self.x_tick_labels.w = self.x_label.w
self.x_tick_labels.h = x_axis_tick_labels_height
self.y_ticks.x = self.y_tick_labels.x + self.y_tick_labels.w
self.y_ticks.y = self.y_tick_labels.y
self.y_ticks.w = options.axis.tickSize
self.y_ticks.h = self.y_label.h
self.x_ticks.x = self.x_tick_labels.x
self.x_ticks.y = self.x_tick_labels.y - options.axis.tickSize
self.x_ticks.w = self.x_label.w
self.x_ticks.h = options.axis.tickSize
self.chart.x = self.y_ticks.x + self.y_ticks.w
self.chart.y = self.title.y + self.title.h
self.chart.w = self.x_ticks.w
self.chart.h = self.y_ticks.h
def render(self, cx):
def draw_area(area, r, g, b):
cx.rectangle(area.x, area.y, area.w, area.h)
cx.set_source_rgba(r, g, b, 0.5)
cx.fill()
cx.save()
for area, color in self._areas:
draw_area(area, *color)
cx.restore()
def _getAxisTickLabelsSize(self, cx, options, axis, ticks):
cx.save()
cx.select_font_face(options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(options.axis.tickFontSize)
max_width = max_height = 0.0
if not axis.hide:
extents = [cx.text_extents(safe_unicode(
tick[1], options.encoding,
))[2:4] # get width and height as a tuple
for tick in ticks]
if extents:
widths, heights = zip(*extents)
max_width, max_height = max(widths), max(heights)
if axis.rotate:
radians = math.radians(axis.rotate)
sin = math.sin(radians)
cos = math.cos(radians)
max_width, max_height = (
max_width * cos + max_height * sin,
max_width * sin + max_height * cos,
)
cx.restore()
return max_width, max_height
class Option(dict):
"""Useful dict that allow attribute-like access to its keys"""
def __getattr__(self, name):
if name in self.keys():
return self[name]
else:
raise AttributeError(name)
def merge(self, other):
"""Recursive merge with other Option or dict object"""
for key, value in other.items():
if key in self:
if isinstance(self[key], Option):
self[key].merge(other[key])
else:
self[key] = other[key]
DEFAULT_OPTIONS = Option(
axis=Option(
lineWidth=1.0,
lineColor='#0f0000',
tickSize=3.0,
labelColor='#666666',
labelFont='Tahoma',
labelFontSize=9,
tickFont='Tahoma',
tickFontSize=9,
x=Option(
hide=False,
ticks=None,
tickCount=10,
tickPrecision=1,
range=None,
rotate=None,
label=None,
interval=0,
showLines=False,
),
y=Option(
hide=False,
ticks=None,
tickCount=10,
tickPrecision=1,
range=None,
rotate=None,
label=None,
interval=0,
showLines=True,
),
),
background=Option(
hide=False,
baseColor=None,
chartColor='#f5f5f5',
lineColor='#ffffff',
lineWidth=1.5,
),
legend=Option(
opacity=0.8,
borderColor='#000000',
borderWidth=2,
hide=False,
legendFont='Tahoma',
legendFontSize=9,
position=Option(top=20, left=40, bottom=None, right=None),
),
padding=Option(
left=10,
right=10,
top=10,
bottom=10,
),
stroke=Option(
color='#ffffff',
hide=False,
shadow=True,
width=2
),
yvals=Option(
show=False,
inside=False,
fontSize=11,
fontColor='#000000',
skipSmallValues=True,
snapToOrigin=False,
renderer=None
),
fillOpacity=1.0,
shouldFill=True,
barWidthFillFraction=0.75,
pieRadius=0.4,
colorScheme=Option(
name='gradient',
args=Option(
initialColor=DEFAULT_COLOR,
colors=None,
),
),
title=None,
titleColor='#000000',
titleFont='Tahoma',
titleFontSize=12,
encoding='utf-8',
)
pycha-0.7.0/pycha/polygonal.py 0000664 0001750 0001750 00000030126 12130334357 015705 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2011 by Roberto Garcia Carvajal
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import math
import cairo
from pycha.chart import Chart
from pycha.line import Point
from pycha.color import hex2rgb
from pycha.utils import safe_unicode
class PolygonalChart(Chart):
def __init__(self, surface=None, options={}):
super(PolygonalChart, self).__init__(surface, options)
self.points = []
def _updateChart(self):
"""Evaluates measures for polygonal charts"""
self.points = []
for i, (name, store) in enumerate(self.datasets):
for item in store:
xval, yval = item
x = (xval - self.minxval) * self.xscale
y = 1.0 - (yval - self.minyval) * self.yscale
point = Point(x, y, xval, yval, name)
if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0:
self.points.append(point)
def _renderBackground(self, cx):
"""Renders the background area of the chart"""
if self.options.background.hide:
return
cx.save()
if self.options.background.baseColor:
cx.set_source_rgb(*hex2rgb(self.options.background.baseColor))
cx.paint()
if self.options.background.chartColor:
cx.set_source_rgb(*hex2rgb(self.options.background.chartColor))
cx.set_line_width(10.0)
cx.new_path()
init = None
count = len(self.xticks)
for index, tick in enumerate(self.xticks):
ang = math.pi / 2 - index * 2 * math.pi / count
x = (self.layout.chart.x + self.layout.chart.w / 2
- math.cos(ang)
* min(self.layout.chart.w / 2, self.layout.chart.h / 2))
y = (self.layout.chart.y + self.layout.chart.h / 2
- math.sin(ang)
* min(self.layout.chart.w / 2, self.layout.chart.h / 2))
if init is None:
cx.move_to(x, y)
init = (x, y)
else:
cx.line_to(x, y)
cx.line_to(init[0], init[1])
cx.close_path()
cx.fill()
if self.options.background.lineColor:
cx.set_source_rgb(*hex2rgb(self.options.background.lineColor))
cx.set_line_width(self.options.axis.lineWidth)
self._renderLines(cx)
cx.restore()
def _renderLine(self, cx, tick, horiz):
"""Aux function for _renderLines"""
rad = (self.layout.chart.h / 2) * (1 - tick[0])
cx.new_path()
init = None
count = len(self.xticks)
for index, tick in enumerate(self.xticks):
ang = math.pi / 2 - index * 2 * math.pi / count
x = (self.layout.chart.x + self.layout.chart.w / 2
- math.cos(ang) * rad)
y = (self.layout.chart.y + self.layout.chart.h / 2
- math.sin(ang) * rad)
if init is None:
cx.move_to(x, y)
init = (x, y)
else:
cx.line_to(x, y)
cx.line_to(init[0], init[1])
cx.close_path()
cx.stroke()
def _renderXAxis(self, cx):
"""Draws the horizontal line representing the X axis"""
count = len(self.xticks)
centerx = self.layout.chart.x + self.layout.chart.w / 2
centery = self.layout.chart.y + self.layout.chart.h / 2
for i in range(0, count):
offset1 = i * 2 * math.pi / count
offset = math.pi / 2 - offset1
rad = self.layout.chart.h / 2
(r1, r2) = (0, rad + 5)
x1 = centerx - math.cos(offset) * r1
x2 = centerx - math.cos(offset) * r2
y1 = centery - math.sin(offset) * r1
y2 = centery - math.sin(offset) * r2
cx.new_path()
cx.move_to(x1, y1)
cx.line_to(x2, y2)
cx.close_path()
cx.stroke()
def _renderYTick(self, cx, tick, center):
"""Aux method for _renderAxis"""
i = tick
tick = self.yticks[i]
count = len(self.yticks)
if callable(tick):
return
x = center[0]
y = center[1] - i * (self.layout.chart.h / 2) / count
cx.new_path()
cx.move_to(x, y)
cx.line_to(x - self.options.axis.tickSize, y)
cx.close_path()
cx.stroke()
cx.select_font_face(self.options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.axis.tickFontSize)
label = safe_unicode(tick[1], self.options.encoding)
extents = cx.text_extents(label)
labelWidth = extents[2]
labelHeight = extents[3]
if self.options.axis.y.rotate:
radians = math.radians(self.options.axis.y.rotate)
cx.move_to(x - self.options.axis.tickSize
- (labelWidth * math.cos(radians))
- 4,
y + (labelWidth * math.sin(radians))
+ labelHeight / (2.0 / math.cos(radians)))
cx.rotate(-radians)
cx.show_text(label)
cx.rotate(radians) # this is probably faster than a save/restore
else:
cx.move_to(x - self.options.axis.tickSize - labelWidth - 4,
y + labelHeight / 2.0)
cx.rel_move_to(0.0, -labelHeight / 2.0)
cx.show_text(label)
return label
def _renderYAxis(self, cx):
"""Draws the vertical line for the Y axis"""
centerx = self.layout.chart.x + self.layout.chart.w / 2
centery = self.layout.chart.y + self.layout.chart.h / 2
offset = math.pi / 2
r1 = self.layout.chart.h / 2
x1 = centerx - math.cos(offset) * r1
y1 = centery - math.sin(offset) * r1
cx.new_path()
cx.move_to(centerx, centery)
cx.line_to(x1, y1)
cx.close_path()
cx.stroke()
def _renderAxis(self, cx):
"""Renders axis"""
if self.options.axis.x.hide and self.options.axis.y.hide:
return
cx.save()
cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
cx.set_line_width(self.options.axis.lineWidth)
centerx = self.layout.chart.x + self.layout.chart.w / 2
centery = self.layout.chart.y + self.layout.chart.h / 2
if not self.options.axis.y.hide:
if self.yticks:
count = len(self.yticks)
for i in range(0, count):
self._renderYTick(cx, i, (centerx, centery))
if self.options.axis.y.label:
self._renderYAxisLabel(cx, self.options.axis.y.label)
self._renderYAxis(cx)
if not self.options.axis.x.hide:
fontAscent = cx.font_extents()[0]
if self.xticks:
count = len(self.xticks)
for i in range(0, count):
self._renderXTick(cx, i, fontAscent, (centerx, centery))
if self.options.axis.x.label:
self._renderXAxisLabel(cx, self.options.axis.x.label)
self._renderXAxis(cx)
cx.restore()
def _renderXTick(self, cx, i, fontAscent, center):
tick = self.xticks[i]
if callable(tick):
return
count = len(self.xticks)
cx.select_font_face(self.options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.axis.tickFontSize)
label = safe_unicode(tick[1], self.options.encoding)
extents = cx.text_extents(label)
labelWidth = extents[2]
labelHeight = extents[3]
x, y = center
cx.move_to(x, y)
if self.options.axis.x.rotate:
radians = math.radians(self.options.axis.x.rotate)
cx.move_to(x - (labelHeight * math.cos(radians)),
y + self.options.axis.tickSize
+ (labelHeight * math.cos(radians))
+ 4.0)
cx.rotate(radians)
cx.show_text(label)
cx.rotate(-radians)
else:
offset1 = i * 2 * math.pi / count
offset = math.pi / 2 - offset1
rad = self.layout.chart.h / 2 + 10
x = center[0] - math.cos(offset) * rad
y = center[1] - math.sin(offset) * rad
cx.move_to(x, y)
cx.rotate(offset - math.pi / 2)
if math.sin(offset) < 0.0:
cx.rotate(math.pi)
cx.rel_move_to(0.0, 5.0)
cx.rel_move_to(-labelWidth / 2.0, 0)
cx.show_text(label)
if math.sin(offset) < 0.0:
cx.rotate(-math.pi)
cx.rotate(-(offset - math.pi / 2))
return label
def _renderChart(self, cx):
"""Renders a polygonal chart"""
# draw the polygon.
def preparePath(storeName):
cx.new_path()
firstPoint = True
count = len(self.points) / len(self.datasets)
centerx = self.layout.chart.x + self.layout.chart.w / 2
centery = self.layout.chart.y + self.layout.chart.h / 2
firstPointCoord = None
for index, point in enumerate(self.points):
if point.name == storeName:
offset1 = index * 2 * math.pi / count
offset = math.pi / 2 - offset1
rad = (self.layout.chart.h / 2) * (1 - point.y)
x = centerx - math.cos(offset) * rad
y = centery - math.sin(offset) * rad
if firstPointCoord is None:
firstPointCoord = (x, y)
if not self.options.shouldFill and firstPoint:
# starts the first point of the line
cx.move_to(x, y)
firstPoint = False
continue
cx.line_to(x, y)
if not firstPointCoord is None:
cx.line_to(firstPointCoord[0], firstPointCoord[1])
if self.options.shouldFill:
# Close the path to the start point
y = ((1.0 - self.origin)
* self.layout.chart.h + self.layout.chart.y)
else:
cx.set_source_rgb(*self.colorScheme[storeName])
cx.stroke()
cx.save()
cx.set_line_width(self.options.stroke.width)
if self.options.shouldFill:
def drawLine(storeName):
if self.options.stroke.shadow:
# draw shadow
cx.save()
cx.set_source_rgba(0, 0, 0, 0.15)
cx.translate(2, -2)
preparePath(storeName)
cx.fill()
cx.restore()
# fill the line
cx.set_source_rgb(*self.colorScheme[storeName])
preparePath(storeName)
cx.fill()
if not self.options.stroke.hide:
# draw stroke
cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
preparePath(storeName)
cx.stroke()
# draw the lines
for key in self._getDatasetsKeys():
drawLine(key)
else:
for key in self._getDatasetsKeys():
preparePath(key)
cx.restore()
pycha-0.7.0/pycha/pie.py 0000664 0001750 0001750 00000030273 12130334357 014461 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import math
import cairo
from pycha.chart import Chart, Option, Layout, Area, get_text_extents
from pycha.color import hex2rgb
class PieChart(Chart):
def __init__(self, surface=None, options={}, debug=False):
super(PieChart, self).__init__(surface, options, debug)
self.slices = []
self.centerx = 0
self.centery = 0
self.layout = PieLayout(self.slices)
def _updateChart(self):
"""Evaluates measures for pie charts"""
slices = [dict(name=key,
value=(i, value[0][1]))
for i, (key, value) in enumerate(self.datasets)]
s = float(sum([slice['value'][1] for slice in slices]))
fraction = angle = 0.0
del self.slices[:]
for slice in slices:
if slice['value'][1] > 0:
angle += fraction
fraction = slice['value'][1] / s
self.slices.append(Slice(slice['name'], fraction,
slice['value'][0], slice['value'][1],
angle))
def _updateTicks(self):
"""Evaluates pie ticks"""
self.xticks = []
if self.options.axis.x.ticks:
lookup = dict([(slice.xval, slice) for slice in self.slices])
for tick in self.options.axis.x.ticks:
if not isinstance(tick, Option):
tick = Option(tick)
slice = lookup.get(tick.v, None)
label = tick.label or str(tick.v)
if slice is not None:
label += ' (%.1f%%)' % (slice.fraction * 100)
self.xticks.append((tick.v, label))
else:
for slice in self.slices:
label = '%s (%.1f%%)' % (slice.name, slice.fraction * 100)
self.xticks.append((slice.xval, label))
def _renderLines(self, cx):
"""Aux function for _renderBackground"""
# there are no lines in a Pie Chart
def _renderChart(self, cx):
"""Renders a pie chart"""
self.centerx = self.layout.chart.x + self.layout.chart.w * 0.5
self.centery = self.layout.chart.y + self.layout.chart.h * 0.5
cx.set_line_join(cairo.LINE_JOIN_ROUND)
if self.options.stroke.shadow and False:
cx.save()
cx.set_source_rgba(0, 0, 0, 0.15)
cx.new_path()
cx.move_to(self.centerx, self.centery)
cx.arc(self.centerx + 1, self.centery + 2,
self.layout.radius + 1, 0, math.pi * 2)
cx.line_to(self.centerx, self.centery)
cx.close_path()
cx.fill()
cx.restore()
cx.save()
for slice in self.slices:
if slice.isBigEnough():
cx.set_source_rgb(*self.colorScheme[slice.name])
if self.options.shouldFill:
slice.draw(cx, self.centerx, self.centery,
self.layout.radius)
cx.fill()
if not self.options.stroke.hide:
slice.draw(cx, self.centerx, self.centery,
self.layout.radius)
cx.set_line_width(self.options.stroke.width)
cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
cx.stroke()
cx.restore()
if self.debug:
cx.set_source_rgba(1, 0, 0, 0.5)
px = max(cx.device_to_user_distance(1, 1))
for x, y in self.layout._lines:
cx.arc(x, y, 5 * px, 0, 2 * math.pi)
cx.fill()
cx.new_path()
cx.move_to(self.centerx, self.centery)
cx.line_to(x, y)
cx.stroke()
def _renderAxis(self, cx):
"""Renders the axis for pie charts"""
if self.options.axis.x.hide or not self.xticks:
return
self.xlabels = []
if self.debug:
px = max(cx.device_to_user_distance(1, 1))
cx.set_source_rgba(0, 0, 1, 0.5)
for x, y, w, h in self.layout.ticks:
cx.rectangle(x, y, w, h)
cx.stroke()
cx.arc(x + w / 2.0, y + h / 2.0, 5 * px, 0, 2 * math.pi)
cx.fill()
cx.arc(x, y, 2 * px, 0, 2 * math.pi)
cx.fill()
cx.select_font_face(self.options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.axis.tickFontSize)
cx.set_source_rgb(*hex2rgb(self.options.axis.labelColor))
for i, tick in enumerate(self.xticks):
label = tick[1]
x, y, w, h = self.layout.ticks[i]
xb, yb, width, height, xa, ya = cx.text_extents(label)
# draw label with text tick[1]
cx.move_to(x - xb, y - yb)
cx.show_text(label)
self.xlabels.append(label)
class Slice(object):
def __init__(self, name, fraction, xval, yval, angle):
self.name = name
self.fraction = fraction
self.xval = xval
self.yval = yval
self.startAngle = 2 * angle * math.pi
self.endAngle = 2 * (angle + fraction) * math.pi
def __str__(self):
return ("" %
(self.startAngle, self.endAngle, self.fraction))
def isBigEnough(self):
return abs(self.startAngle - self.endAngle) > 0.001
def draw(self, cx, centerx, centery, radius):
cx.new_path()
cx.move_to(centerx, centery)
cx.arc(centerx, centery, radius, -self.endAngle, -self.startAngle)
cx.close_path()
def getNormalisedAngle(self):
normalisedAngle = (self.startAngle + self.endAngle) / 2
if normalisedAngle > math.pi * 2:
normalisedAngle -= math.pi * 2
elif normalisedAngle < 0:
normalisedAngle += math.pi * 2
return normalisedAngle
class PieLayout(Layout):
"""Set of chart areas for pie charts"""
def __init__(self, slices):
self.slices = slices
self.title = Area()
self.chart = Area()
self.ticks = []
self.radius = 0
self._areas = (
(self.title, (1, 126 / 255.0, 0)), # orange
(self.chart, (75 / 255.0, 75 / 255.0, 1.0)), # blue
)
self._lines = []
def update(self, cx, options, width, height, xticks, yticks):
self.title.x = options.padding.left
self.title.y = options.padding.top
self.title.w = width - (options.padding.left + options.padding.right)
self.title.h = get_text_extents(cx,
options.title,
options.titleFont,
options.titleFontSize,
options.encoding)[1]
lookup = dict([(slice.xval, slice) for slice in self.slices])
self.chart.x = self.title.x
self.chart.y = self.title.y + self.title.h
self.chart.w = self.title.w
self.chart.h = height - self.title.h - (options.padding.top
+ options.padding.bottom)
centerx = self.chart.x + self.chart.w * 0.5
centery = self.chart.y + self.chart.h * 0.5
self.radius = min(self.chart.w / 2.0, self.chart.h / 2.0)
for tick in xticks:
slice = lookup.get(tick[0], None)
width, height = get_text_extents(cx, tick[1],
options.axis.tickFont,
options.axis.tickFontSize,
options.encoding)
angle = slice.getNormalisedAngle()
radius = self._get_min_radius(angle, centerx, centery,
width, height)
self.radius = min(self.radius, radius)
# Now that we now the radius we move the ticks as close as we can
# to the circle
for i, tick in enumerate(xticks):
slice = lookup.get(tick[0], None)
angle = slice.getNormalisedAngle()
self.ticks[i] = self._get_tick_position(self.radius, angle,
self.ticks[i],
centerx, centery)
def _get_min_radius(self, angle, centerx, centery, width, height):
min_radius = None
# precompute some common values
tan = math.tan(angle)
half_width = width / 2.0
half_height = height / 2.0
offset_x = half_width * tan
offset_y = half_height / tan
def intersect_horizontal_line(y):
return centerx + (centery - y) / tan
def intersect_vertical_line(x):
return centery - tan * (x - centerx)
# computes the intersection between the rect that has
# that angle with the X axis and the bounding chart box
if 0.25 * math.pi <= angle < 0.75 * math.pi:
# intersects with the top rect
y = self.chart.y
x = intersect_horizontal_line(y)
self._lines.append((x, y))
x1 = x - half_width - offset_y
self.ticks.append((x1, self.chart.y, width, height))
min_radius = abs((y + height) - centery)
elif 0.75 * math.pi <= angle < 1.25 * math.pi:
# intersects with the left rect
x = self.chart.x
y = intersect_vertical_line(x)
self._lines.append((x, y))
y1 = y - half_height - offset_x
self.ticks.append((x, y1, width, height))
min_radius = abs(centerx - (x + width))
elif 1.25 * math.pi <= angle < 1.75 * math.pi:
# intersects with the bottom rect
y = self.chart.y + self.chart.h
x = intersect_horizontal_line(y)
self._lines.append((x, y))
x1 = x - half_width + offset_y
self.ticks.append((x1, y - height, width, height))
min_radius = abs((y - height) - centery)
else:
# intersects with the right rect
x = self.chart.x + self.chart.w
y = intersect_vertical_line(x)
self._lines.append((x, y))
y1 = y - half_height + offset_x
self.ticks.append((x - width, y1, width, height))
min_radius = abs((x - width) - centerx)
return min_radius
def _get_tick_position(self, radius, angle, tick, centerx, centery):
text_width, text_height = tick[2:4]
half_width = text_width / 2.0
half_height = text_height / 2.0
if 0 <= angle < 0.5 * math.pi:
# first quadrant
k1 = j1 = k2 = 1
j2 = -1
elif 0.5 * math.pi <= angle < math.pi:
# second quadrant
k1 = k2 = -1
j1 = j2 = 1
elif math.pi <= angle < 1.5 * math.pi:
# third quadrant
k1 = j1 = k2 = -1
j2 = 1
elif 1.5 * math.pi <= angle < 2 * math.pi:
# fourth quadrant
k1 = k2 = 1
j1 = j2 = -1
cx = radius * math.cos(angle) + k1 * half_width
cy = radius * math.sin(angle) + j1 * half_height
radius2 = math.sqrt(cx * cx + cy * cy)
tan = math.tan(angle)
x = math.sqrt((radius2 * radius2) / (1 + tan * tan))
y = tan * x
x = centerx + k2 * x
y = centery + j2 * y
return x - half_width, y - half_height, text_width, text_height
pycha-0.7.0/pycha/radial.py 0000664 0001750 0001750 00000026142 12130334357 015140 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2011 by Roberto Garcia Carvajal
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import math
import cairo
from pycha.chart import Chart
from pycha.line import Point
from pycha.color import hex2rgb
from pycha.utils import safe_unicode
class RadialChart(Chart):
def __init__(self, surface=None, options={}):
super(RadialChart, self).__init__(surface, options)
self.points = []
def _updateChart(self):
"""Evaluates measures for radial charts"""
self.points = []
for i, (name, store) in enumerate(self.datasets):
for item in store:
xval, yval = item
x = (xval - self.minxval) * self.xscale
y = 1.0 - (yval - self.minyval) * self.yscale
point = Point(x, y, xval, yval, name)
if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0:
self.points.append(point)
def _renderBackground(self, cx):
"""Renders the background area of the chart"""
if self.options.background.hide:
return
cx.save()
if self.options.background.baseColor:
cx.set_source_rgb(*hex2rgb(self.options.background.baseColor))
cx.paint()
if self.options.background.chartColor:
cx.set_source_rgb(*hex2rgb(self.options.background.chartColor))
cx.set_line_width(10.0)
cx.arc(self.layout.chart.x + self.layout.chart.w / 2,
self.layout.chart.y + self.layout.chart.h / 2,
min(self.layout.chart.w / 2, self.layout.chart.h / 2),
0, 2 * math.pi)
cx.fill()
if self.options.background.lineColor:
cx.set_source_rgb(*hex2rgb(self.options.background.lineColor))
cx.set_line_width(self.options.axis.lineWidth)
self._renderLines(cx)
cx.restore()
def _renderLine(self, cx, tick, horiz):
"""Aux function for _renderLines"""
rad = (self.layout.chart.h / 2) * (1 - tick[0])
cx.arc(self.layout.chart.x + self.layout.chart.w / 2,
self.layout.chart.y + self.layout.chart.h / 2,
rad, 0, 2 * math.pi)
cx.stroke()
def _renderXAxis(self, cx):
"""Draws the horizontal line representing the X axis"""
count = len(self.xticks)
centerx = self.layout.chart.x + self.layout.chart.w / 2
centery = self.layout.chart.y + self.layout.chart.h / 2
for i in range(0, count):
offset1 = i * 2 * math.pi / count
offset = math.pi / 2 - offset1
rad = self.layout.chart.h / 2
(r1, r2) = (0, rad + 5)
x1 = centerx - math.cos(offset) * r1
x2 = centerx - math.cos(offset) * r2
y1 = centery - math.sin(offset) * r1
y2 = centery - math.sin(offset) * r2
cx.new_path()
cx.move_to(x1, y1)
cx.line_to(x2, y2)
cx.close_path()
cx.stroke()
def _renderYTick(self, cx, tick, center):
"""Aux method for _renderAxis"""
i = tick
tick = self.yticks[i]
count = len(self.yticks)
if callable(tick):
return
x = center[0]
y = center[1] - i * (self.layout.chart.h / 2) / count
cx.new_path()
cx.move_to(x, y)
cx.line_to(x - self.options.axis.tickSize, y)
cx.close_path()
cx.stroke()
cx.select_font_face(self.options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.axis.tickFontSize)
label = safe_unicode(tick[1], self.options.encoding)
extents = cx.text_extents(label)
labelWidth = extents[2]
labelHeight = extents[3]
if self.options.axis.y.rotate:
radians = math.radians(self.options.axis.y.rotate)
cx.move_to(x - self.options.axis.tickSize
- (labelWidth * math.cos(radians))
- 4,
y + (labelWidth * math.sin(radians))
+ labelHeight / (2.0 / math.cos(radians)))
cx.rotate(-radians)
cx.show_text(label)
cx.rotate(radians) # this is probably faster than a save/restore
else:
cx.move_to(x - self.options.axis.tickSize - labelWidth - 4,
y + labelHeight / 2.0)
cx.rel_move_to(0.0, -labelHeight / 2.0)
cx.show_text(label)
return label
def _renderYAxis(self, cx):
"""Draws the vertical line for the Y axis"""
centerx = self.layout.chart.x + self.layout.chart.w / 2
centery = self.layout.chart.y + self.layout.chart.h / 2
offset = math.pi / 2
r1 = self.layout.chart.h / 2
x1 = centerx - math.cos(offset) * r1
y1 = centery - math.sin(offset) * r1
cx.new_path()
cx.move_to(centerx, centery)
cx.line_to(x1, y1)
cx.close_path()
cx.stroke()
def _renderAxis(self, cx):
"""Renders axis"""
if self.options.axis.x.hide and self.options.axis.y.hide:
return
cx.save()
cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor))
cx.set_line_width(self.options.axis.lineWidth)
centerx = self.layout.chart.x + self.layout.chart.w / 2
centery = self.layout.chart.y + self.layout.chart.h / 2
if not self.options.axis.y.hide:
if self.yticks:
count = len(self.yticks)
for i in range(0, count):
self._renderYTick(cx, i, (centerx, centery))
if self.options.axis.y.label:
self._renderYAxisLabel(cx, self.options.axis.y.label)
self._renderYAxis(cx)
if not self.options.axis.x.hide:
fontAscent = cx.font_extents()[0]
if self.xticks:
count = len(self.xticks)
for i in range(0, count):
self._renderXTick(cx, i, fontAscent, (centerx, centery))
if self.options.axis.x.label:
self._renderXAxisLabel(cx, self.options.axis.x.label)
self._renderXAxis(cx)
cx.restore()
def _renderXTick(self, cx, i, fontAscent, center):
tick = self.xticks[i]
if callable(tick):
return
count = len(self.xticks)
cx.select_font_face(self.options.axis.tickFont,
cairo.FONT_SLANT_NORMAL,
cairo.FONT_WEIGHT_NORMAL)
cx.set_font_size(self.options.axis.tickFontSize)
label = safe_unicode(tick[1], self.options.encoding)
extents = cx.text_extents(label)
labelWidth = extents[2]
labelHeight = extents[3]
x, y = center
cx.move_to(x, y)
if self.options.axis.x.rotate:
radians = math.radians(self.options.axis.x.rotate)
cx.move_to(x - (labelHeight * math.cos(radians)),
y + self.options.axis.tickSize
+ (labelHeight * math.cos(radians))
+ 4.0)
cx.rotate(radians)
cx.show_text(label)
cx.rotate(-radians)
else:
offset1 = i * 2 * math.pi / count
offset = math.pi / 2 - offset1
rad = self.layout.chart.h / 2 + 10
x = center[0] - math.cos(offset) * rad
y = center[1] - math.sin(offset) * rad
cx.move_to(x, y)
cx.rotate(offset - math.pi / 2)
if math.sin(offset) < 0.0:
cx.rotate(math.pi)
cx.rel_move_to(0.0, 5.0)
cx.rel_move_to(-labelWidth / 2.0, 0)
cx.show_text(label)
if math.sin(offset) < 0.0:
cx.rotate(-math.pi)
cx.rotate(-(offset - math.pi / 2))
return label
def _renderChart(self, cx):
"""Renders a line chart"""
# draw the circle
def preparePath(storeName):
cx.new_path()
firstPoint = True
count = len(self.points) / len(self.datasets)
centerx = self.layout.chart.x + self.layout.chart.w / 2
centery = self.layout.chart.y + self.layout.chart.h / 2
firstPointCoord = None
for index, point in enumerate(self.points):
if point.name == storeName:
offset1 = index * 2 * math.pi / count
offset = math.pi / 2 - offset1
rad = (self.layout.chart.h / 2) * (1 - point.y)
x = centerx - math.cos(offset) * rad
y = centery - math.sin(offset) * rad
if firstPointCoord is None:
firstPointCoord = (x, y)
if not self.options.shouldFill and firstPoint:
# starts the first point of the line
cx.move_to(x, y)
firstPoint = False
continue
cx.line_to(x, y)
if not firstPointCoord is None:
cx.line_to(firstPointCoord[0], firstPointCoord[1])
if self.options.shouldFill:
# Close the path to the start point
y = ((1.0 - self.origin)
* self.layout.chart.h + self.layout.chart.y)
else:
cx.set_source_rgb(*self.colorScheme[storeName])
cx.stroke()
cx.save()
cx.set_line_width(self.options.stroke.width)
if self.options.shouldFill:
def drawLine(storeName):
if self.options.stroke.shadow:
# draw shadow
cx.save()
cx.set_source_rgba(0, 0, 0, 0.15)
cx.translate(2, -2)
preparePath(storeName)
cx.fill()
cx.restore()
# fill the line
cx.set_source_rgb(*self.colorScheme[storeName])
preparePath(storeName)
cx.fill()
if not self.options.stroke.hide:
# draw stroke
cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
preparePath(storeName)
cx.stroke()
# draw the lines
for key in self._getDatasetsKeys():
drawLine(key)
else:
for key in self._getDatasetsKeys():
preparePath(key)
cx.restore()
pycha-0.7.0/pycha/color.py 0000664 0001750 0001750 00000012775 12130334357 015031 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
# 2009 by Yaco S.L.
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import math
from pycha.utils import clamp
DEFAULT_COLOR = '#3c581a'
def hex2rgb(hexstring, digits=2):
"""Converts a hexstring color to a rgb tuple.
Example: #ff0000 -> (1.0, 0.0, 0.0)
digits is an integer number telling how many characters should be
interpreted for each component in the hexstring.
"""
if isinstance(hexstring, (tuple, list)):
return hexstring
top = float(int(digits * 'f', 16))
r = int(hexstring[1:digits+1], 16)
g = int(hexstring[digits+1:digits*2+1], 16)
b = int(hexstring[digits*2+1:digits*3+1], 16)
return r / top, g / top, b / top
def rgb2hsv(r, g, b):
"""Converts a RGB color into a HSV one
See http://en.wikipedia.org/wiki/HSV_color_space
"""
maximum = max(r, g, b)
minimum = min(r, g, b)
if maximum == minimum:
h = 0.0
elif maximum == r:
h = 60.0 * ((g - b) / (maximum - minimum)) + 360.0
if h >= 360.0:
h -= 360.0
elif maximum == g:
h = 60.0 * ((b - r) / (maximum - minimum)) + 120.0
elif maximum == b:
h = 60.0 * ((r - g) / (maximum - minimum)) + 240.0
if maximum == 0.0:
s = 0.0
else:
s = 1.0 - (minimum / maximum)
v = maximum
return h, s, v
def hsv2rgb(h, s, v):
"""Converts a HSV color into a RGB one
See http://en.wikipedia.org/wiki/HSV_color_space
"""
hi = int(math.floor(h / 60.0)) % 6
f = (h / 60.0) - hi
p = v * (1 - s)
q = v * (1 - f * s)
t = v * (1 - (1 - f) * s)
if hi == 0:
r, g, b = v, t, p
elif hi == 1:
r, g, b = q, v, p
elif hi == 2:
r, g, b = p, v, t
elif hi == 3:
r, g, b = p, q, v
elif hi == 4:
r, g, b = t, p, v
elif hi == 5:
r, g, b = v, p, q
return r, g, b
def lighten(r, g, b, amount):
"""Return a lighter version of the color (r, g, b)"""
return (clamp(0.0, 1.0, r + amount),
clamp(0.0, 1.0, g + amount),
clamp(0.0, 1.0, b + amount))
basicColors = dict(
red='#6d1d1d',
green=DEFAULT_COLOR,
blue='#224565',
grey='#444444',
black='#000000',
darkcyan='#305755',
)
class ColorSchemeMetaclass(type):
"""This metaclass is used to autoregister all ColorScheme classes"""
def __new__(mcs, name, bases, dict):
klass = type.__new__(mcs, name, bases, dict)
klass.registerColorScheme()
return klass
class ColorScheme(dict):
"""A color scheme is a dictionary where the keys match the keys
constructor argument and the values are colors"""
__metaclass__ = ColorSchemeMetaclass
__registry__ = {}
def __init__(self, keys):
super(ColorScheme, self).__init__()
@classmethod
def registerColorScheme(cls):
key = cls.__name__.replace('ColorScheme', '').lower()
if key:
cls.__registry__[key] = cls
@classmethod
def getColorScheme(cls, name, default=None):
return cls.__registry__.get(name, default)
class GradientColorScheme(ColorScheme):
"""In this color scheme each color is a lighter version of initialColor.
This difference is computed based on the number of keys.
The initialColor is given in a hex string format.
"""
def __init__(self, keys, initialColor=DEFAULT_COLOR):
super(GradientColorScheme, self).__init__(keys)
if initialColor in basicColors:
initialColor = basicColors[initialColor]
r, g, b = hex2rgb(initialColor)
light = 1.0 / (len(keys) * 2)
for i, key in enumerate(keys):
self[key] = lighten(r, g, b, light * i)
class FixedColorScheme(ColorScheme):
"""In this color scheme fixed colors are used.
These colors are provided as a list argument in the constructor.
"""
def __init__(self, keys, colors=[]):
super(FixedColorScheme, self).__init__(keys)
if len(keys) != len(colors):
raise ValueError("You must provide as many colors as datasets "
"for the fixed color scheme")
for i, key in enumerate(keys):
self[key] = hex2rgb(colors[i])
class RainbowColorScheme(ColorScheme):
"""In this color scheme the rainbow is divided in N pieces
where N is the number of datasets.
So each dataset gets a color of the rainbow.
"""
def __init__(self, keys, initialColor=DEFAULT_COLOR):
super(RainbowColorScheme, self).__init__(keys)
if initialColor in basicColors:
initialColor = basicColors[initialColor]
r, g, b = hex2rgb(initialColor)
h, s, v = rgb2hsv(r, g, b)
angleDelta = 360.0 / (len(keys) + 1)
for key in keys:
self[key] = hsv2rgb(h, s, v)
h += angleDelta
if h >= 360.0:
h -= 360.0
pycha-0.7.0/pycha/bar.py 0000664 0001750 0001750 00000026144 12130334357 014452 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
from pycha.chart import Chart, uniqueIndices
from pycha.color import hex2rgb
from pycha.utils import safe_unicode
class BarChart(Chart):
def __init__(self, surface=None, options={}, debug=False):
super(BarChart, self).__init__(surface, options, debug)
self.bars = []
self.minxdelta = 0.0
self.barWidthForSet = 0.0
self.barMargin = 0.0
def _updateXY(self):
super(BarChart, self)._updateXY()
# each dataset is centered around a line segment. that's why we
# need n + 1 divisions on the x axis
self.xscale = 1 / (self.xrange + 1.0)
def _updateChart(self):
"""Evaluates measures for vertical bars"""
stores = self._getDatasetsValues()
uniqx = uniqueIndices(stores)
if len(uniqx) == 1:
self.minxdelta = 1.0
else:
self.minxdelta = min([abs(uniqx[j] - uniqx[j-1])
for j in range(1, len(uniqx))])
k = self.minxdelta * self.xscale
barWidth = k * self.options.barWidthFillFraction
self.barWidthForSet = barWidth / len(stores)
self.barMargin = k * (1.0 - self.options.barWidthFillFraction) / 2
self.bars = []
def _renderChart(self, cx):
"""Renders a horizontal/vertical bar chart"""
def drawBar(bar):
stroke_width = self.options.stroke.width
ux, uy = cx.device_to_user_distance(stroke_width, stroke_width)
if ux < uy:
ux = uy
cx.set_line_width(ux)
# gather bar proportions
x = self.layout.chart.x + self.layout.chart.w * bar.x
y = self.layout.chart.y + self.layout.chart.h * bar.y
w = self.layout.chart.w * bar.w
h = self.layout.chart.h * bar.h
if (w < 1 or h < 1) and self.options.yvals.skipSmallValues:
return # don't draw when the bar is too small
if self.options.stroke.shadow:
cx.set_source_rgba(0, 0, 0, 0.15)
rectangle = self._getShadowRectangle(x, y, w, h)
cx.rectangle(*rectangle)
cx.fill()
if self.options.shouldFill or (not self.options.stroke.hide):
if self.options.shouldFill:
cx.set_source_rgb(*self.colorScheme[bar.name])
cx.rectangle(x, y, w, h)
cx.fill()
if not self.options.stroke.hide:
cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
cx.rectangle(x, y, w, h)
cx.stroke()
if bar.yerr:
self._renderError(cx, x, y, w, h, bar.yval, bar.yerr)
# render yvals above/beside bars
if self.options.yvals.show:
cx.save()
cx.set_font_size(self.options.yvals.fontSize)
cx.set_source_rgb(*hex2rgb(self.options.yvals.fontColor))
if callable(self.options.yvals.renderer):
label = safe_unicode(self.options.yvals.renderer(bar),
self.options.encoding)
else:
label = safe_unicode(bar.yval, self.options.encoding)
extents = cx.text_extents(label)
labelW = extents[2]
labelH = extents[3]
self._renderYVal(cx, label, labelW, labelH, x, y, w, h)
cx.restore()
cx.save()
for bar in self.bars:
drawBar(bar)
cx.restore()
def _renderYVal(self, cx, label, width, height, x, y, w, h):
raise NotImplementedError
class VerticalBarChart(BarChart):
def _updateChart(self):
"""Evaluates measures for vertical bars"""
super(VerticalBarChart, self)._updateChart()
for i, (name, store) in enumerate(self.datasets):
for item in store:
if len(item) == 3:
xval, yval, yerr = item
else:
xval, yval = item
yerr = 0.0
x = (((xval - self.minxval) * self.xscale)
+ self.barMargin + (i * self.barWidthForSet))
w = self.barWidthForSet
h = abs(yval) * self.yscale
if yval > 0:
y = (1.0 - h) - self.origin
else:
y = 1 - self.origin
rect = Rect(x, y, w, h, xval, yval, name)
if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
self.bars.append(rect)
def _updateTicks(self):
"""Evaluates bar ticks"""
super(BarChart, self)._updateTicks()
offset = (self.minxdelta * self.xscale) / 2
self.xticks = [(tick[0] + offset, tick[1]) for tick in self.xticks]
def _getShadowRectangle(self, x, y, w, h):
return (x-2, y-2, w+4, h+2)
def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH):
x = barX + (barW / 2.0) - (labelW / 2.0)
if self.options.yvals.snapToOrigin:
y = barY + barH - 0.5 * labelH
elif self.options.yvals.inside:
y = barY + (1.5 * labelH)
else:
y = barY - 0.5 * labelH
# if the label doesn't fit below the bar, put it above the bar
if y > (barY + barH):
y = barY - 0.5 * labelH
cx.move_to(x, y)
cx.show_text(label)
def _renderError(self, cx, barX, barY, barW, barH, value, error):
center = barX + (barW / 2.0)
errorWidth = max(barW * 0.1, 5.0)
left = center - errorWidth
right = center + errorWidth
errorSize = barH * error / value
top = barY + errorSize
bottom = barY - errorSize
cx.set_source_rgb(0, 0, 0)
cx.move_to(left, top)
cx.line_to(right, top)
cx.stroke()
cx.move_to(center, top)
cx.line_to(center, bottom)
cx.stroke()
cx.move_to(left, bottom)
cx.line_to(right, bottom)
cx.stroke()
class HorizontalBarChart(BarChart):
def _updateChart(self):
"""Evaluates measures for horizontal bars"""
super(HorizontalBarChart, self)._updateChart()
for i, (name, store) in enumerate(self.datasets):
for item in store:
if len(item) == 3:
xval, yval, yerr = item
else:
xval, yval = item
yerr = 0.0
y = (((xval - self.minxval) * self.xscale)
+ self.barMargin + (i * self.barWidthForSet))
h = self.barWidthForSet
w = abs(yval) * self.yscale
if yval > 0:
x = self.origin
else:
x = self.origin - w
rect = Rect(x, y, w, h, xval, yval, name, yerr)
if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
self.bars.append(rect)
def _updateTicks(self):
"""Evaluates bar ticks"""
super(BarChart, self)._updateTicks()
offset = (self.minxdelta * self.xscale) / 2
tmp = self.xticks
self.xticks = [(1.0 - tick[0], tick[1]) for tick in self.yticks]
self.yticks = [(tick[0] + offset, tick[1]) for tick in tmp]
def _renderLines(self, cx):
"""Aux function for _renderBackground"""
if self.options.axis.y.showLines and self.yticks:
for tick in self.xticks:
self._renderLine(cx, tick, True)
if self.options.axis.x.showLines and self.xticks:
for tick in self.yticks:
self._renderLine(cx, tick, False)
def _getShadowRectangle(self, x, y, w, h):
return (x, y-2, w+2, h+4)
def _renderXAxisLabel(self, cx, labelText):
labelText = self.options.axis.x.label
super(HorizontalBarChart, self)._renderYAxisLabel(cx, labelText)
def _renderXAxis(self, cx):
"""Draws the horizontal line representing the X axis"""
cx.new_path()
cx.move_to(self.layout.chart.x,
self.layout.chart.y + self.layout.chart.h)
cx.line_to(self.layout.chart.x + self.layout.chart.w,
self.layout.chart.y + self.layout.chart.h)
cx.close_path()
cx.stroke()
def _renderYAxisLabel(self, cx, labelText):
labelText = self.options.axis.y.label
super(HorizontalBarChart, self)._renderXAxisLabel(cx, labelText)
def _renderYAxis(self, cx):
# draws the vertical line representing the Y axis
cx.new_path()
cx.move_to(self.layout.chart.x + self.origin * self.layout.chart.w,
self.layout.chart.y)
cx.line_to(self.layout.chart.x + self.origin * self.layout.chart.w,
self.layout.chart.y + self.layout.chart.h)
cx.close_path()
cx.stroke()
def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH):
y = barY + (barH / 2.0) + (labelH / 2.0)
if self.options.yvals.snapToOrigin:
x = barX + 2
elif self.options.yvals.inside:
x = barX + barW - (1.2 * labelW)
else:
x = barX + barW + 0.2 * labelW
# if the label doesn't fit to the left of the bar, put it to the right
if x < barX:
x = barX + barW + 0.2 * labelW
cx.move_to(x, y)
cx.show_text(label)
def _renderError(self, cx, barX, barY, barW, barH, value, error):
center = barY + (barH / 2.0)
errorHeight = max(barH * 0.1, 5.0)
top = center + errorHeight
bottom = center - errorHeight
errorSize = barW * error / value
right = barX + barW + errorSize
left = barX + barW - errorSize
cx.set_source_rgb(0, 0, 0)
cx.move_to(left, top)
cx.line_to(left, bottom)
cx.stroke()
cx.move_to(left, center)
cx.line_to(right, center)
cx.stroke()
cx.move_to(right, top)
cx.line_to(right, bottom)
cx.stroke()
class Rect(object):
def __init__(self, x, y, w, h, xval, yval, name, yerr=0.0):
self.x, self.y, self.w, self.h = x, y, w, h
self.xval, self.yval, self.yerr = xval, yval, yerr
self.name = name
def __str__(self):
return (""
% (self.x, self.y, self.w, self.h,
self.xval, self.yval, self.yerr,
self.name))
pycha-0.7.0/pycha/scatter.py 0000664 0001750 0001750 00000002556 12130334357 015354 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
import math
from pycha.line import LineChart
class ScatterplotChart(LineChart):
def _renderChart(self, cx):
"""Renders a scatterplot"""
def drawSymbol(point, size):
ox = point.x * self.layout.chart.w + self.layout.chart.x
oy = point.y * self.layout.chart.h + self.layout.chart.y
cx.arc(ox, oy, size, 0.0, 2 * math.pi)
cx.fill()
for key in self._getDatasetsKeys():
cx.set_source_rgb(*self.colorScheme[key])
for point in self.points:
if point.name == key:
drawSymbol(point, self.options.stroke.width)
pycha-0.7.0/pycha/line.py 0000664 0001750 0001750 00000011330 12130334357 014624 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
from pycha.chart import Chart
from pycha.color import hex2rgb
class LineChart(Chart):
def __init__(self, surface=None, options={}, debug=False):
super(LineChart, self).__init__(surface, options, debug)
self.points = []
def _updateChart(self):
"""Evaluates measures for line charts"""
self.points = []
for i, (name, store) in enumerate(self.datasets):
for item in store:
xval, yval = item
x = (xval - self.minxval) * self.xscale
y = 1.0 - (yval - self.minyval) * self.yscale
point = Point(x, y, xval, yval, name)
if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0:
self.points.append(point)
def _renderChart(self, cx):
"""Renders a line chart"""
def preparePath(storeName):
cx.new_path()
firstPoint = True
lastX = None
if self.options.shouldFill:
# Go to the (0,0) coordinate to start drawing the area
#cx.move_to(self.layout.chart.x,
# self.layout.chart.y + self.layout.chart.h)
offset = (1.0 - self.origin) * self.layout.chart.h
cx.move_to(self.layout.chart.x, self.layout.chart.y + offset)
for point in self.points:
if point.name == storeName:
if not self.options.shouldFill and firstPoint:
# starts the first point of the line
cx.move_to(point.x * self.layout.chart.w
+ self.layout.chart.x,
point.y * self.layout.chart.h
+ self.layout.chart.y)
firstPoint = False
continue
cx.line_to(point.x * self.layout.chart.w
+ self.layout.chart.x,
point.y * self.layout.chart.h
+ self.layout.chart.y)
# we remember the last X coordinate to close the area
# properly. See bug #4
lastX = point.x
if self.options.shouldFill:
# Close the path to the start point
y = ((1.0 - self.origin) * self.layout.chart.h
+ self.layout.chart.y)
cx.line_to(lastX * self.layout.chart.w
+ self.layout.chart.x, y)
cx.line_to(self.layout.chart.x, y)
cx.close_path()
else:
cx.set_source_rgb(*self.colorScheme[storeName])
cx.stroke()
cx.save()
cx.set_line_width(self.options.stroke.width)
if self.options.shouldFill:
def drawLine(storeName):
if self.options.stroke.shadow:
# draw shadow
cx.save()
cx.set_source_rgba(0, 0, 0, 0.15)
cx.translate(2, -2)
preparePath(storeName)
cx.fill()
cx.restore()
# fill the line
cx.set_source_rgb(*self.colorScheme[storeName])
preparePath(storeName)
cx.fill()
if not self.options.stroke.hide:
# draw stroke
cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
preparePath(storeName)
cx.stroke()
# draw the lines
for key in self._getDatasetsKeys():
drawLine(key)
else:
for key in self._getDatasetsKeys():
preparePath(key)
cx.restore()
class Point(object):
def __init__(self, x, y, xval, yval, name):
self.x, self.y = x, y
self.xval, self.yval = xval, yval
self.name = name
def __str__(self):
return "" % (self.x, self.y)
pycha-0.7.0/pycha/stackedbar.py 0000664 0001750 0001750 00000010714 12130334357 016005 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2009 by Yaco S.L.
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
from pycha.bar import BarChart, VerticalBarChart, HorizontalBarChart, Rect
from pycha.chart import uniqueIndices
class StackedBarChart(BarChart):
def __init__(self, surface=None, options={}, debug=False):
super(StackedBarChart, self).__init__(surface, options, debug)
self.barWidth = 0.0
def _updateXY(self):
super(StackedBarChart, self)._updateXY()
# each dataset is centered around a line segment. that's why we
# need n + 1 divisions on the x axis
self.xscale = 1 / (self.xrange + 1.0)
if self.options.axis.y.range is None:
# Fix the yscale as we accumulate the y values
stores = self._getDatasetsValues()
n_stores = len(stores)
flat_y = [pair[1] for pair in reduce(lambda a, b: a+b, stores)]
store_size = len(flat_y) / n_stores
accum = [sum(flat_y[j]for j in xrange(i,
i + store_size * n_stores,
store_size))
for i in range(len(flat_y) / n_stores)]
self.yrange = float(max(accum))
if self.yrange == 0:
self.yscale = 1.0
else:
self.yscale = 1.0 / self.yrange
def _updateChart(self):
"""Evaluates measures for vertical bars"""
stores = self._getDatasetsValues()
uniqx = uniqueIndices(stores)
if len(uniqx) == 1:
self.minxdelta = 1.0
else:
self.minxdelta = min([abs(uniqx[j] - uniqx[j-1])
for j in range(1, len(uniqx))])
k = self.minxdelta * self.xscale
self.barWidth = k * self.options.barWidthFillFraction
self.barMargin = k * (1.0 - self.options.barWidthFillFraction) / 2
self.bars = []
class StackedVerticalBarChart(StackedBarChart, VerticalBarChart):
def _updateChart(self):
"""Evaluates measures for vertical bars"""
super(StackedVerticalBarChart, self)._updateChart()
accumulated_heights = {}
for i, (name, store) in enumerate(self.datasets):
for item in store:
xval, yval = item
x = ((xval - self.minxval) * self.xscale) + self.barMargin
w = self.barWidth
h = abs(yval) * self.yscale
if yval > 0:
y = (1.0 - h) - self.origin
else:
y = 1 - self.origin
accumulated_height = accumulated_heights.setdefault(xval, 0)
y -= accumulated_height
accumulated_heights[xval] += h
rect = Rect(x, y, w, h, xval, yval, name)
if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
self.bars.append(rect)
class StackedHorizontalBarChart(StackedBarChart, HorizontalBarChart):
def _updateChart(self):
"""Evaluates measures for horizontal bars"""
super(StackedHorizontalBarChart, self)._updateChart()
accumulated_widths = {}
for i, (name, store) in enumerate(self.datasets):
for item in store:
xval, yval = item
y = ((xval - self.minxval) * self.xscale) + self.barMargin
h = self.barWidth
w = abs(yval) * self.yscale
if yval > 0:
x = self.origin
else:
x = self.origin - w
accumulated_width = accumulated_widths.setdefault(xval, 0)
x += accumulated_width
accumulated_widths[xval] += w
rect = Rect(x, y, w, h, xval, yval, name)
if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0):
self.bars.append(rect)
pycha-0.7.0/pycha/__init__.py 0000664 0001750 0001750 00000001377 12130337717 015451 0 ustar lgs lgs 0000000 0000000 # Copyright(c) 2007-2010 by Lorenzo Gil Sanchez
#
# This file is part of PyCha.
#
# PyCha is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PyCha is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with PyCha. If not, see .
version = "0.7.0"
pycha-0.7.0/PKG-INFO 0000664 0001750 0001750 00000013771 12130340466 013325 0 ustar lgs lgs 0000000 0000000 Metadata-Version: 1.0
Name: pycha
Version: 0.7.0
Summary: A library for making charts with Python
Home-page: http://bitbucket.org/lgs/pycha/
Author: Lorenzo Gil Sanchez
Author-email: lorenzo.gil.sanchez@gmail.com
License: LGPL 3
Description: .. contents::
=====
PyCha
=====
Pycha is a very simple Python package for drawing charts using the great
`Cairo `_ library. Its goals are:
* Lightweight
* Simple to use
* Nice looking with default values
* Customization
It won't try to draw any possible chart on earth but draw the most common ones
nicely. There are some other options you may want to look at like
`pyCairoChart `_.
Pycha is based on `Plotr `_ which is based on
`PlotKit `_. Both libraries are written in
JavaScript and are great for client web programming. I needed the same for the
server side so that's the reason I ported Plotr to Python. Now we can deliver
charts to people with JavaScript disabled or embed them in PDF reports.
Pycha is distributed under the terms of the `GNU Lesser General Public License
`_.
Documentation
-------------
You can find Pycha's documentation at http://packages.python.org/pycha
Development
-----------
You can get the last bleeding edge version of pycha by getting a clone of
the Mercurial repository::
hg clone https://bitbucket.org/lgs/pycha
Don't forget to check the
`Release Notes `_
for each version to learn the new features and incompatible changes.
Contact
-------
There is a mailing list about PyCha at http://groups.google.com/group/pycha
You can join it to ask questions about its use or simply to talk about its
development. Your ideas and feedback are greatly appreciated!
Changes
=======
0.7.0 (2012-04-07)
------------------
- Radial Chart by Roberto Garcia Carvajal
- Polygonal Chart by Roberto Garcia Carvajal
- Ring Chart by Roberto Garcia Carvajal
- Minor cleanups in the code
0.6.0 (2010-12-31)
------------------
- Buildout support
- Documentation revamped
- Debug improvements
- Autopadding
- Make the unicode strings used in labels safer
0.5.3 (2010-03-29)
------------------
- New title color option
- Fix crash in chavier application
- New horizontal axis lines. Options to turn it (and vertical ones) on and off
- Improve precision in axis ticks
- Add some examples and update old ones
0.5.2 (2009-09-26)
------------------
- Add a MANIFEST.in to explictly include all files in the source distribution
0.5.1 (2009-09-19)
------------------
- Several bug fixes (Lorenzo)
- Draw circles instead of lines for scatter chart symbols (Lorenzo)
- Error bars (Yang Zhang)
- Improve tick labels (Simon)
- Add labels with yvals next to the bars (Simon (Vsevolod) Ilyushchenko)
- Change the project website (Lorenzo)
0.5.0 (2009-03-22)
------------------
- Bar chart fixes (Adam)
- Support for custon fonts in the ticks (Ged)
- Support for an 'interval' option (Nicolas)
- New color scheme system (Lorenzo)
- Stacked bar charts support (Lorenzo)
0.4.2 (2009-02-15)
------------------
- Much better documentation (Adam)
- Fixes integer division when computing xscale (Laurent)
- Fix for a broken example (Lorenzo)
- Use labelFontSize when rendering the axis (Adam Przywecki)
- Code cleanups. Now it should pass pyflakes and pep8 in most files (Lorenzo)
- Support for running the test suite with python setup.py test (Lorenzo)
- Support for SVG (and PDF, Postscript, Win32, Quartz) by changing the way
we compute the surface dimensions (Lorenzo)
0.4.1 (2008-10-29)
------------------
- Fix a colon in the README.txt file (Lorenzo)
- Add a test_suite option to setup.py so we can run the tests before deployment
(Lorenzo)
0.4.0 (2008-10-28)
------------------
- Improved test suite (Lorenzo, Nicolas)
- Many bugs fixed (Lorenzo, Stephane Wirtel)
- Support for negative values in the datasets (Nicolas, Lorenzo)
- Chavier, a simple pygtk application for playing with Pycha charts (Lorenzo)
- Allow the legend to be placed relative to the right and bottom of the canvas
(Nicolas Evrard)
- Easier debugging by adding __str__ methods to aux classes (rectangle, point,
area, ...) (Lorenzo)
- Do not overlap Y axis label when ticks label are not rotated (John Eikenberry)
0.3.0 (2008-03-22)
------------------
- Scattered charts (Tamas Nepusz )
- Chart titles (John Eikenberry )
- Axis labels and rotated ticks (John)
- Chart background and surface background (John)
- Automatically augment the light in large color schemes (John)
- Lots of bug fixes (John and Lorenzo)
0.2.0 (2007-10-25)
------------------
- Test suite
- Python 2.4 compatibility (patch by Miguel Hernandez)
- API docs
- Small fixes
0.1.0 (2007-10-17)
------------------
- Initial release
Keywords: chart cairo
Platform: UNKNOWN
pycha-0.7.0/AUTHORS 0000664 0001750 0001750 00000000072 12130334357 013270 0 ustar lgs lgs 0000000 0000000 Lorenzo Gil Sanchez