pycha-0.7.0/0000775000175000017500000000000012130340466012217 5ustar lgslgs00000000000000pycha-0.7.0/setup.cfg0000664000175000017500000000007312130340466014040 0ustar lgslgs00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pycha-0.7.0/MANIFEST.in0000664000175000017500000000024712130334357013762 0ustar lgslgs00000000000000recursive-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/COPYING0000664000175000017500000001672712130334357013271 0ustar lgslgs00000000000000 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/0000775000175000017500000000000012130340466015015 5ustar lgslgs00000000000000pycha-0.7.0/pycha.egg-info/dependency_links.txt0000664000175000017500000000000112130340366021062 0ustar lgslgs00000000000000 pycha-0.7.0/pycha.egg-info/SOURCES.txt0000664000175000017500000000163312130340366016703 0ustar lgslgs00000000000000AUTHORS 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.pypycha-0.7.0/pycha.egg-info/top_level.txt0000664000175000017500000000001612130340366017543 0ustar lgslgs00000000000000pycha chavier pycha-0.7.0/pycha.egg-info/PKG-INFO0000664000175000017500000001377112130340366016122 0ustar lgslgs00000000000000Metadata-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.txt0000664000175000017500000000005212130340366020307 0ustar lgslgs00000000000000[gui_scripts] chavier = chavier.app:main pycha-0.7.0/pycha.egg-info/zip-safe0000664000175000017500000000000112130334552016444 0ustar lgslgs00000000000000 pycha-0.7.0/README.txt0000664000175000017500000000313612130334357013722 0ustar lgslgs00000000000000.. 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.py0000664000175000017500000000306712130334357013741 0ustar lgslgs00000000000000# 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/0000775000175000017500000000000012130340466013640 5ustar lgslgs00000000000000pycha-0.7.0/chavier/dialogs.py0000664000175000017500000001560012130334357015640 0ustar lgslgs00000000000000# 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.py0000664000175000017500000004667312130334357015020 0ustar lgslgs00000000000000# 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.py0000664000175000017500000001067112130334357015001 0ustar lgslgs00000000000000# 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__.py0000664000175000017500000000000012130334357015741 0ustar lgslgs00000000000000pycha-0.7.0/tests/0000775000175000017500000000000012130340466013361 5ustar lgslgs00000000000000pycha-0.7.0/tests/utils.py0000664000175000017500000000321612130334357015077 0ustar lgslgs00000000000000# -*- 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.py0000664000175000017500000002603512130334357015044 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000215512130334357015251 0ustar lgslgs00000000000000# 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.py0000664000175000017500000001444512130334357014522 0ustar lgslgs00000000000000# 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.py0000664000175000017500000001277712130334357015071 0ustar lgslgs00000000000000# 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.py0000664000175000017500000003366212130334357014513 0ustar lgslgs00000000000000# 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.py0000664000175000017500000001036212130334357014666 0ustar lgslgs00000000000000# 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.py0000664000175000017500000001726312130334357016051 0ustar lgslgs00000000000000# 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__.py0000664000175000017500000000000012130334357015462 0ustar lgslgs00000000000000pycha-0.7.0/CHANGES.txt0000664000175000017500000000573612130337453014045 0ustar lgslgs00000000000000Changes ======= 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/0000775000175000017500000000000012130340466014035 5ustar lgslgs00000000000000pycha-0.7.0/examples/scatterchart.py0000664000175000017500000000372512130334357017107 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000464012130334357015374 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000345112130334357017541 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000342312130334357016364 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000311312130334357016233 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000450612130334357017435 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000272712130334357016220 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000325112130334357017252 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000167712130334357015536 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000421012130334357016174 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000267312130334357015220 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000340312130334671016371 0ustar lgslgs00000000000000# 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/0000775000175000017500000000000012130340466015153 5ustar lgslgs00000000000000pycha-0.7.0/examples/color/colorschemes.py0000664000175000017500000000352412130334357020221 0ustar lgslgs00000000000000# 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/0000775000175000017500000000000012130340466013323 5ustar lgslgs00000000000000pycha-0.7.0/pycha/utils.py0000664000175000017500000000254412130334357015044 0ustar lgslgs00000000000000# 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.py0000664000175000017500000003056712130334671014650 0ustar lgslgs00000000000000# 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.py0000664000175000017500000007454512130334357015017 0ustar lgslgs00000000000000# 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.py0000664000175000017500000003012612130334357015705 0ustar lgslgs00000000000000# 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.py0000664000175000017500000003027312130334357014461 0ustar lgslgs00000000000000# 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.py0000664000175000017500000002614212130334357015140 0ustar lgslgs00000000000000# 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.py0000664000175000017500000001277512130334357015031 0ustar lgslgs00000000000000# 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.py0000664000175000017500000002614412130334357014452 0ustar lgslgs00000000000000# 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.py0000664000175000017500000000255612130334357015354 0ustar lgslgs00000000000000# 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.py0000664000175000017500000001133012130334357014624 0ustar lgslgs00000000000000# 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.py0000664000175000017500000001071412130334357016005 0ustar lgslgs00000000000000# 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__.py0000664000175000017500000000137712130337717015451 0ustar lgslgs00000000000000# 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-INFO0000664000175000017500000001377112130340466013325 0ustar lgslgs00000000000000Metadata-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/AUTHORS0000664000175000017500000000007212130334357013270 0ustar lgslgs00000000000000Lorenzo Gil Sanchez