From 84d66d5328cce8845997dc87bacd884c3f87f75e Mon Sep 17 00:00:00 2001 From: estudiante Date: Tue, 14 Feb 2012 12:08:56 +0000 Subject: Plot:Actividad de graficar --- diff --git a/COPYING b/COPYING new file mode 100755 index 0000000..d159169 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the 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 Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/MANIFEST b/MANIFEST new file mode 100755 index 0000000..4d661a6 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,59 @@ +plot.py +gtkplotactivity.py +setup.py +cairoplot/series.py +cairoplot/handlers/png.py +cairoplot/handlers/pdf.py +cairoplot/handlers/fixedsize.py +cairoplot/handlers/ps.py +cairoplot/handlers/__init__.py +cairoplot/handlers/gtk.py +cairoplot/handlers/handler.py +cairoplot/handlers/svg.py +cairoplot/handlers/vector.py +cairoplot/__init__.py +data/puzzle/addition.svg +data/puzzle/blank.svg +data/puzzle/constant.svg +data/puzzle/exponentiation.svg +data/puzzle/identity.svg +data/puzzle/leftparen.svg +data/puzzle/multiplication.svg +data/puzzle/rightparen.svg +plotter/json.py +plotter/settings.py +plotter/view/equation.py +plotter/view/__init__.py +plotter/plot.py +plotter/__init__.py +plotter/parse.py +activity/activity-plot.svg +activity/activity.info +plotter/view/puzzletree/display.py +plotter/view/puzzletree/palette.py +plotter/view/puzzletree/__init__.py +plotter/view/puzzletree/nodes/identity.py +plotter/view/puzzletree/nodes/constant.py +plotter/view/puzzletree/nodes/__init__.py +plotter/view/puzzletree/nodes/multiplication.py +plotter/view/puzzletree/nodes/node.py +plotter/view/puzzletree/nodes/exponentiation.py +plotter/view/puzzletree/nodes/addition.py +plotter/view/puzzletree/nodes/binaryoperator.py +data/puzzle/composition.svg +plotter/view/puzzletree/nodes/composition.py +data/puzzle/absolutevalue.svg +data/puzzle/sine.svg +plotter/view/puzzletree/nodes/sine.py +plotter/view/puzzletree/nodes/absolutevalue.py +plotter/view/puzzletree/nodes/simplenode.py +COPYING +data/puzzle/pi.svg +data/puzzle/e.svg +plotter/view/puzzletree/nodes/pi.py +plotter/view/puzzletree/nodes/e.py +locale/en-US/activity.linfo +locale/en-US/LC_MESSAGES/edu.wisc.cs.nest.PlotActivity.mo +po/POTFILES.in +po/en-US.po +po/Plot.pot diff --git a/activity/activity-plot.svg b/activity/activity-plot.svg new file mode 100755 index 0000000..00eb1d0 --- /dev/null +++ b/activity/activity-plot.svg @@ -0,0 +1,33 @@ + + + +]> + + + + + + + + + + diff --git a/activity/activity.info b/activity/activity.info new file mode 100755 index 0000000..30abaf9 --- /dev/null +++ b/activity/activity.info @@ -0,0 +1,9 @@ +[Activity] +name = Plot +activity_version = 6 +host_version = 1 +bundle_id = edu.wisc.cs.nest.PlotActivity +license = GPLv2+ +icon = activity-plot +class = plot.PlotActivity + diff --git a/cairoplot/__init__.py b/cairoplot/__init__.py new file mode 100755 index 0000000..d47bdaf --- /dev/null +++ b/cairoplot/__init__.py @@ -0,0 +1,2378 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# CairoPlot.py +# +# Copyright (c) 2008 Rodrigo Moreira Araújo +# +# Author: Rodrigo Moreiro Araujo +# +# This program 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 2 of +# the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA + +#Contributor: João S. O. Bueno + +#TODO: review BarPlot Code +#TODO: x_label colision problem on Horizontal Bar Plot +#TODO: y_label's eat too much space on HBP + + +__version__ = 1.1 + +import cairo +import math +import random +from series import Series, Group, Data + +import cairoplot.handlers + +HORZ = 0 +VERT = 1 +NORM = 2 + +COLORS = {"red" : (1.0,0.0,0.0,1.0), "lime" : (0.0,1.0,0.0,1.0), "blue" : (0.0,0.0,1.0,1.0), + "maroon" : (0.5,0.0,0.0,1.0), "green" : (0.0,0.5,0.0,1.0), "navy" : (0.0,0.0,0.5,1.0), + "yellow" : (1.0,1.0,0.0,1.0), "magenta" : (1.0,0.0,1.0,1.0), "cyan" : (0.0,1.0,1.0,1.0), + "orange" : (1.0,0.5,0.0,1.0), "white" : (1.0,1.0,1.0,1.0), "black" : (0.0,0.0,0.0,1.0), + "gray" : (0.5,0.5,0.5,1.0), "light_gray" : (0.9,0.9,0.9,1.0), + "transparent" : (0.0,0.0,0.0,0.0)} + +THEMES = {"black_red" : [(0.0,0.0,0.0,1.0), (1.0,0.0,0.0,1.0)], + "red_green_blue" : [(1.0,0.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0)], + "red_orange_yellow" : [(1.0,0.2,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,1.0,0.0,1.0)], + "yellow_orange_red" : [(1.0,1.0,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,0.2,0.0,1.0)], + "rainbow" : [(1.0,0.0,0.0,1.0), (1.0,0.5,0.0,1.0), (1.0,1.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0), (0.3, 0.0, 0.5,1.0), (0.5, 0.0, 1.0, 1.0)]} + +def colors_from_theme( theme, series_length, mode = 'solid' ): + colors = [] + if theme not in THEMES.keys() : + raise Exception, "Theme not defined" + color_steps = THEMES[theme] + n_colors = len(color_steps) + if series_length <= n_colors: + colors = [color + tuple([mode]) for color in color_steps[0:n_colors]] + else: + iterations = [(series_length - n_colors)/(n_colors - 1) for i in color_steps[:-1]] + over_iterations = (series_length - n_colors) % (n_colors - 1) + for i in range(n_colors - 1): + if over_iterations <= 0: + break + iterations[i] += 1 + over_iterations -= 1 + for index,color in enumerate(color_steps[:-1]): + colors.append(color + tuple([mode])) + if iterations[index] == 0: + continue + next_color = color_steps[index+1] + color_step = ((next_color[0] - color[0])/(iterations[index] + 1), + (next_color[1] - color[1])/(iterations[index] + 1), + (next_color[2] - color[2])/(iterations[index] + 1), + (next_color[3] - color[3])/(iterations[index] + 1)) + for i in range( iterations[index] ): + colors.append((color[0] + color_step[0]*(i+1), + color[1] + color_step[1]*(i+1), + color[2] + color_step[2]*(i+1), + color[3] + color_step[3]*(i+1), + mode)) + colors.append(color_steps[-1] + tuple([mode])) + return colors + + +def other_direction(direction): + "explicit is better than implicit" + if direction == HORZ: + return VERT + else: + return HORZ + +#Class definition + +class Plot(object): + def __init__(self, + surface=None, + data=None, + width=640, + height=480, + background=None, + border = 0, + x_labels = None, + y_labels = None, + series_colors = None): + random.seed(2) + self.create_surface(surface, width, height) + self.dimensions = {} + self.dimensions[HORZ] = width + self.dimensions[VERT] = height + self.context = None + self.labels={} + self.labels[HORZ] = x_labels + self.labels[VERT] = y_labels + self.load_series(data, x_labels, y_labels, series_colors) + self.font_size = 10 + self.set_background (background) + self.border = border + self.borders = {} + self.line_color = (0.5, 0.5, 0.5) + self.line_width = 0.5 + self.label_color = (0.0, 0.0, 0.0) + self.grid_color = (0.8, 0.8, 0.8) + + def create_surface(self, surface, width=None, height=None): + self.filename = None + if isinstance(surface, cairo.Surface): + self.handler = cairoplot.handlers.VectorHandler(surface, width, + height) + return + if isinstance(surface, cairoplot.handlers.Handler): + self.handler = surface + return + if not type(surface) in (str, unicode): + raise TypeError("Surface should be either a Cairo surface or a filename, not %s" % surface) + + # choose handler based on file extension (svg is default) + sufix = surface.rsplit(".")[-1].lower() + filename = surface + handlerclass = cairoplot.handlers.SVGHandler + if sufix == "png": + handlerclass = cairoplot.handlers.PNGHandler + elif sufix == "ps": + handlerclass = cairoplot.handlers.PSHandler + elif sufix == "pdf": + handlerclass = cairoplot.handlers.PDFHandler + elif sufix != "svg": + filename += ".svg" + self.handler = handlerclass(filename, width, height) + + def commit(self): + try: + self.handler.commit(self) + except cairo.Error: + pass + + def load_series (self, data, x_labels=None, y_labels=None, series_colors=None): + self.series_labels = [] + self.series = None + + #The pretty way + #if not isinstance(data, Series): + # # Not an instance of Series + # self.series = Series(data) + #else: + # self.series = data + # + #self.series_labels = self.series.get_names() + + #TODO: Remove on next version + # The ugly way, keeping retrocompatibility... + if callable(data) or type(data) is list and callable(data[0]): # Lambda or List of lambdas + self.series = data + self.series_labels = None + elif isinstance(data, Series): # Instance of Series + self.series = data + self.series_labels = data.get_names() + else: # Anything else + self.series = Series(data) + self.series_labels = self.series.get_names() + + #TODO: allow user passed series_widths + self.series_widths = [1.0 for group in self.series] + + #TODO: Remove on next version + self.process_colors( series_colors ) + + def process_colors( self, series_colors, length = None, mode = 'solid' ): + #series_colors might be None, a theme, a string of colors names or a list of color tuples + if length is None : + length = len( self.series.to_list() ) + + #no colors passed + if not series_colors: + #Randomize colors + self.series_colors = [ [random.random() for i in range(3)] + [1.0, mode] for series in range( length ) ] + else: + #Just theme pattern + if not hasattr( series_colors, "__iter__" ): + theme = series_colors + self.series_colors = colors_from_theme( theme.lower(), length ) + + #Theme pattern and mode + elif not hasattr(series_colors, '__delitem__') and not hasattr( series_colors[0], "__iter__" ): + theme = series_colors[0] + mode = series_colors[1] + self.series_colors = colors_from_theme( theme.lower(), length, mode ) + + #List + else: + self.series_colors = series_colors + for index, color in enumerate( self.series_colors ): + #element is a color name + if not hasattr(color, "__iter__"): + self.series_colors[index] = COLORS[color.lower()] + tuple([mode]) + #element is rgb tuple instead of rgba + elif len( color ) == 3 : + self.series_colors[index] += (1.0,mode) + #element has 4 elements, might be rgba tuple or rgb tuple with mode + elif len( color ) == 4 : + #last element is mode + if not hasattr(color[3], "__iter__"): + self.series_colors[index] += tuple([color[3]]) + self.series_colors[index][3] = 1.0 + #last element is alpha + else: + self.series_colors[index] += tuple([mode]) + + def set_background(self, background): + if background is None: + self.background = (0.0,0.0,0.0,0.0) + elif type(background) in (cairo.LinearGradient, tuple): + self.background = background + elif not hasattr(background,"__iter__"): + colors = background.split(" ") + if len(colors) == 1 and colors[0] in COLORS: + self.background = COLORS[background] + elif len(colors) > 1: + self.background = cairo.LinearGradient(self.dimensions[HORZ] / 2, 0, self.dimensions[HORZ] / 2, self.dimensions[VERT]) + for index,color in enumerate(colors): + self.background.add_color_stop_rgba(float(index)/(len(colors)-1),*COLORS[color]) + else: + raise TypeError ("Background should be either cairo.LinearGradient or a 3/4-tuple, not %s" % type(background)) + + def render_background(self): + if isinstance(self.background, cairo.LinearGradient): + self.context.set_source(self.background) + else: + self.context.set_source_rgba(*self.background) + self.context.rectangle(0,0, self.dimensions[HORZ], self.dimensions[VERT]) + self.context.fill() + + def render_bounding_box(self): + self.context.set_source_rgba(*self.line_color) + self.context.set_line_width(self.line_width) + self.context.rectangle(self.border, self.border, + self.dimensions[HORZ] - 2 * self.border, + self.dimensions[VERT] - 2 * self.border) + self.context.stroke() + + def render(self): + """All plots must prepare their context before rendering.""" + self.handler.prepare(self) + + + +class ScatterPlot( Plot ): + def __init__(self, + surface=None, + data=None, + errorx=None, + errory=None, + width=640, + height=480, + background=None, + border=0, + axis = False, + dash = False, + discrete = False, + dots = 0, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + z_bounds = None, + x_title = None, + y_title = None, + series_colors = None, + circle_colors = None ): + + self.bounds = {} + self.bounds[HORZ] = x_bounds + self.bounds[VERT] = y_bounds + self.bounds[NORM] = z_bounds + self.titles = {} + self.titles[HORZ] = x_title + self.titles[VERT] = y_title + self.max_value = {} + self.axis = axis + self.discrete = discrete + self.dots = dots + self.grid = grid + self.series_legend = series_legend + self.variable_radius = False + self.x_label_angle = math.pi / 2.5 + self.circle_colors = circle_colors + + Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors) + + self.dash = None + if dash: + if hasattr(dash, "keys"): + self.dash = [dash[key] for key in self.series_labels] + elif max([hasattr(item,'__delitem__') for item in data]) : + self.dash = dash + else: + self.dash = [dash] + + self.load_errors(errorx, errory) + + def convert_list_to_tuple(self, data): + #Data must be converted from lists of coordinates to a single + # list of tuples + out_data = zip(*data) + if len(data) == 3: + self.variable_radius = True + return out_data + + def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): + #TODO: In cairoplot 2.0 keep only the Series instances + + # Convert Data and Group to Series + if isinstance(data, Data) or isinstance(data, Group): + data = Series(data) + + # Series + if isinstance(data, Series): + for group in data: + for item in group: + if len(item) is 3: + self.variable_radius = True + + #Dictionary with lists + if hasattr(data, "keys") : + if hasattr( data.values()[0][0], "__delitem__" ) : + for key in data.keys() : + data[key] = self.convert_list_to_tuple(data[key]) + elif len(data.values()[0][0]) == 3: + self.variable_radius = True + #List + elif hasattr(data[0], "__delitem__") : + #List of lists + if hasattr(data[0][0], "__delitem__") : + for index,value in enumerate(data) : + data[index] = self.convert_list_to_tuple(value) + #List + elif type(data[0][0]) != type((0,0)): + data = self.convert_list_to_tuple(data) + #Three dimensional data + elif len(data[0][0]) == 3: + self.variable_radius = True + + #List with three dimensional tuples + elif len(data[0]) == 3: + self.variable_radius = True + Plot.load_series(self, data, x_labels, y_labels, series_colors) + self.calc_boundaries() + self.calc_labels() + + def load_errors(self, errorx, errory): + self.errors = None + if errorx == None and errory == None: + return + self.errors = {} + self.errors[HORZ] = None + self.errors[VERT] = None + #asimetric errors + if errorx and hasattr(errorx[0], "__delitem__"): + self.errors[HORZ] = errorx + #simetric errors + elif errorx: + self.errors[HORZ] = [errorx] + #asimetric errors + if errory and hasattr(errory[0], "__delitem__"): + self.errors[VERT] = errory + #simetric errors + elif errory: + self.errors[VERT] = [errory] + + def calc_labels(self): + if not self.labels[HORZ]: + amplitude = self.bounds[HORZ][1] - self.bounds[HORZ][0] + if amplitude % 10: #if horizontal labels need floating points + self.labels[HORZ] = ["%.2lf" % (float(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ] + else: + self.labels[HORZ] = ["%d" % (int(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ] + if not self.labels[VERT]: + amplitude = self.bounds[VERT][1] - self.bounds[VERT][0] + if amplitude % 10: #if vertical labels need floating points + self.labels[VERT] = ["%.2lf" % (float(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ] + else: + self.labels[VERT] = ["%d" % (int(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ] + + def calc_extents(self, direction): + self.context.set_font_size(self.font_size * 0.8) + self.max_value[direction] = max(self.context.text_extents(item)[2] for item in self.labels[direction]) + self.borders[other_direction(direction)] = self.max_value[direction] + self.border + 20 + + def calc_boundaries(self): + #HORZ = 0, VERT = 1, NORM = 2 + min_data_value = [0,0,0] + max_data_value = [0,0,0] + + for group in self.series: + if type(group[0].content) in (int, float, long): + group = [Data((index, item.content)) for index,item in enumerate(group)] + + for point in group: + for index, item in enumerate(point.content): + if item > max_data_value[index]: + max_data_value[index] = item + elif item < min_data_value[index]: + min_data_value[index] = item + + if not self.bounds[HORZ]: + self.bounds[HORZ] = (min_data_value[HORZ], max_data_value[HORZ]) + if not self.bounds[VERT]: + self.bounds[VERT] = (min_data_value[VERT], max_data_value[VERT]) + if not self.bounds[NORM]: + self.bounds[NORM] = (min_data_value[NORM], max_data_value[NORM]) + + def calc_all_extents(self): + self.calc_extents(HORZ) + self.calc_extents(VERT) + + self.plot_height = self.dimensions[VERT] - 2 * self.borders[VERT] + self.plot_width = self.dimensions[HORZ] - 2* self.borders[HORZ] + + self.plot_top = self.dimensions[VERT] - self.borders[VERT] + + def calc_steps(self): + #Calculates all the x, y, z and color steps + series_amplitude = [self.bounds[index][1] - self.bounds[index][0] for index in range(3)] + + if series_amplitude[HORZ]: + self.horizontal_step = float (self.plot_width) / series_amplitude[HORZ] + else: + self.horizontal_step = 0.00 + + if series_amplitude[VERT]: + self.vertical_step = float (self.plot_height) / series_amplitude[VERT] + else: + self.vertical_step = 0.00 + + if series_amplitude[NORM]: + if self.variable_radius: + self.z_step = float (self.bounds[NORM][1]) / series_amplitude[NORM] + if self.circle_colors: + self.circle_color_step = tuple([float(self.circle_colors[1][i]-self.circle_colors[0][i])/series_amplitude[NORM] for i in range(4)]) + else: + self.z_step = 0.00 + self.circle_color_step = ( 0.0, 0.0, 0.0, 0.0 ) + + def get_circle_color(self, value): + return tuple( [self.circle_colors[0][i] + value*self.circle_color_step[i] for i in range(4)] ) + + def render(self): + Plot.render(self) + + self.calc_all_extents() + self.calc_steps() + self.render_background() + self.render_bounding_box() + if self.axis: + self.render_axis() + if self.grid: + self.render_grid() + self.render_labels() + self.render_plot() + if self.errors: + self.render_errors() + if self.series_legend and self.series_labels: + self.render_legend() + + def render_axis(self): + #Draws both the axis lines and their titles + cr = self.context + cr.set_source_rgba(*self.line_color) + cr.move_to(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) + cr.line_to(self.borders[HORZ], self.borders[VERT]) + cr.stroke() + + cr.move_to(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) + cr.line_to(self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) + cr.stroke() + + cr.set_source_rgba(*self.label_color) + self.context.set_font_size( 1.2 * self.font_size ) + if self.titles[HORZ]: + title_width,title_height = cr.text_extents(self.titles[HORZ])[2:4] + cr.move_to( self.dimensions[HORZ]/2 - title_width/2, self.borders[VERT] - title_height/2 ) + cr.show_text( self.titles[HORZ] ) + + if self.titles[VERT]: + title_width,title_height = cr.text_extents(self.titles[VERT])[2:4] + cr.move_to( self.dimensions[HORZ] - self.borders[HORZ] + title_height/2, self.dimensions[VERT]/2 - title_width/2) + cr.rotate( math.pi/2 ) + cr.show_text( self.titles[VERT] ) + cr.rotate( -math.pi/2 ) + + def render_grid(self): + cr = self.context + horizontal_step = float( self.plot_height ) / ( len( self.labels[VERT] ) - 1 ) + vertical_step = float( self.plot_width ) / ( len( self.labels[HORZ] ) - 1 ) + + x = self.borders[HORZ] + vertical_step + y = self.plot_top - horizontal_step + + for label in self.labels[HORZ][:-1]: + cr.set_source_rgba(*self.grid_color) + cr.move_to(x, self.dimensions[VERT] - self.borders[VERT]) + cr.line_to(x, self.borders[VERT]) + cr.stroke() + x += vertical_step + for label in self.labels[VERT][:-1]: + cr.set_source_rgba(*self.grid_color) + cr.move_to(self.borders[HORZ], y) + cr.line_to(self.dimensions[HORZ] - self.borders[HORZ], y) + cr.stroke() + y -= horizontal_step + + def render_labels(self): + self.context.set_font_size(self.font_size * 0.8) + self.render_horz_labels() + self.render_vert_labels() + + def render_horz_labels(self): + cr = self.context + step = float( self.plot_width ) / ( len( self.labels[HORZ] ) - 1 ) + x = self.borders[HORZ] + for item in self.labels[HORZ]: + cr.set_source_rgba(*self.label_color) + width = cr.text_extents(item)[2] + cr.move_to(x, self.dimensions[VERT] - self.borders[VERT] + 5) + cr.rotate(self.x_label_angle) + cr.show_text(item) + cr.rotate(-self.x_label_angle) + x += step + + def render_vert_labels(self): + cr = self.context + step = ( self.plot_height ) / ( len( self.labels[VERT] ) - 1 ) + y = self.plot_top + for item in self.labels[VERT]: + cr.set_source_rgba(*self.label_color) + width = cr.text_extents(item)[2] + cr.move_to(self.borders[HORZ] - width - 5,y) + cr.show_text(item) + y -= step + + def render_legend(self): + cr = self.context + cr.set_font_size(self.font_size) + cr.set_line_width(self.line_width) + + widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2]) + tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3]) + max_width = self.context.text_extents(widest_word)[2] + max_height = self.context.text_extents(tallest_word)[3] * 1.1 + + color_box_height = max_height / 2 + color_box_width = color_box_height * 2 + + #Draw a bounding box + bounding_box_width = max_width + color_box_width + 15 + bounding_box_height = (len(self.series_labels)+0.5) * max_height + cr.set_source_rgba(1,1,1) + cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - bounding_box_width, self.borders[VERT], + bounding_box_width, bounding_box_height) + cr.fill() + + cr.set_source_rgba(*self.line_color) + cr.set_line_width(self.line_width) + cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - bounding_box_width, self.borders[VERT], + bounding_box_width, bounding_box_height) + cr.stroke() + + for idx,key in enumerate(self.series_labels): + #Draw color box + cr.set_source_rgba(*self.series_colors[idx][:4]) + cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - max_width - color_box_width - 10, + self.borders[VERT] + color_box_height + (idx*max_height) , + color_box_width, color_box_height) + cr.fill() + + cr.set_source_rgba(0, 0, 0) + cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - max_width - color_box_width - 10, + self.borders[VERT] + color_box_height + (idx*max_height), + color_box_width, color_box_height) + cr.stroke() + + #Draw series labels + cr.set_source_rgba(0, 0, 0) + cr.move_to(self.dimensions[HORZ] - self.borders[HORZ] - max_width - 5, self.borders[VERT] + ((idx+1)*max_height)) + cr.show_text(key) + + def render_errors(self): + cr = self.context + cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) + cr.clip() + radius = self.dots + x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step + y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step + for index, group in enumerate(self.series): + cr.set_source_rgba(*self.series_colors[index][:4]) + for number, data in enumerate(group): + x = x0 + self.horizontal_step * data.content[0] + y = self.dimensions[VERT] - y0 - self.vertical_step * data.content[1] + if self.errors[HORZ]: + cr.move_to(x, y) + x1 = x - self.horizontal_step * self.errors[HORZ][0][number] + cr.line_to(x1, y) + cr.line_to(x1, y - radius) + cr.line_to(x1, y + radius) + cr.stroke() + if self.errors[HORZ] and len(self.errors[HORZ]) == 2: + cr.move_to(x, y) + x1 = x + self.horizontal_step * self.errors[HORZ][1][number] + cr.line_to(x1, y) + cr.line_to(x1, y - radius) + cr.line_to(x1, y + radius) + cr.stroke() + if self.errors[VERT]: + cr.move_to(x, y) + y1 = y + self.vertical_step * self.errors[VERT][0][number] + cr.line_to(x, y1) + cr.line_to(x - radius, y1) + cr.line_to(x + radius, y1) + cr.stroke() + if self.errors[VERT] and len(self.errors[VERT]) == 2: + cr.move_to(x, y) + y1 = y - self.vertical_step * self.errors[VERT][1][number] + cr.line_to(x, y1) + cr.line_to(x - radius, y1) + cr.line_to(x + radius, y1) + cr.stroke() + + + def render_plot(self): + """Draws the actual plot lines.""" + + cr = self.context + if self.discrete: + cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) + cr.clip() + x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step + y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step + radius = self.dots + for number, group in enumerate (self.series): + cr.set_source_rgba(*self.series_colors[number][:4]) + for data in group : + if self.variable_radius: + radius = data.content[2] * self.z_step + if self.circle_colors: + cr.set_source_rgba( *self.get_circle_color( data.content[2])) + x = x0 + self.horizontal_step * data.content[0] + y = y0 + self.vertical_step * data.content[1] + cr.arc(x, self.dimensions[VERT] - y, radius, 0, 2*math.pi) + cr.fill() + else: + cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) + cr.clip() + x0 = self.borders[HORZ] - self.bounds[HORZ][0] * self.horizontal_step + y0 = self.borders[VERT] - self.bounds[VERT][0] * self.vertical_step + + radius = self.dots + for number, group in enumerate (self.series): + last_data = None + cr.set_source_rgba(*self.series_colors[number][:4]) + for data in group : + x = x0 + self.horizontal_step * data.content[0] + y = y0 + self.vertical_step * data.content[1] + + # only draw a line for valid points + if y != y: # math.isnan only in 2.6+ + last_data = None + continue + + if self.dots: + if self.variable_radius: + radius = data.content[2]*self.z_step + cr.arc(x, self.dimensions[VERT] - y, radius, 0, 2*math.pi) + cr.fill() + if last_data: + old_x = x0 + self.horizontal_step * last_data.content[0] + old_y = y0 + self.vertical_step * last_data.content[1] + cr.move_to( old_x, self.dimensions[VERT] - old_y ) + cr.line_to( x, self.dimensions[VERT] - y) + cr.set_line_width(self.series_widths[number]) + + # Display line as dash line + if self.dash and self.dash[number]: + s = self.series_widths[number] + cr.set_dash([s*3, s*3], 0) + + cr.stroke() + cr.set_dash([]) + last_data = data + + + +class DotLinePlot(ScatterPlot): + def __init__(self, + surface=None, + data=None, + width=640, + height=480, + background=None, + border=0, + axis = False, + dash = False, + dots = 0, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + x_title = None, + y_title = None, + series_colors = None): + + ScatterPlot.__init__(self, surface, data, None, None, width, height, background, border, + axis, dash, False, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, None, x_title, y_title, series_colors, None ) + + + def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + for group in self.series : + for index,data in enumerate(group): + group[index].content = (index, data.content) + + self.calc_boundaries() + self.calc_labels() + +class FunctionPlot(ScatterPlot): + def __init__(self, + surface=None, + data=None, + width=640, + height=480, + background=None, + border=0, + axis = False, + discrete = False, + dots = 0, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + x_title = None, + y_title = None, + series_colors = None, + step = 1): + + self.function = data + + # step should not be zero + self.step = step + if self.step <= 0: + self.step = 1 + + self.discrete = discrete + + data, x_bounds = self.load_series_from_function( self.function, x_bounds ) + + ScatterPlot.__init__(self, surface, data, None, None, width, height, background, border, + axis, False, discrete, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, None, x_title, y_title, series_colors, None ) + + def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + + if len(self.series[0][0]) is 1: + for group_id, group in enumerate(self.series) : + for index,data in enumerate(group): + group[index].content = (self.bounds[HORZ][0] + self.step*index, data.content) + + self.calc_boundaries() + self.calc_labels() + + def load_series_from_function(self, function, x_bounds): + """Converts a function (or functions) into array of data. + + Multiple functions can be defined by a list of functions or + a dictionary of functions into its corresponding array of data. + """ + # TODO: Add the possibility for the user to define multiple functions with different discretization parameters + + series = Series() + + if isinstance(function, Group) or isinstance(function, Data): + function = Series(function) + + # is already a Series + # overwrite any bounds passed by the function + if isinstance(function, Series): + x_bounds = (function.range[0],function.range[-1]) + + # no bounds are provided + if x_bounds == None: + x_bounds = (0,10) + + # convert a single function into a "group" + def convert_function(singlefunction, group): + """Converts function into usable data. + + Math bounds errors correspond to nan values.""" + + def trygetpoint(inx): + """Attempt to evaluate point, returns nan on errors""" + try: + return singlefunction(inx) + except (ValueError, ZeroDivisionError, OverflowError): + return float("nan") + + i = x_bounds[0] + while i <= x_bounds[1]: + group.add_data(trygetpoint(i)) + i += self.step + + # TODO: Finish the dict translation + if hasattr(function, "keys"): #dictionary: + for key in function.keys(): + group = Group(name=key) + convert_function(function[key], group) + series.add_group(group) + + elif hasattr(function, "__delitem__"): #list of functions + for f in function: + group = Group() + convert_function(f, group) + series.add_group(group) + + elif isinstance(function, Series): # instance of Series + series = function + + else: # function + group = Group() + convert_function(function, group) + series.add_group(group) + + return series, x_bounds + + + def calc_labels(self): + """Create labels from bounds""" + + boundrange = float(self.bounds[HORZ][1] - self.bounds[HORZ][0]) + + # based on range, change number of decimals displayed + digits = 0 + if 0 < boundrange < 10: + digits = -math.floor(math.log10(boundrange)) + digits += 1 + labelformat = "%%.%df" % digits + + # make 10 labels (must be > 0) + boundstep = boundrange / 10 + if boundstep <= 0: + boundstep = 1 + + # create string for each label + if not self.labels[HORZ]: + self.labels[HORZ] = [] + i = self.bounds[HORZ][0] + while i<=self.bounds[HORZ][1]: + self.labels[HORZ].append(labelformat % i) + i += boundstep + ScatterPlot.calc_labels(self) + + + def render_plot(self): + if not self.discrete: + ScatterPlot.render_plot(self) + else: + last = None + cr = self.context + for number, group in enumerate (self.series): + cr.set_source_rgba(*self.series_colors[number][:4]) + x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step + y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step + for data in group: + x = x0 + self.horizontal_step * data.content[0] + y = y0 + self.vertical_step * data.content[1] + + cr.move_to(x, self.dimensions[VERT] - y) + cr.line_to(x, self.plot_top) + cr.set_line_width(self.series_widths[number]) + cr.stroke() + if self.dots: + cr.new_path() + cr.arc(x, self.dimensions[VERT] - y, 3, 0, 2.1 * math.pi) + cr.close_path() + cr.fill() + +class BarPlot(Plot): + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + series_colors = None, + main_dir = None): + + self.bounds = {} + self.bounds[HORZ] = x_bounds + self.bounds[VERT] = y_bounds + self.display_values = display_values + self.grid = grid + self.rounded_corners = rounded_corners + self.stack = stack + self.three_dimension = three_dimension + self.x_label_angle = math.pi / 2.5 + self.main_dir = main_dir + self.max_value = {} + self.plot_dimensions = {} + self.steps = {} + self.value_label_color = (0.5,0.5,0.5,1.0) + + Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors) + + def load_series(self, data, x_labels = None, y_labels = None, series_colors = None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + self.calc_boundaries() + + def process_colors(self, series_colors): + #Data for a BarPlot might be a List or a List of Lists. + #On the first case, colors must be generated for all bars, + #On the second, colors must be generated for each of the inner lists. + + #TODO: Didn't get it... + #if hasattr(self.data[0], '__getitem__'): + # length = max(len(series) for series in self.data) + #else: + # length = len( self.data ) + + length = max(len(group) for group in self.series) + + Plot.process_colors( self, series_colors, length, 'linear') + + def calc_boundaries(self): + if not self.bounds[self.main_dir]: + if self.stack: + max_data_value = max(sum(group.to_list()) for group in self.series) + else: + max_data_value = max(max(group.to_list()) for group in self.series) + self.bounds[self.main_dir] = (0, max_data_value) + if not self.bounds[other_direction(self.main_dir)]: + self.bounds[other_direction(self.main_dir)] = (0, len(self.series)) + + def calc_extents(self, direction): + self.max_value[direction] = 0 + if self.labels[direction]: + widest_word = max(self.labels[direction], key = lambda item: self.context.text_extents(item)[2]) + self.max_value[direction] = self.context.text_extents(widest_word)[3 - direction] + self.borders[other_direction(direction)] = (2-direction)*self.max_value[direction] + self.border + direction*(5) + else: + self.borders[other_direction(direction)] = self.border + + def calc_horz_extents(self): + self.calc_extents(HORZ) + + def calc_vert_extents(self): + self.calc_extents(VERT) + + def calc_all_extents(self): + self.calc_horz_extents() + self.calc_vert_extents() + other_dir = other_direction(self.main_dir) + self.value_label = 0 + if self.display_values: + if self.stack: + self.value_label = self.context.text_extents(str(max(sum(group.to_list()) for group in self.series)))[2 + self.main_dir] + else: + self.value_label = self.context.text_extents(str(max(max(group.to_list()) for group in self.series)))[2 + self.main_dir] + if self.labels[self.main_dir]: + self.plot_dimensions[self.main_dir] = self.dimensions[self.main_dir] - 2*self.borders[self.main_dir] - self.value_label + else: + self.plot_dimensions[self.main_dir] = self.dimensions[self.main_dir] - self.borders[self.main_dir] - 1.2*self.border - self.value_label + self.plot_dimensions[other_dir] = self.dimensions[other_dir] - self.borders[other_dir] - self.border + self.plot_top = self.dimensions[VERT] - self.borders[VERT] + + def calc_steps(self): + other_dir = other_direction(self.main_dir) + self.series_amplitude = self.bounds[self.main_dir][1] - self.bounds[self.main_dir][0] + if self.series_amplitude: + self.steps[self.main_dir] = float(self.plot_dimensions[self.main_dir])/self.series_amplitude + else: + self.steps[self.main_dir] = 0.00 + series_length = len(self.series) + self.steps[other_dir] = float(self.plot_dimensions[other_dir])/(series_length + 0.1*(series_length + 1)) + self.space = 0.1*self.steps[other_dir] + + def render(self): + Plot.render(self) + + self.calc_all_extents() + self.calc_steps() + self.render_background() + self.render_bounding_box() + if self.grid: + self.render_grid() + if self.three_dimension: + self.render_ground() + if self.display_values: + self.render_values() + self.render_labels() + self.render_plot() + if self.series_labels: + self.render_legend() + + def draw_3d_rectangle_front(self, x0, y0, x1, y1, shift): + self.context.rectangle(x0-shift, y0+shift, x1-x0, y1-y0) + + def draw_3d_rectangle_side(self, x0, y0, x1, y1, shift): + self.context.move_to(x1-shift,y0+shift) + self.context.line_to(x1, y0) + self.context.line_to(x1, y1) + self.context.line_to(x1-shift, y1+shift) + self.context.line_to(x1-shift, y0+shift) + self.context.close_path() + + def draw_3d_rectangle_top(self, x0, y0, x1, y1, shift): + self.context.move_to(x0-shift,y0+shift) + self.context.line_to(x0, y0) + self.context.line_to(x1, y0) + self.context.line_to(x1-shift, y0+shift) + self.context.line_to(x0-shift, y0+shift) + self.context.close_path() + + def draw_round_rectangle(self, x0, y0, x1, y1): + self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) + self.context.line_to(x1-5, y0) + self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) + self.context.line_to(x1, y1-5) + self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) + self.context.line_to(x0+5, y1) + self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) + self.context.line_to(x0, y0+5) + self.context.close_path() + + def render_ground(self): + self.draw_3d_rectangle_front(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + self.draw_3d_rectangle_side (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + self.draw_3d_rectangle_top (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + def render_labels(self): + self.context.set_font_size(self.font_size * 0.8) + if self.labels[HORZ]: + self.render_horz_labels() + if self.labels[VERT]: + self.render_vert_labels() + + def render_legend(self): + cr = self.context + cr.set_font_size(self.font_size) + cr.set_line_width(self.line_width) + + widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2]) + tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3]) + max_width = self.context.text_extents(widest_word)[2] + max_height = self.context.text_extents(tallest_word)[3] * 1.1 + 5 + + color_box_height = max_height / 2 + color_box_width = color_box_height * 2 + + #Draw a bounding box + bounding_box_width = max_width + color_box_width + 15 + bounding_box_height = (len(self.series_labels)+0.5) * max_height + cr.set_source_rgba(1,1,1) + cr.rectangle(self.dimensions[HORZ] - self.border - bounding_box_width, self.border, + bounding_box_width, bounding_box_height) + cr.fill() + + cr.set_source_rgba(*self.line_color) + cr.set_line_width(self.line_width) + cr.rectangle(self.dimensions[HORZ] - self.border - bounding_box_width, self.border, + bounding_box_width, bounding_box_height) + cr.stroke() + + for idx,key in enumerate(self.series_labels): + #Draw color box + cr.set_source_rgba(*self.series_colors[idx][:4]) + cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10, + self.border + color_box_height + (idx*max_height) , + color_box_width, color_box_height) + cr.fill() + + cr.set_source_rgba(0, 0, 0) + cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10, + self.border + color_box_height + (idx*max_height), + color_box_width, color_box_height) + cr.stroke() + + #Draw series labels + cr.set_source_rgba(0, 0, 0) + cr.move_to(self.dimensions[HORZ] - self.border - max_width - 5, self.border + ((idx+1)*max_height)) + cr.show_text(key) + + +class HorizontalBarPlot(BarPlot): + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + series_labels = None, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + series_colors = None): + + BarPlot.__init__(self, surface, data, width, height, background, border, + display_values, grid, rounded_corners, stack, three_dimension, + x_labels, y_labels, x_bounds, y_bounds, series_colors, HORZ) + self.series_labels = series_labels + + def calc_vert_extents(self): + self.calc_extents(VERT) + if self.labels[HORZ] and not self.labels[VERT]: + self.borders[HORZ] += 10 + + def draw_rectangle_bottom(self, x0, y0, x1, y1): + self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) + self.context.line_to(x0, y0+5) + self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) + self.context.line_to(x1, y0) + self.context.line_to(x1, y1) + self.context.line_to(x0+5, y1) + self.context.close_path() + + def draw_rectangle_top(self, x0, y0, x1, y1): + self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) + self.context.line_to(x1, y1-5) + self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) + self.context.line_to(x0, y1) + self.context.line_to(x0, y0) + self.context.line_to(x1, y0) + self.context.close_path() + + def draw_rectangle(self, index, length, x0, y0, x1, y1): + if length == 1: + BarPlot.draw_rectangle(self, x0, y0, x1, y1) + elif index == 0: + self.draw_rectangle_bottom(x0, y0, x1, y1) + elif index == length-1: + self.draw_rectangle_top(x0, y0, x1, y1) + else: + self.context.rectangle(x0, y0, x1-x0, y1-y0) + + #TODO: Review BarPlot.render_grid code + def render_grid(self): + self.context.set_source_rgba(0.8, 0.8, 0.8) + if self.labels[HORZ]: + self.context.set_font_size(self.font_size * 0.8) + step = (self.dimensions[HORZ] - 2*self.borders[HORZ] - self.value_label)/(len(self.labels[HORZ])-1) + x = self.borders[HORZ] + next_x = 0 + for item in self.labels[HORZ]: + width = self.context.text_extents(item)[2] + if x - width/2 > next_x and x - width/2 > self.border: + self.context.move_to(x, self.border) + self.context.line_to(x, self.dimensions[VERT] - self.borders[VERT]) + self.context.stroke() + next_x = x + width/2 + x += step + else: + lines = 11 + horizontal_step = float(self.plot_dimensions[HORZ])/(lines-1) + x = self.borders[HORZ] + for y in xrange(0, lines): + self.context.move_to(x, self.border) + self.context.line_to(x, self.dimensions[VERT] - self.borders[VERT]) + self.context.stroke() + x += horizontal_step + + def render_horz_labels(self): + step = (self.dimensions[HORZ] - 2*self.borders[HORZ])/(len(self.labels[HORZ])-1) + x = self.borders[HORZ] + next_x = 0 + + for item in self.labels[HORZ]: + self.context.set_source_rgba(*self.label_color) + width = self.context.text_extents(item)[2] + if x - width/2 > next_x and x - width/2 > self.border: + self.context.move_to(x - width/2, self.dimensions[VERT] - self.borders[VERT] + self.max_value[HORZ] + 3) + self.context.show_text(item) + next_x = x + width/2 + x += step + + def render_vert_labels(self): + series_length = len(self.labels[VERT]) + step = (self.plot_dimensions[VERT] - (series_length + 1)*self.space)/(len(self.labels[VERT])) + y = self.border + step/2 + self.space + + for item in self.labels[VERT]: + self.context.set_source_rgba(*self.label_color) + width, height = self.context.text_extents(item)[2:4] + self.context.move_to(self.borders[HORZ] - width - 5, y + height/2) + self.context.show_text(item) + y += step + self.space + self.labels[VERT].reverse() + + def render_values(self): + self.context.set_source_rgba(*self.value_label_color) + self.context.set_font_size(self.font_size * 0.8) + if self.stack: + for i,group in enumerate(self.series): + value = sum(group.to_list()) + height = self.context.text_extents(str(value))[3] + x = self.borders[HORZ] + value*self.steps[HORZ] + 2 + y = self.borders[VERT] + (i+0.5)*self.steps[VERT] + (i+1)*self.space + height/2 + self.context.move_to(x, y) + self.context.show_text(str(value)) + else: + for i,group in enumerate(self.series): + inner_step = self.steps[VERT]/len(group) + y0 = self.border + i*self.steps[VERT] + (i+1)*self.space + for number,data in enumerate(group): + height = self.context.text_extents(str(data.content))[3] + self.context.move_to(self.borders[HORZ] + data.content*self.steps[HORZ] + 2, y0 + 0.5*inner_step + height/2, ) + self.context.show_text(str(data.content)) + y0 += inner_step + + def render_plot(self): + if self.stack: + for i,group in enumerate(self.series): + x0 = self.borders[HORZ] + y0 = self.borders[VERT] + i*self.steps[VERT] + (i+1)*self.space + for number,data in enumerate(group): + if self.series_colors[number][4] in ('radial','linear') : + linear = cairo.LinearGradient( data.content*self.steps[HORZ]/2, y0, data.content*self.steps[HORZ]/2, y0 + self.steps[VERT] ) + color = self.series_colors[number] + linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1.0, *color[:4]) + self.context.set_source(linear) + elif self.series_colors[number][4] == 'solid': + self.context.set_source_rgba(*self.series_colors[number][:4]) + if self.rounded_corners: + self.draw_rectangle(number, len(group), x0, y0, x0+data.content*self.steps[HORZ], y0+self.steps[VERT]) + self.context.fill() + else: + self.context.rectangle(x0, y0, data.content*self.steps[HORZ], self.steps[VERT]) + self.context.fill() + x0 += data.content*self.steps[HORZ] + else: + for i,group in enumerate(self.series): + inner_step = self.steps[VERT]/len(group) + x0 = self.borders[HORZ] + y0 = self.border + i*self.steps[VERT] + (i+1)*self.space + for number,data in enumerate(group): + linear = cairo.LinearGradient(data.content*self.steps[HORZ]/2, y0, data.content*self.steps[HORZ]/2, y0 + inner_step) + color = self.series_colors[number] + linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1.0, *color[:4]) + self.context.set_source(linear) + if self.rounded_corners and data.content != 0: + BarPlot.draw_round_rectangle(self,x0, y0, x0 + data.content*self.steps[HORZ], y0 + inner_step) + self.context.fill() + else: + self.context.rectangle(x0, y0, data.content*self.steps[HORZ], inner_step) + self.context.fill() + y0 += inner_step + +class VerticalBarPlot(BarPlot): + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + series_labels = None, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + series_colors = None): + + BarPlot.__init__(self, surface, data, width, height, background, border, + display_values, grid, rounded_corners, stack, three_dimension, + x_labels, y_labels, x_bounds, y_bounds, series_colors, VERT) + self.series_labels = series_labels + + def calc_vert_extents(self): + self.calc_extents(VERT) + if self.labels[VERT] and not self.labels[HORZ]: + self.borders[VERT] += 10 + + def draw_rectangle_bottom(self, x0, y0, x1, y1): + self.context.move_to(x1,y1) + self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) + self.context.line_to(x0+5, y1) + self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) + self.context.line_to(x0, y0) + self.context.line_to(x1, y0) + self.context.line_to(x1, y1) + self.context.close_path() + + def draw_rectangle_top(self, x0, y0, x1, y1): + self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) + self.context.line_to(x1-5, y0) + self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) + self.context.line_to(x1, y1) + self.context.line_to(x0, y1) + self.context.line_to(x0, y0) + self.context.close_path() + + def draw_rectangle(self, index, length, x0, y0, x1, y1): + if length == 1: + BarPlot.draw_rectangle(self, x0, y0, x1, y1) + elif index == 0: + self.draw_rectangle_bottom(x0, y0, x1, y1) + elif index == length-1: + self.draw_rectangle_top(x0, y0, x1, y1) + else: + self.context.rectangle(x0, y0, x1-x0, y1-y0) + + def render_grid(self): + self.context.set_source_rgba(0.8, 0.8, 0.8) + if self.labels[VERT]: + lines = len(self.labels[VERT]) + vertical_step = float(self.plot_dimensions[self.main_dir])/(lines-1) + y = self.borders[VERT] + self.value_label + else: + lines = 11 + vertical_step = float(self.plot_dimensions[self.main_dir])/(lines-1) + y = 1.2*self.border + self.value_label + for x in xrange(0, lines): + self.context.move_to(self.borders[HORZ], y) + self.context.line_to(self.dimensions[HORZ] - self.border, y) + self.context.stroke() + y += vertical_step + + def render_ground(self): + self.draw_3d_rectangle_front(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + self.draw_3d_rectangle_side (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + self.draw_3d_rectangle_top (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + def render_horz_labels(self): + series_length = len(self.labels[HORZ]) + step = float (self.plot_dimensions[HORZ] - (series_length + 1)*self.space)/len(self.labels[HORZ]) + x = self.borders[HORZ] + step/2 + self.space + next_x = 0 + + for item in self.labels[HORZ]: + self.context.set_source_rgba(*self.label_color) + width = self.context.text_extents(item)[2] + if x - width/2 > next_x and x - width/2 > self.borders[HORZ]: + self.context.move_to(x - width/2, self.dimensions[VERT] - self.borders[VERT] + self.max_value[HORZ] + 3) + self.context.show_text(item) + next_x = x + width/2 + x += step + self.space + + def render_vert_labels(self): + self.context.set_source_rgba(*self.label_color) + y = self.borders[VERT] + self.value_label + step = (self.dimensions[VERT] - 2*self.borders[VERT] - self.value_label)/(len(self.labels[VERT]) - 1) + self.labels[VERT].reverse() + for item in self.labels[VERT]: + width, height = self.context.text_extents(item)[2:4] + self.context.move_to(self.borders[HORZ] - width - 5, y + height/2) + self.context.show_text(item) + y += step + self.labels[VERT].reverse() + + def render_values(self): + self.context.set_source_rgba(*self.value_label_color) + self.context.set_font_size(self.font_size * 0.8) + if self.stack: + for i,group in enumerate(self.series): + value = sum(group.to_list()) + width = self.context.text_extents(str(value))[2] + x = self.borders[HORZ] + (i+0.5)*self.steps[HORZ] + (i+1)*self.space - width/2 + y = value*self.steps[VERT] + 2 + self.context.move_to(x, self.plot_top-y) + self.context.show_text(str(value)) + else: + for i,group in enumerate(self.series): + inner_step = self.steps[HORZ]/len(group) + x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space + for number,data in enumerate(group): + width = self.context.text_extents(str(data.content))[2] + self.context.move_to(x0 + 0.5*inner_step - width/2, self.plot_top - data.content*self.steps[VERT] - 2) + self.context.show_text(str(data.content)) + x0 += inner_step + + def render_plot(self): + if self.stack: + for i,group in enumerate(self.series): + x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space + y0 = 0 + for number,data in enumerate(group): + if self.series_colors[number][4] in ('linear','radial'): + linear = cairo.LinearGradient( x0, data.content*self.steps[VERT]/2, x0 + self.steps[HORZ], data.content*self.steps[VERT]/2 ) + color = self.series_colors[number] + linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1.0, *color[:4]) + self.context.set_source(linear) + elif self.series_colors[number][4] == 'solid': + self.context.set_source_rgba(*self.series_colors[number][:4]) + if self.rounded_corners: + self.draw_rectangle(number, len(group), x0, self.plot_top - y0 - data.content*self.steps[VERT], x0 + self.steps[HORZ], self.plot_top - y0) + self.context.fill() + else: + self.context.rectangle(x0, self.plot_top - y0 - data.content*self.steps[VERT], self.steps[HORZ], data.content*self.steps[VERT]) + self.context.fill() + y0 += data.content*self.steps[VERT] + else: + for i,group in enumerate(self.series): + inner_step = self.steps[HORZ]/len(group) + y0 = self.borders[VERT] + x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space + for number,data in enumerate(group): + if self.series_colors[number][4] == 'linear': + linear = cairo.LinearGradient( x0, data.content*self.steps[VERT]/2, x0 + inner_step, data.content*self.steps[VERT]/2 ) + color = self.series_colors[number] + linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1.0, *color[:4]) + self.context.set_source(linear) + elif self.series_colors[number][4] == 'solid': + self.context.set_source_rgba(*self.series_colors[number][:4]) + if self.rounded_corners and data.content != 0: + BarPlot.draw_round_rectangle(self, x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top) + self.context.fill() + elif self.three_dimension: + self.draw_3d_rectangle_front(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) + self.context.fill() + self.draw_3d_rectangle_side(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) + self.context.fill() + self.draw_3d_rectangle_top(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) + self.context.fill() + else: + self.context.rectangle(x0, self.plot_top - data.content*self.steps[VERT], inner_step, data.content*self.steps[VERT]) + self.context.fill() + + x0 += inner_step + +class StreamChart(VerticalBarPlot): + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + grid = False, + series_legend = None, + x_labels = None, + x_bounds = None, + y_bounds = None, + series_colors = None): + + VerticalBarPlot.__init__(self, surface, data, width, height, background, border, + False, grid, False, True, False, + None, x_labels, None, x_bounds, y_bounds, series_colors) + + def calc_steps(self): + other_dir = other_direction(self.main_dir) + self.series_amplitude = self.bounds[self.main_dir][1] - self.bounds[self.main_dir][0] + if self.series_amplitude: + self.steps[self.main_dir] = float(self.plot_dimensions[self.main_dir])/self.series_amplitude + else: + self.steps[self.main_dir] = 0.00 + series_length = len(self.data) + self.steps[other_dir] = float(self.plot_dimensions[other_dir])/series_length + + def render_legend(self): + pass + + def ground(self, index): + sum_values = sum(self.data[index]) + return -0.5*sum_values + + def calc_angles(self): + middle = self.plot_top - self.plot_dimensions[VERT]/2.0 + self.angles = [tuple([0.0 for x in range(len(self.data)+1)])] + for x_index in range(1, len(self.data)-1): + t = [] + x0 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] + x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] + y0 = middle - self.ground(x_index-1)*self.steps[VERT] + y2 = middle - self.ground(x_index+1)*self.steps[VERT] + t.append(math.atan(float(y0-y2)/(x0-x2))) + for data_index in range(len(self.data[x_index])): + x0 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] + x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] + y0 = middle - self.ground(x_index-1)*self.steps[VERT] - self.data[x_index-1][data_index]*self.steps[VERT] + y2 = middle - self.ground(x_index+1)*self.steps[VERT] - self.data[x_index+1][data_index]*self.steps[VERT] + + for i in range(0,data_index): + y0 -= self.data[x_index-1][i]*self.steps[VERT] + y2 -= self.data[x_index+1][i]*self.steps[VERT] + + if data_index == len(self.data[0])-1 and False: + self.context.set_source_rgba(0.0,0.0,0.0,0.3) + self.context.move_to(x0,y0) + self.context.line_to(x2,y2) + self.context.stroke() + self.context.arc(x0,y0,2,0,2*math.pi) + self.context.fill() + t.append(math.atan(float(y0-y2)/(x0-x2))) + self.angles.append(tuple(t)) + self.angles.append(tuple([0.0 for x in range(len(self.data)+1)])) + + def render_plot(self): + self.calc_angles() + middle = self.plot_top - self.plot_dimensions[VERT]/2.0 + p = 0.4*self.steps[HORZ] + for data_index in range(len(self.data[0])-1,-1,-1): + self.context.set_source_rgba(*self.series_colors[data_index][:4]) + + #draw the upper line + for x_index in range(len(self.data)-1) : + x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] + y1 = middle - self.ground(x_index)*self.steps[VERT] - self.data[x_index][data_index]*self.steps[VERT] + x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] + y2 = middle - self.ground(x_index + 1)*self.steps[VERT] - self.data[x_index + 1][data_index]*self.steps[VERT] + + for i in range(0,data_index): + y1 -= self.data[x_index][i]*self.steps[VERT] + y2 -= self.data[x_index+1][i]*self.steps[VERT] + + if x_index == 0: + self.context.move_to(x1,y1) + + ang1 = self.angles[x_index][data_index+1] + ang2 = self.angles[x_index+1][data_index+1] + math.pi + self.context.curve_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1), + x2+p*math.cos(ang2),y2+p*math.sin(ang2), + x2,y2) + + for x_index in range(len(self.data)-1,0,-1) : + x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] + y1 = middle - self.ground(x_index)*self.steps[VERT] + x2 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] + y2 = middle - self.ground(x_index - 1)*self.steps[VERT] + + for i in range(0,data_index): + y1 -= self.data[x_index][i]*self.steps[VERT] + y2 -= self.data[x_index-1][i]*self.steps[VERT] + + if x_index == len(self.data)-1: + self.context.line_to(x1,y1+2) + + #revert angles by pi degrees to take the turn back + ang1 = self.angles[x_index][data_index] + math.pi + ang2 = self.angles[x_index-1][data_index] + self.context.curve_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1), + x2+p*math.cos(ang2),y2+p*math.sin(ang2), + x2,y2+2) + + self.context.close_path() + self.context.fill() + + if False: + self.context.move_to(self.borders[HORZ] + 0.5*self.steps[HORZ], middle) + for x_index in range(len(self.data)-1) : + x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] + y1 = middle - self.ground(x_index)*self.steps[VERT] - self.data[x_index][data_index]*self.steps[VERT] + x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] + y2 = middle - self.ground(x_index + 1)*self.steps[VERT] - self.data[x_index + 1][data_index]*self.steps[VERT] + + for i in range(0,data_index): + y1 -= self.data[x_index][i]*self.steps[VERT] + y2 -= self.data[x_index+1][i]*self.steps[VERT] + + ang1 = self.angles[x_index][data_index+1] + ang2 = self.angles[x_index+1][data_index+1] + math.pi + self.context.set_source_rgba(1.0,0.0,0.0) + self.context.arc(x1+p*math.cos(ang1),y1+p*math.sin(ang1),2,0,2*math.pi) + self.context.fill() + self.context.set_source_rgba(0.0,0.0,0.0) + self.context.arc(x2+p*math.cos(ang2),y2+p*math.sin(ang2),2,0,2*math.pi) + self.context.fill() + """self.context.set_source_rgba(0.0,0.0,0.0,0.3) + self.context.arc(x2,y2,2,0,2*math.pi) + self.context.fill()""" + self.context.move_to(x1,y1) + self.context.line_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1)) + self.context.stroke() + self.context.move_to(x2,y2) + self.context.line_to(x2+p*math.cos(ang2),y2+p*math.sin(ang2)) + self.context.stroke() + if False: + for x_index in range(len(self.data)-1,0,-1) : + x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] + y1 = middle - self.ground(x_index)*self.steps[VERT] + x2 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] + y2 = middle - self.ground(x_index - 1)*self.steps[VERT] + + for i in range(0,data_index): + y1 -= self.data[x_index][i]*self.steps[VERT] + y2 -= self.data[x_index-1][i]*self.steps[VERT] + + #revert angles by pi degrees to take the turn back + ang1 = self.angles[x_index][data_index] + math.pi + ang2 = self.angles[x_index-1][data_index] + self.context.set_source_rgba(0.0,1.0,0.0) + self.context.arc(x1+p*math.cos(ang1),y1+p*math.sin(ang1),2,0,2*math.pi) + self.context.fill() + self.context.set_source_rgba(0.0,0.0,1.0) + self.context.arc(x2+p*math.cos(ang2),y2+p*math.sin(ang2),2,0,2*math.pi) + self.context.fill() + """self.context.set_source_rgba(0.0,0.0,0.0,0.3) + self.context.arc(x2,y2,2,0,2*math.pi) + self.context.fill()""" + self.context.move_to(x1,y1) + self.context.line_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1)) + self.context.stroke() + self.context.move_to(x2,y2) + self.context.line_to(x2+p*math.cos(ang2),y2+p*math.sin(ang2)) + self.context.stroke() + #break + + #self.context.arc(self.dimensions[HORZ]/2, self.dimensions[VERT]/2,50,0,3*math.pi/2) + #self.context.fill() + + +class PiePlot(Plot): + #TODO: Check the old cairoplot, graphs aren't matching + def __init__ (self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + gradient = False, + shadow = False, + colors = None): + + Plot.__init__( self, surface, data, width, height, background, series_colors = colors ) + self.center = (self.dimensions[HORZ]/2, self.dimensions[VERT]/2) + self.total = sum( self.series.to_list() ) + self.radius = min(self.dimensions[HORZ]/3,self.dimensions[VERT]/3) + self.gradient = gradient + self.shadow = shadow + + def sort_function(x,y): + return x.content - y.content + + def load_series(self, data, x_labels=None, y_labels=None, series_colors=None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + # Already done inside series + #self.data = sorted(self.data) + + def draw_piece(self, angle, next_angle): + self.context.move_to(self.center[0],self.center[1]) + self.context.line_to(self.center[0] + self.radius*math.cos(angle), self.center[1] + self.radius*math.sin(angle)) + self.context.arc(self.center[0], self.center[1], self.radius, angle, next_angle) + self.context.line_to(self.center[0], self.center[1]) + self.context.close_path() + + def render(self): + Plot.render(self) + + self.render_background() + self.render_bounding_box() + if self.shadow: + self.render_shadow() + self.render_plot() + self.render_series_labels() + + def render_shadow(self): + horizontal_shift = 3 + vertical_shift = 3 + self.context.set_source_rgba(0, 0, 0, 0.5) + self.context.arc(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.radius, 0, 2*math.pi) + self.context.fill() + + def render_series_labels(self): + angle = 0 + next_angle = 0 + x0,y0 = self.center + cr = self.context + for number,key in enumerate(self.series_labels): + # self.data[number] should be just a number + data = sum(self.series[number].to_list()) + + next_angle = angle + 2.0*math.pi*data/self.total + cr.set_source_rgba(*self.series_colors[number][:4]) + w = cr.text_extents(key)[2] + if (angle + next_angle)/2 < math.pi/2 or (angle + next_angle)/2 > 3*math.pi/2: + cr.move_to(x0 + (self.radius+10)*math.cos((angle+next_angle)/2), y0 + (self.radius+10)*math.sin((angle+next_angle)/2) ) + else: + cr.move_to(x0 + (self.radius+10)*math.cos((angle+next_angle)/2) - w, y0 + (self.radius+10)*math.sin((angle+next_angle)/2) ) + cr.show_text(key) + angle = next_angle + + def render_plot(self): + angle = 0 + next_angle = 0 + x0,y0 = self.center + cr = self.context + for number,group in enumerate(self.series): + # Group should be just a number + data = sum(group.to_list()) + next_angle = angle + 2.0*math.pi*data/self.total + if self.gradient or self.series_colors[number][4] in ('linear','radial'): + gradient_color = cairo.RadialGradient(self.center[0], self.center[1], 0, self.center[0], self.center[1], self.radius) + gradient_color.add_color_stop_rgba(0.3, *self.series_colors[number][:4]) + gradient_color.add_color_stop_rgba(1, self.series_colors[number][0]*0.7, + self.series_colors[number][1]*0.7, + self.series_colors[number][2]*0.7, + self.series_colors[number][3]) + cr.set_source(gradient_color) + else: + cr.set_source_rgba(*self.series_colors[number][:4]) + + self.draw_piece(angle, next_angle) + cr.fill() + + cr.set_source_rgba(1.0, 1.0, 1.0) + self.draw_piece(angle, next_angle) + cr.stroke() + + angle = next_angle + +class DonutPlot(PiePlot): + def __init__ (self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + gradient = False, + shadow = False, + colors = None, + inner_radius=-1): + + Plot.__init__( self, surface, data, width, height, background, series_colors = colors ) + + self.center = ( self.dimensions[HORZ]/2, self.dimensions[VERT]/2 ) + self.total = sum( self.series.to_list() ) + self.radius = min( self.dimensions[HORZ]/3,self.dimensions[VERT]/3 ) + self.inner_radius = inner_radius*self.radius + + if inner_radius == -1: + self.inner_radius = self.radius/3 + + self.gradient = gradient + self.shadow = shadow + + def draw_piece(self, angle, next_angle): + self.context.move_to(self.center[0] + (self.inner_radius)*math.cos(angle), self.center[1] + (self.inner_radius)*math.sin(angle)) + self.context.line_to(self.center[0] + self.radius*math.cos(angle), self.center[1] + self.radius*math.sin(angle)) + self.context.arc(self.center[0], self.center[1], self.radius, angle, next_angle) + self.context.line_to(self.center[0] + (self.inner_radius)*math.cos(next_angle), self.center[1] + (self.inner_radius)*math.sin(next_angle)) + self.context.arc_negative(self.center[0], self.center[1], self.inner_radius, next_angle, angle) + self.context.close_path() + + def render_shadow(self): + horizontal_shift = 3 + vertical_shift = 3 + self.context.set_source_rgba(0, 0, 0, 0.5) + self.context.arc(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.inner_radius, 0, 2*math.pi) + self.context.arc_negative(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.radius, 0, -2*math.pi) + self.context.fill() + +class GanttChart (Plot) : + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + x_labels = None, + y_labels = None, + colors = None): + self.bounds = {} + self.max_value = {} + Plot.__init__(self, surface, data, width, height, x_labels = x_labels, y_labels = y_labels, series_colors = colors) + + def load_series(self, data, x_labels=None, y_labels=None, series_colors=None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + self.calc_boundaries() + + def calc_boundaries(self): + self.bounds[HORZ] = (0,len(self.series)) + end_pos = max(self.series.to_list()) + + #for group in self.series: + # if hasattr(item, "__delitem__"): + # for sub_item in item: + # end_pos = max(sub_item) + # else: + # end_pos = max(item) + self.bounds[VERT] = (0,end_pos) + + def calc_extents(self, direction): + self.max_value[direction] = 0 + if self.labels[direction]: + self.max_value[direction] = max(self.context.text_extents(item)[2] for item in self.labels[direction]) + else: + self.max_value[direction] = self.context.text_extents( str(self.bounds[direction][1] + 1) )[2] + + def calc_horz_extents(self): + self.calc_extents(HORZ) + self.borders[HORZ] = 100 + self.max_value[HORZ] + + def calc_vert_extents(self): + self.calc_extents(VERT) + self.borders[VERT] = self.dimensions[VERT]/(self.bounds[HORZ][1] + 1) + + def calc_steps(self): + self.horizontal_step = (self.dimensions[HORZ] - self.borders[HORZ])/(len(self.labels[VERT])) + self.vertical_step = self.borders[VERT] + + def render(self): + Plot.render(self) + + self.calc_horz_extents() + self.calc_vert_extents() + self.calc_steps() + self.render_background() + + self.render_labels() + self.render_grid() + self.render_plot() + + def render_background(self): + cr = self.context + cr.set_source_rgba(255,255,255) + cr.rectangle(0,0,self.dimensions[HORZ], self.dimensions[VERT]) + cr.fill() + for number,group in enumerate(self.series): + linear = cairo.LinearGradient(self.dimensions[HORZ]/2, self.borders[VERT] + number*self.vertical_step, + self.dimensions[HORZ]/2, self.borders[VERT] + (number+1)*self.vertical_step) + linear.add_color_stop_rgba(0,1.0,1.0,1.0,1.0) + linear.add_color_stop_rgba(1.0,0.9,0.9,0.9,1.0) + cr.set_source(linear) + cr.rectangle(0,self.borders[VERT] + number*self.vertical_step,self.dimensions[HORZ],self.vertical_step) + cr.fill() + + def render_grid(self): + cr = self.context + cr.set_source_rgba(0.7, 0.7, 0.7) + cr.set_dash((1,0,0,0,0,0,1)) + cr.set_line_width(0.5) + for number,label in enumerate(self.labels[VERT]): + h = cr.text_extents(label)[3] + cr.move_to(self.borders[HORZ] + number*self.horizontal_step, self.vertical_step/2 + h) + cr.line_to(self.borders[HORZ] + number*self.horizontal_step, self.dimensions[VERT]) + cr.stroke() + + def render_labels(self): + self.context.set_font_size(0.02 * self.dimensions[HORZ]) + + self.render_horz_labels() + self.render_vert_labels() + + def render_horz_labels(self): + cr = self.context + labels = self.labels[HORZ] + if not labels: + labels = [str(i) for i in range(1, self.bounds[HORZ][1] + 1) ] + for number,label in enumerate(labels): + if label != None: + cr.set_source_rgba(0.5, 0.5, 0.5) + w,h = cr.text_extents(label)[2], cr.text_extents(label)[3] + cr.move_to(40,self.borders[VERT] + number*self.vertical_step + self.vertical_step/2 + h/2) + cr.show_text(label) + + def render_vert_labels(self): + cr = self.context + labels = self.labels[VERT] + if not labels: + labels = [str(i) for i in range(1, self.bounds[VERT][1] + 1) ] + for number,label in enumerate(labels): + w,h = cr.text_extents(label)[2], cr.text_extents(label)[3] + cr.move_to(self.borders[HORZ] + number*self.horizontal_step - w/2, self.vertical_step/2) + cr.show_text(label) + + def render_rectangle(self, x0, y0, x1, y1, color): + self.draw_shadow(x0, y0, x1, y1) + self.draw_rectangle(x0, y0, x1, y1, color) + + def draw_rectangular_shadow(self, gradient, x0, y0, w, h): + self.context.set_source(gradient) + self.context.rectangle(x0,y0,w,h) + self.context.fill() + + def draw_circular_shadow(self, x, y, radius, ang_start, ang_end, mult, shadow): + gradient = cairo.RadialGradient(x, y, 0, x, y, 2*radius) + gradient.add_color_stop_rgba(0, 0, 0, 0, shadow) + gradient.add_color_stop_rgba(1, 0, 0, 0, 0) + self.context.set_source(gradient) + self.context.move_to(x,y) + self.context.line_to(x + mult[0]*radius,y + mult[1]*radius) + self.context.arc(x, y, 8, ang_start, ang_end) + self.context.line_to(x,y) + self.context.close_path() + self.context.fill() + + def draw_rectangle(self, x0, y0, x1, y1, color): + cr = self.context + middle = (x0+x1)/2 + linear = cairo.LinearGradient(middle,y0,middle,y1) + linear.add_color_stop_rgba(0,3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1,*color[:4]) + cr.set_source(linear) + + cr.arc(x0+5, y0+5, 5, 0, 2*math.pi) + cr.arc(x1-5, y0+5, 5, 0, 2*math.pi) + cr.arc(x0+5, y1-5, 5, 0, 2*math.pi) + cr.arc(x1-5, y1-5, 5, 0, 2*math.pi) + cr.rectangle(x0+5,y0,x1-x0-10,y1-y0) + cr.rectangle(x0,y0+5,x1-x0,y1-y0-10) + cr.fill() + + def draw_shadow(self, x0, y0, x1, y1): + shadow = 0.4 + h_mid = (x0+x1)/2 + v_mid = (y0+y1)/2 + h_linear_1 = cairo.LinearGradient(h_mid,y0-4,h_mid,y0+4) + h_linear_2 = cairo.LinearGradient(h_mid,y1-4,h_mid,y1+4) + v_linear_1 = cairo.LinearGradient(x0-4,v_mid,x0+4,v_mid) + v_linear_2 = cairo.LinearGradient(x1-4,v_mid,x1+4,v_mid) + + h_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0) + h_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow) + h_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow) + h_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0) + v_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0) + v_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow) + v_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow) + v_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0) + + self.draw_rectangular_shadow(h_linear_1,x0+4,y0-4,x1-x0-8,8) + self.draw_rectangular_shadow(h_linear_2,x0+4,y1-4,x1-x0-8,8) + self.draw_rectangular_shadow(v_linear_1,x0-4,y0+4,8,y1-y0-8) + self.draw_rectangular_shadow(v_linear_2,x1-4,y0+4,8,y1-y0-8) + + self.draw_circular_shadow(x0+4, y0+4, 4, math.pi, 3*math.pi/2, (-1,0), shadow) + self.draw_circular_shadow(x1-4, y0+4, 4, 3*math.pi/2, 2*math.pi, (0,-1), shadow) + self.draw_circular_shadow(x0+4, y1-4, 4, math.pi/2, math.pi, (0,1), shadow) + self.draw_circular_shadow(x1-4, y1-4, 4, 0, math.pi/2, (1,0), shadow) + + def render_plot(self): + for index,group in enumerate(self.series): + for data in group: + self.render_rectangle(self.borders[HORZ] + data.content[0]*self.horizontal_step, + self.borders[VERT] + index*self.vertical_step + self.vertical_step/4.0, + self.borders[HORZ] + data.content[1]*self.horizontal_step, + self.borders[VERT] + index*self.vertical_step + 3.0*self.vertical_step/4.0, + self.series_colors[index]) + +# Function definition + +def scatter_plot(name, + data = None, + errorx = None, + errory = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + axis = False, + dash = False, + discrete = False, + dots = False, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + z_bounds = None, + x_title = None, + y_title = None, + series_colors = None, + circle_colors = None): + + """ + - Function to plot scatter data. + + - Parameters + + data - The values to be ploted might be passed in a two basic: + list of points: [(0,0), (0,1), (0,2)] or [(0,0,1), (0,1,4), (0,2,1)] + lists of coordinates: [ [0,0,0] , [0,1,2] ] or [ [0,0,0] , [0,1,2] , [1,4,1] ] + Notice that these kinds of that can be grouped in order to form more complex data + using lists of lists or dictionaries; + series_colors - Define color values for each of the series + circle_colors - Define a lower and an upper bound for the circle colors for variable radius + (3 dimensions) series + """ + + plot = ScatterPlot( name, data, errorx, errory, width, height, background, border, + axis, dash, discrete, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, z_bounds, x_title, y_title, series_colors, circle_colors ) + plot.render() + plot.commit() + +def dot_line_plot(name, + data, + width, + height, + background = "white light_gray", + border = 0, + axis = False, + dash = False, + dots = False, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + x_title = None, + y_title = None, + series_colors = None): + """ + - Function to plot graphics using dots and lines. + + dot_line_plot (name, data, width, height, background = "white light_gray", border = 0, axis = False, grid = False, x_labels = None, y_labels = None, x_bounds = None, y_bounds = None) + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + border - Distance in pixels of a square border into which the graphics will be drawn; + axis - Whether or not the axis are to be drawn; + dash - Boolean or a list or a dictionary of booleans indicating whether or not the associated series should be drawn in dashed mode; + dots - Whether or not dots should be drawn on each point; + grid - Whether or not the gris is to be drawn; + series_legend - Whether or not the legend is to be drawn; + x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; + x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; + x_title - Whether or not to plot a title over the x axis. + y_title - Whether or not to plot a title over the y axis. + + - Examples of use + + data = [0, 1, 3, 8, 9, 0, 10, 10, 2, 1] + CairoPlot.dot_line_plot('teste', data, 400, 300) + + data = { "john" : [10, 10, 10, 10, 30], "mary" : [0, 0, 3, 5, 15], "philip" : [13, 32, 11, 25, 2] } + x_labels = ["jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008" ] + CairoPlot.dot_line_plot( 'test', data, 400, 300, axis = True, grid = True, + series_legend = True, x_labels = x_labels ) + """ + plot = DotLinePlot( name, data, width, height, background, border, + axis, dash, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, x_title, y_title, series_colors ) + plot.render() + plot.commit() + +def function_plot(name, + data, + width, + height, + background = "white light_gray", + border = 0, + axis = True, + dots = False, + discrete = False, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + x_title = None, + y_title = None, + series_colors = None, + step = 1): + + """ + - Function to plot functions. + + function_plot(name, data, width, height, background = "white light_gray", border = 0, axis = True, grid = False, dots = False, x_labels = None, y_labels = None, x_bounds = None, y_bounds = None, step = 1, discrete = False) + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + border - Distance in pixels of a square border into which the graphics will be drawn; + axis - Whether or not the axis are to be drawn; + grid - Whether or not the gris is to be drawn; + dots - Whether or not dots should be shown at each point; + x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; + x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; + step - the horizontal distance from one point to the other. The smaller, the smoother the curve will be; + discrete - whether or not the function should be plotted in discrete format. + + - Example of use + + data = lambda x : x**2 + CairoPlot.function_plot('function4', data, 400, 300, grid = True, x_bounds=(-10,10), step = 0.1) + """ + + plot = FunctionPlot( name, data, width, height, background, border, + axis, discrete, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, x_title, y_title, series_colors, step ) + plot.render() + plot.commit() + +def pie_plot( name, data, width, height, background = "white light_gray", gradient = False, shadow = False, colors = None ): + + """ + - Function to plot pie graphics. + + pie_plot(name, data, width, height, background = "white light_gray", gradient = False, colors = None) + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + gradient - Whether or not the pie color will be painted with a gradient; + shadow - Whether or not there will be a shadow behind the pie; + colors - List of slices colors. + + - Example of use + + teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235} + CairoPlot.pie_plot("pie_teste", teste_data, 500, 500) + """ + + plot = PiePlot( name, data, width, height, background, gradient, shadow, colors ) + plot.render() + plot.commit() + +def donut_plot(name, data, width, height, background = "white light_gray", gradient = False, shadow = False, colors = None, inner_radius = -1): + + """ + - Function to plot donut graphics. + + donut_plot(name, data, width, height, background = "white light_gray", gradient = False, inner_radius = -1) + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + shadow - Whether or not there will be a shadow behind the donut; + gradient - Whether or not the donut color will be painted with a gradient; + colors - List of slices colors; + inner_radius - The radius of the donut's inner circle. + + - Example of use + + teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235} + CairoPlot.donut_plot("donut_teste", teste_data, 500, 500) + """ + + plot = DonutPlot(name, data, width, height, background, gradient, shadow, colors, inner_radius) + plot.render() + plot.commit() + +def gantt_chart(name, pieces, width, height, x_labels, y_labels, colors): + + """ + - Function to generate Gantt Charts. + + gantt_chart(name, pieces, width, height, x_labels, y_labels, colors): + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + pieces - A list defining the spaces to be drawn. The user must pass, for each line, the index of its start and the index of its end. If a line must have two or more spaces, they must be passed inside a list; + width, height - Dimensions of the output image; + x_labels - A list of names for each of the vertical lines; + y_labels - A list of names for each of the horizontal spaces; + colors - List containing the colors expected for each of the horizontal spaces + + - Example of use + + pieces = [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,8)] + x_labels = [ 'teste01', 'teste02', 'teste03', 'teste04'] + y_labels = [ '0001', '0002', '0003', '0004', '0005', '0006', '0007', '0008', '0009', '0010' ] + colors = [ (1.0, 0.0, 0.0), (1.0, 0.7, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0) ] + CairoPlot.gantt_chart('gantt_teste', pieces, 600, 300, x_labels, y_labels, colors) + """ + + plot = GanttChart(name, pieces, width, height, x_labels, y_labels, colors) + plot.render() + plot.commit() + +def vertical_bar_plot(name, + data, + width, + height, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + series_labels = None, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + colors = None): + #TODO: Fix docstring for vertical_bar_plot + """ + - Function to generate vertical Bar Plot Charts. + + bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension, + x_labels, y_labels, x_bounds, y_bounds, colors): + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtime; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + border - Distance in pixels of a square border into which the graphics will be drawn; + grid - Whether or not the gris is to be drawn; + rounded_corners - Whether or not the bars should have rounded corners; + three_dimension - Whether or not the bars should be drawn in pseudo 3D; + x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; + x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; + colors - List containing the colors expected for each of the bars. + + - Example of use + + data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + CairoPlot.vertical_bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False) + """ + + plot = VerticalBarPlot(name, data, width, height, background, border, + display_values, grid, rounded_corners, stack, three_dimension, + series_labels, x_labels, y_labels, x_bounds, y_bounds, colors) + plot.render() + plot.commit() + +def horizontal_bar_plot(name, + data, + width, + height, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + series_labels = None, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + colors = None): + + #TODO: Fix docstring for horizontal_bar_plot + """ + - Function to generate Horizontal Bar Plot Charts. + + bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension, + x_labels, y_labels, x_bounds, y_bounds, colors): + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtime; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + border - Distance in pixels of a square border into which the graphics will be drawn; + grid - Whether or not the gris is to be drawn; + rounded_corners - Whether or not the bars should have rounded corners; + three_dimension - Whether or not the bars should be drawn in pseudo 3D; + x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; + x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; + colors - List containing the colors expected for each of the bars. + + - Example of use + + data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + CairoPlot.bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False) + """ + + plot = HorizontalBarPlot(name, data, width, height, background, border, + display_values, grid, rounded_corners, stack, three_dimension, + series_labels, x_labels, y_labels, x_bounds, y_bounds, colors) + plot.render() + plot.commit() + +def stream_chart(name, + data, + width, + height, + background = "white light_gray", + border = 0, + grid = False, + series_legend = None, + x_labels = None, + x_bounds = None, + y_bounds = None, + colors = None): + + #TODO: Fix docstring for horizontal_bar_plot + plot = StreamChart(name, data, width, height, background, border, + grid, series_legend, x_labels, x_bounds, y_bounds, colors) + plot.render() + plot.commit() + + +if __name__ == "__main__": + import tests + import seriestests diff --git a/cairoplot/handlers/__init__.py b/cairoplot/handlers/__init__.py new file mode 100755 index 0000000..364fb90 --- /dev/null +++ b/cairoplot/handlers/__init__.py @@ -0,0 +1,10 @@ +__all__ = ["handler", "png", "pdf", "ps", "svg", "vector"] + +# default handlers +from .handler import Handler +from .png import PNGHandler +from .pdf import PDFHandler +from .ps import PSHandler +from .svg import SVGHandler +from .vector import VectorHandler + diff --git a/cairoplot/handlers/fixedsize.py b/cairoplot/handlers/fixedsize.py new file mode 100755 index 0000000..3c25fdf --- /dev/null +++ b/cairoplot/handlers/fixedsize.py @@ -0,0 +1,30 @@ + +import cairo +import cairoplot +from .handler import Handler as _Handler + +class FixedSizeHandler(_Handler): + """Base class for handlers with a fixed size.""" + + def __init__(self, width, height): + """Create with fixed width and height.""" + self.dimensions = {} + self.dimensions[cairoplot.HORZ] = width + self.dimensions[cairoplot.VERT] = height + + # sub-classes must create a surface + self.surface = None + + def prepare(self, plot): + """Prepare plot to render by setting its dimensions.""" + _Handler.prepare(self, plot) + plot.dimensions = self.dimensions + plot.context = cairo.Context(self.surface) + + def commit(self, plot): + """Commit the plot (to a file).""" + _Handler.commit(self, plot) + + # since pngs are different from other fixed size handlers, + # sub-classes are in charge of actual file writing + diff --git a/cairoplot/handlers/gtk.py b/cairoplot/handlers/gtk.py new file mode 100755 index 0000000..897c86f --- /dev/null +++ b/cairoplot/handlers/gtk.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import + +import gtk +import cairo +import cairoplot +from .handler import Handler as _Handler + +class GTKHandler(_Handler, gtk.DrawingArea): + """Handler to create plots that output to vector files.""" + + def __init__(self, *args, **kwargs): + """Create Handler for arbitrary surfaces.""" + _Handler.__init__(self) + gtk.DrawingArea.__init__(self) + + # users of this class must set plot manually + self.plot = None + self.context = None + + # connect events for resizing/redrawing + self.connect("expose_event", self.on_expose_event) + + def on_expose_event(self, widget, data): + """Redraws plot if need be.""" + + self.context = widget.window.cairo_create() + if (self.plot is not None): + self.plot.render() + + def prepare(self, plot): + """Update plot's size and context with custom widget.""" + _Handler.prepare(self, plot) + self.plot = plot + plot.context = self.context + + allocation = self.get_allocation() + plot.dimensions[cairoplot.HORZ] = allocation.width + plot.dimensions[cairoplot.VERT] = allocation.height + diff --git a/cairoplot/handlers/handler.py b/cairoplot/handlers/handler.py new file mode 100755 index 0000000..0ec54a7 --- /dev/null +++ b/cairoplot/handlers/handler.py @@ -0,0 +1,11 @@ + +class Handler(object): + """Base class for all handlers.""" + + def prepare(self, plot): + pass + + def commit(self, plot): + """All handlers need to finalize the cairo context.""" + plot.context.show_page() + diff --git a/cairoplot/handlers/pdf.py b/cairoplot/handlers/pdf.py new file mode 100755 index 0000000..30838c7 --- /dev/null +++ b/cairoplot/handlers/pdf.py @@ -0,0 +1,12 @@ + +import cairo +from .vector import VectorHandler as _VectorHandler + +class PDFHandler(_VectorHandler): + """Handler to create plots that output to pdf files.""" + + def __init__(self, filename, width, height): + """Creates a surface to be used by Cairo.""" + _VectorHandler.__init__(self, None, width, height) + self.surface = cairo.PDFSurface(filename, width, height) + diff --git a/cairoplot/handlers/png.py b/cairoplot/handlers/png.py new file mode 100755 index 0000000..6cce422 --- /dev/null +++ b/cairoplot/handlers/png.py @@ -0,0 +1,19 @@ + +import cairo + +from .fixedsize import FixedSizeHandler as _FixedSizeHandler + +class PNGHandler(_FixedSizeHandler): + """Handler to create plots that output to png files.""" + + def __init__(self, filename, width, height): + """Creates a surface to be used by Cairo.""" + _FixedSizeHandler.__init__(self, width, height) + self.filename = filename + self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + + def commit(self, plot): + """Writes plot to file.""" + _FixedSizeHandler.commit(self, plot) + self.surface.write_to_png(self.filename) + diff --git a/cairoplot/handlers/ps.py b/cairoplot/handlers/ps.py new file mode 100755 index 0000000..7a77781 --- /dev/null +++ b/cairoplot/handlers/ps.py @@ -0,0 +1,12 @@ + +import cairo +from .vector import VectorHandler as _VectorHandler + +class PSHandler(_VectorHandler): + """Handler to create plots that output to PostScript files.""" + + def __init__(self, filename, width, height): + """Creates a surface to be used by Cairo.""" + _VectorHandler.__init__(self, None, width, height) + self.surface = cairo.PSSurface(filename, width, height) + diff --git a/cairoplot/handlers/svg.py b/cairoplot/handlers/svg.py new file mode 100755 index 0000000..032fdc8 --- /dev/null +++ b/cairoplot/handlers/svg.py @@ -0,0 +1,12 @@ + +import cairo +from .vector import VectorHandler as _VectorHandler + +class SVGHandler(_VectorHandler): + """Handler to create plots that output to svg files.""" + + def __init__(self, filename, width, height): + """Creates a surface to be used by Cairo.""" + _VectorHandler.__init__(self, None, width, height) + self.surface = cairo.SVGSurface(filename, width, height) + diff --git a/cairoplot/handlers/vector.py b/cairoplot/handlers/vector.py new file mode 100755 index 0000000..0c4d4bc --- /dev/null +++ b/cairoplot/handlers/vector.py @@ -0,0 +1,17 @@ + +import cairo +from .fixedsize import FixedSizeHandler as _FixedSizeHandler + +class VectorHandler(_FixedSizeHandler): + """Handler to create plots that output to vector files.""" + + def __init__(self, surface, *args, **kwargs): + """Create Handler for arbitrary surfaces.""" + _FixedSizeHandler.__init__(self, *args, **kwargs) + self.surface = surface + + def commit(self, plot): + """Writes plot to file.""" + _FixedSizeHandler.commit(self, plot) + self.surface.finish() + diff --git a/cairoplot/series.py b/cairoplot/series.py new file mode 100755 index 0000000..79fee2f --- /dev/null +++ b/cairoplot/series.py @@ -0,0 +1,1140 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Serie.py +# +# Copyright (c) 2008 Magnun Leno da Silva +# +# Author: Magnun Leno da Silva +# +# This program 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 2 of +# the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA + +# Contributor: Rodrigo Moreiro Araujo + +#import cairoplot +import doctest + +NUMTYPES = (int, float, long) +LISTTYPES = (list, tuple) +STRTYPES = (str, unicode) +FILLING_TYPES = ['linear', 'solid', 'gradient'] +DEFAULT_COLOR_FILLING = 'solid' +#TODO: Define default color list +DEFAULT_COLOR_LIST = None + +class Data(object): + """ + Class that models the main data structure. + It can hold: + - a number type (int, float or long) + - a tuple, witch represents a point and can have 2 or 3 items (x,y,z) + - if a list is passed it will be converted to a tuple. + + obs: In case a tuple is passed it will convert to tuple + """ + def __init__(self, data=None, name=None, parent=None): + """ + Starts main atributes from the Data class + @name - Name for each point; + @content - The real data, can be an int, float, long or tuple, which + represents a point (x,y) or (x,y,z); + @parent - A pointer that give the data access to it's parent. + + Usage: + >>> d = Data(name='empty'); print d + empty: () + >>> d = Data((1,1),'point a'); print d + point a: (1, 1) + >>> d = Data((1,2,3),'point b'); print d + point b: (1, 2, 3) + >>> d = Data([2,3],'point c'); print d + point c: (2, 3) + >>> d = Data(12, 'simple value'); print d + simple value: 12 + """ + # Initial values + self.__content = None + self.__name = None + + # Setting passed values + self.parent = parent + self.name = name + self.content = data + + # Name property + @apply + def name(): + doc = """ + Name is a read/write property that controls the input of name. + - If passed an invalid value it cleans the name with None + + Usage: + >>> d = Data(13); d.name = 'name_test'; print d + name_test: 13 + >>> d.name = 11; print d + 13 + >>> d.name = 'other_name'; print d + other_name: 13 + >>> d.name = None; print d + 13 + >>> d.name = 'last_name'; print d + last_name: 13 + >>> d.name = ''; print d + 13 + """ + def fget(self): + """ + returns the name as a string + """ + return self.__name + + def fset(self, name): + """ + Sets the name of the Data + """ + if type(name) in STRTYPES and len(name) > 0: + self.__name = name + else: + self.__name = None + + + + return property(**locals()) + + # Content property + @apply + def content(): + doc = """ + Content is a read/write property that validate the data passed + and return it. + + Usage: + >>> d = Data(); d.content = 13; d.content + 13 + >>> d = Data(); d.content = (1,2); d.content + (1, 2) + >>> d = Data(); d.content = (1,2,3); d.content + (1, 2, 3) + >>> d = Data(); d.content = [1,2,3]; d.content + (1, 2, 3) + >>> d = Data(); d.content = [1.5,.2,3.3]; d.content + (1.5, 0.20000000000000001, 3.2999999999999998) + """ + def fget(self): + """ + Return the content of Data + """ + return self.__content + + def fset(self, data): + """ + Ensures that data is a valid tuple/list or a number (int, float + or long) + """ + # Type: None + if data is None: + self.__content = None + return + + # Type: Int or Float + elif type(data) in NUMTYPES: + self.__content = data + + # Type: List or Tuple + elif type(data) in LISTTYPES: + # Ensures the correct size + if len(data) not in (2, 3): + raise TypeError, "Data (as list/tuple) must have 2 or 3 items" + return + + # Ensures that all items in list/tuple is a number + isnum = lambda x : type(x) not in NUMTYPES + + if max(map(isnum, data)): + # An item in data isn't an int or a float + raise TypeError, "All content of data must be a number (int or float)" + + # Convert the tuple to list + if type(data) is list: + data = tuple(data) + + # Append a copy and sets the type + self.__content = data[:] + + # Unknown type! + else: + self.__content = None + raise TypeError, "Data must be an int, float or a tuple with two or three items" + return + + return property(**locals()) + + + def clear(self): + """ + Clear the all Data (content, name and parent) + """ + self.content = None + self.name = None + self.parent = None + + def copy(self): + """ + Returns a copy of the Data structure + """ + # The copy + new_data = Data() + if self.content is not None: + # If content is a point + if type(self.content) is tuple: + new_data.__content = self.content[:] + + # If content is a number + else: + new_data.__content = self.content + + # If it has a name + if self.name is not None: + new_data.__name = self.name + + return new_data + + def __str__(self): + """ + Return a string representation of the Data structure + """ + if self.name is None: + if self.content is None: + return '' + return str(self.content) + else: + if self.content is None: + return self.name+": ()" + return self.name+": "+str(self.content) + + def __len__(self): + """ + Return the length of the Data. + - If it's a number return 1; + - If it's a list return it's length; + - If its None return 0. + """ + if self.content is None: + return 0 + elif type(self.content) in NUMTYPES: + return 1 + return len(self.content) + + + + +class Group(object): + """ + Class that models a group of data. Every value (int, float, long, tuple + or list) passed is converted to a list of Data. + It can receive: + - A single number (int, float, long); + - A list of numbers; + - A tuple of numbers; + - An instance of Data; + - A list of Data; + + Obs: If a tuple with 2 or 3 items is passed it is converted to a point. + If a tuple with only 1 item is passed it's converted to a number; + If a tuple with more than 2 items is passed it's converted to a + list of numbers + """ + def __init__(self, group=None, name=None, parent=None): + """ + Starts main atributes in Group instance. + @data_list - a list of data which forms the group; + @range - a range that represent the x axis of possible functions; + @name - name of the data group; + @parent - the Serie parent of this group. + + Usage: + >>> g = Group(13, 'simple number'); print g + simple number ['13'] + >>> g = Group((1,2), 'simple point'); print g + simple point ['(1, 2)'] + >>> g = Group([1,2,3,4], 'list of numbers'); print g + list of numbers ['1', '2', '3', '4'] + >>> g = Group((1,2,3,4),'int in tuple'); print g + int in tuple ['1', '2', '3', '4'] + >>> g = Group([(1,2),(2,3),(3,4)], 'list of points'); print g + list of points ['(1, 2)', '(2, 3)', '(3, 4)'] + >>> g = Group([[1,2,3],[1,2,3]], '2D coordinate lists'); print g + 2D coordinated lists ['(1, 1)', '(2, 2)', '(3, 3)'] + >>> g = Group([[1,2],[1,2],[1,2]], '3D coordinate lists'); print g + 3D coordinated lists ['(1, 1, 1)', '(2, 2, 2)'] + """ + # Initial values + self.__data_list = [] + self.__range = [] + self.__name = None + + + self.parent = parent + self.name = name + self.data_list = group + + # Name property + @apply + def name(): + doc = """ + Name is a read/write property that controls the input of name. + - If passed an invalid value it cleans the name with None + + Usage: + >>> g = Group(13); g.name = 'name_test'; print g + name_test ['13'] + >>> g.name = 11; print g + ['13'] + >>> g.name = 'other_name'; print g + other_name ['13'] + >>> g.name = None; print g + ['13'] + >>> g.name = 'last_name'; print g + last_name ['13'] + >>> g.name = ''; print g + ['13'] + """ + def fget(self): + """ + Returns the name as a string + """ + return self.__name + + def fset(self, name): + """ + Sets the name of the Group + """ + if type(name) in STRTYPES and len(name) > 0: + self.__name = name + else: + self.__name = None + + return property(**locals()) + + # data_list property + @apply + def data_list(): + doc = """ + The data_list is a read/write property that can be a list of + numbers, a list of points or a list of 2 or 3 coordinate lists. This + property uses mainly the self.add_data method. + + Usage: + >>> g = Group(); g.data_list = 13; print g + ['13'] + >>> g.data_list = (1,2); print g + ['(1, 2)'] + >>> g.data_list = Data((1,2),'point a'); print g + ['point a: (1, 2)'] + >>> g.data_list = [1,2,3]; print g + ['1', '2', '3'] + >>> g.data_list = (1,2,3,4); print g + ['1', '2', '3', '4'] + >>> g.data_list = [(1,2),(2,3),(3,4)]; print g + ['(1, 2)', '(2, 3)', '(3, 4)'] + >>> g.data_list = [[1,2],[1,2]]; print g + ['(1, 1)', '(2, 2)'] + >>> g.data_list = [[1,2],[1,2],[1,2]]; print g + ['(1, 1, 1)', '(2, 2, 2)'] + >>> g.range = (10); g.data_list = lambda x:x**2; print g + ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)'] + """ + def fget(self): + """ + Returns the value of data_list + """ + return self.__data_list + + def fset(self, group): + """ + Ensures that group is valid. + """ + # None + if group is None: + self.__data_list = [] + + # Int/float/long or Instance of Data + elif type(group) in NUMTYPES or isinstance(group, Data): + # Clean data_list + self.__data_list = [] + self.add_data(group) + + # One point + elif type(group) is tuple and len(group) in (2,3): + self.__data_list = [] + self.add_data(group) + + # list of items + elif type(group) in LISTTYPES and type(group[0]) is not list: + # Clean data_list + self.__data_list = [] + for item in group: + # try to append and catch an exception + self.add_data(item) + + # function lambda + elif callable(group): + # Explicit is better than implicit + function = group + # Has range + if len(self.range) is not 0: + # Clean data_list + self.__data_list = [] + # Generate values for the lambda function + for x in self.range: + #self.add_data((x,round(group(x),2))) + self.add_data((x,function(x))) + + # Only have range in parent + elif self.parent is not None and len(self.parent.range) is not 0: + # Copy parent range + self.__range = self.parent.range[:] + # Clean data_list + self.__data_list = [] + # Generate values for the lambda function + for x in self.range: + #self.add_data((x,round(group(x),2))) + self.add_data((x,function(x))) + + # Don't have range anywhere + else: + # x_data don't exist + raise Exception, "Data argument is valid but to use function type please set x_range first" + + # Coordinate Lists + elif type(group) in LISTTYPES and type(group[0]) is list: + # Clean data_list + self.__data_list = [] + data = [] + if len(group) == 3: + data = zip(group[0], group[1], group[2]) + elif len(group) == 2: + data = zip(group[0], group[1]) + else: + raise TypeError, "Only one list of coordinates was received." + + for item in data: + self.add_data(item) + + else: + raise TypeError, "Group type not supported" + + return property(**locals()) + + @apply + def range(): + doc = """ + The range is a read/write property that generates a range of values + for the x axis of the functions. When passed a tuple it almost works + like the built-in range funtion: + - 1 item, represent the end of the range started from 0; + - 2 items, represents the start and the end, respectively; + - 3 items, the last one represents the step; + + When passed a list the range function understands as a valid range. + + Usage: + >>> g = Group(); g.range = 10; print g.range + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + >>> g = Group(); g.range = (5); print g.range + [0.0, 1.0, 2.0, 3.0, 4.0] + >>> g = Group(); g.range = (1,7); print g.range + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + >>> g = Group(); g.range = (0,10,2); print g.range + [0.0, 2.0, 4.0, 6.0, 8.0] + >>> + >>> g = Group(); g.range = [0]; print g.range + [0.0] + >>> g = Group(); g.range = [0,10,20]; print g.range + [0.0, 10.0, 20.0] + """ + def fget(self): + """ + Returns the range + """ + return self.__range + + def fset(self, x_range): + """ + Controls the input of a valid type and generate the range + """ + # if passed a simple number convert to tuple + if type(x_range) in NUMTYPES: + x_range = (x_range,) + + # A list, just convert to float + if type(x_range) is list and len(x_range) > 0: + # Convert all to float + x_range = map(float, x_range) + # Prevents repeated values and convert back to list + self.__range = list(set(x_range[:])) + # Sort the list to ascending order + self.__range.sort() + + # A tuple, must check the lengths and generate the values + elif type(x_range) is tuple and len(x_range) in (1,2,3): + # Convert all to float + x_range = map(float, x_range) + + # Inital values + start = 0.0 + step = 1.0 + end = 0.0 + + # Only the end and it can't be less or iqual to 0 + if len(x_range) is 1 and x_range > 0: + end = x_range[0] + + # The start and the end but the start must be less then the end + elif len(x_range) is 2 and x_range[0] < x_range[1]: + start = x_range[0] + end = x_range[1] + + # All 3, but the start must be less then the end + elif x_range[0] <= x_range[1]: + start = x_range[0] + end = x_range[1] + step = x_range[2] + + # Starts the range + self.__range = [] + # Generate the range + # Can't use the range function because it doesn't support float values + while start < end: + self.__range.append(start) + start += step + + # Incorrect type + else: + raise Exception, "x_range must be a list with one or more items or a tuple with 2 or 3 items" + + return property(**locals()) + + def add_data(self, data, name=None): + """ + Append a new data to the data_list. + - If data is an instance of Data, append it + - If it's an int, float, tuple or list create an instance of Data and append it + + Usage: + >>> g = Group() + >>> g.add_data(12); print g + ['12'] + >>> g.add_data(7,'other'); print g + ['12', 'other: 7'] + >>> + >>> g = Group() + >>> g.add_data((1,1),'a'); print g + ['a: (1, 1)'] + >>> g.add_data((2,2),'b'); print g + ['a: (1, 1)', 'b: (2, 2)'] + >>> + >>> g.add_data(Data((1,2),'c')); print g + ['a: (1, 1)', 'b: (2, 2)', 'c: (1, 2)'] + """ + if not isinstance(data, Data): + # Try to convert + data = Data(data,name,self) + + if data.content is not None: + self.__data_list.append(data.copy()) + self.__data_list[-1].parent = self + + + def to_list(self): + """ + Returns the group as a list of numbers (int, float or long) or a + list of tuples (points 2D or 3D). + + Usage: + >>> g = Group([1,2,3,4],'g1'); g.to_list() + [1, 2, 3, 4] + >>> g = Group([(1,2),(2,3),(3,4)],'g2'); g.to_list() + [(1, 2), (2, 3), (3, 4)] + >>> g = Group([(1,2,3),(3,4,5)],'g2'); g.to_list() + [(1, 2, 3), (3, 4, 5)] + """ + return [data.content for data in self] + + def copy(self): + """ + Returns a copy of this group + """ + new_group = Group() + new_group.__name = self.__name + if self.__range is not None: + new_group.__range = self.__range[:] + for data in self: + new_group.add_data(data.copy()) + return new_group + + def get_names(self): + """ + Return a list with the names of all data in this group + """ + names = [] + for data in self: + if data.name is None: + names.append('Data '+str(data.index()+1)) + else: + names.append(data.name) + return names + + + def __str__ (self): + """ + Returns a string representing the Group + """ + ret = "" + if self.name is not None: + ret += self.name + " " + if len(self) > 0: + list_str = [str(item) for item in self] + ret += str(list_str) + else: + ret += "[]" + return ret + + def __getitem__(self, key): + """ + Makes a Group iterable, based in the data_list property + """ + return self.data_list[key] + + def __len__(self): + """ + Returns the length of the Group, based in the data_list property + """ + return len(self.data_list) + + +class Colors(object): + """ + Class that models the colors its labels (names) and its properties, RGB + and filling type. + + It can receive: + - A list where each item is a list with 3 or 4 items. The + first 3 items represent the RGB values and the last argument + defines the filling type. The list will be converted to a dict + and each color will receve a name based in its position in the + list. + - A dictionary where each key will be the color name and its item + can be a list with 3 or 4 items. The first 3 items represent + the RGB colors and the last argument defines the filling type. + """ + def __init__(self, color_list=None): + """ + Start the color_list property + @ color_list - the list or dict contaning the colors properties. + """ + self.__color_list = None + + self.color_list = color_list + + @apply + def color_list(): + doc = """ + >>> c = Colors([[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']]) + >>> print c.color_list + {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']} + >>> c.color_list = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')] + >>> print c.color_list + {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']} + >>> c.color_list = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)} + >>> print c.color_list + {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']} + """ + def fget(self): + """ + Return the color list + """ + return self.__color_list + + def fset(self, color_list): + """ + Format the color list to a dictionary + """ + if color_list is None: + self.__color_list = None + return + + if type(color_list) in LISTTYPES and type(color_list[0]) in LISTTYPES: + old_color_list = color_list[:] + color_list = {} + for index, color in enumerate(old_color_list): + if len(color) is 3 and max(map(type, color)) in NUMTYPES: + color_list['Color '+str(index+1)] = list(color)+[DEFAULT_COLOR_FILLING] + elif len(color) is 4 and max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES: + color_list['Color '+str(index+1)] = list(color) + else: + raise TypeError, "Unsuported color format" + elif type(color_list) is not dict: + raise TypeError, "Unsuported color format" + + for name, color in color_list.items(): + if len(color) is 3: + if max(map(type, color)) in NUMTYPES: + color_list[name] = list(color)+[DEFAULT_COLOR_FILLING] + else: + raise TypeError, "Unsuported color format" + elif len(color) is 4: + if max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES: + color_list[name] = list(color) + else: + raise TypeError, "Unsuported color format" + self.__color_list = color_list.copy() + + return property(**locals()) + + +class Series(object): + """ + Class that models a Series (group of groups). Every value (int, float, + long, tuple or list) passed is converted to a list of Group or Data. + It can receive: + - a single number or point, will be converted to a Group of one Data; + - a list of numbers, will be converted to a group of numbers; + - a list of tuples, will converted to a single Group of points; + - a list of lists of numbers, each 'sublist' will be converted to a + group of numbers; + - a list of lists of tuples, each 'sublist' will be converted to a + group of points; + - a list of lists of lists, the content of the 'sublist' will be + processed as coordinated lists and the result will be converted to + a group of points; + - a Dictionary where each item can be the same of the list: number, + point, list of numbers, list of points or list of lists (coordinated + lists); + - an instance of Data; + - an instance of group. + """ + def __init__(self, series=None, name=None, property=[], colors=None): + """ + Starts main atributes in Group instance. + @series - a list, dict of data of which the series is composed; + @name - name of the series; + @property - a list/dict of properties to be used in the plots of + this Series + + Usage: + >>> print Series([1,2,3,4]) + ["Group 1 ['1', '2', '3', '4']"] + >>> print Series([[1,2,3],[4,5,6]]) + ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"] + >>> print Series((1,2)) + ["Group 1 ['(1, 2)']"] + >>> print Series([(1,2),(2,3)]) + ["Group 1 ['(1, 2)', '(2, 3)']"] + >>> print Series([[(1,2),(2,3)],[(4,5),(5,6)]]) + ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"] + >>> print Series([[[1,2,3],[1,2,3],[1,2,3]]]) + ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"] + >>> print Series({'g1':[1,2,3], 'g2':[4,5,6]}) + ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"] + >>> print Series({'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}) + ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"] + >>> print Series({'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}) + ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"] + >>> print Series(Data(1,'d1')) + ["Group 1 ['d1: 1']"] + >>> print Series(Group([(1,2),(2,3)],'g1')) + ["g1 ['(1, 2)', '(2, 3)']"] + """ + # Intial values + self.__group_list = [] + self.__name = None + self.__range = None + + # TODO: Implement colors with filling + self.__colors = None + + self.name = name + self.group_list = series + self.colors = colors + + # Name property + @apply + def name(): + doc = """ + Name is a read/write property that controls the input of name. + - If passed an invalid value it cleans the name with None + + Usage: + >>> s = Series(13); s.name = 'name_test'; print s + name_test ["Group 1 ['13']"] + >>> s.name = 11; print s + ["Group 1 ['13']"] + >>> s.name = 'other_name'; print s + other_name ["Group 1 ['13']"] + >>> s.name = None; print s + ["Group 1 ['13']"] + >>> s.name = 'last_name'; print s + last_name ["Group 1 ['13']"] + >>> s.name = ''; print s + ["Group 1 ['13']"] + """ + def fget(self): + """ + Returns the name as a string + """ + return self.__name + + def fset(self, name): + """ + Sets the name of the Group + """ + if type(name) in STRTYPES and len(name) > 0: + self.__name = name + else: + self.__name = None + + return property(**locals()) + + + + # Colors property + @apply + def colors(): + doc = """ + >>> s = Series() + >>> s.colors = [[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']] + >>> print s.colors + {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']} + >>> s.colors = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')] + >>> print s.colors + {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']} + >>> s.colors = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)} + >>> print s.colors + {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']} + """ + def fget(self): + """ + Return the color list + """ + return self.__colors.color_list + + def fset(self, colors): + """ + Format the color list to a dictionary + """ + self.__colors = Colors(colors) + + return property(**locals()) + + @apply + def range(): + doc = """ + The range is a read/write property that generates a range of values + for the x axis of the functions. When passed a tuple it almost works + like the built-in range funtion: + - 1 item, represent the end of the range started from 0; + - 2 items, represents the start and the end, respectively; + - 3 items, the last one represents the step; + + When passed a list the range function understands as a valid range. + + Usage: + >>> s = Series(); s.range = 10; print s.range + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + >>> s = Series(); s.range = (5); print s.range + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + >>> s = Series(); s.range = (1,7); print s.range + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] + >>> s = Series(); s.range = (0,10,2); print s.range + [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] + >>> + >>> s = Series(); s.range = [0]; print s.range + [0.0] + >>> s = Series(); s.range = [0,10,20]; print s.range + [0.0, 10.0, 20.0] + """ + def fget(self): + """ + Returns the range + """ + return self.__range + + def fset(self, x_range): + """ + Controls the input of a valid type and generate the range + """ + # if passed a simple number convert to tuple + if type(x_range) in NUMTYPES: + x_range = (x_range,) + + # A list, just convert to float + if type(x_range) is list and len(x_range) > 0: + # Convert all to float + x_range = map(float, x_range) + # Prevents repeated values and convert back to list + self.__range = list(set(x_range[:])) + # Sort the list to ascending order + self.__range.sort() + + # A tuple, must check the lengths and generate the values + elif type(x_range) is tuple and len(x_range) in (1,2,3): + # Convert all to float + x_range = map(float, x_range) + + # Inital values + start = 0.0 + step = 1.0 + end = 0.0 + + # Only the end and it can't be less or iqual to 0 + if len(x_range) is 1 and x_range > 0: + end = x_range[0] + + # The start and the end but the start must be lesser then the end + elif len(x_range) is 2 and x_range[0] < x_range[1]: + start = x_range[0] + end = x_range[1] + + # All 3, but the start must be lesser then the end + elif x_range[0] < x_range[1]: + start = x_range[0] + end = x_range[1] + step = x_range[2] + + # Starts the range + self.__range = [] + # Generate the range + # Cnat use the range function becouse it don't suport float values + while start <= end: + self.__range.append(start) + start += step + + # Incorrect type + else: + raise Exception, "x_range must be a list with one or more item or a tuple with 2 or 3 items" + + return property(**locals()) + + @apply + def group_list(): + doc = """ + The group_list is a read/write property used to pre-process the list + of Groups. + It can be: + - a single number, point or lambda, will be converted to a single + Group of one Data; + - a list of numbers, will be converted to a group of numbers; + - a list of tuples, will converted to a single Group of points; + - a list of lists of numbers, each 'sublist' will be converted to + a group of numbers; + - a list of lists of tuples, each 'sublist' will be converted to a + group of points; + - a list of lists of lists, the content of the 'sublist' will be + processed as coordinated lists and the result will be converted + to a group of points; + - a list of lambdas, each lambda represents a Group; + - a Dictionary where each item can be the same of the list: number, + point, list of numbers, list of points, list of lists + (coordinated lists) or lambdas + - an instance of Data; + - an instance of group. + + Usage: + >>> s = Series() + >>> s.group_list = [1,2,3,4]; print s + ["Group 1 ['1', '2', '3', '4']"] + >>> s.group_list = [[1,2,3],[4,5,6]]; print s + ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"] + >>> s.group_list = (1,2); print s + ["Group 1 ['(1, 2)']"] + >>> s.group_list = [(1,2),(2,3)]; print s + ["Group 1 ['(1, 2)', '(2, 3)']"] + >>> s.group_list = [[(1,2),(2,3)],[(4,5),(5,6)]]; print s + ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"] + >>> s.group_list = [[[1,2,3],[1,2,3],[1,2,3]]]; print s + ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"] + >>> s.group_list = [(0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)]; print s + ["Group 1 ['(0.5, 5.5)']", "Group 2 ['(0, 4)', '(6, 8)']", "Group 3 ['(5.5, 7)']", "Group 4 ['(7, 9)']"] + >>> s.group_list = {'g1':[1,2,3], 'g2':[4,5,6]}; print s + ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"] + >>> s.group_list = {'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}; print s + ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"] + >>> s.group_list = {'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}; print s + ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"] + >>> s.range = 10 + >>> s.group_list = lambda x:x*2 + >>> s.group_list = [lambda x:x*2, lambda x:x**2, lambda x:x**3]; print s + ["Group 1 ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "Group 2 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']", "Group 3 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']"] + >>> s.group_list = {'linear':lambda x:x*2, 'square':lambda x:x**2, 'cubic':lambda x:x**3}; print s + ["cubic ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']", "linear ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "square ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']"] + >>> s.group_list = Data(1,'d1'); print s + ["Group 1 ['d1: 1']"] + >>> s.group_list = Group([(1,2),(2,3)],'g1'); print s + ["g1 ['(1, 2)', '(2, 3)']"] + """ + def fget(self): + """ + Return the group list. + """ + return self.__group_list + + def fset(self, series): + """ + Controls the input of a valid group list. + """ + #TODO: Add support to the following strem of data: [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)] + + # Type: None + if series is None: + self.__group_list = [] + + # List or Tuple + elif type(series) in LISTTYPES: + self.__group_list = [] + + is_function = lambda x: callable(x) + # Groups + if list in map(type, series) or max(map(is_function, series)): + for group in series: + self.add_group(group) + + # single group + else: + self.add_group(series) + + #old code + ## List of numbers + #if type(series[0]) in NUMTYPES or type(series[0]) is tuple: + # print series + # self.add_group(series) + # + ## List of anything else + #else: + # for group in series: + # self.add_group(group) + + # Dict representing series of groups + elif type(series) is dict: + self.__group_list = [] + names = series.keys() + names.sort() + for name in names: + self.add_group(Group(series[name],name,self)) + + # A single lambda + elif callable(series): + self.__group_list = [] + self.add_group(series) + + # Int/float, instance of Group or Data + elif type(series) in NUMTYPES or isinstance(series, Group) or isinstance(series, Data): + self.__group_list = [] + self.add_group(series) + + # Default + else: + raise TypeError, "Serie type not supported" + + return property(**locals()) + + def add_group(self, group, name=None): + """ + Append a new group in group_list + """ + if not isinstance(group, Group): + #Try to convert + group = Group(group, name, self) + + if len(group.data_list) is not 0: + # Auto naming groups + if group.name is None: + group.name = "Group "+str(len(self.__group_list)+1) + + self.__group_list.append(group) + self.__group_list[-1].parent = self + + def copy(self): + """ + Returns a copy of the Series + """ + new_series = Series() + new_series.__name = self.__name + if self.__range is not None: + new_series.__range = self.__range[:] + #Add color property in the copy method + #self.__colors = None + + for group in self: + new_series.add_group(group.copy()) + + return new_series + + def get_names(self): + """ + Returns a list of the names of all groups in the Serie + """ + names = [] + for group in self: + if group.name is None: + names.append('Group '+str(group.index()+1)) + else: + names.append(group.name) + + return names + + def to_list(self): + """ + Returns a list with the content of all groups and data + """ + big_list = [] + for group in self: + for data in group: + if type(data.content) in NUMTYPES: + big_list.append(data.content) + else: + big_list = big_list + list(data.content) + return big_list + + def __getitem__(self, key): + """ + Makes the Series iterable, based in the group_list property + """ + return self.__group_list[key] + + def __str__(self): + """ + Returns a string that represents the Series + """ + ret = "" + if self.name is not None: + ret += self.name + " " + if len(self) > 0: + list_str = [str(item) for item in self] + ret += str(list_str) + else: + ret += "[]" + return ret + + def __len__(self): + """ + Returns the length of the Series, based in the group_lsit property + """ + return len(self.group_list) + + +if __name__ == '__main__': + doctest.testmod() diff --git a/data/icons/plot-gtk-inkscape.svg b/data/icons/plot-gtk-inkscape.svg new file mode 100755 index 0000000..dea4722 --- /dev/null +++ b/data/icons/plot-gtk-inkscape.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + diff --git a/data/icons/plot-gtk.png b/data/icons/plot-gtk.png new file mode 100755 index 0000000..1a2698e --- /dev/null +++ b/data/icons/plot-gtk.png Binary files differ diff --git a/data/icons/plot-small-inkscape.svg b/data/icons/plot-small-inkscape.svg new file mode 100755 index 0000000..247bcf4 --- /dev/null +++ b/data/icons/plot-small-inkscape.svg @@ -0,0 +1,102 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/data/icons/plot-tiny-inkscape.svg b/data/icons/plot-tiny-inkscape.svg new file mode 100755 index 0000000..6be3512 --- /dev/null +++ b/data/icons/plot-tiny-inkscape.svg @@ -0,0 +1,102 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/data/puzzle/absolutevalue-inkscape.svg b/data/puzzle/absolutevalue-inkscape.svg new file mode 100755 index 0000000..d11b926 --- /dev/null +++ b/data/puzzle/absolutevalue-inkscape.svg @@ -0,0 +1,99 @@ + +image/svg+xml + + + + cosh + + + + + + + + \ No newline at end of file diff --git a/data/puzzle/absolutevalue.svg b/data/puzzle/absolutevalue.svg new file mode 100755 index 0000000..c68d85b --- /dev/null +++ b/data/puzzle/absolutevalue.svg @@ -0,0 +1,39 @@ + + + + + + + + cosh + + + + + + + + \ No newline at end of file diff --git a/data/puzzle/addition-inkscape.svg b/data/puzzle/addition-inkscape.svg new file mode 100755 index 0000000..63d049c --- /dev/null +++ b/data/puzzle/addition-inkscape.svg @@ -0,0 +1,83 @@ + +image/svg+xml + + + + cosh + + + + + \ No newline at end of file diff --git a/data/puzzle/addition.svg b/data/puzzle/addition.svg new file mode 100755 index 0000000..f9c875e --- /dev/null +++ b/data/puzzle/addition.svg @@ -0,0 +1,36 @@ + + + + + + + + cosh + + + + + \ No newline at end of file diff --git a/data/puzzle/blank-inkscape.svg b/data/puzzle/blank-inkscape.svg new file mode 100755 index 0000000..c21ab44 --- /dev/null +++ b/data/puzzle/blank-inkscape.svg @@ -0,0 +1,110 @@ + +image/svg+xml + + + + cosh + + + + + + + + \ No newline at end of file diff --git a/data/puzzle/blank.svg b/data/puzzle/blank.svg new file mode 100755 index 0000000..cbb883c --- /dev/null +++ b/data/puzzle/blank.svg @@ -0,0 +1,48 @@ + + + + + + + + cosh + + + + + + + + \ No newline at end of file diff --git a/data/puzzle/composition-inkscape.svg b/data/puzzle/composition-inkscape.svg new file mode 100755 index 0000000..73ca4ba --- /dev/null +++ b/data/puzzle/composition-inkscape.svg @@ -0,0 +1,90 @@ + +image/svg+xml + + + + cosh + + + + + + + + \ No newline at end of file diff --git a/data/puzzle/composition.svg b/data/puzzle/composition.svg new file mode 100755 index 0000000..b584026 --- /dev/null +++ b/data/puzzle/composition.svg @@ -0,0 +1,40 @@ + + + + + + + + cosh + + + + + + + + \ No newline at end of file diff --git a/data/puzzle/constant-inkscape.svg b/data/puzzle/constant-inkscape.svg new file mode 100755 index 0000000..b9402f8 --- /dev/null +++ b/data/puzzle/constant-inkscape.svg @@ -0,0 +1,92 @@ + +image/svg+xml + + + + cosh + + + + + + + \ No newline at end of file diff --git a/data/puzzle/constant.svg b/data/puzzle/constant.svg new file mode 100755 index 0000000..af3236c --- /dev/null +++ b/data/puzzle/constant.svg @@ -0,0 +1,38 @@ + + + + + + + + cosh + + + + + + + \ No newline at end of file diff --git a/data/puzzle/e-inkscape.svg b/data/puzzle/e-inkscape.svg new file mode 100755 index 0000000..4e506ad --- /dev/null +++ b/data/puzzle/e-inkscape.svg @@ -0,0 +1,83 @@ + +image/svg+xml + + diff --git a/data/puzzle/e.svg b/data/puzzle/e.svg new file mode 100755 index 0000000..6e905ca --- /dev/null +++ b/data/puzzle/e.svg @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/data/puzzle/exponentiation-inkscape.svg b/data/puzzle/exponentiation-inkscape.svg new file mode 100755 index 0000000..affc5d5 --- /dev/null +++ b/data/puzzle/exponentiation-inkscape.svg @@ -0,0 +1,84 @@ + +image/svg+xml + + + + cosh + + + + + + + \ No newline at end of file diff --git a/data/puzzle/exponentiation.svg b/data/puzzle/exponentiation.svg new file mode 100755 index 0000000..8a7947b --- /dev/null +++ b/data/puzzle/exponentiation.svg @@ -0,0 +1,39 @@ + + + + + + + + cosh + + + + + + + \ No newline at end of file diff --git a/data/puzzle/identity-inkscape.svg b/data/puzzle/identity-inkscape.svg new file mode 100755 index 0000000..40f3049 --- /dev/null +++ b/data/puzzle/identity-inkscape.svg @@ -0,0 +1,90 @@ + +image/svg+xml + + + + cosh + + + + + + \ No newline at end of file diff --git a/data/puzzle/identity.svg b/data/puzzle/identity.svg new file mode 100755 index 0000000..de50504 --- /dev/null +++ b/data/puzzle/identity.svg @@ -0,0 +1,37 @@ + + + + + + + + cosh + + + + + + \ No newline at end of file diff --git a/data/puzzle/leftparen-inkscape.svg b/data/puzzle/leftparen-inkscape.svg new file mode 100755 index 0000000..50d05ed --- /dev/null +++ b/data/puzzle/leftparen-inkscape.svg @@ -0,0 +1,89 @@ + +image/svg+xml + + + + cosh + + + + + +( + \ No newline at end of file diff --git a/data/puzzle/leftparen.svg b/data/puzzle/leftparen.svg new file mode 100755 index 0000000..71fdb6b --- /dev/null +++ b/data/puzzle/leftparen.svg @@ -0,0 +1,44 @@ + + + + + + + + cosh + + + + + +( + \ No newline at end of file diff --git a/data/puzzle/multiplication-inkscape.svg b/data/puzzle/multiplication-inkscape.svg new file mode 100755 index 0000000..cce20ed --- /dev/null +++ b/data/puzzle/multiplication-inkscape.svg @@ -0,0 +1,84 @@ + +image/svg+xml + + + + cosh + + + + + + \ No newline at end of file diff --git a/data/puzzle/multiplication.svg b/data/puzzle/multiplication.svg new file mode 100755 index 0000000..e92f475 --- /dev/null +++ b/data/puzzle/multiplication.svg @@ -0,0 +1,37 @@ + + + + + + + + cosh + + + + + + \ No newline at end of file diff --git a/data/puzzle/pi-inkscape.svg b/data/puzzle/pi-inkscape.svg new file mode 100755 index 0000000..e51cc58 --- /dev/null +++ b/data/puzzle/pi-inkscape.svg @@ -0,0 +1,84 @@ + +image/svg+xml + + + diff --git a/data/puzzle/pi.svg b/data/puzzle/pi.svg new file mode 100755 index 0000000..5ae6bb9 --- /dev/null +++ b/data/puzzle/pi.svg @@ -0,0 +1,24 @@ + + + + + + + \ No newline at end of file diff --git a/data/puzzle/rightparen-inkscape.svg b/data/puzzle/rightparen-inkscape.svg new file mode 100755 index 0000000..8930f3e --- /dev/null +++ b/data/puzzle/rightparen-inkscape.svg @@ -0,0 +1,92 @@ + +image/svg+xml + + + + cosh + + + + + + +( + + \ No newline at end of file diff --git a/data/puzzle/rightparen.svg b/data/puzzle/rightparen.svg new file mode 100755 index 0000000..1ac78ca --- /dev/null +++ b/data/puzzle/rightparen.svg @@ -0,0 +1,47 @@ + + + + + + + + cosh + + + + + + +( + + \ No newline at end of file diff --git a/data/puzzle/sine-inkscape.svg b/data/puzzle/sine-inkscape.svg new file mode 100755 index 0000000..e692291 --- /dev/null +++ b/data/puzzle/sine-inkscape.svg @@ -0,0 +1,80 @@ + +image/svg+xml + diff --git a/data/puzzle/sine.svg b/data/puzzle/sine.svg new file mode 100755 index 0000000..2f4ce64 --- /dev/null +++ b/data/puzzle/sine.svg @@ -0,0 +1,19 @@ + + + + + \ No newline at end of file diff --git a/gtkplotactivity.py b/gtkplotactivity.py new file mode 100755 index 0000000..8026709 --- /dev/null +++ b/gtkplotactivity.py @@ -0,0 +1,288 @@ +"""gtkplotactivity: plotting application not dependent on sugar. +""" + +from gettext import gettext as _ + +import plotter.plot +import plotter.settings +import plotter.view + +import gtk + +import collections +import os.path +import codecs +import plotter.json as json + +# file version info, just in case we break backward compatibility +_FILE_VERSION = 1 +_xmin_adjustment = gtk.Adjustment( + value=-10, step_incr=1, lower=-1e9, upper=1e9) +_xmax_adjustment = gtk.Adjustment( + value=10, step_incr=1, lower=-1e9, upper=1e9) + + +class Plotter(gtk.Window): + """Stand-only gtk window with Plot activity.""" + + def __init__(self): + """Creates a plotter application window.""" + gtk.Window.__init__(self) + self.connect("delete_event", gtk.main_quit) + + self.init_undo() + + main_vbox = gtk.VBox() + main_vbox.add_with_properties(self.init_menu(), "expand", False) + main_vbox.add(self.init_plot()) + + main_vbox.show_all() + self.add(main_vbox) + self.set_default_size(800, 600) + + + def init_menu(self): + """Creates MenuBar for gtk activity.""" + menu = gtk.MenuBar() + + # file menu + fileitem = gtk.MenuItem(_("_File")) + filemenu = gtk.Menu() + fileitem.set_submenu(filemenu) + + openitem = gtk.MenuItem(_("_Open")) + openitem.connect("activate", self.on_open) + filemenu.add(openitem) + + saveitem = gtk.MenuItem(_("_Save")) + saveitem.connect("activate", self.on_save) + filemenu.add(saveitem) + + filemenu.add(gtk.SeparatorMenuItem()) + quititem = gtk.MenuItem(_("_Quit")) + quititem.connect("activate", gtk.main_quit) + filemenu.add(quititem) + menu.add(fileitem) + + # edit menu + edititem = gtk.MenuItem(_("_Edit")) + editmenu = gtk.Menu() + edititem.set_submenu(editmenu) + + undoitem = gtk.MenuItem(_("_Undo")) + undoitem.connect("activate", self.on_undo) + editmenu.add(undoitem) + + redoitem = gtk.MenuItem(_("_Redo")) + redoitem.connect("activate", self.on_redo) + editmenu.add(redoitem) + + editmenu.add(gtk.SeparatorMenuItem()) + copyitem = gtk.MenuItem(_("_Copy")) + copyitem.connect("activate", self.on_copy) + editmenu.add(copyitem) + + pasteitem = gtk.MenuItem(_("_Paste")) + pasteitem.connect("activate", self.on_paste) + editmenu.add(pasteitem) + menu.add(edititem) + + return menu + + + def init_undo(self): + """Sets up queues need for undo/redo.""" + self._undo = collections.deque() + self._redo = collections.deque() + + + def init_plot(self): + """Setup up needed properties for displaying a plot.""" + + # make box for equations + equationbox = gtk.HBox() + + # create input for equations + self.equations = plotter.view.EquationList(self) + equationbox.add(self.equations) + + # create button to initiate plot + plotbutton = gtk.Button(_("Go!")) + plotbutton.connect("clicked", self.on_plot) + equationbox.pack_start(plotbutton, expand=False) + + # make box for x-axis configuration + axisbox = gtk.HBox() + xminlabel = gtk.Label(_("x min.")) + axisbox.add(xminlabel) + self.xmin_spin = gtk.SpinButton(_xmin_adjustment) + axisbox.add(self.xmin_spin) + xmaxlabel = gtk.Label(_("x max.")) + axisbox.add(xmaxlabel) + self.xmax_spin = gtk.SpinButton(_xmax_adjustment) + axisbox.add(self.xmax_spin) + + # create canvas for plotting + self.canvas = None + + # add pieces to ScrolledWindow (so never have too many inputs) + self.plot_scrolledwindow = gtk.ScrolledWindow() + self.plot_vbox = gtk.VBox(spacing=2) + self.plot_vbox.pack_start(equationbox, expand=False) + self.plot_vbox.pack_start(axisbox, expand=False) + self.plot_scrolledwindow.add_with_viewport(self.plot_vbox) + self.plot_scrolledwindow.set_policy(gtk.POLICY_NEVER, + gtk.POLICY_AUTOMATIC) + + return self.plot_scrolledwindow + + + def get_functions(self): + """Gets model from equations list.""" + return self.equations.get_model() + + + def on_plot(self, widget, data=None): + """Tells self to draw a plot.""" + self.plot() + + + def plot(self): + """Draws a plot from points.""" + if self.canvas is not None: + self.plot_vbox.remove(self.canvas) + + self.canvas = plotter.plot.CairoPlotCanvas.fromapp(self) + self.canvas.show() + self.plot_vbox.pack_end(self.canvas, True, True) + + + def on_save(self, widget, data=None): + save_popup = gtk.FileChooserDialog(title=_("Save.."), + action=gtk.FILE_CHOOSER_ACTION_SAVE, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_SAVE, gtk.RESPONSE_OK)) + save_popup.set_default_response(gtk.RESPONSE_OK) + + response = save_popup.run() + + if response == gtk.RESPONSE_OK: + # write settings to selected file + # TODO: catch possible exceptions and log error + filename = save_popup.get_filename() + self.write_file(filename) + + save_popup.destroy() + + + def write_file(self, file_path): + """Writes settings to a file.""" + + # TODO: document possible errors that can occur + fp = codecs.open(file_path, "w", "utf-8") + settings = { + "version": _FILE_VERSION, + "plot_config": plotter.settings.PlotSettings.fromapp(self).save(), + "equations": self.equations.save() + } + json.dump(settings, fp) + + + def on_open(self, widget, data=None): + open_popup = gtk.FileChooserDialog(title=_("Open.."), + action=gtk.FILE_CHOOSER_ACTION_OPEN, + buttons=(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, + gtk.STOCK_OPEN, gtk.RESPONSE_OK)) + open_popup.set_default_response(gtk.RESPONSE_OK) + + response = open_popup.run() + + if response == gtk.RESPONSE_OK: + filename = open_popup.get_filename() + self.read_file(filename) + + open_popup.destroy() + + + def read_file(self, file_path): + """Loads settings from a file.""" + + # TODO: document possible errors that can occur + fp = codecs.open(file_path, "r", "utf-8") + settings = json.load(fp) + + # TODO: throw appropriate exception here? + if settings["version"] > _FILE_VERSION: + return + + # load setting and equations + plotter.settings.PlotSettings.load(settings["plot_config"]).toapp(self) + self.equations.load(settings["equations"]) + + # make sure graph is shown + self.plot() + + + def register_action(self, action, inverse): + """Adds an action and its inverse to the undo stack.""" + self._undo.append((action, inverse)) + self._redo.clear() + + + def on_undo(self, widget, data=None): + """Undoes the last actition performed.""" + + if len(self._undo) != 0: + action = self._undo.pop() + action[1]() + self._redo.append(action) + + + def on_redo(self, widget, data=None): + """Redoes the last actition undone.""" + + if len(self._redo) != 0: + action = self._redo.pop() + action[0]() + self._undo.append(action) + + + def _get_focus_widget(self, widget): + """Gets the widget that is a child of parent with the focus.""" + if widget.flags() & gtk.HAS_FOCUS: + return widget + + # get currently focused child (get_focus_child requires gtk 2.14) + focus = None + if hasattr(widget, "get_children"): + for child in widget.get_children(): + focus = self._get_focus_widget(child) + if focus is not None: + break + return focus + + + def on_copy(self, widget, data=None): + """Copies currently selected text.""" + focus = self._get_focus_widget(self.plot_vbox) + if focus is not None and hasattr(focus, "copy_clipboard"): + focus.copy_clipboard() + + + def on_paste(self, widget, data=None): + """Pastes text from Clipboard.""" + focus = self._get_focus_widget(self.plot_vbox) + if focus is not None and hasattr(focus, "paste_clipboard"): + focus.paste_clipboard() + + +if __name__ == '__main__': + # set default icon for the application + gtk.window_set_default_icon_from_file(os.path.join( + "data", "icons", "plot-gtk.png")) + + # run standalone application + app = Plotter() + app.show_all() + gtk.main() + diff --git a/locale/en-US/LC_MESSAGES/edu.wisc.cs.nest.PlotActivity.mo b/locale/en-US/LC_MESSAGES/edu.wisc.cs.nest.PlotActivity.mo new file mode 100755 index 0000000..e386219 --- /dev/null +++ b/locale/en-US/LC_MESSAGES/edu.wisc.cs.nest.PlotActivity.mo Binary files differ diff --git a/locale/en-US/activity.linfo b/locale/en-US/activity.linfo new file mode 100755 index 0000000..f88cb97 --- /dev/null +++ b/locale/en-US/activity.linfo @@ -0,0 +1,2 @@ +[Activity] +name = Plot diff --git a/plot.py b/plot.py new file mode 100755 index 0000000..ad03bc6 --- /dev/null +++ b/plot.py @@ -0,0 +1,52 @@ + +from gtkplotactivity import Plotter + +import gtk +from sugar.activity import activity +from gettext import gettext as _ + + +class PlotActivity(activity.Activity): + def __init__(self, handle): + """Creates a plotter application window.""" + activity.Activity.__init__(self, handle) + + # create toolbox: this provides default sugar controls + toolbox = activity.ActivityToolbox(self) + self.set_toolbox(toolbox) + toolbox.show() + + # setup container for glade widgets + main_view = gtk.VBox() + + # load Glade XML and get main window + # get the VBox that's a child of the glade window + self._app = app = Plotter() + app.plot_scrolledwindow.reparent(main_view) + app.plot_scrolledwindow.show() + + # create edit toolbar + edit_toolbar = activity.EditToolbar() + toolbox.add_toolbar(_("Edit"), edit_toolbar) + edit_toolbar.show() + + # connect undo/redo to app events + edit_toolbar.undo.connect("clicked", app.on_undo) + edit_toolbar.redo.connect("clicked", app.on_redo) + edit_toolbar.copy.connect("clicked", app.on_copy) + edit_toolbar.paste.connect("clicked", app.on_paste) + + # make main_view act as our canvas + main_view.show() + self.set_canvas(main_view) + self.show_all() + + + def write_file(self, file_path): + """Tells application to write to file.""" + self._app.write_file(file_path) + + def read_file(self, file_path): + """Tells application to load from file.""" + self._app.read_file(file_path) + diff --git a/plotter/__init__.py b/plotter/__init__.py new file mode 100755 index 0000000..0231166 --- /dev/null +++ b/plotter/__init__.py @@ -0,0 +1,2 @@ +__all__ = ["parse", "settings"] + diff --git a/plotter/json.py b/plotter/json.py new file mode 100755 index 0000000..d464abb --- /dev/null +++ b/plotter/json.py @@ -0,0 +1,33 @@ +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +""" +Unify usage of simplejson in Python 2.5/2.6 + +In Python 2.5 it imports simplejson module, in 2.6 native json module. + +Usage: + + import port.json as json + + # and using regular simplejson interface with module json + json.dumps([]) + +""" + +try: + from json import * + dumps +except (ImportError, NameError): + from simplejson import * diff --git a/plotter/parse.py b/plotter/parse.py new file mode 100755 index 0000000..473e196 --- /dev/null +++ b/plotter/parse.py @@ -0,0 +1,41 @@ + +import math +import copy + + +# create list of "safe" methods to allow in equations +_safe_dict = { + "__builtins__": {"__import__": __import__}, + "abs": abs, +} + +# grab desired methods from math library +_math_funcs = ['ceil', 'floor', + 'exp', 'log', 'log10', 'pow', 'sqrt', + 'acos', 'asin', 'atan', 'atan2', 'cos', 'hypot', 'sin', 'tan', + 'cosh', 'sinh', 'tanh', + 'pi', 'e' +] +for symbol in _math_funcs: + _safe_dict[symbol] = getattr(math, symbol) + + +def parse(stringfunc): + """Returns a method corresponding to stringfunc.""" + + # create a function using user input as the return line + stringfunction = ("from __future__ import division\n" + "del __builtins__['__import__']\n" + "def plot(x):\n" + " return %s" + % stringfunc.replace("^", "**")) + compiledfunction = compile(stringfunction, "", "exec") + + # plot the method using "safe" globals and locals + # http://lybniz2.sourceforge.net/safeeval.html + # this should also be thread-safe (I think...) + globalscopy = copy.deepcopy(_safe_dict) + localscopy = {} + exec compiledfunction in globalscopy, localscopy + return localscopy["plot"] + diff --git a/plotter/plot.py b/plotter/plot.py new file mode 100755 index 0000000..36775e2 --- /dev/null +++ b/plotter/plot.py @@ -0,0 +1,31 @@ +"""Methods for creating plot figures.""" + +import cairoplot +from cairoplot.handlers.gtk import GTKHandler + +class CairoPlotCanvas(GTKHandler): + """GTK canvas displaying plots from.""" + + @staticmethod + def fromapp(app): + """Creates a CairoPlotCanvas from application.""" + + # plotsettings = plotter.settings.PlotSettings.fromapp(self) + xmin = app.xmin_spin.get_value() + xmax = app.xmax_spin.get_value() + xstep = (xmax - xmin) / 100.0 + + canvas = CairoPlotCanvas() + + # get data (functions in a list) + functions = app.get_functions() + + # create plot + plot = cairoplot.FunctionPlot(canvas, data=functions, + x_bounds=(xmin, xmax), step=xstep, + width=500, height=500, background="white", + border=20, axis=True, grid=True) + canvas.plot = plot + + return canvas + diff --git a/plotter/settings.py b/plotter/settings.py new file mode 100755 index 0000000..61d4434 --- /dev/null +++ b/plotter/settings.py @@ -0,0 +1,55 @@ +"""Objects for keeping track of graph settings.""" + +# version of settings (in case we break backward compatibility) +_FILE_VERSION = 1 + + +class PlotSettings(object): + """Settings for displaying a plot.""" + + def __init__(self, xmin, xmax): + """Saves settings.""" + + self.xmin = xmin + self.xmax = xmax + + @classmethod + def fromapp(settingsclass, app): + """Loads settings from a Plotter application.""" + + xmin = app.xmin_spin.get_value() + xmax = app.xmax_spin.get_value() + + return settingsclass(xmin, xmax) + + @classmethod + def load(settingsclass, settings): + """Loads settings from a file created by write().""" + + # TODO: throw exception for old versions + if settings["version"] > _FILE_VERSION: + return settingsclass(0, 0) + + xmin = settings["xmin"] + xmax = settings["xmax"] + + return settingsclass(xmin, xmax) + + + def save(self): + """Returns serialized version of settings as dictionary.""" + + return { + "version": _FILE_VERSION, + "xmin": self.xmin, + "xmax": self.xmax, + } + + + def toapp(self, app): + """Makes application reflect values from settings.""" + + app.xmin_spin.set_value(self.xmin) + app.xmax_spin.set_value(self.xmax) + + diff --git a/plotter/view/__init__.py b/plotter/view/__init__.py new file mode 100755 index 0000000..2bed743 --- /dev/null +++ b/plotter/view/__init__.py @@ -0,0 +1,243 @@ +"""Module for code-defined view objects. + +WARNING: do NOT put business logic in here. +Classes needing logic should use corresponding data models via +get_model(), load_model() methods. +""" + +import gtk +from gettext import gettext as _ + +from .equation import EquationInput as _EquationInput +from .puzzletree import PuzzleInput as _PuzzleInput + +# just in case we break backward compatibility +_FILE_VERSION = 1 +_EQUATION_VERSION = 1 + + +class EquationList(gtk.HBox): + """A list of equations.""" + + def __init__(self, app): + """Creates it...""" + gtk.HBox.__init__(self) + + # save application instance for undo/redo + self.app = app + + # create box for equation inputs + self.vbox = gtk.VBox() + self.vbox.show() + self.pack_start(self.vbox) + + # create add button for multiple equations + imagevbox = gtk.VBox() + addimage = gtk.Image() + addimage.set_from_stock(gtk.STOCK_ADD, gtk.ICON_SIZE_BUTTON) + addbutton = gtk.Button() + addbutton.add(addimage) + addbutton.show() + imagevbox.pack_end(addbutton, expand=False, fill=False) + imagevbox.show() + self.pack_start(imagevbox, expand=False) + addbutton.connect("clicked", self._on_add, None) + + # created default equation input + self._equations = [] + self._add_equation(_PuzzleInput(app)) + + + def save(self): + """Returns dictionary to be used in saving JSON.""" + + # create list of equations + equations_settings = [] + for equation in self._equations: + equations_settings.append(self._saveequation(equation)) + + settings = { + "version": _FILE_VERSION, + "equations": equations_settings + } + + return settings + + + def _saveequation(self, equation): + """Creates dictionary to save state of an equation.""" + + return { + "version": _EQUATION_VERSION, + "class": equation.CLASS, + "settings": equation.save() + } + + + def load(self, settings): + """Updates view with equations loaded from settings.""" + + if settings["version"] > _FILE_VERSION: + return + + # remove all existing equations + self.clear() + + # create new equations from settings + for equation in settings["equations"]: + self._loadequation(equation) + + + def _loadequation(self, equation): + """Loads an equation from saved state.""" + + if equation["version"] > _EQUATION_VERSION: + return + + # load input view from specified class + loaded = _VIEW_CLASSES[equation["class"]].load( + self.app, equation["settings"]) + loaded_input = self._add_equation(loaded) + return (loaded, loaded_input) + + + def get_model(self): + """Returns a list of equations.""" + + equations = [] + for equation in self._equations: + equations.append(equation.get_model()) + return equations + + + def _register_addremove(self, equation, equation_input, isadding=False): + """Register an add/remove equation action.""" + + # create actions (which preserve equation order) + i = self._equations.index(equation) + def remove(): + self._remove_equation(equation, equation_input) + def add(): + self._insert_equation_input(i, equation, equation_input) + + # register the action (inverse switched for adding vs removing) + action = remove + inverse = add + if isadding: + action = add + inverse = remove + self.app.register_action(action, inverse) + + + def _on_add(self, widget, data=None): + """Event to add a new equation input.""" + + # add a new equation + equation = _PuzzleInput(self.app) + equation_input = self._add_equation(equation) + + # register the action for undo/redo + self._register_addremove(equation, equation_input, isadding=True) + + + def _on_remove(self, widget, equation, equation_input): + """Event to remove an equation.""" + self._register_addremove(equation, equation_input, isadding=False) + self._remove_equation(equation, equation_input) + + + def _remove_equation(self, equation, equation_input): + """Removes an equation from the list.""" + self._equations.remove(equation) + self.vbox.remove(equation_input) + + + def _add_equation_input(self, equation, equation_input): + """Adds an equation to the end of the list.""" + self._equations.append(equation) + self.vbox.pack_start(equation_input, expand=False) + + + def _insert_equation_input(self, i, equation, equation_input): + """Inserts an equation to the list at position i.""" + self._equations.insert(i, equation) + self.vbox.pack_start(equation_input, expand=False) + self.vbox.reorder_child(equation_input, i) + + + def _convert_equation(self, equation, equation_input): + """Converts an equation into a Python input.""" + + # create new control with python equation text + i = self._equations.index(equation) + equationstring = equation.get_equation_string() + pythonequation = _EquationInput(self.app, equationstring) + pythonequation_input = self._create_equation(pythonequation) + + # register the action (for undo/redo) + def inverse(): + self._remove_equation(pythonequation, pythonequation_input) + self._insert_equation_input(i, equation, equation_input) + def action(): + self._remove_equation(equation, equation_input) + self._insert_equation_input(i, pythonequation, pythonequation_input) + self.app.register_action(action, inverse) + action() + + + def _create_equation(self, equation): + """Creates input for equation without adding it to the list.""" + + # can only convert equation inputs that have equation string conversion + canconvert = hasattr(equation, "get_equation_string") + + # create remove button + equation_input = gtk.HBox() + removeimage = gtk.Image() + removeimage.set_from_stock(gtk.STOCK_REMOVE, gtk.ICON_SIZE_BUTTON) + removebutton = gtk.Button() + removebutton.add(removeimage) + removebutton.show_all() + equation_input.pack_start(removebutton, expand=False) + equation_input.pack_start(equation) + + # connect button click to remove equation + removebutton.connect("clicked", self._on_remove, equation, + equation_input) + + # create convert to text button + if canconvert: + convertbutton = gtk.Button(_("To Python")) + convertbutton.show_all() + equation_input.pack_start(convertbutton, expand=False) + + def convertequation(widget, data=None): + self._convert_equation(equation, equation_input) + convertbutton.connect("clicked", convertequation) + + # display equation input + equation_input.show() + return equation_input + + + def _add_equation(self, equation): + """Adds an equation to the list.""" + # display equation input + equation_input = self._create_equation(equation) + self._add_equation_input(equation, equation_input) + return equation_input + + + def clear(self): + """Removes all equations from view.""" + self.vbox.foreach(self.vbox.remove) + self._equations = [] + + + +# dictionary of all input view classes +_VIEW_CLASSES = { + _EquationInput.CLASS: _EquationInput, + _PuzzleInput.CLASS: _PuzzleInput +} + diff --git a/plotter/view/equation.py b/plotter/view/equation.py new file mode 100755 index 0000000..da00919 --- /dev/null +++ b/plotter/view/equation.py @@ -0,0 +1,54 @@ +"""Widgets for equation input.""" + +from gettext import gettext as _ +import gtk +import plotter.parse + + +_FILE_VERSION = 1 + + +class EquationInput(gtk.HBox): + """A text box...""" + + # unique identifier for this input type + CLASS = "text" + + def __init__(self, app, equation="x"): + """Creates input...""" + gtk.HBox.__init__(self) + + # add f(x) rather than y, to imply this is a function + self.pack_start(gtk.Label(_("f(x) =")), expand=False, + padding=5) + + # create equation entry w/ default text + self._equation = gtk.Entry() + self._equation.set_text(equation) + self.pack_start(self._equation) + self.show_all() + + + @staticmethod + def load(app, settings): + """Creates new equation entry from saved state.""" + + if settings["version"] > _FILE_VERSION: + return EquationInput(app) + return EquationInput(app, settings["text"]) + + + def save(self): + """Returns settings dictionary with current state.""" + + settings = { + "version": _FILE_VERSION, + "text": self._equation.get_text() + } + return settings + + + def get_model(self): + """Returns function with entry.""" + return plotter.parse.parse(self._equation.get_text()) + diff --git a/plotter/view/puzzletree/__init__.py b/plotter/view/puzzletree/__init__.py new file mode 100755 index 0000000..093ef8d --- /dev/null +++ b/plotter/view/puzzletree/__init__.py @@ -0,0 +1,110 @@ +"""Module for mouse function input.""" + +import gtk + +from .display import PuzzleDisplay +from .palette import PuzzlePalette +from .nodes import Node + +_FILE_VERSION = 1 + +class PuzzleInput(gtk.VBox): + """PuzzleInput is a control that uses mouse input to create functions.""" + + CLASS = "puzzle" + + def __init__(self, app, rootnode=None): + """Creates input (and display) of tree. + + We'd like to support multiple trees for click-and-drag, + but for now, just one tree is made at a time. + """ + gtk.VBox.__init__(self) + + self.app = app + + # create box to hold display and undo button + displaybox = gtk.HBox() + self.pack_start(displaybox) + + # create display for drawing function tree + displayscroll = gtk.ScrolledWindow() + displayscroll.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_NEVER) + displayview = gtk.Viewport() + self._display = PuzzleDisplay(rootnode) + displayview.add(self._display) + displayscroll.add(displayview) + displaybox.pack_start(displayscroll) + + # undo button (TODO: this isn't really undo) + undoimage = gtk.Image() + undoimage.set_from_stock(gtk.STOCK_UNDO, gtk.ICON_SIZE_BUTTON) + undobutton = gtk.Button() + undobutton.add(undoimage) + undobutton.show_all() + displaybox.pack_start(undobutton, expand=False) + + # connect undo event (should show palette and undo) + undobutton.connect("clicked", self.on_undo) + + # create buttons for adding nodes + palette = PuzzlePalette(app, self._display) + self.pack_start(palette) + self._palette = palette + + # hide palette if it cannot be used + self.show_all() + if (self._display.rootnode is not None and + self._display.nextnode is None): + palette.hide() + + + @staticmethod + def load(app, settings): + """Create new input from dictionary state.""" + + if settings["version"] > _FILE_VERSION: + return PuzzleInput() + return PuzzleInput(app, Node.load(settings["settings"])) + + + def save(self): + """Create a dictionary to save current state.""" + + nodesettings = None + if self._display.rootnode is not None: + nodesettings = self._display.rootnode.save() + + settings = { + "version": _FILE_VERSION, + "settings": nodesettings + } + return settings + + + def on_undo(self, widget, data=None): + """Undoes the previous action (removes bottom-right node).""" + + removednode = self._display.undo() + self._palette.show_all() + + # register action for undo/redo + def action(): + self._display.undo() + self._palette.show_all() + def inverse(): + self._display.addnode(removednode) + if self._display.nextnode is None: + self._palette.hide() + self.app.register_action(action, inverse) + + + def get_model(self): + """Returns callable model from the tree.""" + return self._display.get_model() + + + def get_equation_string(self): + """Returns a string representing the current equation.""" + return Node.get_equation_string(self._display.rootnode, "x") + diff --git a/plotter/view/puzzletree/display.py b/plotter/view/puzzletree/display.py new file mode 100755 index 0000000..86c5b64 --- /dev/null +++ b/plotter/view/puzzletree/display.py @@ -0,0 +1,323 @@ + +import copy +import gtk +import gtk.gdk +import os.path +import bisect + +from .nodes import Node, NODE_WIDTH, NODE_HEIGHT + +_PAREN_WIDTH = 16 +_BINARY_OPERATOR_WIDTH = 2 * _PAREN_WIDTH + 2 * NODE_WIDTH + + +class PuzzleDisplay(gtk.DrawingArea): + """Displays graphical representation of function tree.""" + + _leftparen = gtk.gdk.pixbuf_new_from_file_at_size( + os.path.join("data", "puzzle", "leftparen.svg"), + _PAREN_WIDTH, NODE_HEIGHT) + + _rightparen = gtk.gdk.pixbuf_new_from_file_at_size( + os.path.join("data", "puzzle", "rightparen.svg"), + _PAREN_WIDTH, NODE_HEIGHT) + + _blank = gtk.gdk.pixbuf_new_from_file_at_size( + os.path.join("data", "puzzle", "blank.svg"), + NODE_WIDTH, NODE_HEIGHT) + + def __init__(self, rootnode=None): + """Creates a tree, and initializes code for modifying it. + + For now, there is only one spot that new operators/functions + can be added, and that is the bottom-left-most null pointer. + """ + gtk.DrawingArea.__init__(self) + + self.rootnode = rootnode + self._updatenextnode() + + # keep track of all nodes and their positions (for tooltip) + self._nodes = [] + self._positions = [] + + # setup tooltips + self.set_property("has-tooltip", True) + self.connect("query-tooltip", self.on_query_tooltip) + + # connect events for resizing/redrawing + self.connect("expose_event", self.on_expose_event) + + # make sure we have space for the puzzle pieces + self.width = PuzzleDisplay._getwidth(rootnode) + self.height = NODE_HEIGHT + self.set_size_request(self.width, self.height) + + + def on_expose_event(self, widget, data): + """Redraws plot if need be.""" + self.draw(widget.window.cairo_create()) + + + def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip, *args): + """Updates the tooltip based on which node mouse is over.""" + + node = self._nodeatposition(x) + if node is not None: + # draw tooltip for node under the cursor + tooltip.set_text(Node.get_equation_string(node, "x")) + else: + # draw tooltip for whole tree + tooltip.set_text(Node.get_equation_string(self.rootnode, "x")) + + return True + + + def get_model(self): + """Returns a copy of the rootnode, which should be callable.""" + + # TODO: if tree state is invalid, we shouldn't be calling this + if self.rootnode is None: + return lambda x: 0 + return copy.deepcopy(self.rootnode) + + + def draw(self, context): + """Draws the current tree (with next node highlighted). + + Draws from left-to-right, so it walks the node tree depth-first. + """ + + # reset position list + self._nodes = [] + self._positions = [] + + # draw the tree (and update position list) + context.save() + self._drawtree(self.rootnode, context) + context.restore() + + + @staticmethod + def _getwidth(node): + """Returns desired width for drawing tree rooted at node.""" + + # just on piece + if node is None or len(node.children) == 0: + return NODE_WIDTH + + # make room for parentheses on binary operators + nodewidth = NODE_WIDTH + if len(node.children) == 2: + nodewidth += _PAREN_WIDTH * 2 + + for c in node.children: + nodewidth += PuzzleDisplay._getwidth(c) + return nodewidth + + + @staticmethod + def _drawparen(paren, context): + """Draws a parenthesis and translates by correct amount.""" + context.set_source_pixbuf(paren, 0, 0) + context.paint() + context.translate(_PAREN_WIDTH, 0) + + + def _drawtree(self, node, context, isfirstblank=True): + """Draws the tree rooted at node from left-to-right. + + Returns True if no blank has yet been drawn, else False. + """ + + # for empty nodes, just translate to right + # if it is the first empty node that we're drawing, + # indicate visually that this is where the next node will be + if node is None: + if isfirstblank: + context.set_source_pixbuf(PuzzleDisplay._blank, 0, 0) + context.paint() + context.translate(NODE_WIDTH, 0) + return False + + # unary operators, binary operators, and leaves are drawn differently + totalchildren = len(node.children) + + def drawnode(): + """After each draw, translate by node width.""" + + # keep track of all nodes and their positions (for tooltip) + self._nodes.append(node) + matrix = context.get_matrix() + self._positions.append(matrix[4]) + + # draw node and move to next place + node.draw(context) + context.translate(NODE_WIDTH, 0) + + if totalchildren == 2: + # draw parentheses around all BinaryOperators + PuzzleDisplay._drawparen(PuzzleDisplay._leftparen, context) + + # BinaryOperator + # draw left-most child first, then node, then right child + isfirstblank = self._drawtree( + node.children[0], context, isfirstblank) + drawnode() + isfirstblank = self._drawtree( + node.children[1], context, isfirstblank) + + PuzzleDisplay._drawparen(PuzzleDisplay._rightparen, context) + + elif totalchildren == 1: + # UnaryOperator + # operator gets drawn first (e.g. -x) + drawnode() + isfirstblank = self._drawtree( + node.children[0], context, isfirstblank) + + else: + drawnode() + + return isfirstblank + + + def addnode(self, node): + """Adds node to appropriate location.""" + + # make it the root of the tree if no root exists + if self.rootnode is None: + self.rootnode = node + + # no open slots for adding nodes exist + elif self.nextnode is None: + return + + # add to left-most empty slot in nextnode + else: + for i in range(len(self.nextnode.children)): + if self.nextnode.children[i] is None: + self.nextnode.children[i] = node + break + + # calculate desired width as we add pieces + # new size is based on how many blank spaces are added + totalblanks = len(node.children) + if totalblanks != 0: + if totalblanks == 2: + # BinaryOperators need space for two blanks and parentheses + self.width += _BINARY_OPERATOR_WIDTH + else: + # UnaryOperators just need space for one blank + self.width += NODE_WIDTH + self.set_size_request(self.width, self.height) + + # refresh the view, since the tree has changed + self._updatenextnode() + self.queue_draw() + + + def undo(self): + """Removes the node that was last added.""" + + # can't undo anymore if the tree is empty + if self.rootnode is None: + return + + # find parent of node to remove + lastparent = PuzzleDisplay._findlastnode(self.rootnode) + + # determine which node are we removing and remove it + removednode = None + if lastparent is None: + removednode = self.rootnode + self.rootnode = None + else: + # find right-most child + for nodeindex in reversed(range(len(lastparent.children))): + node = lastparent.children[nodeindex] + if node is not None: + lastparent.children[nodeindex] = None + removednode = node + break + + # calculate desired width as we remove pieces + # new size is based on how many blank spaces are removed + totalblanks = len(removednode.children) + if totalblanks != 0: + if totalblanks == 2: + # BinaryOperators need space for two blanks and parentheses + self.width -= _BINARY_OPERATOR_WIDTH + else: + # UnaryOperators just need space for one blank + self.width -= NODE_WIDTH + self.set_size_request(self.width, self.height) + + # refresh the view, since the tree has changed + self._updatenextnode() + self.queue_draw() + return removednode + + + def _updatenextnode(self): + """Sets next pointer to bottom-left-most node with null child.""" + + # if no nodes in tree, next should create rootnode + self.nextnode = None + if self.rootnode is None: + return + + # do depth-first search to find bottom-left-most node + self.nextnode = PuzzleDisplay._findnullchild(self.rootnode) + + + def _nodeatposition(self, x): + """Returns node at position, x, or None if no node is there.""" + + nodeindex = bisect.bisect(self._positions, x) - 1 + if nodeindex >= 0 and NODE_WIDTH > x - self._positions[nodeindex]: + return self._nodes[nodeindex] + return None + + + @staticmethod + def _findnullchild(node): + """Finds first node that has a null child. + + Returns bottom-left-most node with null child or + None if no such nodes are in the tree.""" + + for child in node.children: + # does this node have a null child + if child is None: + return node + + bottomleft = PuzzleDisplay._findnullchild(child) + if bottomleft is not None: + return bottomleft + + # no nodes with null children + return None + + + @staticmethod + def _findlastnode(node): + """Finds bottom-right-most node in the tree. + + Returns parent of bottom-rigth-most node or + None if node has no children.""" + + for child in reversed(node.children): + # don't traverse null children + if child is None: + continue + + lastparent = PuzzleDisplay._findlastnode(child) + if lastparent is None: + return node + else: + return lastparent + + # this node is not the parent of any nodes + return None + diff --git a/plotter/view/puzzletree/nodes/__init__.py b/plotter/view/puzzletree/nodes/__init__.py new file mode 100755 index 0000000..b49b4a7 --- /dev/null +++ b/plotter/view/puzzletree/nodes/__init__.py @@ -0,0 +1,56 @@ +"""All possible nodes that can be created are listed here.""" + +# group nodes into types (operators & functions) +# operators +from .addition import Addition +from .composition import Composition +from .exponentiation import Exponentiation +from .multiplication import Multiplication + +OPERATORS = [ + Addition, + Multiplication, + Exponentiation, + Composition +] + +# constants +from .constant import Constant +from .pi import Pi +from .e import E + +CONSTANTS = [ + Pi, + E, + Constant, +] + +# functions +from .identity import Identity +from .absolutevalue import AbsoluteValue +from .sine import Sine + +FUNCTIONS = [ + Identity, + AbsoluteValue, + Sine, +] + +# list categories in the order they should be displayed +from gettext import gettext as _ +CATEGORIES = [ + (_("Operators"), OPERATORS), + (_("Functions"), FUNCTIONS), + (_("Constants"), CONSTANTS), +] + +# generate class list for loading from a dictionary +from itertools import chain as _chain + +CLASSES = {} +for node in _chain(*(c[1] for c in CATEGORIES)): + CLASSES[node.CLASS] = node + +# get this last, since they aren't nodes we can make +from .node import Node, NODE_WIDTH, NODE_HEIGHT + diff --git a/plotter/view/puzzletree/nodes/absolutevalue.py b/plotter/view/puzzletree/nodes/absolutevalue.py new file mode 100755 index 0000000..db569f5 --- /dev/null +++ b/plotter/view/puzzletree/nodes/absolutevalue.py @@ -0,0 +1,23 @@ + +from .simplenode import SimpleNode +from gettext import gettext as _ + + +class AbsoluteValue(SimpleNode): + """Node representing the absolute value function: f(x) = abs(x).""" + + CLASS = "absolutevalue" + background = SimpleNode.loadbackground("absolutevalue.svg") + title = _("Absolute Value") + description = _(u"Returns the distance a value is from zero.\n" + u"For example, f(-3) = 3 and f(3) = 3.") + + def __call__(self, x): + """Calls the absolute value function.""" + return abs(x) + + + def get_equation_string(self, variable): + """Returns abs(x) given variable, x.""" + return "abs(%s)" % variable + diff --git a/plotter/view/puzzletree/nodes/addition.py b/plotter/view/puzzletree/nodes/addition.py new file mode 100755 index 0000000..d6b37dc --- /dev/null +++ b/plotter/view/puzzletree/nodes/addition.py @@ -0,0 +1,19 @@ + +from .binaryoperator import BinaryOperator as _BinaryOperator +from gettext import gettext as _ + + +class Addition(_BinaryOperator): + """Addition is a BinaryOperator that adds its two children together.""" + + CLASS = "addition" + background = _BinaryOperator.loadbackground("addition.svg") + operatorstring = "+" + title = _("Addition") + description = _("Combines two objects together into a larger collection." + " For example, x + x = 2x.") + + def __call__(self, x): + """Returns self's leftchild(x) + rightchild(x).""" + return self.children[0](x) + self.children[1](x) + diff --git a/plotter/view/puzzletree/nodes/binaryoperator.py b/plotter/view/puzzletree/nodes/binaryoperator.py new file mode 100755 index 0000000..97208e5 --- /dev/null +++ b/plotter/view/puzzletree/nodes/binaryoperator.py @@ -0,0 +1,30 @@ +from .node import Node as _Node + + +class BinaryOperator(_Node): + """BinaryOperators have at most two children.""" + + def __init__(self, leftchild=None, rightchild=None): + """Create binary operator (possibly with children).""" + _Node.__init__(self) + self.children = [leftchild, rightchild] + + + @classmethod + def load(nodeclass, settings): + """Creates a new BinaryOperator of type nodeclass. + + Default BinaryOperators don't have any parameters, + so this just returns a new node. + """ + return nodeclass() + + + def get_equation_string(self, variable): + """Returns a string representing the current equation.""" + + return "(%s %s %s)" % ( + _Node.get_equation_string(self.children[0], variable), + self.operatorstring, + _Node.get_equation_string(self.children[1], variable)) + diff --git a/plotter/view/puzzletree/nodes/composition.py b/plotter/view/puzzletree/nodes/composition.py new file mode 100755 index 0000000..120318f --- /dev/null +++ b/plotter/view/puzzletree/nodes/composition.py @@ -0,0 +1,29 @@ +# coding=utf-8 + +from .node import Node +from .binaryoperator import BinaryOperator +from gettext import gettext as _ + + +class Composition(BinaryOperator): + """Composition: left compose right function.""" + + CLASS = "composition" + background = BinaryOperator.loadbackground("composition.svg") + title = _("Function Composition") + description = _(u"Combines two functions by applying the result of the " + u"right function to the left.\n" + u"For example, (x ** 2) ∘ (x + 1) = (x + 1) ** 2.") + + def __call__(self, x): + """Returns self's leftchild(rightchild(x)).""" + return self.children[0](self.children[1](x)) + + + def get_equation_string(self, variable): + """Returns a string representing the current equation.""" + + # result from right will be new variable in left + variable = Node.get_equation_string(self.children[1], variable) + return Node.get_equation_string(self.children[0], variable) + diff --git a/plotter/view/puzzletree/nodes/constant.py b/plotter/view/puzzletree/nodes/constant.py new file mode 100755 index 0000000..77231fd --- /dev/null +++ b/plotter/view/puzzletree/nodes/constant.py @@ -0,0 +1,67 @@ + +from .node import Node as _Node +from gettext import gettext as _ + +import gtk + +_FILE_VERSION = 1 + +# default values for editing the constant parameter +_constant_adjustment = gtk.Adjustment( + value=2, step_incr=1, lower=-1e9, upper=1e9) + +def _create_constant_spin(): + """Creates spin button for changing constant's value.""" + return gtk.SpinButton(_constant_adjustment, + climb_rate=1, digits=2) + + + +class Constant(_Node): + """Constant represent constant real-number values.""" + + CLASS = "constant" + background = _Node.loadbackground("constant.svg") + title = _("Constant") + description = _(u"Returns the same value, ignoring the input.\n" + "For example, f(x) = 2.") + + parameters = [ + (_("Value"), _create_constant_spin, gtk.SpinButton.get_value) + ] + + def __init__(self, value=2): + """Create Constant with value of value.""" + _Node.__init__(self) + self.value = value + + + def __call__(self, x): + """Ignore the parameter and return a constant value.""" + return self.value + + + @staticmethod + def load(settings): + """Loads constant from value in dictionary.""" + if settings["version"] > _FILE_VERSION: + return Constant() + if "value" in settings: + return Constant(settings["value"]) + + + def save(self): + """Saves constant to a dictionary.""" + headersettings = _Node.save(self) + settings = { + "version": _FILE_VERSION, + "value": self.value + } + headersettings["settings"] = settings + return headersettings + + + def get_equation_string(self, variable): + """Returns string form of value (ignoring variable).""" + return str(self.value) + diff --git a/plotter/view/puzzletree/nodes/e.py b/plotter/view/puzzletree/nodes/e.py new file mode 100755 index 0000000..f26a7af --- /dev/null +++ b/plotter/view/puzzletree/nodes/e.py @@ -0,0 +1,27 @@ +# coding=utf-8 + +from .simplenode import SimpleNode +from gettext import gettext as _ + +import math + + +class E(SimpleNode): + """Node representing e.""" + + CLASS = "e" + background = SimpleNode.loadbackground("e.svg") + title = _("e") + description = _(u"e is an irrational number such that the derivative " + u"of f(x) = e ** x is f(x).\n" + u"e is approximately 2.71828") + + def __call__(self, x): + """Returns e.""" + return math.e + + + def get_equation_string(self, variable): + """Returns e, ignoring the variable.""" + return "e" + diff --git a/plotter/view/puzzletree/nodes/exponentiation.py b/plotter/view/puzzletree/nodes/exponentiation.py new file mode 100755 index 0000000..b2936a8 --- /dev/null +++ b/plotter/view/puzzletree/nodes/exponentiation.py @@ -0,0 +1,20 @@ + +from .binaryoperator import BinaryOperator as _BinaryOperator +from gettext import gettext as _ + + +class Exponentiation(_BinaryOperator): + """Exponentiation: a BinaryOperator for powers.""" + + CLASS = "exponentiation" + background = _BinaryOperator.loadbackground("exponentiation.svg") + operatorstring = "**" + title = _("Exponentiation") + description = _(u"Combines two objects by multiplying the left one " + u"for the right number of times.\n" + u"For example, x ** 2 = x * x.") + + def __call__(self, x): + """Returns self's leftchild(x) ** rightchild(x).""" + return self.children[0](x) ** self.children[1](x) + diff --git a/plotter/view/puzzletree/nodes/identity.py b/plotter/view/puzzletree/nodes/identity.py new file mode 100755 index 0000000..19f8803 --- /dev/null +++ b/plotter/view/puzzletree/nodes/identity.py @@ -0,0 +1,23 @@ + +from .simplenode import SimpleNode +from gettext import gettext as _ + + +class Identity(SimpleNode): + """Node representing the identity function: f(x) = x.""" + + CLASS = "identity" + background = SimpleNode.loadbackground("identity.svg") + title = _("Identity") + description = _(u"Returns whatever was input to it.\n" + u"For example, f(x) = x.") + + def __call__(self, x): + """Identity function (f(x) = x).""" + return x + + + def get_equation_string(self, variable): + """Returns variable, since this is the identity function.""" + return variable + diff --git a/plotter/view/puzzletree/nodes/multiplication.py b/plotter/view/puzzletree/nodes/multiplication.py new file mode 100755 index 0000000..c871b8d --- /dev/null +++ b/plotter/view/puzzletree/nodes/multiplication.py @@ -0,0 +1,20 @@ + +from .binaryoperator import BinaryOperator as _BinaryOperator +from gettext import gettext as _ + + +class Multiplication(_BinaryOperator): + """Multiplication is a BinaryOperator that multiplies its two children.""" + + CLASS = "multiplication" + background = _BinaryOperator.loadbackground("multiplication.svg") + operatorstring = "*" + title = _("Multiplication") + description = _(u"Combines two objects by adding the left one " + u"for the right number of times.\n" + u"For example, x * 3 = x + x + x.") + + def __call__(self, x): + """Returns self's leftchild(x) * rightchild(x).""" + return self.children[0](x) * self.children[1](x) + diff --git a/plotter/view/puzzletree/nodes/node.py b/plotter/view/puzzletree/nodes/node.py new file mode 100755 index 0000000..a38664d --- /dev/null +++ b/plotter/view/puzzletree/nodes/node.py @@ -0,0 +1,82 @@ +"""Base class for all nodes (or at least nodes that aren't extremely wierd).""" + +import gtk.gdk +import os.path + +_FILE_VERSION = 1 +NODE_WIDTH = 32 +NODE_HEIGHT = 32 + + +class Node(object): + """Node is a piece of an equation.""" + + title = "Node" + description = "Element of a function." + + # a list of (tile, constructor, getvalue) for nodes w/ params + parameters = [] + + def __init__(self): + """Initialize children property, since all nodes will need it.""" + self.children = [] + + + @staticmethod + def load(settings): + """Loads a node from a dictionary.""" + + if settings is None or settings["version"] > _FILE_VERSION: + return None + + # this import is here to prevent cyclic imports + from ..nodes import CLASSES + + # load input view from specified class + node = CLASSES[settings["class"]].load(settings["settings"]) + node.children = [Node.load(c) for c in settings["children"]] + return node + + + def save(self): + """Creates dictionary representing current state. + + In dictionary, settings is reserved for sub-classes with + parameters (like value in Constant). + """ + + settings = { + "version": _FILE_VERSION, + "class": self.CLASS, + "settings": None, + "children": [None if c is None else c.save() + for c in self.children] + } + return settings + + + @staticmethod + def loadbackground(filename): + """Loads an image from the puzzle piece folder.""" + return gtk.gdk.pixbuf_new_from_file_at_size( + os.path.join("data", "puzzle", filename), + NODE_WIDTH, NODE_HEIGHT) + + + @staticmethod + def get_equation_string(node, variable): + """Returns equation string for node (even when null).""" + if node is None: + return "" + return node.get_equation_string(variable) + + + def draw(self, context): + """Draws puzzle piece at current location on context.""" + + # TODO: draw piece background as well (like a puzzle piece) + + # draw image representing the node type + context.set_source_pixbuf(self.background, 0, 0) + context.paint() + diff --git a/plotter/view/puzzletree/nodes/pi.py b/plotter/view/puzzletree/nodes/pi.py new file mode 100755 index 0000000..7be94e5 --- /dev/null +++ b/plotter/view/puzzletree/nodes/pi.py @@ -0,0 +1,27 @@ +# coding=utf-8 + +from .simplenode import SimpleNode +from gettext import gettext as _ + +import math + + +class Pi(SimpleNode): + """Node representing pi.""" + + CLASS = "pi" + background = SimpleNode.loadbackground("pi.svg") + title = _("π") + description = _(u"Returns the ratio of a circle's circumference to its " + u"diameter.\n" + u"π (pi) is approximately 3.14159") + + def __call__(self, x): + """Returns pi.""" + return math.pi + + + def get_equation_string(self, variable): + """Returns pi, ignoring the variable.""" + return "pi" + diff --git a/plotter/view/puzzletree/nodes/simplenode.py b/plotter/view/puzzletree/nodes/simplenode.py new file mode 100755 index 0000000..7844218 --- /dev/null +++ b/plotter/view/puzzletree/nodes/simplenode.py @@ -0,0 +1,12 @@ + +from .node import Node as _Node + + +class SimpleNode(_Node): + """Nodes with no parameters, so load just uses default constructor.""" + + @classmethod + def load(nodeclass, settings): + """Ignore settings, since no parameters for SimpleNode.""" + return nodeclass() + diff --git a/plotter/view/puzzletree/nodes/sine.py b/plotter/view/puzzletree/nodes/sine.py new file mode 100755 index 0000000..493354e --- /dev/null +++ b/plotter/view/puzzletree/nodes/sine.py @@ -0,0 +1,26 @@ + +from .simplenode import SimpleNode +from gettext import gettext as _ + +import math + + +class Sine(SimpleNode): + """Node representing the sine function: f(x) = sin(x).""" + + CLASS = "sine" + background = SimpleNode.loadbackground("sine.svg") + title = _("Sine") + description = _(u"Returns the ratio of the length of a side opposite an " + u"acute angle in a right trianale to the length of the hypotenuse.\n" + u"For example, sin(pi / 4) = 1 / sqrt(2)") + + def __call__(self, x): + """Calls the sine function.""" + return math.sin(x) + + + def get_equation_string(self, variable): + """Returns sin(x) given x.""" + return "sin(%s)" % variable + diff --git a/plotter/view/puzzletree/palette.py b/plotter/view/puzzletree/palette.py new file mode 100755 index 0000000..859ef98 --- /dev/null +++ b/plotter/view/puzzletree/palette.py @@ -0,0 +1,108 @@ + +from . import nodes + +import gtk + + + +class PuzzlePalette(gtk.VBox): + """PuzzlePalette has buttons for adding nodes to function tree.""" + + def __init__(self, app, display): + """Create a palette of nodes to use in making funtions. + + Parameters + ========== + + :display: PuzzleDisplay that contains the function tree. + """ + gtk.VBox.__init__(self, spacing=8) + + self.app = app + self._display = display + + # make buttons for operators + isfirstcategory = True + for title, category in nodes.CATEGORIES: + # create label for title + titlelabel = gtk.Label("%s" % title) + titlelabel.set_use_markup(True) + self.pack_start(titlelabel) + + # add nodes to category palette + for node in category: + nodebox = gtk.HBox() + + # create button with node image + # (also, create a vbox, so its not stretched out) + imagevbox = gtk.VBox() + nodeimage = gtk.Image() + nodeimage.set_from_pixbuf(node.background) + nodebutton = gtk.Button() + nodebutton.add(nodeimage) + imagevbox.pack_start(nodebutton, expand=False, fill=False) + nodebox.pack_start(imagevbox, expand=False) + + # add labels for title and description + descrbox = gtk.VBox() + + # title (align left) + nodetitlelabel = gtk.Label(node.title) + nodetitlelabel.set_alignment(0.02, 0.0) + descrbox.pack_start(nodetitlelabel, padding=1) + + # (optional) parameters for node + parameterfetchers = [] + totalparameters = len(node.parameters) + if totalparameters != 0: + paramtable = gtk.Table(totalparameters, 2) + descrbox.pack_start(paramtable) + + for row, paramvalue in enumerate(node.parameters): + paramtitle, paramclass, paramvalue = paramvalue + + # parameter title label + paramlabel = gtk.Label(paramtitle) + paramtable.attach(paramlabel, 0, 1, row, row + 1) + + # parameter input + paraminput = paramclass() + paramtable.attach(paraminput, 1, 2, row, row + 1) + + # add method to get parameter to parameterfetchers + parameterfetchers.append((paramvalue, paraminput)) + + # description (align mostly left) + descrlabel = gtk.Label(node.description) + descrlabel.set_line_wrap(True) + descrlabel.set_alignment(0.1, 0.0) + descrbox.pack_start(descrlabel) + nodebox.pack_start(descrbox) + + # connect click event to add node + nodebutton.connect("clicked", self.on_add_event, node, + *parameterfetchers) + self.pack_start(nodebox, padding=1) + + + def addnode(self, node): + """Adds node to display.""" + + def action(): + # add node and hide palette if no more nodes can be added + self._display.addnode(node) + if self._display.nextnode is None: + self.hide() + def inverse(): + self._display.undo() + self.show_all() + self.app.register_action(action, inverse) + + # actually add the node + action() + + + def on_add_event(self, widget, nodeclass, *args): + """Adds node of type nodeclass to display.""" + self.addnode(nodeclass(*[v(p) for v, p in args])) + diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100755 index 0000000..871da2d --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,16 @@ +encoding: UTF-8 +plot.py +gtkplotactivity.py +plotter/view/__init__.py +plotter/view/equation.py +plotter/view/puzzletree/nodes/__init__.py +plotter/view/puzzletree/nodes/absolutevalue.py +plotter/view/puzzletree/nodes/addition.py +plotter/view/puzzletree/nodes/composition.py +plotter/view/puzzletree/nodes/constant.py +plotter/view/puzzletree/nodes/e.py +plotter/view/puzzletree/nodes/exponentiation.py +plotter/view/puzzletree/nodes/identity.py +plotter/view/puzzletree/nodes/multiplication.py +plotter/view/puzzletree/nodes/pi.py +plotter/view/puzzletree/nodes/sine.py diff --git a/po/Plot.pot b/po/Plot.pot new file mode 100755 index 0000000..2074005 --- /dev/null +++ b/po/Plot.pot @@ -0,0 +1,209 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-13 22:05-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: activity/activity.info:2 +msgid "Plot" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plot.py:30 +msgid "Edit" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:49 +msgid "_File" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:53 +msgid "_Open" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:57 +msgid "_Save" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:62 +msgid "_Quit" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:68 +msgid "_Edit" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:72 +msgid "_Undo" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:76 +msgid "_Redo" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:81 +msgid "_Copy" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:85 +msgid "_Paste" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:110 +msgid "Go!" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:116 +msgid "x min." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:120 +msgid "x max." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:160 +msgid "Save.." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:191 +msgid "Open.." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/equation.py:22 +msgid "f(x) =" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/__init__.py:210 +msgid "To Python" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/composition.py:13 +msgid "Function Composition" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/composition.py:14 +msgid "" +"Combines two functions by applying the result of the right function to the " +"left.\n" +"For example, (x ** 2) ∘ (x + 1) = (x + 1) ** 2." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/sine.py:13 +msgid "Sine" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/sine.py:14 +msgid "" +"Returns the ratio of the length of a side opposite an acute angle in a right " +"trianale to the length of the hypotenuse.\n" +"For example, sin(pi / 4) = 1 / sqrt(2)" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/pi.py:14 +msgid "π" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/pi.py:15 +msgid "" +"Returns the ratio of a circle's circumference to its diameter.\n" +"π (pi) is approximately 3.14159" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/identity.py:11 +msgid "Identity" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/identity.py:12 +msgid "" +"Returns whatever was input to it.\n" +"For example, f(x) = x." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/constant.py:25 +msgid "Constant" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/constant.py:26 +msgid "" +"Returns the same value, ignoring the input.\n" +"For example, f(x) = 2." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/constant.py:30 +msgid "Value" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/absolutevalue.py:11 +msgid "Absolute Value" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/absolutevalue.py:12 +msgid "" +"Returns the distance a value is from zero.\n" +"For example, f(-3) = 3 and f(3) = 3." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/__init__.py:42 +msgid "Operators" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/__init__.py:43 +msgid "Functions" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/__init__.py:44 +msgid "Constants" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/e.py:14 +msgid "e" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/e.py:15 +msgid "" +"e is an irrational number such that the derivative of f(x) = e ** x is f" +"(x).\n" +"e is approximately 2.71828" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/multiplication.py:12 +msgid "Multiplication" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/multiplication.py:13 +msgid "" +"Combines two objects by adding the left one for the right number of times.\n" +"For example, x * 3 = x + x + x." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/exponentiation.py:12 +msgid "Exponentiation" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/exponentiation.py:13 +msgid "" +"Combines two objects by multiplying the left one for the right number of " +"times.\n" +"For example, x ** 2 = x * x." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/addition.py:12 +msgid "Addition" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/addition.py:13 +msgid "" +"Combines two objects together into a larger collection. For example, x + x = " +"2x." +msgstr "" diff --git a/po/en-US.po b/po/en-US.po new file mode 100755 index 0000000..4e1e455 --- /dev/null +++ b/po/en-US.po @@ -0,0 +1,222 @@ +# Language en-US translations for PACKAGE package. +# Copyright (C) 2010 THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# Tim Swast , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-04-13 22:05-0500\n" +"PO-Revision-Date: 2010-04-13 22:08-0500\n" +"Last-Translator: Tim Swast \n" +"Language-Team: Language en-US\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: activity/activity.info:2 +msgid "Plot" +msgstr "Plot" + +#: /home/tswast/Desktop/plot-trunk/plot.py:30 +msgid "Edit" +msgstr "Edit" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:49 +msgid "_File" +msgstr "_File" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:53 +msgid "_Open" +msgstr "_Open" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:57 +msgid "_Save" +msgstr "_Save" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:62 +msgid "_Quit" +msgstr "_Quit" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:68 +msgid "_Edit" +msgstr "_Edit" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:72 +msgid "_Undo" +msgstr "_Undo" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:76 +msgid "_Redo" +msgstr "_Redo" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:81 +msgid "_Copy" +msgstr "_Copy" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:85 +msgid "_Paste" +msgstr "_Paste" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:110 +msgid "Go!" +msgstr "Go!" + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:116 +msgid "x min." +msgstr "x min." + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:120 +msgid "x max." +msgstr "x max." + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:160 +msgid "Save.." +msgstr "Save.." + +#: /home/tswast/Desktop/plot-trunk/gtkplotactivity.py:191 +msgid "Open.." +msgstr "Open.." + +#: /home/tswast/Desktop/plot-trunk/plotter/view/equation.py:22 +msgid "f(x) =" +msgstr "f(x) =" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/__init__.py:210 +msgid "To Python" +msgstr "To Python" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/composition.py:13 +msgid "Function Composition" +msgstr "Function Composition" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/composition.py:14 +msgid "" +"Combines two functions by applying the result of the right function to the " +"left.\n" +"For example, (x ** 2) ∘ (x + 1) = (x + 1) ** 2." +msgstr "" +"Combines two functions by applying the result of the right function to the " +"left.\n" +"For example, (x ** 2) ∘ (x + 1) = (x + 1) ** 2." + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/sine.py:13 +msgid "Sine" +msgstr "Sine" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/sine.py:14 +msgid "" +"Returns the ratio of the length of a side opposite an acute angle in a right " +"trianale to the length of the hypotenuse.\n" +"For example, sin(pi / 4) = 1 / sqrt(2)" +msgstr "" +"Returns the ratio of the length of a side opposite an acute angle in a right " +"trianale to the length of the hypotenuse.\n" +"For example, sin(pi / 4) = 1 / sqrt(2)" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/pi.py:14 +msgid "π" +msgstr "π" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/pi.py:15 +msgid "" +"Returns the ratio of a circle's circumference to its diameter.\n" +"π (pi) is approximately 3.14159" +msgstr "" +"Returns the ratio of a circle's circumference to its diameter.\n" +"π (pi) is approximately 3.14159" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/identity.py:11 +msgid "Identity" +msgstr "Identity" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/identity.py:12 +msgid "" +"Returns whatever was input to it.\n" +"For example, f(x) = x." +msgstr "" +"Returns whatever was input to it.\n" +"For example, f(x) = x." + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/constant.py:25 +msgid "Constant" +msgstr "Constant" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/constant.py:26 +msgid "" +"Returns the same value, ignoring the input.\n" +"For example, f(x) = 2." +msgstr "" +"Returns the same value, ignoring the input.\n" +"For example, f(x) = 2." + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/constant.py:30 +msgid "Value" +msgstr "Value" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/absolutevalue.py:11 +msgid "Absolute Value" +msgstr "Absolute Value" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/absolutevalue.py:12 +msgid "" +"Returns the distance a value is from zero.\n" +"For example, f(-3) = 3 and f(3) = 3." +msgstr "" +"Returns the distance a value is from zero.\n" +"For example, f(-3) = 3 and f(3) = 3." + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/__init__.py:42 +msgid "Operators" +msgstr "Operators" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/__init__.py:43 +msgid "Functions" +msgstr "Functions" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/__init__.py:44 +msgid "Constants" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/e.py:14 +msgid "e" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/e.py:15 +msgid "" +"e is an irrational number such that the derivative of f(x) = e ** x is f" +"(x).\n" +"e is approximately 2.71828" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/multiplication.py:12 +msgid "Multiplication" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/multiplication.py:13 +msgid "" +"Combines two objects by adding the left one for the right number of times.\n" +"For example, x * 3 = x + x + x." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/exponentiation.py:12 +msgid "Exponentiation" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/exponentiation.py:13 +msgid "" +"Combines two objects by multiplying the left one for the right number of " +"times.\n" +"For example, x ** 2 = x * x." +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/addition.py:12 +msgid "Addition" +msgstr "" + +#: /home/tswast/Desktop/plot-trunk/plotter/view/puzzletree/nodes/addition.py:13 +msgid "" +"Combines two objects together into a larger collection. For example, x + x = " +"2x." +msgstr "" diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..e4aaea2 --- /dev/null +++ b/setup.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +from sugar.activity import bundlebuilder + +# ignore directories not needed for activity +bundlebuilder.IGNORE_DIRS.append(".bzr") +bundlebuilder.IGNORE_DIRS.append("thirdparty") + +# ignore files (vi *.swp, bzr, and inkscape icons) +bundlebuilder.IGNORE_FILES.append(".bzrignore") +bundlebuilder.IGNORE_FILES.append("*.swp") +bundlebuilder.IGNORE_FILES.append("*-inkscape.svg") +bundlebuilder.IGNORE_FILES.append("*-gtk.png") + +bundlebuilder.start() + diff --git a/thirdparty/README.txt b/thirdparty/README.txt new file mode 100755 index 0000000..50eba48 --- /dev/null +++ b/thirdparty/README.txt @@ -0,0 +1,11 @@ +To get CairoPlot: + +From the project directory, run the following: + +cd thirdparty +bzr branch lp:~tswast/cairoplot/trunk cairoplot-trunk + + +Hopefully nested-branches will be fully implemented in the near future by +Bazaar, and this will happen automatically. + diff --git a/thirdparty/cairoplot-trunk/.bzr/README b/thirdparty/cairoplot-trunk/.bzr/README new file mode 100755 index 0000000..4f8e767 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/README @@ -0,0 +1,3 @@ +This is a Bazaar control directory. +Do not change any files in this directory. +See http://bazaar-vcs.org/ for more information about Bazaar. diff --git a/thirdparty/cairoplot-trunk/.bzr/branch-format b/thirdparty/cairoplot-trunk/.bzr/branch-format new file mode 100755 index 0000000..9eb09b7 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/branch-format @@ -0,0 +1 @@ +Bazaar-NG meta directory, format 1 diff --git a/thirdparty/cairoplot-trunk/.bzr/branch/branch.conf b/thirdparty/cairoplot-trunk/.bzr/branch/branch.conf new file mode 100755 index 0000000..196afa7 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/branch/branch.conf @@ -0,0 +1 @@ +parent_location = http://bazaar.launchpad.net/~tswast/cairoplot/trunk/ diff --git a/thirdparty/cairoplot-trunk/.bzr/branch/format b/thirdparty/cairoplot-trunk/.bzr/branch/format new file mode 100755 index 0000000..dc392f4 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/branch/format @@ -0,0 +1 @@ +Bazaar Branch Format 7 (needs bzr 1.6) diff --git a/thirdparty/cairoplot-trunk/.bzr/branch/last-revision b/thirdparty/cairoplot-trunk/.bzr/branch/last-revision new file mode 100755 index 0000000..6a1b013 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/branch/last-revision @@ -0,0 +1 @@ +50 tswast@gmail.com-20100406200021-v8p6s04ed923q0y4 diff --git a/thirdparty/cairoplot-trunk/.bzr/branch/tags b/thirdparty/cairoplot-trunk/.bzr/branch/tags new file mode 100755 index 0000000..e69de29 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/branch/tags diff --git a/thirdparty/cairoplot-trunk/.bzr/checkout/conflicts b/thirdparty/cairoplot-trunk/.bzr/checkout/conflicts new file mode 100755 index 0000000..0dc2d3a --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/checkout/conflicts @@ -0,0 +1 @@ +BZR conflict list format 1 diff --git a/thirdparty/cairoplot-trunk/.bzr/checkout/dirstate b/thirdparty/cairoplot-trunk/.bzr/checkout/dirstate new file mode 100755 index 0000000..087e92a --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/checkout/dirstate Binary files differ diff --git a/thirdparty/cairoplot-trunk/.bzr/checkout/format b/thirdparty/cairoplot-trunk/.bzr/checkout/format new file mode 100755 index 0000000..e0261c7 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/checkout/format @@ -0,0 +1 @@ +Bazaar Working Tree Format 6 (bzr 1.14) diff --git a/thirdparty/cairoplot-trunk/.bzr/checkout/views b/thirdparty/cairoplot-trunk/.bzr/checkout/views new file mode 100755 index 0000000..e69de29 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/checkout/views diff --git a/thirdparty/cairoplot-trunk/.bzr/repository/format b/thirdparty/cairoplot-trunk/.bzr/repository/format new file mode 100755 index 0000000..b200528 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/repository/format @@ -0,0 +1 @@ +Bazaar repository format 2a (needs bzr 1.16 or later) diff --git a/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.cix b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.cix new file mode 100755 index 0000000..f15b564 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.cix Binary files differ diff --git a/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.iix b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.iix new file mode 100755 index 0000000..b82ad35 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.iix Binary files differ diff --git a/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.rix b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.rix new file mode 100755 index 0000000..2a8f9de --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.rix Binary files differ diff --git a/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.six b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.six new file mode 100755 index 0000000..b4733cb --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.six Binary files differ diff --git a/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.tix b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.tix new file mode 100755 index 0000000..9ac77f6 --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.tix Binary files differ diff --git a/thirdparty/cairoplot-trunk/.bzr/repository/pack-names b/thirdparty/cairoplot-trunk/.bzr/repository/pack-names new file mode 100755 index 0000000..614c18e --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/repository/pack-names @@ -0,0 +1,6 @@ +B+Tree Graph Index 2 +node_ref_lists=0 +key_elements=1 +len=1 +row_lengths=1 +x1 PgO+C=qYpK|,3͜qm/3HƄV \ No newline at end of file diff --git a/thirdparty/cairoplot-trunk/.bzr/repository/packs/a0d10fb8e2e8f056a13a69885ff7ecda.pack b/thirdparty/cairoplot-trunk/.bzr/repository/packs/a0d10fb8e2e8f056a13a69885ff7ecda.pack new file mode 100755 index 0000000..9e2766d --- /dev/null +++ b/thirdparty/cairoplot-trunk/.bzr/repository/packs/a0d10fb8e2e8f056a13a69885ff7ecda.pack Binary files differ diff --git a/thirdparty/cairoplot-trunk/trunk/COPYING b/thirdparty/cairoplot-trunk/trunk/COPYING new file mode 100755 index 0000000..5ab7695 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/COPYING @@ -0,0 +1,504 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. 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 not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the 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 +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library 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 2.1 of the License, or (at your option) any later version. + + This library 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 this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! + + diff --git a/thirdparty/cairoplot-trunk/trunk/ChangeLog b/thirdparty/cairoplot-trunk/trunk/ChangeLog new file mode 100755 index 0000000..80a3bac --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/ChangeLog @@ -0,0 +1,134 @@ + +2009-07-09 Rodrigo Araújo + * CairoPlot.py: Correctiong naming conventions (Series.py -> + series.py). Added series.py to setup.py. + +2009-07-05 Rodrigo Araújo + * CairoPlot.py: Merged Magnun's branch. Series, Group and Data + input formats are now available. + +2009-03-10 Rodrigo Araújo + * CairoPlot.py: Color themes now work correctly for backgrounds. + besides allowing the user to change the background + color using strings, it's also possible to define + gradients using a string of colors like "red white" + which would create a red-to-white gradient. + Furthermore, background=None now implies on + transparent backgrounds; + Value labels above the bars are now possible through + the display_values attribute; + The series_labels parameter is now enabled for BarPlot + charts. Whenever it's present, a legend box is + plotted on the right upper corner. + +2009-03-09 Rodrigo Araújo + * CairoPlot.py: BarPlot, HorizontalBarPlot and VerticalBarPlot revision + and refactoring; + Color themes are now allowed for backgrounds. + +2009-03-08 Rodrigo Araújo + * CairoPlot.py: Code revision and refactoring; + The BarPlot class is now a base class on top of which + the classes HorizontalBarPlot and VerticalBarPlot are + built + Color themes are now available; + PiePlot and DonutPlot data is now ordered; + Horizontal label collision problem was solved for + BarPlots; + Axis titles are now allowed for ScatterPlot, DotLinePlot + and FunctionPlot classes. + * tests.py: Theme tests were added; + BarPlot tests were changed into HorizontalBarPlot + and VerticalBarPlot tests. + +2009-01-30 Rodrigo Araújo + * CairoPlot.py: Class structure was refactored as it was noted that + the new ScatterPlot was able to render DotLine and + FunctionPlots; + Following the refactoring, the bounds associated with + a FunctionPlot chart (x_bounds) now define a closed + interval, which means the last value is also taken + taken into account; + Error bars support was added to the ScatterPlot; + All h_* and v_* parameters have been changed to x_* + and y_*. + * tests.py: FunctionPlot tests were changed to fit the new bounds; + All tests were changed to reflect the h_* and v_* to + x_* and y_* parameter change. + +2009-01-27 Rodrigo Araújo + * CairoPlot.py: Bug fixes; + Code refactoring for the Plot and DotLinePlot classes; + Added ScatterPlot class; + Changed the code Josselin added to draw the y axis + title. Moved the code to render_axis and changed + the way the titles are drawn; + * tests.py: Added tests for ScatterPlot. + +2009-01-07 Rodrigo Araújo + * CairoPlot.py: Bug fixes; + Sebastien Cote was responsible for the addition of + series' colors and legends and also for a bug fix when + drawing h_labels; + Paul Hummer (from Canonical) was responsible for the + 'python setup.py install' new installation method; + Josselin Mouette and her crew were responsible for + some code refactoring and the ability to add a title + to the y axis; + * tests.py: Updated tests for FunctionPlot. + +2008-08-15 Rodrigo Araújo + * CairoPlot.py: Added discrete series option to FunctionPlot + * tests.py: Added new tests for FunctionPlot new option + +2008-08-14 Rodrigo Araújo + * CairoPlot.py: Added DonutPlot + Added FunctionPlot + Added rounded corners option to Bar Plot + Added pseudo 3D option to Bar Plot + Set the default for the DotLinePlot to plot without + the dots + * tests.py: Added new tests for the donut_plot function + Added new tests for the function_plot function + Added new tests for BarPlot new options + +2008-07-12 Rodrigo Araújo + * CairoPlot.py: Bug fixes for BarPlot + * tests.py: Added new tests for the bar_plot function + +2008-07-11 Rodrigo Araújo + * CairoPlot.py: Added BarPlot working draft + * tests.py: Added tests regarding the use of bar_plot function + +2008-07-07 Rodrigo Araújo + * CairoPlot.py: Changed PizzaPlot Class and pizza_plot function names + to PiePlot and pie_plot respectively; + Refactored Gantt Chart into Object Oriented mode; + Fixed the function DotLinePlot.calc_extents; + Added color_series argument to Plot constructor; + +2008-07-04 Rodrigo Araújo + * tests.py: Test suit used to check if the module is working correctly + * CairoPlot.py: Refactored Pizza Graphic into Object Oriented mode; + Fixed the function Plot.render_bounding_box which + wasn't drawing the line after defining it. + +2008-07-04 João S. O. Bueno + + * NEWS: initial NEWS + * CairoPlot.py: Refactored DotLine Graphic into Object Oriented mode + Added support for other kind of Cairo Surfaces + Added suport for changing some parameters of plot, + through modification of object properties. + Temporarily disabled series legend plotting + +2008-07-03 João S. O. Bueno + + * ChangeLog: initial ChangeLog + * COPYING: License file added (GNU LGPL 2.1) + * TODO: initial TODO + + +2008-06-13 Rodrigo Araújo + + * Initial commit diff --git a/thirdparty/cairoplot-trunk/trunk/MANIFEST b/thirdparty/cairoplot-trunk/trunk/MANIFEST new file mode 100755 index 0000000..70905ae --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/MANIFEST @@ -0,0 +1,7 @@ +COPYING +cairoplot.py +ChangeLog +NEWS +TODO +setup.py +tests.py diff --git a/thirdparty/cairoplot-trunk/trunk/NEWS b/thirdparty/cairoplot-trunk/trunk/NEWS new file mode 100755 index 0000000..c4e6b55 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/NEWS @@ -0,0 +1,19 @@ + +Version 1.1: + - Refactored code into OO form for dot and line code + - Refactored code into OO form for pizza plot + - Pizza plot renamed to Pie plot + - Added support for Bar Plots + - Refactored code into OO form for gantt chart + - Allow use of other types of cairo surfaces + - Added rounded corners and pseudo 3D option to Bar Plots + - Added support for Donut Plots + - Added support for Function Plots + - Added discrete series option to Function Plots + +Version 1.2 + - Added support for Scatter Plots + - Added support for error bars on Scatter Plots + - Added support for Horizontal and Vertical Bar Plots + + diff --git a/thirdparty/cairoplot-trunk/trunk/TODO b/thirdparty/cairoplot-trunk/trunk/TODO new file mode 100755 index 0000000..9670b28 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/TODO @@ -0,0 +1,18 @@ +TODO: +High Priority: + + - Finish refactoring code into OO form + + +Medium Priority: + - Allow changing of plot parameters + - Implement data series as classes and objects + - Implement Bar and Histogram charts + - Enhance conformity with English terminology + - Create more graphic elements (axis titles, etc...) + - Create a python egg and setup-tools style install + - Allow different combinations of line, dot, area plots for linear series + - add pseudo_3D effects + +Low Priority: + - Split or organize tests.py into a test suite diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/__init__.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/__init__.py new file mode 100755 index 0000000..d47bdaf --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/__init__.py @@ -0,0 +1,2378 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# CairoPlot.py +# +# Copyright (c) 2008 Rodrigo Moreira Araújo +# +# Author: Rodrigo Moreiro Araujo +# +# This program 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 2 of +# the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA + +#Contributor: João S. O. Bueno + +#TODO: review BarPlot Code +#TODO: x_label colision problem on Horizontal Bar Plot +#TODO: y_label's eat too much space on HBP + + +__version__ = 1.1 + +import cairo +import math +import random +from series import Series, Group, Data + +import cairoplot.handlers + +HORZ = 0 +VERT = 1 +NORM = 2 + +COLORS = {"red" : (1.0,0.0,0.0,1.0), "lime" : (0.0,1.0,0.0,1.0), "blue" : (0.0,0.0,1.0,1.0), + "maroon" : (0.5,0.0,0.0,1.0), "green" : (0.0,0.5,0.0,1.0), "navy" : (0.0,0.0,0.5,1.0), + "yellow" : (1.0,1.0,0.0,1.0), "magenta" : (1.0,0.0,1.0,1.0), "cyan" : (0.0,1.0,1.0,1.0), + "orange" : (1.0,0.5,0.0,1.0), "white" : (1.0,1.0,1.0,1.0), "black" : (0.0,0.0,0.0,1.0), + "gray" : (0.5,0.5,0.5,1.0), "light_gray" : (0.9,0.9,0.9,1.0), + "transparent" : (0.0,0.0,0.0,0.0)} + +THEMES = {"black_red" : [(0.0,0.0,0.0,1.0), (1.0,0.0,0.0,1.0)], + "red_green_blue" : [(1.0,0.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0)], + "red_orange_yellow" : [(1.0,0.2,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,1.0,0.0,1.0)], + "yellow_orange_red" : [(1.0,1.0,0.0,1.0), (1.0,0.7,0.0,1.0), (1.0,0.2,0.0,1.0)], + "rainbow" : [(1.0,0.0,0.0,1.0), (1.0,0.5,0.0,1.0), (1.0,1.0,0.0,1.0), (0.0,1.0,0.0,1.0), (0.0,0.0,1.0,1.0), (0.3, 0.0, 0.5,1.0), (0.5, 0.0, 1.0, 1.0)]} + +def colors_from_theme( theme, series_length, mode = 'solid' ): + colors = [] + if theme not in THEMES.keys() : + raise Exception, "Theme not defined" + color_steps = THEMES[theme] + n_colors = len(color_steps) + if series_length <= n_colors: + colors = [color + tuple([mode]) for color in color_steps[0:n_colors]] + else: + iterations = [(series_length - n_colors)/(n_colors - 1) for i in color_steps[:-1]] + over_iterations = (series_length - n_colors) % (n_colors - 1) + for i in range(n_colors - 1): + if over_iterations <= 0: + break + iterations[i] += 1 + over_iterations -= 1 + for index,color in enumerate(color_steps[:-1]): + colors.append(color + tuple([mode])) + if iterations[index] == 0: + continue + next_color = color_steps[index+1] + color_step = ((next_color[0] - color[0])/(iterations[index] + 1), + (next_color[1] - color[1])/(iterations[index] + 1), + (next_color[2] - color[2])/(iterations[index] + 1), + (next_color[3] - color[3])/(iterations[index] + 1)) + for i in range( iterations[index] ): + colors.append((color[0] + color_step[0]*(i+1), + color[1] + color_step[1]*(i+1), + color[2] + color_step[2]*(i+1), + color[3] + color_step[3]*(i+1), + mode)) + colors.append(color_steps[-1] + tuple([mode])) + return colors + + +def other_direction(direction): + "explicit is better than implicit" + if direction == HORZ: + return VERT + else: + return HORZ + +#Class definition + +class Plot(object): + def __init__(self, + surface=None, + data=None, + width=640, + height=480, + background=None, + border = 0, + x_labels = None, + y_labels = None, + series_colors = None): + random.seed(2) + self.create_surface(surface, width, height) + self.dimensions = {} + self.dimensions[HORZ] = width + self.dimensions[VERT] = height + self.context = None + self.labels={} + self.labels[HORZ] = x_labels + self.labels[VERT] = y_labels + self.load_series(data, x_labels, y_labels, series_colors) + self.font_size = 10 + self.set_background (background) + self.border = border + self.borders = {} + self.line_color = (0.5, 0.5, 0.5) + self.line_width = 0.5 + self.label_color = (0.0, 0.0, 0.0) + self.grid_color = (0.8, 0.8, 0.8) + + def create_surface(self, surface, width=None, height=None): + self.filename = None + if isinstance(surface, cairo.Surface): + self.handler = cairoplot.handlers.VectorHandler(surface, width, + height) + return + if isinstance(surface, cairoplot.handlers.Handler): + self.handler = surface + return + if not type(surface) in (str, unicode): + raise TypeError("Surface should be either a Cairo surface or a filename, not %s" % surface) + + # choose handler based on file extension (svg is default) + sufix = surface.rsplit(".")[-1].lower() + filename = surface + handlerclass = cairoplot.handlers.SVGHandler + if sufix == "png": + handlerclass = cairoplot.handlers.PNGHandler + elif sufix == "ps": + handlerclass = cairoplot.handlers.PSHandler + elif sufix == "pdf": + handlerclass = cairoplot.handlers.PDFHandler + elif sufix != "svg": + filename += ".svg" + self.handler = handlerclass(filename, width, height) + + def commit(self): + try: + self.handler.commit(self) + except cairo.Error: + pass + + def load_series (self, data, x_labels=None, y_labels=None, series_colors=None): + self.series_labels = [] + self.series = None + + #The pretty way + #if not isinstance(data, Series): + # # Not an instance of Series + # self.series = Series(data) + #else: + # self.series = data + # + #self.series_labels = self.series.get_names() + + #TODO: Remove on next version + # The ugly way, keeping retrocompatibility... + if callable(data) or type(data) is list and callable(data[0]): # Lambda or List of lambdas + self.series = data + self.series_labels = None + elif isinstance(data, Series): # Instance of Series + self.series = data + self.series_labels = data.get_names() + else: # Anything else + self.series = Series(data) + self.series_labels = self.series.get_names() + + #TODO: allow user passed series_widths + self.series_widths = [1.0 for group in self.series] + + #TODO: Remove on next version + self.process_colors( series_colors ) + + def process_colors( self, series_colors, length = None, mode = 'solid' ): + #series_colors might be None, a theme, a string of colors names or a list of color tuples + if length is None : + length = len( self.series.to_list() ) + + #no colors passed + if not series_colors: + #Randomize colors + self.series_colors = [ [random.random() for i in range(3)] + [1.0, mode] for series in range( length ) ] + else: + #Just theme pattern + if not hasattr( series_colors, "__iter__" ): + theme = series_colors + self.series_colors = colors_from_theme( theme.lower(), length ) + + #Theme pattern and mode + elif not hasattr(series_colors, '__delitem__') and not hasattr( series_colors[0], "__iter__" ): + theme = series_colors[0] + mode = series_colors[1] + self.series_colors = colors_from_theme( theme.lower(), length, mode ) + + #List + else: + self.series_colors = series_colors + for index, color in enumerate( self.series_colors ): + #element is a color name + if not hasattr(color, "__iter__"): + self.series_colors[index] = COLORS[color.lower()] + tuple([mode]) + #element is rgb tuple instead of rgba + elif len( color ) == 3 : + self.series_colors[index] += (1.0,mode) + #element has 4 elements, might be rgba tuple or rgb tuple with mode + elif len( color ) == 4 : + #last element is mode + if not hasattr(color[3], "__iter__"): + self.series_colors[index] += tuple([color[3]]) + self.series_colors[index][3] = 1.0 + #last element is alpha + else: + self.series_colors[index] += tuple([mode]) + + def set_background(self, background): + if background is None: + self.background = (0.0,0.0,0.0,0.0) + elif type(background) in (cairo.LinearGradient, tuple): + self.background = background + elif not hasattr(background,"__iter__"): + colors = background.split(" ") + if len(colors) == 1 and colors[0] in COLORS: + self.background = COLORS[background] + elif len(colors) > 1: + self.background = cairo.LinearGradient(self.dimensions[HORZ] / 2, 0, self.dimensions[HORZ] / 2, self.dimensions[VERT]) + for index,color in enumerate(colors): + self.background.add_color_stop_rgba(float(index)/(len(colors)-1),*COLORS[color]) + else: + raise TypeError ("Background should be either cairo.LinearGradient or a 3/4-tuple, not %s" % type(background)) + + def render_background(self): + if isinstance(self.background, cairo.LinearGradient): + self.context.set_source(self.background) + else: + self.context.set_source_rgba(*self.background) + self.context.rectangle(0,0, self.dimensions[HORZ], self.dimensions[VERT]) + self.context.fill() + + def render_bounding_box(self): + self.context.set_source_rgba(*self.line_color) + self.context.set_line_width(self.line_width) + self.context.rectangle(self.border, self.border, + self.dimensions[HORZ] - 2 * self.border, + self.dimensions[VERT] - 2 * self.border) + self.context.stroke() + + def render(self): + """All plots must prepare their context before rendering.""" + self.handler.prepare(self) + + + +class ScatterPlot( Plot ): + def __init__(self, + surface=None, + data=None, + errorx=None, + errory=None, + width=640, + height=480, + background=None, + border=0, + axis = False, + dash = False, + discrete = False, + dots = 0, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + z_bounds = None, + x_title = None, + y_title = None, + series_colors = None, + circle_colors = None ): + + self.bounds = {} + self.bounds[HORZ] = x_bounds + self.bounds[VERT] = y_bounds + self.bounds[NORM] = z_bounds + self.titles = {} + self.titles[HORZ] = x_title + self.titles[VERT] = y_title + self.max_value = {} + self.axis = axis + self.discrete = discrete + self.dots = dots + self.grid = grid + self.series_legend = series_legend + self.variable_radius = False + self.x_label_angle = math.pi / 2.5 + self.circle_colors = circle_colors + + Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors) + + self.dash = None + if dash: + if hasattr(dash, "keys"): + self.dash = [dash[key] for key in self.series_labels] + elif max([hasattr(item,'__delitem__') for item in data]) : + self.dash = dash + else: + self.dash = [dash] + + self.load_errors(errorx, errory) + + def convert_list_to_tuple(self, data): + #Data must be converted from lists of coordinates to a single + # list of tuples + out_data = zip(*data) + if len(data) == 3: + self.variable_radius = True + return out_data + + def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): + #TODO: In cairoplot 2.0 keep only the Series instances + + # Convert Data and Group to Series + if isinstance(data, Data) or isinstance(data, Group): + data = Series(data) + + # Series + if isinstance(data, Series): + for group in data: + for item in group: + if len(item) is 3: + self.variable_radius = True + + #Dictionary with lists + if hasattr(data, "keys") : + if hasattr( data.values()[0][0], "__delitem__" ) : + for key in data.keys() : + data[key] = self.convert_list_to_tuple(data[key]) + elif len(data.values()[0][0]) == 3: + self.variable_radius = True + #List + elif hasattr(data[0], "__delitem__") : + #List of lists + if hasattr(data[0][0], "__delitem__") : + for index,value in enumerate(data) : + data[index] = self.convert_list_to_tuple(value) + #List + elif type(data[0][0]) != type((0,0)): + data = self.convert_list_to_tuple(data) + #Three dimensional data + elif len(data[0][0]) == 3: + self.variable_radius = True + + #List with three dimensional tuples + elif len(data[0]) == 3: + self.variable_radius = True + Plot.load_series(self, data, x_labels, y_labels, series_colors) + self.calc_boundaries() + self.calc_labels() + + def load_errors(self, errorx, errory): + self.errors = None + if errorx == None and errory == None: + return + self.errors = {} + self.errors[HORZ] = None + self.errors[VERT] = None + #asimetric errors + if errorx and hasattr(errorx[0], "__delitem__"): + self.errors[HORZ] = errorx + #simetric errors + elif errorx: + self.errors[HORZ] = [errorx] + #asimetric errors + if errory and hasattr(errory[0], "__delitem__"): + self.errors[VERT] = errory + #simetric errors + elif errory: + self.errors[VERT] = [errory] + + def calc_labels(self): + if not self.labels[HORZ]: + amplitude = self.bounds[HORZ][1] - self.bounds[HORZ][0] + if amplitude % 10: #if horizontal labels need floating points + self.labels[HORZ] = ["%.2lf" % (float(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ] + else: + self.labels[HORZ] = ["%d" % (int(self.bounds[HORZ][0] + (amplitude * i / 10.0))) for i in range(11) ] + if not self.labels[VERT]: + amplitude = self.bounds[VERT][1] - self.bounds[VERT][0] + if amplitude % 10: #if vertical labels need floating points + self.labels[VERT] = ["%.2lf" % (float(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ] + else: + self.labels[VERT] = ["%d" % (int(self.bounds[VERT][0] + (amplitude * i / 10.0))) for i in range(11) ] + + def calc_extents(self, direction): + self.context.set_font_size(self.font_size * 0.8) + self.max_value[direction] = max(self.context.text_extents(item)[2] for item in self.labels[direction]) + self.borders[other_direction(direction)] = self.max_value[direction] + self.border + 20 + + def calc_boundaries(self): + #HORZ = 0, VERT = 1, NORM = 2 + min_data_value = [0,0,0] + max_data_value = [0,0,0] + + for group in self.series: + if type(group[0].content) in (int, float, long): + group = [Data((index, item.content)) for index,item in enumerate(group)] + + for point in group: + for index, item in enumerate(point.content): + if item > max_data_value[index]: + max_data_value[index] = item + elif item < min_data_value[index]: + min_data_value[index] = item + + if not self.bounds[HORZ]: + self.bounds[HORZ] = (min_data_value[HORZ], max_data_value[HORZ]) + if not self.bounds[VERT]: + self.bounds[VERT] = (min_data_value[VERT], max_data_value[VERT]) + if not self.bounds[NORM]: + self.bounds[NORM] = (min_data_value[NORM], max_data_value[NORM]) + + def calc_all_extents(self): + self.calc_extents(HORZ) + self.calc_extents(VERT) + + self.plot_height = self.dimensions[VERT] - 2 * self.borders[VERT] + self.plot_width = self.dimensions[HORZ] - 2* self.borders[HORZ] + + self.plot_top = self.dimensions[VERT] - self.borders[VERT] + + def calc_steps(self): + #Calculates all the x, y, z and color steps + series_amplitude = [self.bounds[index][1] - self.bounds[index][0] for index in range(3)] + + if series_amplitude[HORZ]: + self.horizontal_step = float (self.plot_width) / series_amplitude[HORZ] + else: + self.horizontal_step = 0.00 + + if series_amplitude[VERT]: + self.vertical_step = float (self.plot_height) / series_amplitude[VERT] + else: + self.vertical_step = 0.00 + + if series_amplitude[NORM]: + if self.variable_radius: + self.z_step = float (self.bounds[NORM][1]) / series_amplitude[NORM] + if self.circle_colors: + self.circle_color_step = tuple([float(self.circle_colors[1][i]-self.circle_colors[0][i])/series_amplitude[NORM] for i in range(4)]) + else: + self.z_step = 0.00 + self.circle_color_step = ( 0.0, 0.0, 0.0, 0.0 ) + + def get_circle_color(self, value): + return tuple( [self.circle_colors[0][i] + value*self.circle_color_step[i] for i in range(4)] ) + + def render(self): + Plot.render(self) + + self.calc_all_extents() + self.calc_steps() + self.render_background() + self.render_bounding_box() + if self.axis: + self.render_axis() + if self.grid: + self.render_grid() + self.render_labels() + self.render_plot() + if self.errors: + self.render_errors() + if self.series_legend and self.series_labels: + self.render_legend() + + def render_axis(self): + #Draws both the axis lines and their titles + cr = self.context + cr.set_source_rgba(*self.line_color) + cr.move_to(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) + cr.line_to(self.borders[HORZ], self.borders[VERT]) + cr.stroke() + + cr.move_to(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) + cr.line_to(self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT]) + cr.stroke() + + cr.set_source_rgba(*self.label_color) + self.context.set_font_size( 1.2 * self.font_size ) + if self.titles[HORZ]: + title_width,title_height = cr.text_extents(self.titles[HORZ])[2:4] + cr.move_to( self.dimensions[HORZ]/2 - title_width/2, self.borders[VERT] - title_height/2 ) + cr.show_text( self.titles[HORZ] ) + + if self.titles[VERT]: + title_width,title_height = cr.text_extents(self.titles[VERT])[2:4] + cr.move_to( self.dimensions[HORZ] - self.borders[HORZ] + title_height/2, self.dimensions[VERT]/2 - title_width/2) + cr.rotate( math.pi/2 ) + cr.show_text( self.titles[VERT] ) + cr.rotate( -math.pi/2 ) + + def render_grid(self): + cr = self.context + horizontal_step = float( self.plot_height ) / ( len( self.labels[VERT] ) - 1 ) + vertical_step = float( self.plot_width ) / ( len( self.labels[HORZ] ) - 1 ) + + x = self.borders[HORZ] + vertical_step + y = self.plot_top - horizontal_step + + for label in self.labels[HORZ][:-1]: + cr.set_source_rgba(*self.grid_color) + cr.move_to(x, self.dimensions[VERT] - self.borders[VERT]) + cr.line_to(x, self.borders[VERT]) + cr.stroke() + x += vertical_step + for label in self.labels[VERT][:-1]: + cr.set_source_rgba(*self.grid_color) + cr.move_to(self.borders[HORZ], y) + cr.line_to(self.dimensions[HORZ] - self.borders[HORZ], y) + cr.stroke() + y -= horizontal_step + + def render_labels(self): + self.context.set_font_size(self.font_size * 0.8) + self.render_horz_labels() + self.render_vert_labels() + + def render_horz_labels(self): + cr = self.context + step = float( self.plot_width ) / ( len( self.labels[HORZ] ) - 1 ) + x = self.borders[HORZ] + for item in self.labels[HORZ]: + cr.set_source_rgba(*self.label_color) + width = cr.text_extents(item)[2] + cr.move_to(x, self.dimensions[VERT] - self.borders[VERT] + 5) + cr.rotate(self.x_label_angle) + cr.show_text(item) + cr.rotate(-self.x_label_angle) + x += step + + def render_vert_labels(self): + cr = self.context + step = ( self.plot_height ) / ( len( self.labels[VERT] ) - 1 ) + y = self.plot_top + for item in self.labels[VERT]: + cr.set_source_rgba(*self.label_color) + width = cr.text_extents(item)[2] + cr.move_to(self.borders[HORZ] - width - 5,y) + cr.show_text(item) + y -= step + + def render_legend(self): + cr = self.context + cr.set_font_size(self.font_size) + cr.set_line_width(self.line_width) + + widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2]) + tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3]) + max_width = self.context.text_extents(widest_word)[2] + max_height = self.context.text_extents(tallest_word)[3] * 1.1 + + color_box_height = max_height / 2 + color_box_width = color_box_height * 2 + + #Draw a bounding box + bounding_box_width = max_width + color_box_width + 15 + bounding_box_height = (len(self.series_labels)+0.5) * max_height + cr.set_source_rgba(1,1,1) + cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - bounding_box_width, self.borders[VERT], + bounding_box_width, bounding_box_height) + cr.fill() + + cr.set_source_rgba(*self.line_color) + cr.set_line_width(self.line_width) + cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - bounding_box_width, self.borders[VERT], + bounding_box_width, bounding_box_height) + cr.stroke() + + for idx,key in enumerate(self.series_labels): + #Draw color box + cr.set_source_rgba(*self.series_colors[idx][:4]) + cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - max_width - color_box_width - 10, + self.borders[VERT] + color_box_height + (idx*max_height) , + color_box_width, color_box_height) + cr.fill() + + cr.set_source_rgba(0, 0, 0) + cr.rectangle(self.dimensions[HORZ] - self.borders[HORZ] - max_width - color_box_width - 10, + self.borders[VERT] + color_box_height + (idx*max_height), + color_box_width, color_box_height) + cr.stroke() + + #Draw series labels + cr.set_source_rgba(0, 0, 0) + cr.move_to(self.dimensions[HORZ] - self.borders[HORZ] - max_width - 5, self.borders[VERT] + ((idx+1)*max_height)) + cr.show_text(key) + + def render_errors(self): + cr = self.context + cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) + cr.clip() + radius = self.dots + x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step + y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step + for index, group in enumerate(self.series): + cr.set_source_rgba(*self.series_colors[index][:4]) + for number, data in enumerate(group): + x = x0 + self.horizontal_step * data.content[0] + y = self.dimensions[VERT] - y0 - self.vertical_step * data.content[1] + if self.errors[HORZ]: + cr.move_to(x, y) + x1 = x - self.horizontal_step * self.errors[HORZ][0][number] + cr.line_to(x1, y) + cr.line_to(x1, y - radius) + cr.line_to(x1, y + radius) + cr.stroke() + if self.errors[HORZ] and len(self.errors[HORZ]) == 2: + cr.move_to(x, y) + x1 = x + self.horizontal_step * self.errors[HORZ][1][number] + cr.line_to(x1, y) + cr.line_to(x1, y - radius) + cr.line_to(x1, y + radius) + cr.stroke() + if self.errors[VERT]: + cr.move_to(x, y) + y1 = y + self.vertical_step * self.errors[VERT][0][number] + cr.line_to(x, y1) + cr.line_to(x - radius, y1) + cr.line_to(x + radius, y1) + cr.stroke() + if self.errors[VERT] and len(self.errors[VERT]) == 2: + cr.move_to(x, y) + y1 = y - self.vertical_step * self.errors[VERT][1][number] + cr.line_to(x, y1) + cr.line_to(x - radius, y1) + cr.line_to(x + radius, y1) + cr.stroke() + + + def render_plot(self): + """Draws the actual plot lines.""" + + cr = self.context + if self.discrete: + cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) + cr.clip() + x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step + y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step + radius = self.dots + for number, group in enumerate (self.series): + cr.set_source_rgba(*self.series_colors[number][:4]) + for data in group : + if self.variable_radius: + radius = data.content[2] * self.z_step + if self.circle_colors: + cr.set_source_rgba( *self.get_circle_color( data.content[2])) + x = x0 + self.horizontal_step * data.content[0] + y = y0 + self.vertical_step * data.content[1] + cr.arc(x, self.dimensions[VERT] - y, radius, 0, 2*math.pi) + cr.fill() + else: + cr.rectangle(self.borders[HORZ], self.borders[VERT], self.plot_width, self.plot_height) + cr.clip() + x0 = self.borders[HORZ] - self.bounds[HORZ][0] * self.horizontal_step + y0 = self.borders[VERT] - self.bounds[VERT][0] * self.vertical_step + + radius = self.dots + for number, group in enumerate (self.series): + last_data = None + cr.set_source_rgba(*self.series_colors[number][:4]) + for data in group : + x = x0 + self.horizontal_step * data.content[0] + y = y0 + self.vertical_step * data.content[1] + + # only draw a line for valid points + if y != y: # math.isnan only in 2.6+ + last_data = None + continue + + if self.dots: + if self.variable_radius: + radius = data.content[2]*self.z_step + cr.arc(x, self.dimensions[VERT] - y, radius, 0, 2*math.pi) + cr.fill() + if last_data: + old_x = x0 + self.horizontal_step * last_data.content[0] + old_y = y0 + self.vertical_step * last_data.content[1] + cr.move_to( old_x, self.dimensions[VERT] - old_y ) + cr.line_to( x, self.dimensions[VERT] - y) + cr.set_line_width(self.series_widths[number]) + + # Display line as dash line + if self.dash and self.dash[number]: + s = self.series_widths[number] + cr.set_dash([s*3, s*3], 0) + + cr.stroke() + cr.set_dash([]) + last_data = data + + + +class DotLinePlot(ScatterPlot): + def __init__(self, + surface=None, + data=None, + width=640, + height=480, + background=None, + border=0, + axis = False, + dash = False, + dots = 0, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + x_title = None, + y_title = None, + series_colors = None): + + ScatterPlot.__init__(self, surface, data, None, None, width, height, background, border, + axis, dash, False, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, None, x_title, y_title, series_colors, None ) + + + def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + for group in self.series : + for index,data in enumerate(group): + group[index].content = (index, data.content) + + self.calc_boundaries() + self.calc_labels() + +class FunctionPlot(ScatterPlot): + def __init__(self, + surface=None, + data=None, + width=640, + height=480, + background=None, + border=0, + axis = False, + discrete = False, + dots = 0, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + x_title = None, + y_title = None, + series_colors = None, + step = 1): + + self.function = data + + # step should not be zero + self.step = step + if self.step <= 0: + self.step = 1 + + self.discrete = discrete + + data, x_bounds = self.load_series_from_function( self.function, x_bounds ) + + ScatterPlot.__init__(self, surface, data, None, None, width, height, background, border, + axis, False, discrete, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, None, x_title, y_title, series_colors, None ) + + def load_series(self, data, x_labels = None, y_labels = None, series_colors=None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + + if len(self.series[0][0]) is 1: + for group_id, group in enumerate(self.series) : + for index,data in enumerate(group): + group[index].content = (self.bounds[HORZ][0] + self.step*index, data.content) + + self.calc_boundaries() + self.calc_labels() + + def load_series_from_function(self, function, x_bounds): + """Converts a function (or functions) into array of data. + + Multiple functions can be defined by a list of functions or + a dictionary of functions into its corresponding array of data. + """ + # TODO: Add the possibility for the user to define multiple functions with different discretization parameters + + series = Series() + + if isinstance(function, Group) or isinstance(function, Data): + function = Series(function) + + # is already a Series + # overwrite any bounds passed by the function + if isinstance(function, Series): + x_bounds = (function.range[0],function.range[-1]) + + # no bounds are provided + if x_bounds == None: + x_bounds = (0,10) + + # convert a single function into a "group" + def convert_function(singlefunction, group): + """Converts function into usable data. + + Math bounds errors correspond to nan values.""" + + def trygetpoint(inx): + """Attempt to evaluate point, returns nan on errors""" + try: + return singlefunction(inx) + except (ValueError, ZeroDivisionError, OverflowError): + return float("nan") + + i = x_bounds[0] + while i <= x_bounds[1]: + group.add_data(trygetpoint(i)) + i += self.step + + # TODO: Finish the dict translation + if hasattr(function, "keys"): #dictionary: + for key in function.keys(): + group = Group(name=key) + convert_function(function[key], group) + series.add_group(group) + + elif hasattr(function, "__delitem__"): #list of functions + for f in function: + group = Group() + convert_function(f, group) + series.add_group(group) + + elif isinstance(function, Series): # instance of Series + series = function + + else: # function + group = Group() + convert_function(function, group) + series.add_group(group) + + return series, x_bounds + + + def calc_labels(self): + """Create labels from bounds""" + + boundrange = float(self.bounds[HORZ][1] - self.bounds[HORZ][0]) + + # based on range, change number of decimals displayed + digits = 0 + if 0 < boundrange < 10: + digits = -math.floor(math.log10(boundrange)) + digits += 1 + labelformat = "%%.%df" % digits + + # make 10 labels (must be > 0) + boundstep = boundrange / 10 + if boundstep <= 0: + boundstep = 1 + + # create string for each label + if not self.labels[HORZ]: + self.labels[HORZ] = [] + i = self.bounds[HORZ][0] + while i<=self.bounds[HORZ][1]: + self.labels[HORZ].append(labelformat % i) + i += boundstep + ScatterPlot.calc_labels(self) + + + def render_plot(self): + if not self.discrete: + ScatterPlot.render_plot(self) + else: + last = None + cr = self.context + for number, group in enumerate (self.series): + cr.set_source_rgba(*self.series_colors[number][:4]) + x0 = self.borders[HORZ] - self.bounds[HORZ][0]*self.horizontal_step + y0 = self.borders[VERT] - self.bounds[VERT][0]*self.vertical_step + for data in group: + x = x0 + self.horizontal_step * data.content[0] + y = y0 + self.vertical_step * data.content[1] + + cr.move_to(x, self.dimensions[VERT] - y) + cr.line_to(x, self.plot_top) + cr.set_line_width(self.series_widths[number]) + cr.stroke() + if self.dots: + cr.new_path() + cr.arc(x, self.dimensions[VERT] - y, 3, 0, 2.1 * math.pi) + cr.close_path() + cr.fill() + +class BarPlot(Plot): + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + series_colors = None, + main_dir = None): + + self.bounds = {} + self.bounds[HORZ] = x_bounds + self.bounds[VERT] = y_bounds + self.display_values = display_values + self.grid = grid + self.rounded_corners = rounded_corners + self.stack = stack + self.three_dimension = three_dimension + self.x_label_angle = math.pi / 2.5 + self.main_dir = main_dir + self.max_value = {} + self.plot_dimensions = {} + self.steps = {} + self.value_label_color = (0.5,0.5,0.5,1.0) + + Plot.__init__(self, surface, data, width, height, background, border, x_labels, y_labels, series_colors) + + def load_series(self, data, x_labels = None, y_labels = None, series_colors = None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + self.calc_boundaries() + + def process_colors(self, series_colors): + #Data for a BarPlot might be a List or a List of Lists. + #On the first case, colors must be generated for all bars, + #On the second, colors must be generated for each of the inner lists. + + #TODO: Didn't get it... + #if hasattr(self.data[0], '__getitem__'): + # length = max(len(series) for series in self.data) + #else: + # length = len( self.data ) + + length = max(len(group) for group in self.series) + + Plot.process_colors( self, series_colors, length, 'linear') + + def calc_boundaries(self): + if not self.bounds[self.main_dir]: + if self.stack: + max_data_value = max(sum(group.to_list()) for group in self.series) + else: + max_data_value = max(max(group.to_list()) for group in self.series) + self.bounds[self.main_dir] = (0, max_data_value) + if not self.bounds[other_direction(self.main_dir)]: + self.bounds[other_direction(self.main_dir)] = (0, len(self.series)) + + def calc_extents(self, direction): + self.max_value[direction] = 0 + if self.labels[direction]: + widest_word = max(self.labels[direction], key = lambda item: self.context.text_extents(item)[2]) + self.max_value[direction] = self.context.text_extents(widest_word)[3 - direction] + self.borders[other_direction(direction)] = (2-direction)*self.max_value[direction] + self.border + direction*(5) + else: + self.borders[other_direction(direction)] = self.border + + def calc_horz_extents(self): + self.calc_extents(HORZ) + + def calc_vert_extents(self): + self.calc_extents(VERT) + + def calc_all_extents(self): + self.calc_horz_extents() + self.calc_vert_extents() + other_dir = other_direction(self.main_dir) + self.value_label = 0 + if self.display_values: + if self.stack: + self.value_label = self.context.text_extents(str(max(sum(group.to_list()) for group in self.series)))[2 + self.main_dir] + else: + self.value_label = self.context.text_extents(str(max(max(group.to_list()) for group in self.series)))[2 + self.main_dir] + if self.labels[self.main_dir]: + self.plot_dimensions[self.main_dir] = self.dimensions[self.main_dir] - 2*self.borders[self.main_dir] - self.value_label + else: + self.plot_dimensions[self.main_dir] = self.dimensions[self.main_dir] - self.borders[self.main_dir] - 1.2*self.border - self.value_label + self.plot_dimensions[other_dir] = self.dimensions[other_dir] - self.borders[other_dir] - self.border + self.plot_top = self.dimensions[VERT] - self.borders[VERT] + + def calc_steps(self): + other_dir = other_direction(self.main_dir) + self.series_amplitude = self.bounds[self.main_dir][1] - self.bounds[self.main_dir][0] + if self.series_amplitude: + self.steps[self.main_dir] = float(self.plot_dimensions[self.main_dir])/self.series_amplitude + else: + self.steps[self.main_dir] = 0.00 + series_length = len(self.series) + self.steps[other_dir] = float(self.plot_dimensions[other_dir])/(series_length + 0.1*(series_length + 1)) + self.space = 0.1*self.steps[other_dir] + + def render(self): + Plot.render(self) + + self.calc_all_extents() + self.calc_steps() + self.render_background() + self.render_bounding_box() + if self.grid: + self.render_grid() + if self.three_dimension: + self.render_ground() + if self.display_values: + self.render_values() + self.render_labels() + self.render_plot() + if self.series_labels: + self.render_legend() + + def draw_3d_rectangle_front(self, x0, y0, x1, y1, shift): + self.context.rectangle(x0-shift, y0+shift, x1-x0, y1-y0) + + def draw_3d_rectangle_side(self, x0, y0, x1, y1, shift): + self.context.move_to(x1-shift,y0+shift) + self.context.line_to(x1, y0) + self.context.line_to(x1, y1) + self.context.line_to(x1-shift, y1+shift) + self.context.line_to(x1-shift, y0+shift) + self.context.close_path() + + def draw_3d_rectangle_top(self, x0, y0, x1, y1, shift): + self.context.move_to(x0-shift,y0+shift) + self.context.line_to(x0, y0) + self.context.line_to(x1, y0) + self.context.line_to(x1-shift, y0+shift) + self.context.line_to(x0-shift, y0+shift) + self.context.close_path() + + def draw_round_rectangle(self, x0, y0, x1, y1): + self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) + self.context.line_to(x1-5, y0) + self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) + self.context.line_to(x1, y1-5) + self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) + self.context.line_to(x0+5, y1) + self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) + self.context.line_to(x0, y0+5) + self.context.close_path() + + def render_ground(self): + self.draw_3d_rectangle_front(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + self.draw_3d_rectangle_side (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + self.draw_3d_rectangle_top (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + def render_labels(self): + self.context.set_font_size(self.font_size * 0.8) + if self.labels[HORZ]: + self.render_horz_labels() + if self.labels[VERT]: + self.render_vert_labels() + + def render_legend(self): + cr = self.context + cr.set_font_size(self.font_size) + cr.set_line_width(self.line_width) + + widest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[2]) + tallest_word = max(self.series_labels, key = lambda item: self.context.text_extents(item)[3]) + max_width = self.context.text_extents(widest_word)[2] + max_height = self.context.text_extents(tallest_word)[3] * 1.1 + 5 + + color_box_height = max_height / 2 + color_box_width = color_box_height * 2 + + #Draw a bounding box + bounding_box_width = max_width + color_box_width + 15 + bounding_box_height = (len(self.series_labels)+0.5) * max_height + cr.set_source_rgba(1,1,1) + cr.rectangle(self.dimensions[HORZ] - self.border - bounding_box_width, self.border, + bounding_box_width, bounding_box_height) + cr.fill() + + cr.set_source_rgba(*self.line_color) + cr.set_line_width(self.line_width) + cr.rectangle(self.dimensions[HORZ] - self.border - bounding_box_width, self.border, + bounding_box_width, bounding_box_height) + cr.stroke() + + for idx,key in enumerate(self.series_labels): + #Draw color box + cr.set_source_rgba(*self.series_colors[idx][:4]) + cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10, + self.border + color_box_height + (idx*max_height) , + color_box_width, color_box_height) + cr.fill() + + cr.set_source_rgba(0, 0, 0) + cr.rectangle(self.dimensions[HORZ] - self.border - max_width - color_box_width - 10, + self.border + color_box_height + (idx*max_height), + color_box_width, color_box_height) + cr.stroke() + + #Draw series labels + cr.set_source_rgba(0, 0, 0) + cr.move_to(self.dimensions[HORZ] - self.border - max_width - 5, self.border + ((idx+1)*max_height)) + cr.show_text(key) + + +class HorizontalBarPlot(BarPlot): + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + series_labels = None, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + series_colors = None): + + BarPlot.__init__(self, surface, data, width, height, background, border, + display_values, grid, rounded_corners, stack, three_dimension, + x_labels, y_labels, x_bounds, y_bounds, series_colors, HORZ) + self.series_labels = series_labels + + def calc_vert_extents(self): + self.calc_extents(VERT) + if self.labels[HORZ] and not self.labels[VERT]: + self.borders[HORZ] += 10 + + def draw_rectangle_bottom(self, x0, y0, x1, y1): + self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) + self.context.line_to(x0, y0+5) + self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) + self.context.line_to(x1, y0) + self.context.line_to(x1, y1) + self.context.line_to(x0+5, y1) + self.context.close_path() + + def draw_rectangle_top(self, x0, y0, x1, y1): + self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) + self.context.line_to(x1, y1-5) + self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) + self.context.line_to(x0, y1) + self.context.line_to(x0, y0) + self.context.line_to(x1, y0) + self.context.close_path() + + def draw_rectangle(self, index, length, x0, y0, x1, y1): + if length == 1: + BarPlot.draw_rectangle(self, x0, y0, x1, y1) + elif index == 0: + self.draw_rectangle_bottom(x0, y0, x1, y1) + elif index == length-1: + self.draw_rectangle_top(x0, y0, x1, y1) + else: + self.context.rectangle(x0, y0, x1-x0, y1-y0) + + #TODO: Review BarPlot.render_grid code + def render_grid(self): + self.context.set_source_rgba(0.8, 0.8, 0.8) + if self.labels[HORZ]: + self.context.set_font_size(self.font_size * 0.8) + step = (self.dimensions[HORZ] - 2*self.borders[HORZ] - self.value_label)/(len(self.labels[HORZ])-1) + x = self.borders[HORZ] + next_x = 0 + for item in self.labels[HORZ]: + width = self.context.text_extents(item)[2] + if x - width/2 > next_x and x - width/2 > self.border: + self.context.move_to(x, self.border) + self.context.line_to(x, self.dimensions[VERT] - self.borders[VERT]) + self.context.stroke() + next_x = x + width/2 + x += step + else: + lines = 11 + horizontal_step = float(self.plot_dimensions[HORZ])/(lines-1) + x = self.borders[HORZ] + for y in xrange(0, lines): + self.context.move_to(x, self.border) + self.context.line_to(x, self.dimensions[VERT] - self.borders[VERT]) + self.context.stroke() + x += horizontal_step + + def render_horz_labels(self): + step = (self.dimensions[HORZ] - 2*self.borders[HORZ])/(len(self.labels[HORZ])-1) + x = self.borders[HORZ] + next_x = 0 + + for item in self.labels[HORZ]: + self.context.set_source_rgba(*self.label_color) + width = self.context.text_extents(item)[2] + if x - width/2 > next_x and x - width/2 > self.border: + self.context.move_to(x - width/2, self.dimensions[VERT] - self.borders[VERT] + self.max_value[HORZ] + 3) + self.context.show_text(item) + next_x = x + width/2 + x += step + + def render_vert_labels(self): + series_length = len(self.labels[VERT]) + step = (self.plot_dimensions[VERT] - (series_length + 1)*self.space)/(len(self.labels[VERT])) + y = self.border + step/2 + self.space + + for item in self.labels[VERT]: + self.context.set_source_rgba(*self.label_color) + width, height = self.context.text_extents(item)[2:4] + self.context.move_to(self.borders[HORZ] - width - 5, y + height/2) + self.context.show_text(item) + y += step + self.space + self.labels[VERT].reverse() + + def render_values(self): + self.context.set_source_rgba(*self.value_label_color) + self.context.set_font_size(self.font_size * 0.8) + if self.stack: + for i,group in enumerate(self.series): + value = sum(group.to_list()) + height = self.context.text_extents(str(value))[3] + x = self.borders[HORZ] + value*self.steps[HORZ] + 2 + y = self.borders[VERT] + (i+0.5)*self.steps[VERT] + (i+1)*self.space + height/2 + self.context.move_to(x, y) + self.context.show_text(str(value)) + else: + for i,group in enumerate(self.series): + inner_step = self.steps[VERT]/len(group) + y0 = self.border + i*self.steps[VERT] + (i+1)*self.space + for number,data in enumerate(group): + height = self.context.text_extents(str(data.content))[3] + self.context.move_to(self.borders[HORZ] + data.content*self.steps[HORZ] + 2, y0 + 0.5*inner_step + height/2, ) + self.context.show_text(str(data.content)) + y0 += inner_step + + def render_plot(self): + if self.stack: + for i,group in enumerate(self.series): + x0 = self.borders[HORZ] + y0 = self.borders[VERT] + i*self.steps[VERT] + (i+1)*self.space + for number,data in enumerate(group): + if self.series_colors[number][4] in ('radial','linear') : + linear = cairo.LinearGradient( data.content*self.steps[HORZ]/2, y0, data.content*self.steps[HORZ]/2, y0 + self.steps[VERT] ) + color = self.series_colors[number] + linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1.0, *color[:4]) + self.context.set_source(linear) + elif self.series_colors[number][4] == 'solid': + self.context.set_source_rgba(*self.series_colors[number][:4]) + if self.rounded_corners: + self.draw_rectangle(number, len(group), x0, y0, x0+data.content*self.steps[HORZ], y0+self.steps[VERT]) + self.context.fill() + else: + self.context.rectangle(x0, y0, data.content*self.steps[HORZ], self.steps[VERT]) + self.context.fill() + x0 += data.content*self.steps[HORZ] + else: + for i,group in enumerate(self.series): + inner_step = self.steps[VERT]/len(group) + x0 = self.borders[HORZ] + y0 = self.border + i*self.steps[VERT] + (i+1)*self.space + for number,data in enumerate(group): + linear = cairo.LinearGradient(data.content*self.steps[HORZ]/2, y0, data.content*self.steps[HORZ]/2, y0 + inner_step) + color = self.series_colors[number] + linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1.0, *color[:4]) + self.context.set_source(linear) + if self.rounded_corners and data.content != 0: + BarPlot.draw_round_rectangle(self,x0, y0, x0 + data.content*self.steps[HORZ], y0 + inner_step) + self.context.fill() + else: + self.context.rectangle(x0, y0, data.content*self.steps[HORZ], inner_step) + self.context.fill() + y0 += inner_step + +class VerticalBarPlot(BarPlot): + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + series_labels = None, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + series_colors = None): + + BarPlot.__init__(self, surface, data, width, height, background, border, + display_values, grid, rounded_corners, stack, three_dimension, + x_labels, y_labels, x_bounds, y_bounds, series_colors, VERT) + self.series_labels = series_labels + + def calc_vert_extents(self): + self.calc_extents(VERT) + if self.labels[VERT] and not self.labels[HORZ]: + self.borders[VERT] += 10 + + def draw_rectangle_bottom(self, x0, y0, x1, y1): + self.context.move_to(x1,y1) + self.context.arc(x1-5, y1-5, 5, 0, math.pi/2) + self.context.line_to(x0+5, y1) + self.context.arc(x0+5, y1-5, 5, math.pi/2, math.pi) + self.context.line_to(x0, y0) + self.context.line_to(x1, y0) + self.context.line_to(x1, y1) + self.context.close_path() + + def draw_rectangle_top(self, x0, y0, x1, y1): + self.context.arc(x0+5, y0+5, 5, -math.pi, -math.pi/2) + self.context.line_to(x1-5, y0) + self.context.arc(x1-5, y0+5, 5, -math.pi/2, 0) + self.context.line_to(x1, y1) + self.context.line_to(x0, y1) + self.context.line_to(x0, y0) + self.context.close_path() + + def draw_rectangle(self, index, length, x0, y0, x1, y1): + if length == 1: + BarPlot.draw_rectangle(self, x0, y0, x1, y1) + elif index == 0: + self.draw_rectangle_bottom(x0, y0, x1, y1) + elif index == length-1: + self.draw_rectangle_top(x0, y0, x1, y1) + else: + self.context.rectangle(x0, y0, x1-x0, y1-y0) + + def render_grid(self): + self.context.set_source_rgba(0.8, 0.8, 0.8) + if self.labels[VERT]: + lines = len(self.labels[VERT]) + vertical_step = float(self.plot_dimensions[self.main_dir])/(lines-1) + y = self.borders[VERT] + self.value_label + else: + lines = 11 + vertical_step = float(self.plot_dimensions[self.main_dir])/(lines-1) + y = 1.2*self.border + self.value_label + for x in xrange(0, lines): + self.context.move_to(self.borders[HORZ], y) + self.context.line_to(self.dimensions[HORZ] - self.border, y) + self.context.stroke() + y += vertical_step + + def render_ground(self): + self.draw_3d_rectangle_front(self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + self.draw_3d_rectangle_side (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + self.draw_3d_rectangle_top (self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT], + self.dimensions[HORZ] - self.borders[HORZ], self.dimensions[VERT] - self.borders[VERT] + 5, 10) + self.context.fill() + + def render_horz_labels(self): + series_length = len(self.labels[HORZ]) + step = float (self.plot_dimensions[HORZ] - (series_length + 1)*self.space)/len(self.labels[HORZ]) + x = self.borders[HORZ] + step/2 + self.space + next_x = 0 + + for item in self.labels[HORZ]: + self.context.set_source_rgba(*self.label_color) + width = self.context.text_extents(item)[2] + if x - width/2 > next_x and x - width/2 > self.borders[HORZ]: + self.context.move_to(x - width/2, self.dimensions[VERT] - self.borders[VERT] + self.max_value[HORZ] + 3) + self.context.show_text(item) + next_x = x + width/2 + x += step + self.space + + def render_vert_labels(self): + self.context.set_source_rgba(*self.label_color) + y = self.borders[VERT] + self.value_label + step = (self.dimensions[VERT] - 2*self.borders[VERT] - self.value_label)/(len(self.labels[VERT]) - 1) + self.labels[VERT].reverse() + for item in self.labels[VERT]: + width, height = self.context.text_extents(item)[2:4] + self.context.move_to(self.borders[HORZ] - width - 5, y + height/2) + self.context.show_text(item) + y += step + self.labels[VERT].reverse() + + def render_values(self): + self.context.set_source_rgba(*self.value_label_color) + self.context.set_font_size(self.font_size * 0.8) + if self.stack: + for i,group in enumerate(self.series): + value = sum(group.to_list()) + width = self.context.text_extents(str(value))[2] + x = self.borders[HORZ] + (i+0.5)*self.steps[HORZ] + (i+1)*self.space - width/2 + y = value*self.steps[VERT] + 2 + self.context.move_to(x, self.plot_top-y) + self.context.show_text(str(value)) + else: + for i,group in enumerate(self.series): + inner_step = self.steps[HORZ]/len(group) + x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space + for number,data in enumerate(group): + width = self.context.text_extents(str(data.content))[2] + self.context.move_to(x0 + 0.5*inner_step - width/2, self.plot_top - data.content*self.steps[VERT] - 2) + self.context.show_text(str(data.content)) + x0 += inner_step + + def render_plot(self): + if self.stack: + for i,group in enumerate(self.series): + x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space + y0 = 0 + for number,data in enumerate(group): + if self.series_colors[number][4] in ('linear','radial'): + linear = cairo.LinearGradient( x0, data.content*self.steps[VERT]/2, x0 + self.steps[HORZ], data.content*self.steps[VERT]/2 ) + color = self.series_colors[number] + linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1.0, *color[:4]) + self.context.set_source(linear) + elif self.series_colors[number][4] == 'solid': + self.context.set_source_rgba(*self.series_colors[number][:4]) + if self.rounded_corners: + self.draw_rectangle(number, len(group), x0, self.plot_top - y0 - data.content*self.steps[VERT], x0 + self.steps[HORZ], self.plot_top - y0) + self.context.fill() + else: + self.context.rectangle(x0, self.plot_top - y0 - data.content*self.steps[VERT], self.steps[HORZ], data.content*self.steps[VERT]) + self.context.fill() + y0 += data.content*self.steps[VERT] + else: + for i,group in enumerate(self.series): + inner_step = self.steps[HORZ]/len(group) + y0 = self.borders[VERT] + x0 = self.borders[HORZ] + i*self.steps[HORZ] + (i+1)*self.space + for number,data in enumerate(group): + if self.series_colors[number][4] == 'linear': + linear = cairo.LinearGradient( x0, data.content*self.steps[VERT]/2, x0 + inner_step, data.content*self.steps[VERT]/2 ) + color = self.series_colors[number] + linear.add_color_stop_rgba(0.0, 3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1.0, *color[:4]) + self.context.set_source(linear) + elif self.series_colors[number][4] == 'solid': + self.context.set_source_rgba(*self.series_colors[number][:4]) + if self.rounded_corners and data.content != 0: + BarPlot.draw_round_rectangle(self, x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top) + self.context.fill() + elif self.three_dimension: + self.draw_3d_rectangle_front(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) + self.context.fill() + self.draw_3d_rectangle_side(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) + self.context.fill() + self.draw_3d_rectangle_top(x0, self.plot_top - data.content*self.steps[VERT], x0+inner_step, self.plot_top, 5) + self.context.fill() + else: + self.context.rectangle(x0, self.plot_top - data.content*self.steps[VERT], inner_step, data.content*self.steps[VERT]) + self.context.fill() + + x0 += inner_step + +class StreamChart(VerticalBarPlot): + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + grid = False, + series_legend = None, + x_labels = None, + x_bounds = None, + y_bounds = None, + series_colors = None): + + VerticalBarPlot.__init__(self, surface, data, width, height, background, border, + False, grid, False, True, False, + None, x_labels, None, x_bounds, y_bounds, series_colors) + + def calc_steps(self): + other_dir = other_direction(self.main_dir) + self.series_amplitude = self.bounds[self.main_dir][1] - self.bounds[self.main_dir][0] + if self.series_amplitude: + self.steps[self.main_dir] = float(self.plot_dimensions[self.main_dir])/self.series_amplitude + else: + self.steps[self.main_dir] = 0.00 + series_length = len(self.data) + self.steps[other_dir] = float(self.plot_dimensions[other_dir])/series_length + + def render_legend(self): + pass + + def ground(self, index): + sum_values = sum(self.data[index]) + return -0.5*sum_values + + def calc_angles(self): + middle = self.plot_top - self.plot_dimensions[VERT]/2.0 + self.angles = [tuple([0.0 for x in range(len(self.data)+1)])] + for x_index in range(1, len(self.data)-1): + t = [] + x0 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] + x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] + y0 = middle - self.ground(x_index-1)*self.steps[VERT] + y2 = middle - self.ground(x_index+1)*self.steps[VERT] + t.append(math.atan(float(y0-y2)/(x0-x2))) + for data_index in range(len(self.data[x_index])): + x0 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] + x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] + y0 = middle - self.ground(x_index-1)*self.steps[VERT] - self.data[x_index-1][data_index]*self.steps[VERT] + y2 = middle - self.ground(x_index+1)*self.steps[VERT] - self.data[x_index+1][data_index]*self.steps[VERT] + + for i in range(0,data_index): + y0 -= self.data[x_index-1][i]*self.steps[VERT] + y2 -= self.data[x_index+1][i]*self.steps[VERT] + + if data_index == len(self.data[0])-1 and False: + self.context.set_source_rgba(0.0,0.0,0.0,0.3) + self.context.move_to(x0,y0) + self.context.line_to(x2,y2) + self.context.stroke() + self.context.arc(x0,y0,2,0,2*math.pi) + self.context.fill() + t.append(math.atan(float(y0-y2)/(x0-x2))) + self.angles.append(tuple(t)) + self.angles.append(tuple([0.0 for x in range(len(self.data)+1)])) + + def render_plot(self): + self.calc_angles() + middle = self.plot_top - self.plot_dimensions[VERT]/2.0 + p = 0.4*self.steps[HORZ] + for data_index in range(len(self.data[0])-1,-1,-1): + self.context.set_source_rgba(*self.series_colors[data_index][:4]) + + #draw the upper line + for x_index in range(len(self.data)-1) : + x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] + y1 = middle - self.ground(x_index)*self.steps[VERT] - self.data[x_index][data_index]*self.steps[VERT] + x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] + y2 = middle - self.ground(x_index + 1)*self.steps[VERT] - self.data[x_index + 1][data_index]*self.steps[VERT] + + for i in range(0,data_index): + y1 -= self.data[x_index][i]*self.steps[VERT] + y2 -= self.data[x_index+1][i]*self.steps[VERT] + + if x_index == 0: + self.context.move_to(x1,y1) + + ang1 = self.angles[x_index][data_index+1] + ang2 = self.angles[x_index+1][data_index+1] + math.pi + self.context.curve_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1), + x2+p*math.cos(ang2),y2+p*math.sin(ang2), + x2,y2) + + for x_index in range(len(self.data)-1,0,-1) : + x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] + y1 = middle - self.ground(x_index)*self.steps[VERT] + x2 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] + y2 = middle - self.ground(x_index - 1)*self.steps[VERT] + + for i in range(0,data_index): + y1 -= self.data[x_index][i]*self.steps[VERT] + y2 -= self.data[x_index-1][i]*self.steps[VERT] + + if x_index == len(self.data)-1: + self.context.line_to(x1,y1+2) + + #revert angles by pi degrees to take the turn back + ang1 = self.angles[x_index][data_index] + math.pi + ang2 = self.angles[x_index-1][data_index] + self.context.curve_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1), + x2+p*math.cos(ang2),y2+p*math.sin(ang2), + x2,y2+2) + + self.context.close_path() + self.context.fill() + + if False: + self.context.move_to(self.borders[HORZ] + 0.5*self.steps[HORZ], middle) + for x_index in range(len(self.data)-1) : + x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] + y1 = middle - self.ground(x_index)*self.steps[VERT] - self.data[x_index][data_index]*self.steps[VERT] + x2 = self.borders[HORZ] + (0.5 + x_index + 1)*self.steps[HORZ] + y2 = middle - self.ground(x_index + 1)*self.steps[VERT] - self.data[x_index + 1][data_index]*self.steps[VERT] + + for i in range(0,data_index): + y1 -= self.data[x_index][i]*self.steps[VERT] + y2 -= self.data[x_index+1][i]*self.steps[VERT] + + ang1 = self.angles[x_index][data_index+1] + ang2 = self.angles[x_index+1][data_index+1] + math.pi + self.context.set_source_rgba(1.0,0.0,0.0) + self.context.arc(x1+p*math.cos(ang1),y1+p*math.sin(ang1),2,0,2*math.pi) + self.context.fill() + self.context.set_source_rgba(0.0,0.0,0.0) + self.context.arc(x2+p*math.cos(ang2),y2+p*math.sin(ang2),2,0,2*math.pi) + self.context.fill() + """self.context.set_source_rgba(0.0,0.0,0.0,0.3) + self.context.arc(x2,y2,2,0,2*math.pi) + self.context.fill()""" + self.context.move_to(x1,y1) + self.context.line_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1)) + self.context.stroke() + self.context.move_to(x2,y2) + self.context.line_to(x2+p*math.cos(ang2),y2+p*math.sin(ang2)) + self.context.stroke() + if False: + for x_index in range(len(self.data)-1,0,-1) : + x1 = self.borders[HORZ] + (0.5 + x_index)*self.steps[HORZ] + y1 = middle - self.ground(x_index)*self.steps[VERT] + x2 = self.borders[HORZ] + (0.5 + x_index - 1)*self.steps[HORZ] + y2 = middle - self.ground(x_index - 1)*self.steps[VERT] + + for i in range(0,data_index): + y1 -= self.data[x_index][i]*self.steps[VERT] + y2 -= self.data[x_index-1][i]*self.steps[VERT] + + #revert angles by pi degrees to take the turn back + ang1 = self.angles[x_index][data_index] + math.pi + ang2 = self.angles[x_index-1][data_index] + self.context.set_source_rgba(0.0,1.0,0.0) + self.context.arc(x1+p*math.cos(ang1),y1+p*math.sin(ang1),2,0,2*math.pi) + self.context.fill() + self.context.set_source_rgba(0.0,0.0,1.0) + self.context.arc(x2+p*math.cos(ang2),y2+p*math.sin(ang2),2,0,2*math.pi) + self.context.fill() + """self.context.set_source_rgba(0.0,0.0,0.0,0.3) + self.context.arc(x2,y2,2,0,2*math.pi) + self.context.fill()""" + self.context.move_to(x1,y1) + self.context.line_to(x1+p*math.cos(ang1),y1+p*math.sin(ang1)) + self.context.stroke() + self.context.move_to(x2,y2) + self.context.line_to(x2+p*math.cos(ang2),y2+p*math.sin(ang2)) + self.context.stroke() + #break + + #self.context.arc(self.dimensions[HORZ]/2, self.dimensions[VERT]/2,50,0,3*math.pi/2) + #self.context.fill() + + +class PiePlot(Plot): + #TODO: Check the old cairoplot, graphs aren't matching + def __init__ (self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + gradient = False, + shadow = False, + colors = None): + + Plot.__init__( self, surface, data, width, height, background, series_colors = colors ) + self.center = (self.dimensions[HORZ]/2, self.dimensions[VERT]/2) + self.total = sum( self.series.to_list() ) + self.radius = min(self.dimensions[HORZ]/3,self.dimensions[VERT]/3) + self.gradient = gradient + self.shadow = shadow + + def sort_function(x,y): + return x.content - y.content + + def load_series(self, data, x_labels=None, y_labels=None, series_colors=None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + # Already done inside series + #self.data = sorted(self.data) + + def draw_piece(self, angle, next_angle): + self.context.move_to(self.center[0],self.center[1]) + self.context.line_to(self.center[0] + self.radius*math.cos(angle), self.center[1] + self.radius*math.sin(angle)) + self.context.arc(self.center[0], self.center[1], self.radius, angle, next_angle) + self.context.line_to(self.center[0], self.center[1]) + self.context.close_path() + + def render(self): + Plot.render(self) + + self.render_background() + self.render_bounding_box() + if self.shadow: + self.render_shadow() + self.render_plot() + self.render_series_labels() + + def render_shadow(self): + horizontal_shift = 3 + vertical_shift = 3 + self.context.set_source_rgba(0, 0, 0, 0.5) + self.context.arc(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.radius, 0, 2*math.pi) + self.context.fill() + + def render_series_labels(self): + angle = 0 + next_angle = 0 + x0,y0 = self.center + cr = self.context + for number,key in enumerate(self.series_labels): + # self.data[number] should be just a number + data = sum(self.series[number].to_list()) + + next_angle = angle + 2.0*math.pi*data/self.total + cr.set_source_rgba(*self.series_colors[number][:4]) + w = cr.text_extents(key)[2] + if (angle + next_angle)/2 < math.pi/2 or (angle + next_angle)/2 > 3*math.pi/2: + cr.move_to(x0 + (self.radius+10)*math.cos((angle+next_angle)/2), y0 + (self.radius+10)*math.sin((angle+next_angle)/2) ) + else: + cr.move_to(x0 + (self.radius+10)*math.cos((angle+next_angle)/2) - w, y0 + (self.radius+10)*math.sin((angle+next_angle)/2) ) + cr.show_text(key) + angle = next_angle + + def render_plot(self): + angle = 0 + next_angle = 0 + x0,y0 = self.center + cr = self.context + for number,group in enumerate(self.series): + # Group should be just a number + data = sum(group.to_list()) + next_angle = angle + 2.0*math.pi*data/self.total + if self.gradient or self.series_colors[number][4] in ('linear','radial'): + gradient_color = cairo.RadialGradient(self.center[0], self.center[1], 0, self.center[0], self.center[1], self.radius) + gradient_color.add_color_stop_rgba(0.3, *self.series_colors[number][:4]) + gradient_color.add_color_stop_rgba(1, self.series_colors[number][0]*0.7, + self.series_colors[number][1]*0.7, + self.series_colors[number][2]*0.7, + self.series_colors[number][3]) + cr.set_source(gradient_color) + else: + cr.set_source_rgba(*self.series_colors[number][:4]) + + self.draw_piece(angle, next_angle) + cr.fill() + + cr.set_source_rgba(1.0, 1.0, 1.0) + self.draw_piece(angle, next_angle) + cr.stroke() + + angle = next_angle + +class DonutPlot(PiePlot): + def __init__ (self, + surface = None, + data = None, + width = 640, + height = 480, + background = "white light_gray", + gradient = False, + shadow = False, + colors = None, + inner_radius=-1): + + Plot.__init__( self, surface, data, width, height, background, series_colors = colors ) + + self.center = ( self.dimensions[HORZ]/2, self.dimensions[VERT]/2 ) + self.total = sum( self.series.to_list() ) + self.radius = min( self.dimensions[HORZ]/3,self.dimensions[VERT]/3 ) + self.inner_radius = inner_radius*self.radius + + if inner_radius == -1: + self.inner_radius = self.radius/3 + + self.gradient = gradient + self.shadow = shadow + + def draw_piece(self, angle, next_angle): + self.context.move_to(self.center[0] + (self.inner_radius)*math.cos(angle), self.center[1] + (self.inner_radius)*math.sin(angle)) + self.context.line_to(self.center[0] + self.radius*math.cos(angle), self.center[1] + self.radius*math.sin(angle)) + self.context.arc(self.center[0], self.center[1], self.radius, angle, next_angle) + self.context.line_to(self.center[0] + (self.inner_radius)*math.cos(next_angle), self.center[1] + (self.inner_radius)*math.sin(next_angle)) + self.context.arc_negative(self.center[0], self.center[1], self.inner_radius, next_angle, angle) + self.context.close_path() + + def render_shadow(self): + horizontal_shift = 3 + vertical_shift = 3 + self.context.set_source_rgba(0, 0, 0, 0.5) + self.context.arc(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.inner_radius, 0, 2*math.pi) + self.context.arc_negative(self.center[0] + horizontal_shift, self.center[1] + vertical_shift, self.radius, 0, -2*math.pi) + self.context.fill() + +class GanttChart (Plot) : + def __init__(self, + surface = None, + data = None, + width = 640, + height = 480, + x_labels = None, + y_labels = None, + colors = None): + self.bounds = {} + self.max_value = {} + Plot.__init__(self, surface, data, width, height, x_labels = x_labels, y_labels = y_labels, series_colors = colors) + + def load_series(self, data, x_labels=None, y_labels=None, series_colors=None): + Plot.load_series(self, data, x_labels, y_labels, series_colors) + self.calc_boundaries() + + def calc_boundaries(self): + self.bounds[HORZ] = (0,len(self.series)) + end_pos = max(self.series.to_list()) + + #for group in self.series: + # if hasattr(item, "__delitem__"): + # for sub_item in item: + # end_pos = max(sub_item) + # else: + # end_pos = max(item) + self.bounds[VERT] = (0,end_pos) + + def calc_extents(self, direction): + self.max_value[direction] = 0 + if self.labels[direction]: + self.max_value[direction] = max(self.context.text_extents(item)[2] for item in self.labels[direction]) + else: + self.max_value[direction] = self.context.text_extents( str(self.bounds[direction][1] + 1) )[2] + + def calc_horz_extents(self): + self.calc_extents(HORZ) + self.borders[HORZ] = 100 + self.max_value[HORZ] + + def calc_vert_extents(self): + self.calc_extents(VERT) + self.borders[VERT] = self.dimensions[VERT]/(self.bounds[HORZ][1] + 1) + + def calc_steps(self): + self.horizontal_step = (self.dimensions[HORZ] - self.borders[HORZ])/(len(self.labels[VERT])) + self.vertical_step = self.borders[VERT] + + def render(self): + Plot.render(self) + + self.calc_horz_extents() + self.calc_vert_extents() + self.calc_steps() + self.render_background() + + self.render_labels() + self.render_grid() + self.render_plot() + + def render_background(self): + cr = self.context + cr.set_source_rgba(255,255,255) + cr.rectangle(0,0,self.dimensions[HORZ], self.dimensions[VERT]) + cr.fill() + for number,group in enumerate(self.series): + linear = cairo.LinearGradient(self.dimensions[HORZ]/2, self.borders[VERT] + number*self.vertical_step, + self.dimensions[HORZ]/2, self.borders[VERT] + (number+1)*self.vertical_step) + linear.add_color_stop_rgba(0,1.0,1.0,1.0,1.0) + linear.add_color_stop_rgba(1.0,0.9,0.9,0.9,1.0) + cr.set_source(linear) + cr.rectangle(0,self.borders[VERT] + number*self.vertical_step,self.dimensions[HORZ],self.vertical_step) + cr.fill() + + def render_grid(self): + cr = self.context + cr.set_source_rgba(0.7, 0.7, 0.7) + cr.set_dash((1,0,0,0,0,0,1)) + cr.set_line_width(0.5) + for number,label in enumerate(self.labels[VERT]): + h = cr.text_extents(label)[3] + cr.move_to(self.borders[HORZ] + number*self.horizontal_step, self.vertical_step/2 + h) + cr.line_to(self.borders[HORZ] + number*self.horizontal_step, self.dimensions[VERT]) + cr.stroke() + + def render_labels(self): + self.context.set_font_size(0.02 * self.dimensions[HORZ]) + + self.render_horz_labels() + self.render_vert_labels() + + def render_horz_labels(self): + cr = self.context + labels = self.labels[HORZ] + if not labels: + labels = [str(i) for i in range(1, self.bounds[HORZ][1] + 1) ] + for number,label in enumerate(labels): + if label != None: + cr.set_source_rgba(0.5, 0.5, 0.5) + w,h = cr.text_extents(label)[2], cr.text_extents(label)[3] + cr.move_to(40,self.borders[VERT] + number*self.vertical_step + self.vertical_step/2 + h/2) + cr.show_text(label) + + def render_vert_labels(self): + cr = self.context + labels = self.labels[VERT] + if not labels: + labels = [str(i) for i in range(1, self.bounds[VERT][1] + 1) ] + for number,label in enumerate(labels): + w,h = cr.text_extents(label)[2], cr.text_extents(label)[3] + cr.move_to(self.borders[HORZ] + number*self.horizontal_step - w/2, self.vertical_step/2) + cr.show_text(label) + + def render_rectangle(self, x0, y0, x1, y1, color): + self.draw_shadow(x0, y0, x1, y1) + self.draw_rectangle(x0, y0, x1, y1, color) + + def draw_rectangular_shadow(self, gradient, x0, y0, w, h): + self.context.set_source(gradient) + self.context.rectangle(x0,y0,w,h) + self.context.fill() + + def draw_circular_shadow(self, x, y, radius, ang_start, ang_end, mult, shadow): + gradient = cairo.RadialGradient(x, y, 0, x, y, 2*radius) + gradient.add_color_stop_rgba(0, 0, 0, 0, shadow) + gradient.add_color_stop_rgba(1, 0, 0, 0, 0) + self.context.set_source(gradient) + self.context.move_to(x,y) + self.context.line_to(x + mult[0]*radius,y + mult[1]*radius) + self.context.arc(x, y, 8, ang_start, ang_end) + self.context.line_to(x,y) + self.context.close_path() + self.context.fill() + + def draw_rectangle(self, x0, y0, x1, y1, color): + cr = self.context + middle = (x0+x1)/2 + linear = cairo.LinearGradient(middle,y0,middle,y1) + linear.add_color_stop_rgba(0,3.5*color[0]/5.0, 3.5*color[1]/5.0, 3.5*color[2]/5.0,1.0) + linear.add_color_stop_rgba(1,*color[:4]) + cr.set_source(linear) + + cr.arc(x0+5, y0+5, 5, 0, 2*math.pi) + cr.arc(x1-5, y0+5, 5, 0, 2*math.pi) + cr.arc(x0+5, y1-5, 5, 0, 2*math.pi) + cr.arc(x1-5, y1-5, 5, 0, 2*math.pi) + cr.rectangle(x0+5,y0,x1-x0-10,y1-y0) + cr.rectangle(x0,y0+5,x1-x0,y1-y0-10) + cr.fill() + + def draw_shadow(self, x0, y0, x1, y1): + shadow = 0.4 + h_mid = (x0+x1)/2 + v_mid = (y0+y1)/2 + h_linear_1 = cairo.LinearGradient(h_mid,y0-4,h_mid,y0+4) + h_linear_2 = cairo.LinearGradient(h_mid,y1-4,h_mid,y1+4) + v_linear_1 = cairo.LinearGradient(x0-4,v_mid,x0+4,v_mid) + v_linear_2 = cairo.LinearGradient(x1-4,v_mid,x1+4,v_mid) + + h_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0) + h_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow) + h_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow) + h_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0) + v_linear_1.add_color_stop_rgba( 0, 0, 0, 0, 0) + v_linear_1.add_color_stop_rgba( 1, 0, 0, 0, shadow) + v_linear_2.add_color_stop_rgba( 0, 0, 0, 0, shadow) + v_linear_2.add_color_stop_rgba( 1, 0, 0, 0, 0) + + self.draw_rectangular_shadow(h_linear_1,x0+4,y0-4,x1-x0-8,8) + self.draw_rectangular_shadow(h_linear_2,x0+4,y1-4,x1-x0-8,8) + self.draw_rectangular_shadow(v_linear_1,x0-4,y0+4,8,y1-y0-8) + self.draw_rectangular_shadow(v_linear_2,x1-4,y0+4,8,y1-y0-8) + + self.draw_circular_shadow(x0+4, y0+4, 4, math.pi, 3*math.pi/2, (-1,0), shadow) + self.draw_circular_shadow(x1-4, y0+4, 4, 3*math.pi/2, 2*math.pi, (0,-1), shadow) + self.draw_circular_shadow(x0+4, y1-4, 4, math.pi/2, math.pi, (0,1), shadow) + self.draw_circular_shadow(x1-4, y1-4, 4, 0, math.pi/2, (1,0), shadow) + + def render_plot(self): + for index,group in enumerate(self.series): + for data in group: + self.render_rectangle(self.borders[HORZ] + data.content[0]*self.horizontal_step, + self.borders[VERT] + index*self.vertical_step + self.vertical_step/4.0, + self.borders[HORZ] + data.content[1]*self.horizontal_step, + self.borders[VERT] + index*self.vertical_step + 3.0*self.vertical_step/4.0, + self.series_colors[index]) + +# Function definition + +def scatter_plot(name, + data = None, + errorx = None, + errory = None, + width = 640, + height = 480, + background = "white light_gray", + border = 0, + axis = False, + dash = False, + discrete = False, + dots = False, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + z_bounds = None, + x_title = None, + y_title = None, + series_colors = None, + circle_colors = None): + + """ + - Function to plot scatter data. + + - Parameters + + data - The values to be ploted might be passed in a two basic: + list of points: [(0,0), (0,1), (0,2)] or [(0,0,1), (0,1,4), (0,2,1)] + lists of coordinates: [ [0,0,0] , [0,1,2] ] or [ [0,0,0] , [0,1,2] , [1,4,1] ] + Notice that these kinds of that can be grouped in order to form more complex data + using lists of lists or dictionaries; + series_colors - Define color values for each of the series + circle_colors - Define a lower and an upper bound for the circle colors for variable radius + (3 dimensions) series + """ + + plot = ScatterPlot( name, data, errorx, errory, width, height, background, border, + axis, dash, discrete, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, z_bounds, x_title, y_title, series_colors, circle_colors ) + plot.render() + plot.commit() + +def dot_line_plot(name, + data, + width, + height, + background = "white light_gray", + border = 0, + axis = False, + dash = False, + dots = False, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + x_title = None, + y_title = None, + series_colors = None): + """ + - Function to plot graphics using dots and lines. + + dot_line_plot (name, data, width, height, background = "white light_gray", border = 0, axis = False, grid = False, x_labels = None, y_labels = None, x_bounds = None, y_bounds = None) + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + border - Distance in pixels of a square border into which the graphics will be drawn; + axis - Whether or not the axis are to be drawn; + dash - Boolean or a list or a dictionary of booleans indicating whether or not the associated series should be drawn in dashed mode; + dots - Whether or not dots should be drawn on each point; + grid - Whether or not the gris is to be drawn; + series_legend - Whether or not the legend is to be drawn; + x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; + x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; + x_title - Whether or not to plot a title over the x axis. + y_title - Whether or not to plot a title over the y axis. + + - Examples of use + + data = [0, 1, 3, 8, 9, 0, 10, 10, 2, 1] + CairoPlot.dot_line_plot('teste', data, 400, 300) + + data = { "john" : [10, 10, 10, 10, 30], "mary" : [0, 0, 3, 5, 15], "philip" : [13, 32, 11, 25, 2] } + x_labels = ["jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008" ] + CairoPlot.dot_line_plot( 'test', data, 400, 300, axis = True, grid = True, + series_legend = True, x_labels = x_labels ) + """ + plot = DotLinePlot( name, data, width, height, background, border, + axis, dash, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, x_title, y_title, series_colors ) + plot.render() + plot.commit() + +def function_plot(name, + data, + width, + height, + background = "white light_gray", + border = 0, + axis = True, + dots = False, + discrete = False, + grid = False, + series_legend = False, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + x_title = None, + y_title = None, + series_colors = None, + step = 1): + + """ + - Function to plot functions. + + function_plot(name, data, width, height, background = "white light_gray", border = 0, axis = True, grid = False, dots = False, x_labels = None, y_labels = None, x_bounds = None, y_bounds = None, step = 1, discrete = False) + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + border - Distance in pixels of a square border into which the graphics will be drawn; + axis - Whether or not the axis are to be drawn; + grid - Whether or not the gris is to be drawn; + dots - Whether or not dots should be shown at each point; + x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; + x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; + step - the horizontal distance from one point to the other. The smaller, the smoother the curve will be; + discrete - whether or not the function should be plotted in discrete format. + + - Example of use + + data = lambda x : x**2 + CairoPlot.function_plot('function4', data, 400, 300, grid = True, x_bounds=(-10,10), step = 0.1) + """ + + plot = FunctionPlot( name, data, width, height, background, border, + axis, discrete, dots, grid, series_legend, x_labels, y_labels, + x_bounds, y_bounds, x_title, y_title, series_colors, step ) + plot.render() + plot.commit() + +def pie_plot( name, data, width, height, background = "white light_gray", gradient = False, shadow = False, colors = None ): + + """ + - Function to plot pie graphics. + + pie_plot(name, data, width, height, background = "white light_gray", gradient = False, colors = None) + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + gradient - Whether or not the pie color will be painted with a gradient; + shadow - Whether or not there will be a shadow behind the pie; + colors - List of slices colors. + + - Example of use + + teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235} + CairoPlot.pie_plot("pie_teste", teste_data, 500, 500) + """ + + plot = PiePlot( name, data, width, height, background, gradient, shadow, colors ) + plot.render() + plot.commit() + +def donut_plot(name, data, width, height, background = "white light_gray", gradient = False, shadow = False, colors = None, inner_radius = -1): + + """ + - Function to plot donut graphics. + + donut_plot(name, data, width, height, background = "white light_gray", gradient = False, inner_radius = -1) + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + shadow - Whether or not there will be a shadow behind the donut; + gradient - Whether or not the donut color will be painted with a gradient; + colors - List of slices colors; + inner_radius - The radius of the donut's inner circle. + + - Example of use + + teste_data = {"john" : 123, "mary" : 489, "philip" : 890 , "suzy" : 235} + CairoPlot.donut_plot("donut_teste", teste_data, 500, 500) + """ + + plot = DonutPlot(name, data, width, height, background, gradient, shadow, colors, inner_radius) + plot.render() + plot.commit() + +def gantt_chart(name, pieces, width, height, x_labels, y_labels, colors): + + """ + - Function to generate Gantt Charts. + + gantt_chart(name, pieces, width, height, x_labels, y_labels, colors): + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtim; + pieces - A list defining the spaces to be drawn. The user must pass, for each line, the index of its start and the index of its end. If a line must have two or more spaces, they must be passed inside a list; + width, height - Dimensions of the output image; + x_labels - A list of names for each of the vertical lines; + y_labels - A list of names for each of the horizontal spaces; + colors - List containing the colors expected for each of the horizontal spaces + + - Example of use + + pieces = [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,8)] + x_labels = [ 'teste01', 'teste02', 'teste03', 'teste04'] + y_labels = [ '0001', '0002', '0003', '0004', '0005', '0006', '0007', '0008', '0009', '0010' ] + colors = [ (1.0, 0.0, 0.0), (1.0, 0.7, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0) ] + CairoPlot.gantt_chart('gantt_teste', pieces, 600, 300, x_labels, y_labels, colors) + """ + + plot = GanttChart(name, pieces, width, height, x_labels, y_labels, colors) + plot.render() + plot.commit() + +def vertical_bar_plot(name, + data, + width, + height, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + series_labels = None, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + colors = None): + #TODO: Fix docstring for vertical_bar_plot + """ + - Function to generate vertical Bar Plot Charts. + + bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension, + x_labels, y_labels, x_bounds, y_bounds, colors): + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtime; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + border - Distance in pixels of a square border into which the graphics will be drawn; + grid - Whether or not the gris is to be drawn; + rounded_corners - Whether or not the bars should have rounded corners; + three_dimension - Whether or not the bars should be drawn in pseudo 3D; + x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; + x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; + colors - List containing the colors expected for each of the bars. + + - Example of use + + data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + CairoPlot.vertical_bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False) + """ + + plot = VerticalBarPlot(name, data, width, height, background, border, + display_values, grid, rounded_corners, stack, three_dimension, + series_labels, x_labels, y_labels, x_bounds, y_bounds, colors) + plot.render() + plot.commit() + +def horizontal_bar_plot(name, + data, + width, + height, + background = "white light_gray", + border = 0, + display_values = False, + grid = False, + rounded_corners = False, + stack = False, + three_dimension = False, + series_labels = None, + x_labels = None, + y_labels = None, + x_bounds = None, + y_bounds = None, + colors = None): + + #TODO: Fix docstring for horizontal_bar_plot + """ + - Function to generate Horizontal Bar Plot Charts. + + bar_plot(name, data, width, height, background, border, grid, rounded_corners, three_dimension, + x_labels, y_labels, x_bounds, y_bounds, colors): + + - Parameters + + name - Name of the desired output file, no need to input the .svg as it will be added at runtime; + data - The list, list of lists or dictionary holding the data to be plotted; + width, height - Dimensions of the output image; + background - A 3 element tuple representing the rgb color expected for the background or a new cairo linear gradient. + If left None, a gray to white gradient will be generated; + border - Distance in pixels of a square border into which the graphics will be drawn; + grid - Whether or not the gris is to be drawn; + rounded_corners - Whether or not the bars should have rounded corners; + three_dimension - Whether or not the bars should be drawn in pseudo 3D; + x_labels, y_labels - lists of strings containing the horizontal and vertical labels for the axis; + x_bounds, y_bounds - tuples containing the lower and upper value bounds for the data to be plotted; + colors - List containing the colors expected for each of the bars. + + - Example of use + + data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + CairoPlot.bar_plot ('bar2', data, 400, 300, border = 20, grid = True, rounded_corners = False) + """ + + plot = HorizontalBarPlot(name, data, width, height, background, border, + display_values, grid, rounded_corners, stack, three_dimension, + series_labels, x_labels, y_labels, x_bounds, y_bounds, colors) + plot.render() + plot.commit() + +def stream_chart(name, + data, + width, + height, + background = "white light_gray", + border = 0, + grid = False, + series_legend = None, + x_labels = None, + x_bounds = None, + y_bounds = None, + colors = None): + + #TODO: Fix docstring for horizontal_bar_plot + plot = StreamChart(name, data, width, height, background, border, + grid, series_legend, x_labels, x_bounds, y_bounds, colors) + plot.render() + plot.commit() + + +if __name__ == "__main__": + import tests + import seriestests diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/__init__.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/__init__.py new file mode 100755 index 0000000..364fb90 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/__init__.py @@ -0,0 +1,10 @@ +__all__ = ["handler", "png", "pdf", "ps", "svg", "vector"] + +# default handlers +from .handler import Handler +from .png import PNGHandler +from .pdf import PDFHandler +from .ps import PSHandler +from .svg import SVGHandler +from .vector import VectorHandler + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/fixedsize.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/fixedsize.py new file mode 100755 index 0000000..3c25fdf --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/fixedsize.py @@ -0,0 +1,30 @@ + +import cairo +import cairoplot +from .handler import Handler as _Handler + +class FixedSizeHandler(_Handler): + """Base class for handlers with a fixed size.""" + + def __init__(self, width, height): + """Create with fixed width and height.""" + self.dimensions = {} + self.dimensions[cairoplot.HORZ] = width + self.dimensions[cairoplot.VERT] = height + + # sub-classes must create a surface + self.surface = None + + def prepare(self, plot): + """Prepare plot to render by setting its dimensions.""" + _Handler.prepare(self, plot) + plot.dimensions = self.dimensions + plot.context = cairo.Context(self.surface) + + def commit(self, plot): + """Commit the plot (to a file).""" + _Handler.commit(self, plot) + + # since pngs are different from other fixed size handlers, + # sub-classes are in charge of actual file writing + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/gtk.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/gtk.py new file mode 100755 index 0000000..897c86f --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/gtk.py @@ -0,0 +1,39 @@ +from __future__ import absolute_import + +import gtk +import cairo +import cairoplot +from .handler import Handler as _Handler + +class GTKHandler(_Handler, gtk.DrawingArea): + """Handler to create plots that output to vector files.""" + + def __init__(self, *args, **kwargs): + """Create Handler for arbitrary surfaces.""" + _Handler.__init__(self) + gtk.DrawingArea.__init__(self) + + # users of this class must set plot manually + self.plot = None + self.context = None + + # connect events for resizing/redrawing + self.connect("expose_event", self.on_expose_event) + + def on_expose_event(self, widget, data): + """Redraws plot if need be.""" + + self.context = widget.window.cairo_create() + if (self.plot is not None): + self.plot.render() + + def prepare(self, plot): + """Update plot's size and context with custom widget.""" + _Handler.prepare(self, plot) + self.plot = plot + plot.context = self.context + + allocation = self.get_allocation() + plot.dimensions[cairoplot.HORZ] = allocation.width + plot.dimensions[cairoplot.VERT] = allocation.height + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/handler.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/handler.py new file mode 100755 index 0000000..0ec54a7 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/handler.py @@ -0,0 +1,11 @@ + +class Handler(object): + """Base class for all handlers.""" + + def prepare(self, plot): + pass + + def commit(self, plot): + """All handlers need to finalize the cairo context.""" + plot.context.show_page() + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/pdf.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/pdf.py new file mode 100755 index 0000000..30838c7 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/pdf.py @@ -0,0 +1,12 @@ + +import cairo +from .vector import VectorHandler as _VectorHandler + +class PDFHandler(_VectorHandler): + """Handler to create plots that output to pdf files.""" + + def __init__(self, filename, width, height): + """Creates a surface to be used by Cairo.""" + _VectorHandler.__init__(self, None, width, height) + self.surface = cairo.PDFSurface(filename, width, height) + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/png.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/png.py new file mode 100755 index 0000000..6cce422 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/png.py @@ -0,0 +1,19 @@ + +import cairo + +from .fixedsize import FixedSizeHandler as _FixedSizeHandler + +class PNGHandler(_FixedSizeHandler): + """Handler to create plots that output to png files.""" + + def __init__(self, filename, width, height): + """Creates a surface to be used by Cairo.""" + _FixedSizeHandler.__init__(self, width, height) + self.filename = filename + self.surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + + def commit(self, plot): + """Writes plot to file.""" + _FixedSizeHandler.commit(self, plot) + self.surface.write_to_png(self.filename) + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/ps.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/ps.py new file mode 100755 index 0000000..7a77781 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/ps.py @@ -0,0 +1,12 @@ + +import cairo +from .vector import VectorHandler as _VectorHandler + +class PSHandler(_VectorHandler): + """Handler to create plots that output to PostScript files.""" + + def __init__(self, filename, width, height): + """Creates a surface to be used by Cairo.""" + _VectorHandler.__init__(self, None, width, height) + self.surface = cairo.PSSurface(filename, width, height) + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/svg.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/svg.py new file mode 100755 index 0000000..032fdc8 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/svg.py @@ -0,0 +1,12 @@ + +import cairo +from .vector import VectorHandler as _VectorHandler + +class SVGHandler(_VectorHandler): + """Handler to create plots that output to svg files.""" + + def __init__(self, filename, width, height): + """Creates a surface to be used by Cairo.""" + _VectorHandler.__init__(self, None, width, height) + self.surface = cairo.SVGSurface(filename, width, height) + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/vector.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/vector.py new file mode 100755 index 0000000..0c4d4bc --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/handlers/vector.py @@ -0,0 +1,17 @@ + +import cairo +from .fixedsize import FixedSizeHandler as _FixedSizeHandler + +class VectorHandler(_FixedSizeHandler): + """Handler to create plots that output to vector files.""" + + def __init__(self, surface, *args, **kwargs): + """Create Handler for arbitrary surfaces.""" + _FixedSizeHandler.__init__(self, *args, **kwargs) + self.surface = surface + + def commit(self, plot): + """Writes plot to file.""" + _FixedSizeHandler.commit(self, plot) + self.surface.finish() + diff --git a/thirdparty/cairoplot-trunk/trunk/cairoplot/series.py b/thirdparty/cairoplot-trunk/trunk/cairoplot/series.py new file mode 100755 index 0000000..79fee2f --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/cairoplot/series.py @@ -0,0 +1,1140 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Serie.py +# +# Copyright (c) 2008 Magnun Leno da Silva +# +# Author: Magnun Leno da Silva +# +# This program 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 2 of +# the License, or (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA + +# Contributor: Rodrigo Moreiro Araujo + +#import cairoplot +import doctest + +NUMTYPES = (int, float, long) +LISTTYPES = (list, tuple) +STRTYPES = (str, unicode) +FILLING_TYPES = ['linear', 'solid', 'gradient'] +DEFAULT_COLOR_FILLING = 'solid' +#TODO: Define default color list +DEFAULT_COLOR_LIST = None + +class Data(object): + """ + Class that models the main data structure. + It can hold: + - a number type (int, float or long) + - a tuple, witch represents a point and can have 2 or 3 items (x,y,z) + - if a list is passed it will be converted to a tuple. + + obs: In case a tuple is passed it will convert to tuple + """ + def __init__(self, data=None, name=None, parent=None): + """ + Starts main atributes from the Data class + @name - Name for each point; + @content - The real data, can be an int, float, long or tuple, which + represents a point (x,y) or (x,y,z); + @parent - A pointer that give the data access to it's parent. + + Usage: + >>> d = Data(name='empty'); print d + empty: () + >>> d = Data((1,1),'point a'); print d + point a: (1, 1) + >>> d = Data((1,2,3),'point b'); print d + point b: (1, 2, 3) + >>> d = Data([2,3],'point c'); print d + point c: (2, 3) + >>> d = Data(12, 'simple value'); print d + simple value: 12 + """ + # Initial values + self.__content = None + self.__name = None + + # Setting passed values + self.parent = parent + self.name = name + self.content = data + + # Name property + @apply + def name(): + doc = """ + Name is a read/write property that controls the input of name. + - If passed an invalid value it cleans the name with None + + Usage: + >>> d = Data(13); d.name = 'name_test'; print d + name_test: 13 + >>> d.name = 11; print d + 13 + >>> d.name = 'other_name'; print d + other_name: 13 + >>> d.name = None; print d + 13 + >>> d.name = 'last_name'; print d + last_name: 13 + >>> d.name = ''; print d + 13 + """ + def fget(self): + """ + returns the name as a string + """ + return self.__name + + def fset(self, name): + """ + Sets the name of the Data + """ + if type(name) in STRTYPES and len(name) > 0: + self.__name = name + else: + self.__name = None + + + + return property(**locals()) + + # Content property + @apply + def content(): + doc = """ + Content is a read/write property that validate the data passed + and return it. + + Usage: + >>> d = Data(); d.content = 13; d.content + 13 + >>> d = Data(); d.content = (1,2); d.content + (1, 2) + >>> d = Data(); d.content = (1,2,3); d.content + (1, 2, 3) + >>> d = Data(); d.content = [1,2,3]; d.content + (1, 2, 3) + >>> d = Data(); d.content = [1.5,.2,3.3]; d.content + (1.5, 0.20000000000000001, 3.2999999999999998) + """ + def fget(self): + """ + Return the content of Data + """ + return self.__content + + def fset(self, data): + """ + Ensures that data is a valid tuple/list or a number (int, float + or long) + """ + # Type: None + if data is None: + self.__content = None + return + + # Type: Int or Float + elif type(data) in NUMTYPES: + self.__content = data + + # Type: List or Tuple + elif type(data) in LISTTYPES: + # Ensures the correct size + if len(data) not in (2, 3): + raise TypeError, "Data (as list/tuple) must have 2 or 3 items" + return + + # Ensures that all items in list/tuple is a number + isnum = lambda x : type(x) not in NUMTYPES + + if max(map(isnum, data)): + # An item in data isn't an int or a float + raise TypeError, "All content of data must be a number (int or float)" + + # Convert the tuple to list + if type(data) is list: + data = tuple(data) + + # Append a copy and sets the type + self.__content = data[:] + + # Unknown type! + else: + self.__content = None + raise TypeError, "Data must be an int, float or a tuple with two or three items" + return + + return property(**locals()) + + + def clear(self): + """ + Clear the all Data (content, name and parent) + """ + self.content = None + self.name = None + self.parent = None + + def copy(self): + """ + Returns a copy of the Data structure + """ + # The copy + new_data = Data() + if self.content is not None: + # If content is a point + if type(self.content) is tuple: + new_data.__content = self.content[:] + + # If content is a number + else: + new_data.__content = self.content + + # If it has a name + if self.name is not None: + new_data.__name = self.name + + return new_data + + def __str__(self): + """ + Return a string representation of the Data structure + """ + if self.name is None: + if self.content is None: + return '' + return str(self.content) + else: + if self.content is None: + return self.name+": ()" + return self.name+": "+str(self.content) + + def __len__(self): + """ + Return the length of the Data. + - If it's a number return 1; + - If it's a list return it's length; + - If its None return 0. + """ + if self.content is None: + return 0 + elif type(self.content) in NUMTYPES: + return 1 + return len(self.content) + + + + +class Group(object): + """ + Class that models a group of data. Every value (int, float, long, tuple + or list) passed is converted to a list of Data. + It can receive: + - A single number (int, float, long); + - A list of numbers; + - A tuple of numbers; + - An instance of Data; + - A list of Data; + + Obs: If a tuple with 2 or 3 items is passed it is converted to a point. + If a tuple with only 1 item is passed it's converted to a number; + If a tuple with more than 2 items is passed it's converted to a + list of numbers + """ + def __init__(self, group=None, name=None, parent=None): + """ + Starts main atributes in Group instance. + @data_list - a list of data which forms the group; + @range - a range that represent the x axis of possible functions; + @name - name of the data group; + @parent - the Serie parent of this group. + + Usage: + >>> g = Group(13, 'simple number'); print g + simple number ['13'] + >>> g = Group((1,2), 'simple point'); print g + simple point ['(1, 2)'] + >>> g = Group([1,2,3,4], 'list of numbers'); print g + list of numbers ['1', '2', '3', '4'] + >>> g = Group((1,2,3,4),'int in tuple'); print g + int in tuple ['1', '2', '3', '4'] + >>> g = Group([(1,2),(2,3),(3,4)], 'list of points'); print g + list of points ['(1, 2)', '(2, 3)', '(3, 4)'] + >>> g = Group([[1,2,3],[1,2,3]], '2D coordinate lists'); print g + 2D coordinated lists ['(1, 1)', '(2, 2)', '(3, 3)'] + >>> g = Group([[1,2],[1,2],[1,2]], '3D coordinate lists'); print g + 3D coordinated lists ['(1, 1, 1)', '(2, 2, 2)'] + """ + # Initial values + self.__data_list = [] + self.__range = [] + self.__name = None + + + self.parent = parent + self.name = name + self.data_list = group + + # Name property + @apply + def name(): + doc = """ + Name is a read/write property that controls the input of name. + - If passed an invalid value it cleans the name with None + + Usage: + >>> g = Group(13); g.name = 'name_test'; print g + name_test ['13'] + >>> g.name = 11; print g + ['13'] + >>> g.name = 'other_name'; print g + other_name ['13'] + >>> g.name = None; print g + ['13'] + >>> g.name = 'last_name'; print g + last_name ['13'] + >>> g.name = ''; print g + ['13'] + """ + def fget(self): + """ + Returns the name as a string + """ + return self.__name + + def fset(self, name): + """ + Sets the name of the Group + """ + if type(name) in STRTYPES and len(name) > 0: + self.__name = name + else: + self.__name = None + + return property(**locals()) + + # data_list property + @apply + def data_list(): + doc = """ + The data_list is a read/write property that can be a list of + numbers, a list of points or a list of 2 or 3 coordinate lists. This + property uses mainly the self.add_data method. + + Usage: + >>> g = Group(); g.data_list = 13; print g + ['13'] + >>> g.data_list = (1,2); print g + ['(1, 2)'] + >>> g.data_list = Data((1,2),'point a'); print g + ['point a: (1, 2)'] + >>> g.data_list = [1,2,3]; print g + ['1', '2', '3'] + >>> g.data_list = (1,2,3,4); print g + ['1', '2', '3', '4'] + >>> g.data_list = [(1,2),(2,3),(3,4)]; print g + ['(1, 2)', '(2, 3)', '(3, 4)'] + >>> g.data_list = [[1,2],[1,2]]; print g + ['(1, 1)', '(2, 2)'] + >>> g.data_list = [[1,2],[1,2],[1,2]]; print g + ['(1, 1, 1)', '(2, 2, 2)'] + >>> g.range = (10); g.data_list = lambda x:x**2; print g + ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)'] + """ + def fget(self): + """ + Returns the value of data_list + """ + return self.__data_list + + def fset(self, group): + """ + Ensures that group is valid. + """ + # None + if group is None: + self.__data_list = [] + + # Int/float/long or Instance of Data + elif type(group) in NUMTYPES or isinstance(group, Data): + # Clean data_list + self.__data_list = [] + self.add_data(group) + + # One point + elif type(group) is tuple and len(group) in (2,3): + self.__data_list = [] + self.add_data(group) + + # list of items + elif type(group) in LISTTYPES and type(group[0]) is not list: + # Clean data_list + self.__data_list = [] + for item in group: + # try to append and catch an exception + self.add_data(item) + + # function lambda + elif callable(group): + # Explicit is better than implicit + function = group + # Has range + if len(self.range) is not 0: + # Clean data_list + self.__data_list = [] + # Generate values for the lambda function + for x in self.range: + #self.add_data((x,round(group(x),2))) + self.add_data((x,function(x))) + + # Only have range in parent + elif self.parent is not None and len(self.parent.range) is not 0: + # Copy parent range + self.__range = self.parent.range[:] + # Clean data_list + self.__data_list = [] + # Generate values for the lambda function + for x in self.range: + #self.add_data((x,round(group(x),2))) + self.add_data((x,function(x))) + + # Don't have range anywhere + else: + # x_data don't exist + raise Exception, "Data argument is valid but to use function type please set x_range first" + + # Coordinate Lists + elif type(group) in LISTTYPES and type(group[0]) is list: + # Clean data_list + self.__data_list = [] + data = [] + if len(group) == 3: + data = zip(group[0], group[1], group[2]) + elif len(group) == 2: + data = zip(group[0], group[1]) + else: + raise TypeError, "Only one list of coordinates was received." + + for item in data: + self.add_data(item) + + else: + raise TypeError, "Group type not supported" + + return property(**locals()) + + @apply + def range(): + doc = """ + The range is a read/write property that generates a range of values + for the x axis of the functions. When passed a tuple it almost works + like the built-in range funtion: + - 1 item, represent the end of the range started from 0; + - 2 items, represents the start and the end, respectively; + - 3 items, the last one represents the step; + + When passed a list the range function understands as a valid range. + + Usage: + >>> g = Group(); g.range = 10; print g.range + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] + >>> g = Group(); g.range = (5); print g.range + [0.0, 1.0, 2.0, 3.0, 4.0] + >>> g = Group(); g.range = (1,7); print g.range + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0] + >>> g = Group(); g.range = (0,10,2); print g.range + [0.0, 2.0, 4.0, 6.0, 8.0] + >>> + >>> g = Group(); g.range = [0]; print g.range + [0.0] + >>> g = Group(); g.range = [0,10,20]; print g.range + [0.0, 10.0, 20.0] + """ + def fget(self): + """ + Returns the range + """ + return self.__range + + def fset(self, x_range): + """ + Controls the input of a valid type and generate the range + """ + # if passed a simple number convert to tuple + if type(x_range) in NUMTYPES: + x_range = (x_range,) + + # A list, just convert to float + if type(x_range) is list and len(x_range) > 0: + # Convert all to float + x_range = map(float, x_range) + # Prevents repeated values and convert back to list + self.__range = list(set(x_range[:])) + # Sort the list to ascending order + self.__range.sort() + + # A tuple, must check the lengths and generate the values + elif type(x_range) is tuple and len(x_range) in (1,2,3): + # Convert all to float + x_range = map(float, x_range) + + # Inital values + start = 0.0 + step = 1.0 + end = 0.0 + + # Only the end and it can't be less or iqual to 0 + if len(x_range) is 1 and x_range > 0: + end = x_range[0] + + # The start and the end but the start must be less then the end + elif len(x_range) is 2 and x_range[0] < x_range[1]: + start = x_range[0] + end = x_range[1] + + # All 3, but the start must be less then the end + elif x_range[0] <= x_range[1]: + start = x_range[0] + end = x_range[1] + step = x_range[2] + + # Starts the range + self.__range = [] + # Generate the range + # Can't use the range function because it doesn't support float values + while start < end: + self.__range.append(start) + start += step + + # Incorrect type + else: + raise Exception, "x_range must be a list with one or more items or a tuple with 2 or 3 items" + + return property(**locals()) + + def add_data(self, data, name=None): + """ + Append a new data to the data_list. + - If data is an instance of Data, append it + - If it's an int, float, tuple or list create an instance of Data and append it + + Usage: + >>> g = Group() + >>> g.add_data(12); print g + ['12'] + >>> g.add_data(7,'other'); print g + ['12', 'other: 7'] + >>> + >>> g = Group() + >>> g.add_data((1,1),'a'); print g + ['a: (1, 1)'] + >>> g.add_data((2,2),'b'); print g + ['a: (1, 1)', 'b: (2, 2)'] + >>> + >>> g.add_data(Data((1,2),'c')); print g + ['a: (1, 1)', 'b: (2, 2)', 'c: (1, 2)'] + """ + if not isinstance(data, Data): + # Try to convert + data = Data(data,name,self) + + if data.content is not None: + self.__data_list.append(data.copy()) + self.__data_list[-1].parent = self + + + def to_list(self): + """ + Returns the group as a list of numbers (int, float or long) or a + list of tuples (points 2D or 3D). + + Usage: + >>> g = Group([1,2,3,4],'g1'); g.to_list() + [1, 2, 3, 4] + >>> g = Group([(1,2),(2,3),(3,4)],'g2'); g.to_list() + [(1, 2), (2, 3), (3, 4)] + >>> g = Group([(1,2,3),(3,4,5)],'g2'); g.to_list() + [(1, 2, 3), (3, 4, 5)] + """ + return [data.content for data in self] + + def copy(self): + """ + Returns a copy of this group + """ + new_group = Group() + new_group.__name = self.__name + if self.__range is not None: + new_group.__range = self.__range[:] + for data in self: + new_group.add_data(data.copy()) + return new_group + + def get_names(self): + """ + Return a list with the names of all data in this group + """ + names = [] + for data in self: + if data.name is None: + names.append('Data '+str(data.index()+1)) + else: + names.append(data.name) + return names + + + def __str__ (self): + """ + Returns a string representing the Group + """ + ret = "" + if self.name is not None: + ret += self.name + " " + if len(self) > 0: + list_str = [str(item) for item in self] + ret += str(list_str) + else: + ret += "[]" + return ret + + def __getitem__(self, key): + """ + Makes a Group iterable, based in the data_list property + """ + return self.data_list[key] + + def __len__(self): + """ + Returns the length of the Group, based in the data_list property + """ + return len(self.data_list) + + +class Colors(object): + """ + Class that models the colors its labels (names) and its properties, RGB + and filling type. + + It can receive: + - A list where each item is a list with 3 or 4 items. The + first 3 items represent the RGB values and the last argument + defines the filling type. The list will be converted to a dict + and each color will receve a name based in its position in the + list. + - A dictionary where each key will be the color name and its item + can be a list with 3 or 4 items. The first 3 items represent + the RGB colors and the last argument defines the filling type. + """ + def __init__(self, color_list=None): + """ + Start the color_list property + @ color_list - the list or dict contaning the colors properties. + """ + self.__color_list = None + + self.color_list = color_list + + @apply + def color_list(): + doc = """ + >>> c = Colors([[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']]) + >>> print c.color_list + {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']} + >>> c.color_list = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')] + >>> print c.color_list + {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']} + >>> c.color_list = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)} + >>> print c.color_list + {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']} + """ + def fget(self): + """ + Return the color list + """ + return self.__color_list + + def fset(self, color_list): + """ + Format the color list to a dictionary + """ + if color_list is None: + self.__color_list = None + return + + if type(color_list) in LISTTYPES and type(color_list[0]) in LISTTYPES: + old_color_list = color_list[:] + color_list = {} + for index, color in enumerate(old_color_list): + if len(color) is 3 and max(map(type, color)) in NUMTYPES: + color_list['Color '+str(index+1)] = list(color)+[DEFAULT_COLOR_FILLING] + elif len(color) is 4 and max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES: + color_list['Color '+str(index+1)] = list(color) + else: + raise TypeError, "Unsuported color format" + elif type(color_list) is not dict: + raise TypeError, "Unsuported color format" + + for name, color in color_list.items(): + if len(color) is 3: + if max(map(type, color)) in NUMTYPES: + color_list[name] = list(color)+[DEFAULT_COLOR_FILLING] + else: + raise TypeError, "Unsuported color format" + elif len(color) is 4: + if max(map(type, color[:-1])) in NUMTYPES and color[-1] in FILLING_TYPES: + color_list[name] = list(color) + else: + raise TypeError, "Unsuported color format" + self.__color_list = color_list.copy() + + return property(**locals()) + + +class Series(object): + """ + Class that models a Series (group of groups). Every value (int, float, + long, tuple or list) passed is converted to a list of Group or Data. + It can receive: + - a single number or point, will be converted to a Group of one Data; + - a list of numbers, will be converted to a group of numbers; + - a list of tuples, will converted to a single Group of points; + - a list of lists of numbers, each 'sublist' will be converted to a + group of numbers; + - a list of lists of tuples, each 'sublist' will be converted to a + group of points; + - a list of lists of lists, the content of the 'sublist' will be + processed as coordinated lists and the result will be converted to + a group of points; + - a Dictionary where each item can be the same of the list: number, + point, list of numbers, list of points or list of lists (coordinated + lists); + - an instance of Data; + - an instance of group. + """ + def __init__(self, series=None, name=None, property=[], colors=None): + """ + Starts main atributes in Group instance. + @series - a list, dict of data of which the series is composed; + @name - name of the series; + @property - a list/dict of properties to be used in the plots of + this Series + + Usage: + >>> print Series([1,2,3,4]) + ["Group 1 ['1', '2', '3', '4']"] + >>> print Series([[1,2,3],[4,5,6]]) + ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"] + >>> print Series((1,2)) + ["Group 1 ['(1, 2)']"] + >>> print Series([(1,2),(2,3)]) + ["Group 1 ['(1, 2)', '(2, 3)']"] + >>> print Series([[(1,2),(2,3)],[(4,5),(5,6)]]) + ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"] + >>> print Series([[[1,2,3],[1,2,3],[1,2,3]]]) + ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"] + >>> print Series({'g1':[1,2,3], 'g2':[4,5,6]}) + ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"] + >>> print Series({'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}) + ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"] + >>> print Series({'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}) + ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"] + >>> print Series(Data(1,'d1')) + ["Group 1 ['d1: 1']"] + >>> print Series(Group([(1,2),(2,3)],'g1')) + ["g1 ['(1, 2)', '(2, 3)']"] + """ + # Intial values + self.__group_list = [] + self.__name = None + self.__range = None + + # TODO: Implement colors with filling + self.__colors = None + + self.name = name + self.group_list = series + self.colors = colors + + # Name property + @apply + def name(): + doc = """ + Name is a read/write property that controls the input of name. + - If passed an invalid value it cleans the name with None + + Usage: + >>> s = Series(13); s.name = 'name_test'; print s + name_test ["Group 1 ['13']"] + >>> s.name = 11; print s + ["Group 1 ['13']"] + >>> s.name = 'other_name'; print s + other_name ["Group 1 ['13']"] + >>> s.name = None; print s + ["Group 1 ['13']"] + >>> s.name = 'last_name'; print s + last_name ["Group 1 ['13']"] + >>> s.name = ''; print s + ["Group 1 ['13']"] + """ + def fget(self): + """ + Returns the name as a string + """ + return self.__name + + def fset(self, name): + """ + Sets the name of the Group + """ + if type(name) in STRTYPES and len(name) > 0: + self.__name = name + else: + self.__name = None + + return property(**locals()) + + + + # Colors property + @apply + def colors(): + doc = """ + >>> s = Series() + >>> s.colors = [[1,1,1],[2,2,2,'linear'],[3,3,3,'gradient']] + >>> print s.colors + {'Color 2': [2, 2, 2, 'linear'], 'Color 3': [3, 3, 3, 'gradient'], 'Color 1': [1, 1, 1, 'solid']} + >>> s.colors = [[1,1,1],(2,2,2,'solid'),(3,3,3,'linear')] + >>> print s.colors + {'Color 2': [2, 2, 2, 'solid'], 'Color 3': [3, 3, 3, 'linear'], 'Color 1': [1, 1, 1, 'solid']} + >>> s.colors = {'a':[1,1,1],'b':(2,2,2,'solid'),'c':(3,3,3,'linear'), 'd':(4,4,4)} + >>> print s.colors + {'a': [1, 1, 1, 'solid'], 'c': [3, 3, 3, 'linear'], 'b': [2, 2, 2, 'solid'], 'd': [4, 4, 4, 'solid']} + """ + def fget(self): + """ + Return the color list + """ + return self.__colors.color_list + + def fset(self, colors): + """ + Format the color list to a dictionary + """ + self.__colors = Colors(colors) + + return property(**locals()) + + @apply + def range(): + doc = """ + The range is a read/write property that generates a range of values + for the x axis of the functions. When passed a tuple it almost works + like the built-in range funtion: + - 1 item, represent the end of the range started from 0; + - 2 items, represents the start and the end, respectively; + - 3 items, the last one represents the step; + + When passed a list the range function understands as a valid range. + + Usage: + >>> s = Series(); s.range = 10; print s.range + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] + >>> s = Series(); s.range = (5); print s.range + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0] + >>> s = Series(); s.range = (1,7); print s.range + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0] + >>> s = Series(); s.range = (0,10,2); print s.range + [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] + >>> + >>> s = Series(); s.range = [0]; print s.range + [0.0] + >>> s = Series(); s.range = [0,10,20]; print s.range + [0.0, 10.0, 20.0] + """ + def fget(self): + """ + Returns the range + """ + return self.__range + + def fset(self, x_range): + """ + Controls the input of a valid type and generate the range + """ + # if passed a simple number convert to tuple + if type(x_range) in NUMTYPES: + x_range = (x_range,) + + # A list, just convert to float + if type(x_range) is list and len(x_range) > 0: + # Convert all to float + x_range = map(float, x_range) + # Prevents repeated values and convert back to list + self.__range = list(set(x_range[:])) + # Sort the list to ascending order + self.__range.sort() + + # A tuple, must check the lengths and generate the values + elif type(x_range) is tuple and len(x_range) in (1,2,3): + # Convert all to float + x_range = map(float, x_range) + + # Inital values + start = 0.0 + step = 1.0 + end = 0.0 + + # Only the end and it can't be less or iqual to 0 + if len(x_range) is 1 and x_range > 0: + end = x_range[0] + + # The start and the end but the start must be lesser then the end + elif len(x_range) is 2 and x_range[0] < x_range[1]: + start = x_range[0] + end = x_range[1] + + # All 3, but the start must be lesser then the end + elif x_range[0] < x_range[1]: + start = x_range[0] + end = x_range[1] + step = x_range[2] + + # Starts the range + self.__range = [] + # Generate the range + # Cnat use the range function becouse it don't suport float values + while start <= end: + self.__range.append(start) + start += step + + # Incorrect type + else: + raise Exception, "x_range must be a list with one or more item or a tuple with 2 or 3 items" + + return property(**locals()) + + @apply + def group_list(): + doc = """ + The group_list is a read/write property used to pre-process the list + of Groups. + It can be: + - a single number, point or lambda, will be converted to a single + Group of one Data; + - a list of numbers, will be converted to a group of numbers; + - a list of tuples, will converted to a single Group of points; + - a list of lists of numbers, each 'sublist' will be converted to + a group of numbers; + - a list of lists of tuples, each 'sublist' will be converted to a + group of points; + - a list of lists of lists, the content of the 'sublist' will be + processed as coordinated lists and the result will be converted + to a group of points; + - a list of lambdas, each lambda represents a Group; + - a Dictionary where each item can be the same of the list: number, + point, list of numbers, list of points, list of lists + (coordinated lists) or lambdas + - an instance of Data; + - an instance of group. + + Usage: + >>> s = Series() + >>> s.group_list = [1,2,3,4]; print s + ["Group 1 ['1', '2', '3', '4']"] + >>> s.group_list = [[1,2,3],[4,5,6]]; print s + ["Group 1 ['1', '2', '3']", "Group 2 ['4', '5', '6']"] + >>> s.group_list = (1,2); print s + ["Group 1 ['(1, 2)']"] + >>> s.group_list = [(1,2),(2,3)]; print s + ["Group 1 ['(1, 2)', '(2, 3)']"] + >>> s.group_list = [[(1,2),(2,3)],[(4,5),(5,6)]]; print s + ["Group 1 ['(1, 2)', '(2, 3)']", "Group 2 ['(4, 5)', '(5, 6)']"] + >>> s.group_list = [[[1,2,3],[1,2,3],[1,2,3]]]; print s + ["Group 1 ['(1, 1, 1)', '(2, 2, 2)', '(3, 3, 3)']"] + >>> s.group_list = [(0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)]; print s + ["Group 1 ['(0.5, 5.5)']", "Group 2 ['(0, 4)', '(6, 8)']", "Group 3 ['(5.5, 7)']", "Group 4 ['(7, 9)']"] + >>> s.group_list = {'g1':[1,2,3], 'g2':[4,5,6]}; print s + ["g1 ['1', '2', '3']", "g2 ['4', '5', '6']"] + >>> s.group_list = {'g1':[(1,2),(2,3)], 'g2':[(4,5),(5,6)]}; print s + ["g1 ['(1, 2)', '(2, 3)']", "g2 ['(4, 5)', '(5, 6)']"] + >>> s.group_list = {'g1':[[1,2],[1,2]], 'g2':[[4,5],[4,5]]}; print s + ["g1 ['(1, 1)', '(2, 2)']", "g2 ['(4, 4)', '(5, 5)']"] + >>> s.range = 10 + >>> s.group_list = lambda x:x*2 + >>> s.group_list = [lambda x:x*2, lambda x:x**2, lambda x:x**3]; print s + ["Group 1 ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "Group 2 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']", "Group 3 ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']"] + >>> s.group_list = {'linear':lambda x:x*2, 'square':lambda x:x**2, 'cubic':lambda x:x**3}; print s + ["cubic ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 8.0)', '(3.0, 27.0)', '(4.0, 64.0)', '(5.0, 125.0)', '(6.0, 216.0)', '(7.0, 343.0)', '(8.0, 512.0)', '(9.0, 729.0)', '(10.0, 1000.0)']", "linear ['(0.0, 0.0)', '(1.0, 2.0)', '(2.0, 4.0)', '(3.0, 6.0)', '(4.0, 8.0)', '(5.0, 10.0)', '(6.0, 12.0)', '(7.0, 14.0)', '(8.0, 16.0)', '(9.0, 18.0)', '(10.0, 20.0)']", "square ['(0.0, 0.0)', '(1.0, 1.0)', '(2.0, 4.0)', '(3.0, 9.0)', '(4.0, 16.0)', '(5.0, 25.0)', '(6.0, 36.0)', '(7.0, 49.0)', '(8.0, 64.0)', '(9.0, 81.0)', '(10.0, 100.0)']"] + >>> s.group_list = Data(1,'d1'); print s + ["Group 1 ['d1: 1']"] + >>> s.group_list = Group([(1,2),(2,3)],'g1'); print s + ["g1 ['(1, 2)', '(2, 3)']"] + """ + def fget(self): + """ + Return the group list. + """ + return self.__group_list + + def fset(self, series): + """ + Controls the input of a valid group list. + """ + #TODO: Add support to the following strem of data: [ (0.5,5.5) , [(0,4),(6,8)] , (5.5,7) , (7,9)] + + # Type: None + if series is None: + self.__group_list = [] + + # List or Tuple + elif type(series) in LISTTYPES: + self.__group_list = [] + + is_function = lambda x: callable(x) + # Groups + if list in map(type, series) or max(map(is_function, series)): + for group in series: + self.add_group(group) + + # single group + else: + self.add_group(series) + + #old code + ## List of numbers + #if type(series[0]) in NUMTYPES or type(series[0]) is tuple: + # print series + # self.add_group(series) + # + ## List of anything else + #else: + # for group in series: + # self.add_group(group) + + # Dict representing series of groups + elif type(series) is dict: + self.__group_list = [] + names = series.keys() + names.sort() + for name in names: + self.add_group(Group(series[name],name,self)) + + # A single lambda + elif callable(series): + self.__group_list = [] + self.add_group(series) + + # Int/float, instance of Group or Data + elif type(series) in NUMTYPES or isinstance(series, Group) or isinstance(series, Data): + self.__group_list = [] + self.add_group(series) + + # Default + else: + raise TypeError, "Serie type not supported" + + return property(**locals()) + + def add_group(self, group, name=None): + """ + Append a new group in group_list + """ + if not isinstance(group, Group): + #Try to convert + group = Group(group, name, self) + + if len(group.data_list) is not 0: + # Auto naming groups + if group.name is None: + group.name = "Group "+str(len(self.__group_list)+1) + + self.__group_list.append(group) + self.__group_list[-1].parent = self + + def copy(self): + """ + Returns a copy of the Series + """ + new_series = Series() + new_series.__name = self.__name + if self.__range is not None: + new_series.__range = self.__range[:] + #Add color property in the copy method + #self.__colors = None + + for group in self: + new_series.add_group(group.copy()) + + return new_series + + def get_names(self): + """ + Returns a list of the names of all groups in the Serie + """ + names = [] + for group in self: + if group.name is None: + names.append('Group '+str(group.index()+1)) + else: + names.append(group.name) + + return names + + def to_list(self): + """ + Returns a list with the content of all groups and data + """ + big_list = [] + for group in self: + for data in group: + if type(data.content) in NUMTYPES: + big_list.append(data.content) + else: + big_list = big_list + list(data.content) + return big_list + + def __getitem__(self, key): + """ + Makes the Series iterable, based in the group_list property + """ + return self.__group_list[key] + + def __str__(self): + """ + Returns a string that represents the Series + """ + ret = "" + if self.name is not None: + ret += self.name + " " + if len(self) > 0: + list_str = [str(item) for item in self] + ret += str(list_str) + else: + ret += "[]" + return ret + + def __len__(self): + """ + Returns the length of the Series, based in the group_lsit property + """ + return len(self.group_list) + + +if __name__ == '__main__': + doctest.testmod() diff --git a/thirdparty/cairoplot-trunk/trunk/gtktests.py b/thirdparty/cairoplot-trunk/trunk/gtktests.py new file mode 100755 index 0000000..5721260 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/gtktests.py @@ -0,0 +1,32 @@ + +import gtk +import cairoplot +from cairoplot.handlers.gtk import GTKHandler + +class CairoPlotWindow(gtk.Window): + """GtkWindow to display a plot.""" + + def __init__(self): + """Make a plot to test.""" + gtk.Window.__init__(self) + self.connect("destroy", gtk.main_quit) + + # make plot handler + handler = GTKHandler() + + # default data + data = [ (-2,10), (0,0), (0,15), (1,5), (2,0), (3,-10), (3,5) ] + plot = cairoplot.ScatterPlot(handler, data=data, + width=500, height=500, background="white", + border=20, axis=True, grid=True) + + handler.plot = plot + self.add(handler) + handler.show() + + +if __name__ == "__main__": + window = CairoPlotWindow() + window.show() + gtk.main() + diff --git a/thirdparty/cairoplot-trunk/trunk/seriestests.py b/thirdparty/cairoplot-trunk/trunk/seriestests.py new file mode 100755 index 0000000..20eb225 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/seriestests.py @@ -0,0 +1,251 @@ +import cairo, math, random + +import cairoplot +from cairoplot.series import Series + +# Line plotting +test_scatter_plot = 1 +test_dot_line_plot = 1 +test_function_plot = 1 +# Bar plotting +test_vertical_bar_plot = 1 +test_horizontal_bar_plot = 1 +# Pie plotting +test_pie_plot = 1 +test_donut_plot = 1 +# Others +test_gantt_chart = 1 +test_themes = 1 + + +if test_scatter_plot: + #Default data + data = Series([ (-2,10), (0,0), (0,15), (1,5), (2,0), (3,-10), (3,5) ]) + cairoplot.scatter_plot ( 'scatter_1_default_series.png', data = data, width = 500, height = 500, border = 20, axis = True, grid = True ) + + #lists of coordinates x,y + data = Series([[[1,2,3,4,5],[1,1,1,1,1]]]) + cairoplot.scatter_plot ( 'scatter_2_lists_series.png', data = data, width = 500, height = 500, border = 20, axis = True, grid = True ) + + #lists of coordinates x,y,z + data = Series([[[0.5,1,2,3,4,5],[0.5,1,1,1,1,1],[10,6,10,20,10,6]]]) + colors = [ (0,0,0,0.25), (1,0,0,0.75) ] + cairoplot.scatter_plot ( 'scatter_3_lists_series.png', data = data, width = 500, height = 500, border = 20, axis = True, discrete = True, + grid = True, circle_colors = colors ) + + data = Series([(-1, -16, 12), (-12, 17, 11), (-4, 6, 5), (4, -20, 12), (13, -3, 21), (7, 14, 20), (-11, -2, 18), (19, 7, 18), (-10, -19, 15), + (-17, -2, 6), (-9, 4, 10), (14, 11, 16), (13, -11, 18), (20, 20, 16), (7, -8, 15), (-16, 17, 16), (16, 9, 9), (-3, -13, 25), + (-20, -6, 17), (-10, -10, 12), (-7, 17, 25), (10, -10, 13), (10, 13, 20), (17, 6, 15), (18, -11, 14), (18, -12, 11), (-9, 11, 14), + (17, -15, 25), (-2, -8, 5), (5, 20, 20), (18, 20, 23), (-20, -16, 17), (-19, -2, 9), (-11, 19, 18), (17, 16, 12), (-5, -20, 15), + (-20, -13, 10), (-3, 5, 20), (-1, 13, 17), (-11, -9, 11)]) + colors = [ (0,0,0,0.25), (1,0,0,0.75) ] + cairoplot.scatter_plot ( 'scatter_2_variable_radius_series.png', data = data, width = 500, height = 500, border = 20, + axis = True, discrete = True, dots = 2, grid = True, + x_title = "x axis", y_title = "y axis", circle_colors = colors ) + + #Scatter x DotLine error bars + t = [x*0.1 for x in range(0,40)] + f = [math.exp(x) for x in t] + g = [10*math.cos(x) for x in t] + h = [10*math.sin(x) for x in t] + erx = [0.1*random.random() for x in t] + ery = [5*random.random() for x in t] + data = Series({"exp" : [t,f], "cos" : [t,g], "sin" : [t,h]}) + series_colors = [ (1,0,0), (0,0,0), (0,0,1) ] + cairoplot.scatter_plot ( 'cross_r_exponential_series.png', data = data, errorx = [erx,erx], errory = [ery,ery], width = 800, height = 600, border = 20, + axis = True, discrete = False, dots = 5, grid = True, + x_title = "t", y_title = "f(t) g(t)", series_legend=True, series_colors = series_colors ) + + +if test_dot_line_plot: + #Default plot + data = [ 0, 1, 3.5, 8.5, 9, 0, 10, 10, 2, 1 ] + cairoplot.dot_line_plot( "dot_line_1_default_series.png", data, 400, 300, border = 50, axis = True, grid = True, + x_title = "x axis", y_title = "y axis" ) + + #Labels + data = { "john" : [-5, -2, 0, 1, 3], "mary" : [0, 0, 3, 5, 2], "philip" : [-2, -3, -4, 2, 1] } + x_labels = [ "jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008" ] + y_labels = [ "very low", "low", "medium", "high", "very high" ] + cairoplot.dot_line_plot( "dot_line_2_dictionary_labels_series.png", data, 400, 300, x_labels = x_labels, + y_labels = y_labels, axis = True, grid = True, + x_title = "x axis", y_title = "y axis", series_legend=True ) + + #Series legend + data = { "john" : [10, 10, 10, 10, 30], "mary" : [0, 0, 3, 5, 15], "philip" : [13, 32, 11, 25, 2] } + x_labels = [ "jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008" ] + cairoplot.dot_line_plot( 'dot_line_3_series_legend_series.png', data, 400, 300, x_labels = x_labels, + axis = True, grid = True, series_legend = True ) + +if test_function_plot : + #Default Plot + data = lambda x : x**2 + cairoplot.function_plot( 'function_1_default_series.png', data, 400, 300, grid = True, x_bounds=(-10,10), step = 0.1 ) + + #Discrete Plot + data = lambda x : math.sin(0.1*x)*math.cos(x) + cairoplot.function_plot( 'function_2_discrete_series.png', data, 800, 300, discrete = True, dots = True, grid = True, x_bounds=(0,80), + x_title = "t (s)", y_title = "sin(0.1*x)*cos(x)") + + #Labels test + data = lambda x : [1,2,3,4,5][x] + x_labels = [ "4", "3", "2", "1", "0" ] + cairoplot.function_plot( 'function_3_labels_series.png', data, 400, 300, discrete = True, dots = True, grid = True, x_labels = x_labels, x_bounds=(0,4), step = 1 ) + + #Multiple functions + data = [ lambda x : 1, lambda y : y**2, lambda z : -z**2 ] + colors = [ (1.0, 0.0, 0.0 ), ( 0.0, 1.0, 0.0 ), ( 0.0, 0.0, 1.0 ) ] + cairoplot.function_plot( 'function_4_multi_functions_series.png', data, 400, 300, grid = True, series_colors = colors, step = 0.1 ) + + #Gaussian + a = 1 + b = 0 + c = 1.5 + gaussian = lambda x : a*math.exp(-(x-b)*(x-b)/(2*c*c)) + cairoplot.function_plot( 'function_5_gaussian_series.png', data, 400, 300, grid = True, x_bounds = (-10,10), step = 0.1 ) + + #Dict function plot + data = Series() + data.range = (-5,5) + data.group_list = {'linear':lambda x : x*2, 'quadratic':lambda x:x**2, 'cubic':lambda x:(x**3)/2} + cairoplot.function_plot( 'function_6_dict_serie.png', data, 400, 300, grid = True, x_bounds=(-5,5), step = 0.1 ) + + +if test_vertical_bar_plot: + #Passing a dictionary + data = Series({ 'teste00' : [27], 'teste01' : [10], 'teste02' : [18], 'teste03' : [5], 'teste04' : [1], 'teste05' : [22] }) + cairoplot.vertical_bar_plot ( 'vbar_0_dictionary_series.png', data, 400, 300, border = 20, grid = True, rounded_corners = True ) + + #Display values + data = Series({ 'teste00' : [27], 'teste01' : [10], 'teste02' : [18], 'teste03' : [5], 'teste04' : [1], 'teste05' : [22] }) + cairoplot.vertical_bar_plot ( 'vbar_0_dictionary_series.png', data, 400, 300, border = 20, display_values = True, grid = True, rounded_corners = True ) + + #Using default, rounded corners and 3D visualization + data = Series([ [0, 3, 11], [8, 9, 21], [13, 10, 9], [2, 30, 8] ]) + colors = [ (1,0.2,0), (1,0.7,0), (1,1,0) ] + series_labels = ["red", "orange", "yellow"] + cairoplot.vertical_bar_plot ( 'vbar_1_default_series.png', data, 400, 300, border = 20, grid = True, rounded_corners = False, colors = "yellow_orange_red" ) + cairoplot.vertical_bar_plot ( 'vbar_2_rounded_series.png', data, 400, 300, border = 20, series_labels = series_labels, display_values = True, grid = True, rounded_corners = True, colors = colors ) + cairoplot.vertical_bar_plot ( 'vbar_3_3D_series.png', data, 400, 300, border = 20, series_labels = series_labels, grid = True, three_dimension = True, colors = colors ) + + #Mixing groups and columns + data = Series([ [1], [2], [3,4], [4], [5], [6], [7], [8], [9], [10] ]) + cairoplot.vertical_bar_plot ( 'vbar_4_group_series.png', data, 400, 300, border = 20, grid = True ) + + #Using no labels, horizontal and vertical labels + data = Series([[3,4], [4,8], [5,3], [9,1]]) + y_labels = [ "line1", "line2", "line3", "line4", "line5", "line6" ] + x_labels = [ "group1", "group2", "group3", "group4" ] + cairoplot.vertical_bar_plot ( 'vbar_5_no_labels_series.png', data, 600, 200, border = 20, grid = True ) + cairoplot.vertical_bar_plot ( 'vbar_6_x_labels_series.png', data, 600, 200, border = 20, grid = True, x_labels = x_labels ) + cairoplot.vertical_bar_plot ( 'vbar_7_y_labels_series.png', data, 600, 200, border = 20, grid = True, y_labels = y_labels ) + cairoplot.vertical_bar_plot ( 'vbar_8_hy_labels_series.png', data, 600, 200, border = 20, display_values = True, grid = True, x_labels = x_labels, y_labels = y_labels ) + + #Large data set + data = Series([[10*random.random()] for x in range(50)]) + x_labels = ["large label name oh my god it's big" for x in data] + cairoplot.vertical_bar_plot ( 'vbar_9_large_series.png', data, 1000, 800, border = 20, grid = True, rounded_corners = True, x_labels = x_labels ) + + #Stack vertical + data = Series([ [6, 4, 10], [8, 9, 3], [1, 10, 9], [2, 7, 11] ]) + colors = [ (1,0.2,0), (1,0.7,0), (1,1,0) ] + x_labels = ["teste1", "teste2", "testegrande3", "testegrande4"] + cairoplot.vertical_bar_plot ( 'vbar_10_stack_series.png', data, 400, 300, border = 20, display_values = True, grid = True, rounded_corners = True, stack = True, + x_labels = x_labels, colors = colors ) + + +if test_horizontal_bar_plot: + #Passing a dictionary + data = Series({ 'teste00' : [27], 'teste01' : [10], 'teste02' : [18], 'teste03' : [5], 'teste04' : [1], 'teste05' : [22] }) + cairoplot.horizontal_bar_plot ( 'hbar_0_dictionary_series.png', data, 400, 300, border = 20, display_values = True, grid = True, rounded_corners = True ) + + #Using default, rounded corners and 3D visualization + data = Series([ [0, 3, 11], [8, 9, 21], [13, 10, 9], [2, 30, 8] ]) + colors = [ (1,0.2,0), (1,0.7,0), (1,1,0) ] + series_labels = ["red", "orange", "yellow"] + cairoplot.horizontal_bar_plot ( 'hbar_1_default_series.png', data, 400, 300, border = 20, grid = True, rounded_corners = False, colors = "yellow_orange_red" ) + cairoplot.horizontal_bar_plot ( 'hbar_2_rounded_series.png', data, 400, 300, border = 20, series_labels = series_labels, display_values = True, grid = True, rounded_corners = True, colors = colors ) + + + #Mixing groups and columns + data = ([ [1], [2], [3,4], [4], [5], [6], [7], [8], [9], [10] ]) + cairoplot.horizontal_bar_plot ( 'hbar_4_group_series.png', data, 400, 300, border = 20, grid = True ) + + #Using no labels, horizontal and vertical labels + series_labels = ["data11", "data22"] + data = Series([[3,4], [4,8], [5,3], [9,1]]) + x_labels = [ "line1", "line2", "line3", "line4", "line5", "line6" ] + y_labels = [ "group1", "group2", "group3", "group4" ] + cairoplot.horizontal_bar_plot ( 'hbar_5_no_labels_series.png', data, 600, 200, border = 20, series_labels = series_labels, grid = True ) + cairoplot.horizontal_bar_plot ( 'hbar_6_x_labels_series.png', data, 600, 200, border = 20, series_labels = series_labels, grid = True, x_labels = x_labels ) + cairoplot.horizontal_bar_plot ( 'hbar_7_y_labels_series.png', data, 600, 200, border = 20, series_labels = series_labels, grid = True, y_labels = y_labels ) + cairoplot.horizontal_bar_plot ( 'hbar_8_hy_labels_series.png', data, 600, 200, border = 20, series_labels = series_labels, display_values = True, grid = True, x_labels = x_labels, y_labels = y_labels ) + + #Large data set + data = Series([[10*random.random()] for x in range(25)]) + x_labels = ["large label name oh my god it's big" for x in data] + cairoplot.horizontal_bar_plot ( 'hbar_9_large_series.png', data, 1000, 800, border = 20, grid = True, rounded_corners = True, x_labels = x_labels ) + + #Stack horizontal + data = Series([ [6, 4, 10], [8, 9, 3], [1, 10, 9], [2, 7, 11] ]) + colors = [ (1,0.2,0), (1,0.7,0), (1,1,0) ] + y_labels = ["teste1", "teste2", "testegrande3", "testegrande4"] + cairoplot.horizontal_bar_plot ( 'hbar_10_stack_series.png', data, 400, 300, border = 20, display_values = True, grid = True, rounded_corners = True, stack = True, + y_labels = y_labels, colors = colors ) + +if test_pie_plot : + #Define a new backgrond + background = cairo.LinearGradient(300, 0, 300, 400) + background.add_color_stop_rgb(0.0,0.7,0.0,0.0) + background.add_color_stop_rgb(1.0,0.3,0.0,0.0) + + #Plot data + data = {"orcs" : 100, "goblins" : 230, "elves" : 50 , "demons" : 43, "humans" : 332} + cairoplot.pie_plot( "pie_1_default_series.png", data, 600, 400 ) + cairoplot.pie_plot( "pie_2_gradient_shadow_series.png", data, 600, 400, gradient = True, shadow = True ) + cairoplot.pie_plot( "pie_3_background_series.png", data, 600, 400, background = background, gradient = True, shadow = True ) + +if test_donut_plot : + #Define a new backgrond + background = cairo.LinearGradient(300, 0, 300, 400) + background.add_color_stop_rgb(0,0.4,0.4,0.4) + background.add_color_stop_rgb(1.0,0.1,0.1,0.1) + + data = {"john" : 700, "mary" : 100, "philip" : 100 , "suzy" : 50, "yman" : 50} + #Default plot, gradient and shadow, different background + cairoplot.donut_plot( "donut_1_default_series.png", data, 600, 400, inner_radius = 0.3 ) + cairoplot.donut_plot( "donut_2_gradient_shadow_series.png", data, 600, 400, gradient = True, shadow = True, inner_radius = 0.3 ) + cairoplot.donut_plot( "donut_3_background_series.png", data, 600, 400, background = background, gradient = True, shadow = True, inner_radius = 0.3 ) + +if test_gantt_chart : + #Default Plot + pieces = Series([(0.5, 5.5), [(0, 4), (6, 8)], (5.5, 7), (7, 9)]) + x_labels = [ 'teste01', 'teste02', 'teste03', 'teste04'] + y_labels = [ '0001', '0002', '0003', '0004', '0005', '0006', '0007', '0008', '0009', '0010' ] + colors = [ (1.0, 0.0, 0.0), (1.0, 0.7, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0) ] + cairoplot.gantt_chart('gantt_1_default_series.png', pieces, 500, 350, x_labels, y_labels, colors) + +if test_themes : + data = Series([[1,2,3,4,5,6,7,8,9,10,11,12,13,14]]) + cairoplot.vertical_bar_plot ( 'bar_1_color_themes_series.png', data, 400, 300, border = 20, grid = True, colors="rainbow" ) + + data = Series([[1,2,3,4,5,6,7,8,9,10,11,12,13,14]]) + cairoplot.vertical_bar_plot ( 'bar_2_color_themes_series.png', data, 400, 300, background = "white light_gray", border = 20, grid = True, colors="rainbow" ) + + data = Series() + data.range = (0,10,0.1) + data.group_list = [ lambda x : 1, lambda y : y**2, lambda z : -z**2 ] + cairoplot.function_plot( 'function_color_themes_series.png', data, 400, 300, grid = True, series_colors = ["red", "orange", "yellow"], step = 0.1 ) + + #Scatter x DotLine + t = [x*0.1 for x in range(0,40)] + f = [math.exp(x) for x in t] + g = [10*math.cos(x) for x in t] + h = [10*math.sin(x) for x in t] + erx = [0.1*random.random() for x in t] + ery = [5*random.random() for x in t] + data = Series({"exp" : [t,f], "cos" : [t,g], "sin" : [t,h]}) + series_colors = [ (1,0,0), (0,0,0) ] + cairoplot.scatter_plot ( 'scatter_color_themes_series.png', data = data, errorx = [erx,erx], errory = [ery,ery], width = 800, height = 600, border = 20, + axis = True, discrete = False, dots = 5, grid = True, + x_title = "t", y_title = "f(t) g(t)", series_legend=True, series_colors = ["red", "blue", "orange"]) diff --git a/thirdparty/cairoplot-trunk/trunk/setup.py b/thirdparty/cairoplot-trunk/trunk/setup.py new file mode 100755 index 0000000..328b2f2 --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/setup.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +'''Cairoplot installation script.''' + +from distutils.core import setup +import os + +setup( + description='Cairoplot', + license='GNU LGPL 2.1', + long_description=''' + Using Python and PyCairo to develop a module to plot graphics in an + easy and intuitive way, creating beautiful results for presentations. + ''', + name='Cairoplot', + py_modules=['cairoplot','series'], + url='http://linil.wordpress.com/2008/09/16/cairoplot-11/', + version='1.1', + ) + diff --git a/thirdparty/cairoplot-trunk/trunk/tests.py b/thirdparty/cairoplot-trunk/trunk/tests.py new file mode 100755 index 0000000..d4ad4ea --- /dev/null +++ b/thirdparty/cairoplot-trunk/trunk/tests.py @@ -0,0 +1,252 @@ +import cairo, math, random + +import cairoplot + +# Line plotting +test_scatter_plot = 1 +test_dot_line_plot = 1 +test_function_plot = 1 +# Bar plotting +test_vertical_bar_plot = 1 +test_horizontal_bar_plot = 1 +# Pie plotting +test_pie_plot = 1 +test_donut_plot = 1 +# Others +test_gantt_chart = 1 +test_themes = 1 + + +if test_scatter_plot: + #Special data + data = [(50,10),(15,55),(10,70),(15,85),(30,90),(40,85),(50,70),(60,85),(70,90),(85,85),(90,70),(85,55),(50,10)] + cairoplot.scatter_plot ( 'scatter_0.svg', data = data, width = 500, height = 500, border = 20, axis = True, dots = 3, grid = True, + x_bounds=[0,100], y_bounds=[0,100], series_colors = [(1,0,0)] ) + + #Default data + data = [ (-2,10), (0,0), (0,15), (1,5), (2,0), (3,-10), (3,5) ] + cairoplot.scatter_plot ( 'scatter_1_default.svg', data = data, width = 500, height = 500, border = 20, axis = True, grid = True ) + + #lists of coordinates x,y + data = [[1,2,3,4,5],[1,1,1,1,1]] + cairoplot.scatter_plot ( 'scatter_2_lists.svg', data = data, width = 500, height = 500, border = 20, axis = True, grid = True ) + + #lists of coordinates x,y,z + data = [[0.5,1,2,3,4,5],[0.5,1,1,1,1,1],[10,6,10,20,10,6]] + colors = [ (0,0,0,0.25), (1,0,0,0.75) ] + cairoplot.scatter_plot ( 'scatter_3_lists.svg', data = data, width = 500, height = 500, border = 20, axis = True, discrete = True, + grid = True, circle_colors = colors ) + + data = [(-1, -16, 12), (-12, 17, 11), (-4, 6, 5), (4, -20, 12), (13, -3, 21), (7, 14, 20), (-11, -2, 18), (19, 7, 18), (-10, -19, 15), + (-17, -2, 6), (-9, 4, 10), (14, 11, 16), (13, -11, 18), (20, 20, 16), (7, -8, 15), (-16, 17, 16), (16, 9, 9), (-3, -13, 25), + (-20, -6, 17), (-10, -10, 12), (-7, 17, 25), (10, -10, 13), (10, 13, 20), (17, 6, 15), (18, -11, 14), (18, -12, 11), (-9, 11, 14), + (17, -15, 25), (-2, -8, 5), (5, 20, 20), (18, 20, 23), (-20, -16, 17), (-19, -2, 9), (-11, 19, 18), (17, 16, 12), (-5, -20, 15), + (-20, -13, 10), (-3, 5, 20), (-1, 13, 17), (-11, -9, 11)] + colors = [ (0,0,0,0.25), (1,0,0,0.75) ] + cairoplot.scatter_plot ( 'scatter_2_variable_radius.svg', data = data, width = 500, height = 500, border = 20, + axis = True, discrete = True, dots = 2, grid = True, + x_title = "x axis", y_title = "y axis", circle_colors = colors ) + + #Scatter x DotLine error bars + t = [x*0.1 for x in range(0,40)] + f = [math.exp(x) for x in t] + g = [10*math.cos(x) for x in t] + h = [10*math.sin(x) for x in t] + erx = [0.1*random.random() for x in t] + ery = [5*random.random() for x in t] + data = {"exp" : [t,f], "cos" : [t,g], "sin" : [t,h]} + series_colors = [ (1,0,0), (0,0,0), (0,0,1) ] + cairoplot.scatter_plot ( 'cross_r_exponential.svg', data = data, errorx = [erx,erx], errory = [ery,ery], width = 800, height = 600, border = 20, + axis = True, discrete = False, dots = 5, grid = True, + x_title = "t", y_title = "f(t) g(t)", series_legend=True, series_colors = series_colors ) + + +if test_dot_line_plot: + #Default plot + data = [ 0, 1, 3.5, 8.5, 9, 0, 10, 10, 2, 1 ] + cairoplot.dot_line_plot( "dot_line_1_default.svg", data, 400, 300, border = 50, axis = True, grid = True, + x_title = "x axis", y_title = "y axis" ) + + #Labels + data = { "john" : [-5, -2, 0, 1, 3], "mary" : [0, 0, 3, 5, 2], "philip" : [-2, -3, -4, 2, 1] } + x_labels = [ "jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008" ] + y_labels = [ "very low", "low", "medium", "high", "very high" ] + cairoplot.dot_line_plot( "dot_line_2_dictionary_labels.svg", data, 400, 300, x_labels = x_labels, + y_labels = y_labels, axis = True, grid = True, + x_title = "x axis", y_title = "y axis", series_legend=True ) + + #Series legend + data = { "john" : [10, 10, 10, 10, 30], "mary" : [0, 0, 3, 5, 15], "philip" : [13, 32, 11, 25, 2] } + x_labels = [ "jan/2008", "feb/2008", "mar/2008", "apr/2008", "may/2008" ] + cairoplot.dot_line_plot( 'dot_line_3_series_legend.svg', data, 400, 300, x_labels = x_labels, + axis = True, grid = True, series_legend = True ) + +if test_function_plot : + #Default Plot + data = lambda x : x**2 + cairoplot.function_plot( 'function_1_default.svg', data, 400, 300, grid = True, x_bounds=(-10,10), step = 0.1 ) + + #Discrete Plot + data = lambda x : math.sin(0.1*x)*math.cos(x) + cairoplot.function_plot( 'function_2_discrete.svg', data, 800, 300, discrete = True, dots = True, grid = True, x_bounds=(0,80), + x_title = "t (s)", y_title = "sin(0.1*x)*cos(x)") + + #Labels test + data = lambda x : [1,2,3,4,5][x] + x_labels = [ "4", "3", "2", "1", "0" ] + cairoplot.function_plot( 'function_3_labels.svg', data, 400, 300, discrete = True, dots = True, grid = True, x_labels = x_labels, x_bounds=(0,4), step = 1 ) + + #Multiple functions + data = [ lambda x : 1, lambda y : y**2, lambda z : -z**2 ] + colors = [ (1.0, 0.0, 0.0 ), ( 0.0, 1.0, 0.0 ), ( 0.0, 0.0, 1.0 ) ] + cairoplot.function_plot( 'function_4_multi_functions.svg', data, 400, 300, grid = True, series_colors = colors, step = 0.1 ) + + #Gaussian + a = 1 + b = 0 + c = 1.5 + gaussian = lambda x : a*math.exp(-(x-b)*(x-b)/(2*c*c)) + cairoplot.function_plot( 'function_5_gaussian.svg', data, 400, 300, grid = True, x_bounds = (-10,10), step = 0.1 ) + + #Dict function plot + data = {'linear':lambda x : x*2, 'quadratic':lambda x:x**2, 'cubic':lambda x:(x**3)/2} + cairoplot.function_plot( 'function_6_dict.svg', data, 400, 300, grid = True, x_bounds=(-5,5), step = 0.1 ) + + +if test_vertical_bar_plot: + #Passing a dictionary + data = { 'teste00' : [27], 'teste01' : [10], 'teste02' : [18], 'teste03' : [5], 'teste04' : [1], 'teste05' : [22] } + cairoplot.vertical_bar_plot ( 'vbar_0_dictionary.svg', data, 400, 300, border = 20, grid = True, rounded_corners = True ) + + #Display values + data = { 'teste00' : [27], 'teste01' : [10], 'teste02' : [18], 'teste03' : [5], 'teste04' : [1], 'teste05' : [22] } + cairoplot.vertical_bar_plot ( 'vbar_0_dictionary.svg', data, 400, 300, border = 20, display_values = True, grid = True, rounded_corners = True ) + + #Using default, rounded corners and 3D visualization + data = [ [0, 3, 11], [8, 9, 21], [13, 10, 9], [2, 30, 8] ] + colors = [ (1,0.2,0), (1,0.7,0), (1,1,0) ] + series_labels = ["red", "orange", "yellow"] + cairoplot.vertical_bar_plot ( 'vbar_1_default.svg', data, 400, 300, border = 20, grid = True, rounded_corners = False, colors = "yellow_orange_red" ) + cairoplot.vertical_bar_plot ( 'vbar_2_rounded.svg', data, 400, 300, border = 20, series_labels = series_labels, display_values = True, grid = True, rounded_corners = True, colors = colors ) + cairoplot.vertical_bar_plot ( 'vbar_3_3D.svg', data, 400, 300, border = 20, series_labels = series_labels, grid = True, three_dimension = True, colors = colors ) + + #Mixing groups and columns + data = [ [1], [2], [3,4], [4], [5], [6], [7], [8], [9], [10] ] + cairoplot.vertical_bar_plot ( 'vbar_4_group.svg', data, 400, 300, border = 20, grid = True ) + + #Using no labels, horizontal and vertical labels + data = [[3,4], [4,8], [5,3], [9,1]] + y_labels = [ "line1", "line2", "line3", "line4", "line5", "line6" ] + x_labels = [ "group1", "group2", "group3", "group4" ] + cairoplot.vertical_bar_plot ( 'vbar_5_no_labels.svg', data, 600, 200, border = 20, grid = True ) + cairoplot.vertical_bar_plot ( 'vbar_6_x_labels.svg', data, 600, 200, border = 20, grid = True, x_labels = x_labels ) + cairoplot.vertical_bar_plot ( 'vbar_7_y_labels.svg', data, 600, 200, border = 20, grid = True, y_labels = y_labels ) + cairoplot.vertical_bar_plot ( 'vbar_8_hy_labels.svg', data, 600, 200, border = 20, display_values = True, grid = True, x_labels = x_labels, y_labels = y_labels ) + + #Large data set + data = [[10*random.random()] for x in range(50)] + x_labels = ["large label name oh my god it's big" for x in data] + cairoplot.vertical_bar_plot ( 'vbar_9_large.svg', data, 1000, 800, border = 20, grid = True, rounded_corners = True, x_labels = x_labels ) + + #Stack vertical + data = [ [6, 4, 10], [8, 9, 3], [1, 10, 9], [2, 7, 11] ] + colors = [ (1,0.2,0), (1,0.7,0), (1,1,0) ] + x_labels = ["teste1", "teste2", "testegrande3", "testegrande4"] + cairoplot.vertical_bar_plot ( 'vbar_10_stack.svg', data, 400, 300, border = 20, display_values = True, grid = True, rounded_corners = True, stack = True, + x_labels = x_labels, colors = colors ) + + +if test_horizontal_bar_plot: + #Passing a dictionary + data = { 'teste00' : [27], 'teste01' : [10], 'teste02' : [18], 'teste03' : [5], 'teste04' : [1], 'teste05' : [22] } + cairoplot.horizontal_bar_plot ( 'hbar_0_dictionary.svg', data, 400, 300, border = 20, display_values = True, grid = True, rounded_corners = True ) + + #Using default, rounded corners and 3D visualization + data = [ [0, 3, 11], [8, 9, 21], [13, 10, 9], [2, 30, 8] ] + colors = [ (1,0.2,0), (1,0.7,0), (1,1,0) ] + series_labels = ["red", "orange", "yellow"] + cairoplot.horizontal_bar_plot ( 'hbar_1_default.svg', data, 400, 300, border = 20, grid = True, rounded_corners = False, colors = "yellow_orange_red" ) + cairoplot.horizontal_bar_plot ( 'hbar_2_rounded.svg', data, 400, 300, border = 20, series_labels = series_labels, display_values = True, grid = True, rounded_corners = True, colors = colors ) + + + #Mixing groups and columns + data = [ [1], [2], [3,4], [4], [5], [6], [7], [8], [9], [10] ] + cairoplot.horizontal_bar_plot ( 'hbar_4_group.svg', data, 400, 300, border = 20, grid = True ) + + #Using no labels, horizontal and vertical labels + series_labels = ["data11", "data22"] + data = [[3,4], [4,8], [5,3], [9,1]] + x_labels = [ "line1", "line2", "line3", "line4", "line5", "line6" ] + y_labels = [ "group1", "group2", "group3", "group4" ] + cairoplot.horizontal_bar_plot ( 'hbar_5_no_labels.svg', data, 600, 200, border = 20, series_labels = series_labels, grid = True ) + cairoplot.horizontal_bar_plot ( 'hbar_6_x_labels.svg', data, 600, 200, border = 20, series_labels = series_labels, grid = True, x_labels = x_labels ) + cairoplot.horizontal_bar_plot ( 'hbar_7_y_labels.svg', data, 600, 200, border = 20, series_labels = series_labels, grid = True, y_labels = y_labels ) + cairoplot.horizontal_bar_plot ( 'hbar_8_hy_labels.svg', data, 600, 200, border = 20, series_labels = series_labels, display_values = True, grid = True, x_labels = x_labels, y_labels = y_labels ) + + #Large data set + data = [[10*random.random()] for x in range(25)] + x_labels = ["large label name oh my god it's big" for x in data] + cairoplot.horizontal_bar_plot ( 'hbar_9_large.svg', data, 1000, 800, border = 20, grid = True, rounded_corners = True, x_labels = x_labels ) + + #Stack horizontal + data = [ [6, 4, 10], [8, 9, 3], [1, 10, 9], [2, 7, 11] ] + colors = [ (1,0.2,0), (1,0.7,0), (1,1,0) ] + y_labels = ["teste1", "teste2", "testegrande3", "testegrande4"] + cairoplot.horizontal_bar_plot ( 'hbar_10_stack.svg', data, 400, 300, border = 20, display_values = True, grid = True, rounded_corners = True, stack = True, + y_labels = y_labels, colors = colors ) + +if test_pie_plot : + #Define a new backgrond + background = cairo.LinearGradient(300, 0, 300, 400) + background.add_color_stop_rgb(0.0,0.7,0.0,0.0) + background.add_color_stop_rgb(1.0,0.3,0.0,0.0) + + #Plot data + data = {"orcs" : 100, "goblins" : 230, "elves" : 50 , "demons" : 43, "humans" : 332} + cairoplot.pie_plot( "pie_1_default.svg", data, 600, 400 ) + cairoplot.pie_plot( "pie_2_gradient_shadow.svg", data, 600, 400, gradient = True, shadow = True ) + cairoplot.pie_plot( "pie_3_background.svg", data, 600, 400, background = background, gradient = True, shadow = True ) + +if test_donut_plot : + #Define a new backgrond + background = cairo.LinearGradient(300, 0, 300, 400) + background.add_color_stop_rgb(0,0.4,0.4,0.4) + background.add_color_stop_rgb(1.0,0.1,0.1,0.1) + + data = {"john" : 700, "mary" : 100, "philip" : 100 , "suzy" : 50, "yman" : 50} + #Default plot, gradient and shadow, different background + cairoplot.donut_plot( "donut_1_default.svg", data, 600, 400, inner_radius = 0.3 ) + cairoplot.donut_plot( "donut_2_gradient_shadow.svg", data, 600, 400, gradient = True, shadow = True, inner_radius = 0.3 ) + cairoplot.donut_plot( "donut_3_background.svg", data, 600, 400, background = background, gradient = True, shadow = True, inner_radius = 0.3 ) + +if test_gantt_chart : + #Default Plot + pieces = [(0.5, 5.5), [(0, 4), (6, 8)], (5.5, 7), (7, 9)] + x_labels = [ 'teste01', 'teste02', 'teste03', 'teste04'] + y_labels = [ '0001', '0002', '0003', '0004', '0005', '0006', '0007', '0008', '0009', '0010' ] + colors = [ (1.0, 0.0, 0.0), (1.0, 0.7, 0.0), (1.0, 1.0, 0.0), (0.0, 1.0, 0.0) ] + cairoplot.gantt_chart('gantt_1_default.svg', pieces, 500, 350, x_labels, y_labels, colors) + + +if test_themes : + data = [[1,2,3,4,5,6,7,8,9,10,11,12,13,14]] + cairoplot.vertical_bar_plot ( 'bar_1_color_themes.svg', data, 400, 300, border = 20, grid = True, colors="rainbow" ) + + data = [[1,2,3,4,5,6,7,8,9,10,11,12,13,14]] + cairoplot.vertical_bar_plot ( 'bar_2_color_themes.svg', data, 400, 300, background = "black light_gray", border = 20, grid = True, colors="rainbow" ) + + data = [ lambda x : 1, lambda y : y**2, lambda z : -z**2 ] + cairoplot.function_plot( 'function_color_themes.svg', data, 400, 300, grid = True, series_colors = ["red", "orange", "yellow"], step = 0.1 ) + + #Scatter x DotLine + t = [x*0.1 for x in range(0,40)] + f = [math.exp(x) for x in t] + g = [10*math.cos(x) for x in t] + h = [10*math.sin(x) for x in t] + erx = [0.1*random.random() for x in t] + ery = [5*random.random() for x in t] + data = {"exp" : [t,f], "cos" : [t,g], "sin" : [t,h]} + series_colors = [ (1,0,0), (0,0,0) ] + cairoplot.scatter_plot ( 'scatter_color_themes.svg', data = data, errorx = [erx,erx], errory = [ery,ery], width = 800, height = 600, border = 20, + axis = True, discrete = False, dots = 5, grid = True, + x_title = "t", y_title = "f(t) g(t)", series_legend=True, series_colors = ["red", "blue", "orange"]) -- cgit v0.9.1