Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorestudiante <estudiante@estudiante-laptop.(none)>2012-02-14 12:08:56 (GMT)
committer estudiante <estudiante@estudiante-laptop.(none)>2012-02-14 12:08:56 (GMT)
commit84d66d5328cce8845997dc87bacd884c3f87f75e (patch)
tree52805ef84cf9cef2418ade927131e5b72a659d71
Plot:Actividad de graficar
-rwxr-xr-xCOPYING339
-rwxr-xr-xMANIFEST59
-rwxr-xr-xactivity/activity-plot.svg33
-rwxr-xr-xactivity/activity.info9
-rwxr-xr-xcairoplot/__init__.py2378
-rwxr-xr-xcairoplot/handlers/__init__.py10
-rwxr-xr-xcairoplot/handlers/fixedsize.py30
-rwxr-xr-xcairoplot/handlers/gtk.py39
-rwxr-xr-xcairoplot/handlers/handler.py11
-rwxr-xr-xcairoplot/handlers/pdf.py12
-rwxr-xr-xcairoplot/handlers/png.py19
-rwxr-xr-xcairoplot/handlers/ps.py12
-rwxr-xr-xcairoplot/handlers/svg.py12
-rwxr-xr-xcairoplot/handlers/vector.py17
-rwxr-xr-xcairoplot/series.py1140
-rwxr-xr-xdata/icons/plot-gtk-inkscape.svg84
-rwxr-xr-xdata/icons/plot-gtk.pngbin0 -> 7189 bytes
-rwxr-xr-xdata/icons/plot-small-inkscape.svg102
-rwxr-xr-xdata/icons/plot-tiny-inkscape.svg102
-rwxr-xr-xdata/puzzle/absolutevalue-inkscape.svg99
-rwxr-xr-xdata/puzzle/absolutevalue.svg39
-rwxr-xr-xdata/puzzle/addition-inkscape.svg83
-rwxr-xr-xdata/puzzle/addition.svg36
-rwxr-xr-xdata/puzzle/blank-inkscape.svg110
-rwxr-xr-xdata/puzzle/blank.svg48
-rwxr-xr-xdata/puzzle/composition-inkscape.svg90
-rwxr-xr-xdata/puzzle/composition.svg40
-rwxr-xr-xdata/puzzle/constant-inkscape.svg92
-rwxr-xr-xdata/puzzle/constant.svg38
-rwxr-xr-xdata/puzzle/e-inkscape.svg83
-rwxr-xr-xdata/puzzle/e.svg23
-rwxr-xr-xdata/puzzle/exponentiation-inkscape.svg84
-rwxr-xr-xdata/puzzle/exponentiation.svg39
-rwxr-xr-xdata/puzzle/identity-inkscape.svg90
-rwxr-xr-xdata/puzzle/identity.svg37
-rwxr-xr-xdata/puzzle/leftparen-inkscape.svg89
-rwxr-xr-xdata/puzzle/leftparen.svg44
-rwxr-xr-xdata/puzzle/multiplication-inkscape.svg84
-rwxr-xr-xdata/puzzle/multiplication.svg37
-rwxr-xr-xdata/puzzle/pi-inkscape.svg84
-rwxr-xr-xdata/puzzle/pi.svg24
-rwxr-xr-xdata/puzzle/rightparen-inkscape.svg92
-rwxr-xr-xdata/puzzle/rightparen.svg47
-rwxr-xr-xdata/puzzle/sine-inkscape.svg80
-rwxr-xr-xdata/puzzle/sine.svg19
-rwxr-xr-xgtkplotactivity.py288
-rwxr-xr-xlocale/en-US/LC_MESSAGES/edu.wisc.cs.nest.PlotActivity.mobin0 -> 2650 bytes
-rwxr-xr-xlocale/en-US/activity.linfo2
-rwxr-xr-xplot.py52
-rwxr-xr-xplotter/__init__.py2
-rwxr-xr-xplotter/json.py33
-rwxr-xr-xplotter/parse.py41
-rwxr-xr-xplotter/plot.py31
-rwxr-xr-xplotter/settings.py55
-rwxr-xr-xplotter/view/__init__.py243
-rwxr-xr-xplotter/view/equation.py54
-rwxr-xr-xplotter/view/puzzletree/__init__.py110
-rwxr-xr-xplotter/view/puzzletree/display.py323
-rwxr-xr-xplotter/view/puzzletree/nodes/__init__.py56
-rwxr-xr-xplotter/view/puzzletree/nodes/absolutevalue.py23
-rwxr-xr-xplotter/view/puzzletree/nodes/addition.py19
-rwxr-xr-xplotter/view/puzzletree/nodes/binaryoperator.py30
-rwxr-xr-xplotter/view/puzzletree/nodes/composition.py29
-rwxr-xr-xplotter/view/puzzletree/nodes/constant.py67
-rwxr-xr-xplotter/view/puzzletree/nodes/e.py27
-rwxr-xr-xplotter/view/puzzletree/nodes/exponentiation.py20
-rwxr-xr-xplotter/view/puzzletree/nodes/identity.py23
-rwxr-xr-xplotter/view/puzzletree/nodes/multiplication.py20
-rwxr-xr-xplotter/view/puzzletree/nodes/node.py82
-rwxr-xr-xplotter/view/puzzletree/nodes/pi.py27
-rwxr-xr-xplotter/view/puzzletree/nodes/simplenode.py12
-rwxr-xr-xplotter/view/puzzletree/nodes/sine.py26
-rwxr-xr-xplotter/view/puzzletree/palette.py108
-rwxr-xr-xpo/POTFILES.in16
-rwxr-xr-xpo/Plot.pot209
-rwxr-xr-xpo/en-US.po222
-rwxr-xr-xsetup.py15
-rwxr-xr-xthirdparty/README.txt11
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/README3
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/branch-format1
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/branch/branch.conf1
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/branch/format1
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/branch/last-revision1
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/branch/tags0
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/checkout/conflicts1
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/checkout/dirstatebin0 -> 4873 bytes
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/checkout/format1
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/checkout/views0
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/repository/format1
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.cixbin0 -> 2958 bytes
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.iixbin0 -> 2402 bytes
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.rixbin0 -> 2438 bytes
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.sixbin0 -> 230 bytes
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/repository/indices/a0d10fb8e2e8f056a13a69885ff7ecda.tixbin0 -> 11272 bytes
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/repository/pack-names6
-rwxr-xr-xthirdparty/cairoplot-trunk/.bzr/repository/packs/a0d10fb8e2e8f056a13a69885ff7ecda.packbin0 -> 111521 bytes
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/COPYING504
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/ChangeLog134
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/MANIFEST7
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/NEWS19
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/TODO18
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/__init__.py2378
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/__init__.py10
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/fixedsize.py30
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/gtk.py39
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/handler.py11
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/pdf.py12
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/png.py19
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/ps.py12
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/svg.py12
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/handlers/vector.py17
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/cairoplot/series.py1140
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/gtktests.py32
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/seriestests.py251
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/setup.py19
-rwxr-xr-xthirdparty/cairoplot-trunk/trunk/tests.py252
116 files changed, 13247 insertions, 0 deletions
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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+ <signature of Ty Coon>, 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [
+ <!ENTITY stroke_color "#000000">
+ <!ENTITY fill_color "#666666">
+]>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.0"
+ width="55"
+ height="55"
+ id="svg4502">
+ <defs
+ id="defs4504" />
+ <g
+ id="layer4"
+ style="display:inline">
+ <path
+ d="m 12.677347,3.7212878 c -1.259155,0 -4.0490169,2.1250661 -4.0490169,4.5950191 L 8.2495229,38.708597 5.0670994,38.582328 c -2.469953,0 -2.7960021,2.379806 -2.7960021,3.63896 0,1.259155 1.967547,2.281251 4.4375,2.28125 l 2.1722709,0.505076 0,3 c 0,2.469953 2.5373238,3.963675 3.7964788,3.963674 1.259155,0 4.017767,-1.619991 4.017767,-4.089943 l 0,-3 29.70479,0.631346 c 2.469953,0 4.089943,-2.032247 4.089943,-3.291403 0,-1.259155 -1.619988,-3.63896 -4.089943,-3.63896 L 16.442576,38.456059 16.190038,7.3061543 c 0,-2.469953 -2.253536,-3.5848665 -3.512691,-3.5848665 z"
+ id="rect2477"
+ style="fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:3;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 6.2507589,39.926185 50.317195,14.020907 c 3.063354,0.756571 2.896616,1.42509 2.770984,3.462041 l 0.105372,0.179947 c 0.379443,0.64799 -0.358101,1.780418 -1.653689,2.539076 L 9.241193,44.970828 C 7.1616812,46.14176 4.5192257,47.135274 3.4606482,44.611835 2.4020707,42.088396 4.9551715,40.684843 6.2507589,39.926185 z"
+ id="rect3280"
+ style="fill:&fill_color;;fill-opacity:1;stroke:&stroke_color;;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ <path
+ d="M 3.5558389,29.241111 C 6.3337589,29.619918 13.427171,13.710368 19.844549,9.4168665 26.261928,5.123365 22.085947,51.843272 33.860415,50.075506 45.634884,48.307741 36.638335,17.11928 41.689097,15.85659 46.73986,14.593899 47.857631,26.442769 52.043161,23.685272 56.228691,20.927775 45.915051,7.7710638 39.751336,8.6984498 33.587621,9.6258358 40.420281,39.390436 33.355339,40.352789 26.290397,41.315142 31.459412,-1.3472408 19.844548,2.5983371 8.2296845,6.543915 0.8005509,28.86539 3.5558389,29.241111 z"
+ id="path2504"
+ style="fill:&fill_color;;fill-opacity:1;fill-rule:evenodd;stroke:&stroke_color;;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" />
+ </g>
+</svg>
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 <alf.rodrigo@gmail.com>
+#
+# 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 <magnun.leno@gmail.com>
+#
+# 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 <alf.rodrigo@gmail.com>
+
+#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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="55"
+ height="55"
+ id="svg4502"
+ sodipodi:version="0.32"
+ inkscape:version="0.46"
+ version="1.0"
+ sodipodi:docbase="/Users/eben/Desktop"
+ sodipodi:docname="plot-gtk-inkscape.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/home/s/swast/Desktop/plot-gtk-inkscape.png"
+ inkscape:export-xdpi="52.363636"
+ inkscape:export-ydpi="52.363636">
+ <defs
+ id="defs4504">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective2475" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ gridtolerance="10000"
+ guidetolerance="10"
+ objecttolerance="10"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="-14.087351"
+ inkscape:cy="22.993148"
+ inkscape:document-units="px"
+ inkscape:current-layer="svg4502"
+ width="55px"
+ height="55px"
+ inkscape:window-width="1680"
+ inkscape:window-height="975"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ showgrid="false">
+ <inkscape:grid
+ id="GridFromPre046Settings"
+ type="xygrid"
+ originx="0px"
+ originy="0px"
+ spacingx="15px"
+ spacingy="15px"
+ color="#0000ff"
+ empcolor="#0000ff"
+ opacity="0.2"
+ empopacity="0.4"
+ empspacing="5" />
+ </sodipodi:namedview>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="icon"
+ style="display:inline">
+ <path
+ style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="M 12.677347,3.7212878 C 11.418192,3.7212878 10.396097,5.7200848 10.396097,8.1900378 L 10.396097,39.971288 L 6.7085973,39.971288 C 4.2386443,39.971288 2.2710973,40.962134 2.2710973,42.221288 C 2.2710973,43.480443 4.2386443,44.502539 6.7085973,44.502538 L 10.396097,44.502538 L 10.396097,47.502538 C 10.396097,49.972491 11.418192,51.971289 12.677347,51.971288 C 13.936502,51.971288 14.927347,49.97249 14.927347,47.502538 L 14.927347,44.502538 L 46.021097,44.502538 C 48.49105,44.502538 50.489847,43.480444 50.489847,42.221288 C 50.489847,40.962133 48.491052,39.971288 46.021097,39.971288 L 14.927347,39.971288 L 14.927347,8.1900378 C 14.927347,5.7200848 13.936502,3.7212878 12.677347,3.7212878 z"
+ id="rect2477" />
+ <path
+ style="fill:#0000ff;fill-opacity:1;stroke:#000000;stroke-width:1.35851729000000021;stroke-miterlimit:4;stroke-opacity:1"
+ d="M 7.7659877,42.451566 L 50.064657,17.68271 C 51.360244,16.924052 52.708735,16.834957 53.088179,17.482948 L 53.193551,17.662895 C 53.572994,18.310885 52.83545,19.443313 51.539862,20.201971 L 9.241193,44.970828 C 7.9456056,45.729486 6.5971145,45.81858 6.2176706,45.170589 L 6.1122986,44.990642 C 5.7328545,44.342652 6.4704003,43.210224 7.7659877,42.451566 z"
+ id="rect3280" />
+ <path
+ style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 3.5558389,29.241111 C 6.3337589,29.619918 13.426162,8.4228319 19.213204,8.7855212 C 25.251775,9.1639752 26.631633,49.191622 33.607877,49.191623 C 40.584121,49.191624 36.638335,17.11928 41.689097,15.85659 C 46.73986,14.593899 49.332714,25.681446 52.043161,23.685272 C 54.587193,21.811658 46.630561,11.944278 40.50895,12.865329 C 34.345235,13.792715 38.56875,41.915817 33.860415,42.87817 C 28.909421,43.890121 27.923878,3.7035219 19.465741,4.4923731 C 11.257997,5.2578713 0.8005509,28.86539 3.5558389,29.241111 z"
+ id="path2504"
+ sodipodi:nodetypes="czzszzzzz" />
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="55"
+ height="55"
+ id="svg4502"
+ sodipodi:version="0.32"
+ inkscape:version="0.47 r22583"
+ version="1.0"
+ sodipodi:docname="plot-gtk-inkscape.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/home/tswast/Desktop/plot.png"
+ inkscape:export-xdpi="52.363636"
+ inkscape:export-ydpi="52.363636">
+ <metadata
+ id="metadata11">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs4504">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective2475" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ gridtolerance="10000"
+ guidetolerance="10"
+ objecttolerance="10"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="7.919596"
+ inkscape:cx="7.8624971"
+ inkscape:cy="30.168517"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer4"
+ width="55px"
+ height="55px"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ showgrid="false"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ id="GridFromPre046Settings"
+ type="xygrid"
+ originx="0px"
+ originy="0px"
+ spacingx="15px"
+ spacingy="15px"
+ color="#0000ff"
+ empcolor="#0000ff"
+ opacity="0.2"
+ empopacity="0.4"
+ empspacing="5" />
+ </sodipodi:namedview>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="icon"
+ style="display:inline">
+ <path
+ style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 12.677347,3.7212878 c -1.259155,0 -4.0490169,2.1250661 -4.0490169,4.5950191 L 8.2495229,38.708597 5.0670994,38.582328 c -2.469953,0 -2.7960021,2.379806 -2.7960021,3.63896 0,1.259155 1.967547,2.281251 4.4375,2.28125 l 2.1722709,0.505076 0,3 c 0,2.469953 2.5373238,3.963675 3.7964788,3.963674 1.259155,0 4.017767,-1.619991 4.017767,-4.089943 l 0,-3 29.70479,0.631346 c 2.469953,0 4.089943,-2.032247 4.089943,-3.291403 0,-1.259155 -1.619988,-3.63896 -4.089943,-3.63896 L 16.442576,38.456059 16.190038,7.3061543 c 0,-2.469953 -2.253536,-3.5848665 -3.512691,-3.5848665 z"
+ id="rect2477"
+ sodipodi:nodetypes="ccccscccscccscccc" />
+ <path
+ style="fill:#0000ff;fill-opacity:1;stroke:#000000;stroke-width:1.35851729000000021;stroke-miterlimit:4;stroke-opacity:1"
+ d="M 6.2507589,39.926185 50.317195,14.020907 c 3.063354,0.756571 2.896616,1.42509 2.770984,3.462041 l 0.105372,0.179947 c 0.379443,0.64799 -0.358101,1.780418 -1.653689,2.539076 L 9.241193,44.970828 C 7.1616812,46.14176 4.5192257,47.135274 3.4606482,44.611835 2.4020707,42.088396 4.9551715,40.684843 6.2507589,39.926185 z"
+ id="rect3280"
+ sodipodi:nodetypes="cccccczc" />
+ <path
+ style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 3.5558389,29.241111 C 6.3337589,29.619918 13.427171,13.710368 19.844549,9.4168665 26.261928,5.123365 22.085947,51.843272 33.860415,50.075506 45.634884,48.307741 36.638335,17.11928 41.689097,15.85659 46.73986,14.593899 47.857631,26.442769 52.043161,23.685272 56.228691,20.927775 45.915051,7.7710638 39.751336,8.6984498 33.587621,9.6258358 40.420281,39.390436 33.355339,40.352789 26.290397,41.315142 31.459412,-1.3472408 19.844548,2.5983371 8.2296845,6.543915 0.8005509,28.86539 3.5558389,29.241111 z"
+ id="path2504"
+ sodipodi:nodetypes="czzszzzzz" />
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="55"
+ height="55"
+ id="svg4502"
+ sodipodi:version="0.32"
+ inkscape:version="0.47 r22583"
+ version="1.0"
+ sodipodi:docname="plot-tiny-inkscape.svg"
+ inkscape:output_extension="org.inkscape.output.svg.inkscape"
+ inkscape:export-filename="/home/tswast/Desktop/plot-14.png"
+ inkscape:export-xdpi="22.91"
+ inkscape:export-ydpi="22.91">
+ <metadata
+ id="metadata11">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs4504">
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective2475" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ gridtolerance="10000"
+ guidetolerance="10"
+ objecttolerance="10"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="7.919596"
+ inkscape:cx="7.8624971"
+ inkscape:cy="29.663441"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer4"
+ width="55px"
+ height="55px"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ showgrid="false"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ id="GridFromPre046Settings"
+ type="xygrid"
+ originx="0px"
+ originy="0px"
+ spacingx="15px"
+ spacingy="15px"
+ color="#0000ff"
+ empcolor="#0000ff"
+ opacity="0.2"
+ empopacity="0.4"
+ empspacing="5" />
+ </sodipodi:namedview>
+ <g
+ inkscape:groupmode="layer"
+ id="layer4"
+ inkscape:label="icon"
+ style="display:inline">
+ <path
+ style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ d="m 12.677347,3.7212878 c -1.259155,0 -4.0490169,2.1250661 -4.0490169,4.5950191 L 8.2495229,38.708597 5.0670994,38.582328 c -2.469953,0 -2.7960021,2.379806 -2.7960021,3.63896 0,1.259155 1.967547,2.281251 4.4375,2.28125 l 2.1722709,0.505076 0,3 c 0,2.469953 2.5373238,3.963675 3.7964788,3.963674 1.259155,0 4.017767,-1.619991 4.017767,-4.089943 l 0,-3 29.70479,0.631346 c 2.469953,0 4.089943,-2.032247 4.089943,-3.291403 0,-1.259155 -1.619988,-3.63896 -4.089943,-3.63896 L 16.442576,38.456059 16.190038,7.3061543 c 0,-2.469953 -2.253536,-3.5848665 -3.512691,-3.5848665 z"
+ id="rect2477"
+ sodipodi:nodetypes="ccccscccscccscccc" />
+ <path
+ style="fill:#0000ff;fill-opacity:1;stroke:#000000;stroke-width:2;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ d="M 6.2507589,39.926185 50.317195,14.020907 c 3.063354,0.756571 2.896616,1.42509 2.770984,3.462041 l 0.105372,0.179947 c 0.379443,0.64799 -0.358101,1.780418 -1.653689,2.539076 L 9.241193,44.970828 C 7.1616812,46.14176 4.5192257,47.135274 3.4606482,44.611835 2.4020707,42.088396 4.9551715,40.684843 6.2507589,39.926185 z"
+ id="rect3280"
+ sodipodi:nodetypes="cccccczc" />
+ <path
+ style="fill:#ff0000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
+ d="M 3.5558389,29.241111 C 6.3337589,29.619918 13.427171,13.710368 19.844549,9.4168665 26.261928,5.123365 22.085947,51.843272 33.860415,50.075506 45.634884,48.307741 36.638335,17.11928 41.689097,15.85659 46.73986,14.593899 47.857631,26.442769 52.043161,23.685272 56.228691,20.927775 45.915051,7.7710638 39.751336,8.6984498 33.587621,9.6258358 40.420281,39.390436 33.355339,40.352789 26.290397,41.315142 31.459412,-1.3472408 19.844548,2.5983371 8.2296845,6.543915 0.8005509,28.86539 3.5558389,29.241111 z"
+ id="path2504"
+ sodipodi:nodetypes="czzszzzzz" />
+ </g>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="absolutevalue-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /><inkscape:perspective
+ id="perspective3603"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2899"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="4.2909091"
+ inkscape:cx="-44.04661"
+ inkscape:cy="27.730822"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3 3 L 53 3 L 53 53 L 3 53 z"
+ stroke-width="3.5"
+ fill="white"
+ stroke="white"
+ id="path8" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-family="Bitstream Vera Sans"
+ font-weight="bold"
+ fill="black"
+ stroke="none"
+ id="text10">cosh</text>
+
+
+
+
+ </mask>
+ </defs>
+
+<path
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 3.6249998,14.0625 2.96875,12.875 L 10.1875,23.625 23.96875,37.40625 27.5,40.9375 31.03125,37.40625 44.8125,23.625 l 3.5625,3.3125 3,-12.875 -13.9375,2.59375 3.75,3.53125 L 27.5,33.875 13.8125,20.1875 17.53125,16.65625 3.6249998,14.0625 z"
+ id="rect2817"
+ sodipodi:nodetypes="ccccccccccccccc" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+
+ </mask>
+ </defs>
+
+<path
+ d="m 3.6249998,14.0625 2.96875,12.875 L 10.1875,23.625 23.96875,37.40625 27.5,40.9375 31.03125,37.40625 44.8125,23.625 l 3.5625,3.3125 3,-12.875 -13.9375,2.59375 3.75,3.53125 L 27.5,33.875 13.8125,20.1875 17.53125,16.65625 3.6249998,14.0625 z"
+ id="rect2817"
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="trigonometry-cosh.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="4.2909091"
+ inkscape:cx="34.943782"
+ inkscape:cy="28.76213"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3 3 L 53 3 L 53 53 L 3 53 z"
+ stroke-width="3.5"
+ fill="white"
+ stroke="white"
+ id="path8" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-family="Bitstream Vera Sans"
+ font-weight="bold"
+ fill="black"
+ stroke="none"
+ id="text10">cosh</text>
+
+ </mask>
+ </defs>
+
+<path
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none"
+ d="M 24.5 7.5 C 23.392 7.5 22.5 8.392 22.5 9.5 L 22.5 22.5 L 9.5 22.5 C 8.392 22.5 7.5 23.392 7.5 24.5 L 7.5 30.5 C 7.5 31.608 8.392 32.5 9.5 32.5 L 22.5 32.5 L 22.5 45.5 C 22.5 46.608 23.392 47.5 24.5 47.5 L 30.5 47.5 C 31.608 47.5 32.5 46.608 32.5 45.5 L 32.5 32.5 L 45.5 32.5 C 46.608 32.5 47.5 31.608 47.5 30.5 L 47.5 24.5 C 47.5 23.392 46.608 22.5 45.5 22.5 L 32.5 22.5 L 32.5 9.5 C 32.5 8.392 31.608 7.5 30.5 7.5 L 24.5 7.5 z "
+ id="rect2826" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+ </mask>
+ </defs>
+
+<path
+ d="m 24.5,7.5 c -1.108,0 -2,0.892 -2,2 l 0,13 -13,0 c -1.108,0 -2,0.892 -2,2 l 0,6 c 0,1.108 0.892,2 2,2 l 13,0 0,13 c 0,1.108 0.892,2 2,2 l 6,0 c 1.108,0 2,-0.892 2,-2 l 0,-13 13,0 c 1.108,0 2,-0.892 2,-2 l 0,-6 c 0,-1.108 -0.892,-2 -2,-2 l -13,0 0,-13 c 0,-1.108 -0.892,-2 -2,-2 l -6,0 z"
+ id="rect2826"
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="50"
+ version="1.1"
+ viewBox="0 0 50 50"
+ width="50"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="blank-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /><inkscape:perspective
+ id="perspective3603"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="4.2909091"
+ inkscape:cx="-44.04661"
+ inkscape:cy="27.730822"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_"
+ units="pt" /><g
+ display="block"
+ id="stock-xo_1_"
+ style="display:block"
+ transform="translate(0,-5)">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-weight="bold"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+
+ </mask>
+ </defs>
+
+<rect
+ style="fill:#ff0000;fill-opacity:1;stroke:none"
+ id="rect2818"
+ width="41.25"
+ height="6.9915257"
+ x="4.375"
+ y="48.008476"
+ ry="3.4957628" /><path
+ sodipodi:type="star"
+ style="fill:#ff0000;fill-opacity:1;stroke:none"
+ id="path3592"
+ sodipodi:sides="5"
+ sodipodi:cx="14.216102"
+ sodipodi:cy="13.177966"
+ sodipodi:r1="14.002458"
+ sodipodi:r2="7.1423526"
+ sodipodi:arg1="-0.32175056"
+ sodipodi:arg2="0.30656797"
+ inkscape:flatsided="false"
+ inkscape:rounded="0.25"
+ inkscape:randomized="0"
+ d="m 27.499999,8.7500002 c 0.72999,2.1899698 -5.7779,4.3826448 -6.474557,6.5834448 -0.696658,2.2008 3.364061,7.738942 1.506856,9.109942 -1.857206,1.370999 -5.953613,-4.140798 -8.261977,-4.123274 -2.308365,0.01752 -6.320619,5.590877 -8.1984252,4.248232 C 4.1940896,23.2257 8.170261,17.626546 7.4402711,15.436576 6.7102811,13.246607 0.16985193,11.152987 0.8665096,8.9521867 1.5631673,6.7513866 8.1169831,8.8027168 9.9741889,7.4317172 11.831395,6.0607177 11.801442,-0.80656423 14.109806,-0.82408804 c 2.308364,-0.0175238 2.382674,6.84942134 4.26048,8.19206684 1.877806,1.3426454 8.399723,-0.8079484 9.129713,1.3820214 z"
+ transform="translate(10.814164,18.023524)" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="50"
+ height="50"
+ viewBox="0 0 50 50"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ transform="translate(0,-5)"
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+
+ </mask>
+ </defs>
+
+<rect
+ width="41.25"
+ height="6.9915257"
+ ry="3.4957628"
+ x="4.375"
+ y="48.008476"
+ id="rect2818"
+ style="fill:#ff0000;fill-opacity:1;stroke:none" /><path
+ d="m 27.499999,8.7500002 c 0.72999,2.1899698 -5.7779,4.3826448 -6.474557,6.5834448 -0.696658,2.2008 3.364061,7.738942 1.506856,9.109942 -1.857206,1.370999 -5.953613,-4.140798 -8.261977,-4.123274 -2.308365,0.01752 -6.320619,5.590877 -8.1984252,4.248232 C 4.1940896,23.2257 8.170261,17.626546 7.4402711,15.436576 6.7102811,13.246607 0.16985193,11.152987 0.8665096,8.9521867 1.5631673,6.7513866 8.1169831,8.8027168 9.9741889,7.4317172 11.831395,6.0607177 11.801442,-0.80656423 14.109806,-0.82408804 c 2.308364,-0.0175238 2.382674,6.84942134 4.26048,8.19206684 1.877806,1.3426454 8.399723,-0.8079484 9.129713,1.3820214 z"
+ transform="translate(10.814164,18.023524)"
+ id="path3592"
+ style="fill:#ff0000;fill-opacity:1;stroke:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="50"
+ version="1.1"
+ viewBox="0 0 50 50"
+ width="50"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="composition-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="8.5818182"
+ inkscape:cx="4.530736"
+ inkscape:cy="24.321347"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_"
+ style="display:block"
+ transform="translate(0,-5)">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-weight="bold"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+
+ </mask>
+ </defs>
+
+<path
+ sodipodi:type="arc"
+ style="fill:none;stroke:#000000;stroke-width:8.07729017;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+ id="path2817"
+ sodipodi:cx="23.246822"
+ sodipodi:cy="24.65572"
+ sodipodi:rx="12.409958"
+ sodipodi:ry="12.409958"
+ d="m 35.65678,24.65572 a 12.409958,12.409958 0 1 1 -24.819916,0 12.409958,12.409958 0 1 1 24.819916,0 z"
+ transform="matrix(0.80472533,0,0,0.80472533,6.2926932,10.158918)" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="50"
+ height="50"
+ viewBox="0 0 50 50"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ transform="translate(0,-5)"
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+
+ </mask>
+ </defs>
+
+<path
+ d="m 35.65678,24.65572 a 12.409958,12.409958 0 1 1 -24.819916,0 12.409958,12.409958 0 1 1 24.819916,0 z"
+ transform="matrix(0.80472533,0,0,0.80472533,6.2926932,10.158918)"
+ style="fill:none;stroke:#000000;stroke-width:8.07729053;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="constant-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /><inkscape:perspective
+ id="perspective3603"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="4.2909091"
+ inkscape:cx="-26.721904"
+ inkscape:cy="9.5528556"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3 3 L 53 3 L 53 53 L 3 53 z"
+ stroke-width="3.5"
+ fill="white"
+ stroke="white"
+ id="path8" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-family="Bitstream Vera Sans"
+ font-weight="bold"
+ fill="black"
+ stroke="none"
+ id="text10">cosh</text>
+
+
+
+ </mask>
+ </defs>
+
+<path
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 50.581482,27.973158 -11.674531,-8.011177 0.146337,5.124626 -23.268402,0.03399 -0.145456,-5.08044 -11.2209121,6.986685 11.6745311,8.011176 -0.164466,-4.925351 23.2684,-0.03399 0.163587,4.881166 11.220912,-6.986685 z"
+ id="rect2817"
+ sodipodi:nodetypes="ccccccccccc" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+ </mask>
+ </defs>
+
+<path
+ d="m 50.581482,27.973158 -11.674531,-8.011177 0.146337,5.124626 -23.268402,0.03399 -0.145456,-5.08044 -11.2209121,6.986685 11.6745311,8.011176 -0.164466,-4.925351 23.2684,-0.03399 0.163587,4.881166 11.220912,-6.986685 z"
+ id="rect2817"
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="e-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /><inkscape:perspective
+ id="perspective3603"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2899"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2844"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2865"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="6.0682618"
+ inkscape:cx="-15.761659"
+ inkscape:cy="38.776825"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_">
+
+<g
+ style="font-size:40px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ id="text2823"
+ transform="translate(0.859621,-1.091599)"><path
+ d="m 38.37866,28.513474 0,1.992187 -16.347656,0 c 0.169262,1.640632 0.761709,2.8711 1.777344,3.691406 1.015613,0.820317 2.434882,1.230473 4.257812,1.230469 1.471338,4e-6 2.975243,-0.214839 4.511719,-0.644531 1.549458,-0.442703 3.137998,-1.106765 4.765625,-1.992188 l 0,5.390625 c -1.653668,0.625001 -3.307313,1.09375 -4.960938,1.40625 -1.653663,0.325521 -3.307307,0.488281 -4.960937,0.488282 -3.958344,-1e-6 -7.037768,-1.002604 -9.238281,-3.007813 -2.187503,-2.018225 -3.281252,-4.843743 -3.28125,-8.476562 -2e-6,-3.567694 1.074216,-6.373681 3.222656,-8.417969 2.161451,-2.04425 5.130198,-3.066384 8.90625,-3.066406 3.437483,2.2e-5 6.184876,1.035177 8.242187,3.105468 2.070289,2.07033 3.105444,4.837254 3.105469,8.300782 m -7.1875,-2.324219 c -1.8e-5,-1.328111 -0.390642,-2.395818 -1.171875,-3.203125 -0.768245,-0.820295 -1.777359,-1.230451 -3.027344,-1.230469 -1.354179,1.8e-5 -2.454438,0.384132 -3.300781,1.152344 -0.846364,0.755224 -1.373707,1.848973 -1.582031,3.28125 l 9.082031,0"
+ style="font-weight:bold;font-family:Sans;-inkscape-font-specification:Sans Bold"
+ id="path2828" /></g></g></svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ id="stock-xo_1_"
+ style="display:block">
+
+<g
+ transform="translate(0.859621,-1.091599)"
+ id="text2823"
+ style="font-size:40px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"><path
+ d="m 38.37866,28.513474 0,1.992187 -16.347656,0 c 0.169262,1.640632 0.761709,2.8711 1.777344,3.691406 1.015613,0.820317 2.434882,1.230473 4.257812,1.230469 1.471338,4e-6 2.975243,-0.214839 4.511719,-0.644531 1.549458,-0.442703 3.137998,-1.106765 4.765625,-1.992188 l 0,5.390625 c -1.653668,0.625001 -3.307313,1.09375 -4.960938,1.40625 -1.653663,0.325521 -3.307307,0.488281 -4.960937,0.488282 -3.958344,-1e-6 -7.037768,-1.002604 -9.238281,-3.007813 -2.187503,-2.018225 -3.281252,-4.843743 -3.28125,-8.476562 -2e-6,-3.567694 1.074216,-6.373681 3.222656,-8.417969 2.161451,-2.04425 5.130198,-3.066384 8.90625,-3.066406 3.437483,2.2e-5 6.184876,1.035177 8.242187,3.105468 2.070289,2.07033 3.105444,4.837254 3.105469,8.300782 m -7.1875,-2.324219 c -1.8e-5,-1.328111 -0.390642,-2.395818 -1.171875,-3.203125 -0.768245,-0.820295 -1.777359,-1.230451 -3.027344,-1.230469 -1.354179,1.8e-5 -2.454438,0.384132 -3.300781,1.152344 -0.846364,0.755224 -1.373707,1.848973 -1.582031,3.28125 l 9.082031,0"
+ id="path2828"
+ style="font-weight:bold;font-family:Sans;-inkscape-font-specification:Sans Bold" /></g></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="50"
+ version="1.1"
+ viewBox="0 0 50 50"
+ width="50"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="exponentiation-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="4.2909091"
+ inkscape:cx="-36.136726"
+ inkscape:cy="27.829926"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_"
+ style="display:block"
+ transform="translate(0,-5)">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-weight="bold"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+ </mask>
+ </defs>
+
+<path
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 18.369002,25.414333 -8.6202995,8.620298 c -0.734714,0.734714 -0.734714,1.917685 0,2.652399 l 3.9786005,3.9786 c 0.734715,0.734714 1.917685,0.734714 2.652399,0 L 25,32.045332 33.620298,40.66563 c 0.734714,0.734714 1.917685,0.734714 2.652399,0 l 3.9786,-3.9786 c 0.734714,-0.734714 0.734714,-1.917685 0,-2.652399 L 31.631,25.414333 25.000001,18.783334 18.369002,25.414333 z"
+ id="rect2826"
+ sodipodi:nodetypes="ccccccccccccc" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="50"
+ height="50"
+ viewBox="0 0 50 50"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ transform="translate(0,-5)"
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+ </mask>
+ </defs>
+
+<path
+ d="m 18.369002,25.414333 -8.6202995,8.620298 c -0.734714,0.734714 -0.734714,1.917685 0,2.652399 l 3.9786005,3.9786 c 0.734715,0.734714 1.917685,0.734714 2.652399,0 L 25,32.045332 33.620298,40.66563 c 0.734714,0.734714 1.917685,0.734714 2.652399,0 l 3.9786,-3.9786 c 0.734714,-0.734714 0.734714,-1.917685 0,-2.652399 L 31.631,25.414333 25.000001,18.783334 18.369002,25.414333 z"
+ id="rect2826"
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="identity-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /><inkscape:perspective
+ id="perspective3603"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="4.2909091"
+ inkscape:cx="-5.75321"
+ inkscape:cy="37.386872"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3 3 L 53 3 L 53 53 L 3 53 z"
+ stroke-width="3.5"
+ fill="white"
+ stroke="white"
+ id="path8" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-family="Bitstream Vera Sans"
+ font-weight="bold"
+ fill="black"
+ stroke="none"
+ id="text10">cosh</text>
+
+
+ </mask>
+ </defs>
+
+<path
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none"
+ d="M 51.03125,5.6875 37.0625,8 40.71875,11.59375 10.84375,40.0625 7.21875,36.5 3.96875,49.3125 17.9375,47 l -3.53125,-3.4375 29.875,-28.46875 3.5,3.40625 3.25,-12.8125 z"
+ id="rect2817" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+ </mask>
+ </defs>
+
+<path
+ d="M 51.03125,5.6875 37.0625,8 40.71875,11.59375 10.84375,40.0625 7.21875,36.5 3.96875,49.3125 17.9375,47 l -3.53125,-3.4375 29.875,-28.46875 3.5,3.40625 3.25,-12.8125 z"
+ id="rect2817"
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="50"
+ version="1.1"
+ viewBox="0 0 25 50"
+ width="25"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="leftparen-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="8.5818182"
+ inkscape:cx="16.208624"
+ inkscape:cy="28.272974"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_"
+ style="display:block"
+ transform="translate(0,-5)">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-weight="bold"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+ </mask>
+ </defs>
+
+<text
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="4.5800781"
+ y="42.539062"
+ id="text2820"><tspan
+ sodipodi:role="line"
+ id="tspan2822"
+ x="4.5800781"
+ y="42.539062">(</tspan></text>
+</g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="25"
+ height="50"
+ viewBox="0 0 25 50"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ transform="translate(0,-5)"
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+ </mask>
+ </defs>
+
+<text
+ x="4.5800781"
+ y="42.539062"
+ id="text2820"
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"><tspan
+ x="4.5800781"
+ y="42.539062"
+ id="tspan2822">(</tspan></text>
+</g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="multiplication-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="4.2909091"
+ inkscape:cx="-6.6557942"
+ inkscape:cy="28.296028"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3 3 L 53 3 L 53 53 L 3 53 z"
+ stroke-width="3.5"
+ fill="white"
+ stroke="white"
+ id="path8" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-family="Bitstream Vera Sans"
+ font-weight="bold"
+ fill="black"
+ stroke="none"
+ id="text10">cosh</text>
+
+
+ </mask>
+ </defs>
+
+<path
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none"
+ d="m 11.236544,15.479185 c -0.783474,0.783474 -0.783474,2.044953 0,2.828427 L 20.428932,27.5 11.236544,36.692388 c -0.783474,0.783474 -0.783474,2.044953 0,2.828427 l 4.242641,4.242641 c 0.783474,0.783474 2.044953,0.783474 2.828427,0 L 27.5,34.571068 l 9.192388,9.192388 c 0.783474,0.783474 2.044953,0.783474 2.828427,0 l 4.242641,-4.242641 c 0.783474,-0.783474 0.783474,-2.044953 0,-2.828427 L 34.571068,27.5 43.763456,18.307612 c 0.783474,-0.783474 0.783474,-2.044953 0,-2.828427 l -4.242641,-4.242641 c -0.783474,-0.783474 -2.044953,-0.783474 -2.828427,0 L 27.5,20.428932 18.307612,11.236544 c -0.783474,-0.783474 -2.044953,-0.783474 -2.828427,0 l -4.242641,4.242641 z"
+ id="rect2826" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+ </mask>
+ </defs>
+
+<path
+ d="m 11.236544,15.479185 c -0.783474,0.783474 -0.783474,2.044953 0,2.828427 L 20.428932,27.5 11.236544,36.692388 c -0.783474,0.783474 -0.783474,2.044953 0,2.828427 l 4.242641,4.242641 c 0.783474,0.783474 2.044953,0.783474 2.828427,0 L 27.5,34.571068 l 9.192388,9.192388 c 0.783474,0.783474 2.044953,0.783474 2.828427,0 l 4.242641,-4.242641 c 0.783474,-0.783474 0.783474,-2.044953 0,-2.828427 L 34.571068,27.5 43.763456,18.307612 c 0.783474,-0.783474 0.783474,-2.044953 0,-2.828427 l -4.242641,-4.242641 c -0.783474,-0.783474 -2.044953,-0.783474 -2.828427,0 L 27.5,20.428932 18.307612,11.236544 c -0.783474,-0.783474 -2.044953,-0.783474 -2.828427,0 l -4.242641,4.242641 z"
+ id="rect2826"
+ style="stroke-width:1.25;stroke-miterlimit:4;stroke-dasharray:none" /></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="pi-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /><inkscape:perspective
+ id="perspective3603"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2899"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2844"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2865"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="6.0682618"
+ inkscape:cx="25.930675"
+ inkscape:cy="31.855567"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_">
+
+
+<g
+ style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ id="text2821"
+ transform="translate(-0.00832,0.3597885)"><path
+ d="m 13.709492,15.821857 27.597656,0 0,5 -3.828125,0 0,9.804688 c -2.5e-5,1.679693 0.156224,2.727869 0.46875,3.144531 0.325495,0.40365 0.852838,0.605472 1.582031,0.605469 0.742159,3e-6 1.321586,-0.02604 1.738281,-0.07813 l 0,3.671875 c -0.80732,0.32552 -2.591172,0.48828 -5.351562,0.488281 -0.872419,-10e-7 -1.731793,-0.123698 -2.578125,-0.371094 -1.354187,-0.403645 -2.220071,-1.197915 -2.597656,-2.382812 -0.16929,-0.559893 -0.253925,-1.412757 -0.253907,-2.558594 l 0,-12.324219 -5.957031,0 0,16.875 -6.992187,0 0,-16.875 -3.828125,0 0,-5"
+ style="font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;font-family:Bitstream Charter;-inkscape-font-specification:Bitstream Charter Bold"
+ id="path2826" /></g></g></svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ id="stock-xo_1_"
+ style="display:block">
+
+
+<g
+ transform="translate(-0.00832,0.3597885)"
+ id="text2821"
+ style="font-size:40px;font-style:normal;font-weight:normal;line-height:125%;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"><path
+ d="m 13.709492,15.821857 27.597656,0 0,5 -3.828125,0 0,9.804688 c -2.5e-5,1.679693 0.156224,2.727869 0.46875,3.144531 0.325495,0.40365 0.852838,0.605472 1.582031,0.605469 0.742159,3e-6 1.321586,-0.02604 1.738281,-0.07813 l 0,3.671875 c -0.80732,0.32552 -2.591172,0.48828 -5.351562,0.488281 -0.872419,-10e-7 -1.731793,-0.123698 -2.578125,-0.371094 -1.354187,-0.403645 -2.220071,-1.197915 -2.597656,-2.382812 -0.16929,-0.559893 -0.253925,-1.412757 -0.253907,-2.558594 l 0,-12.324219 -5.957031,0 0,16.875 -6.992187,0 0,-16.875 -3.828125,0 0,-5"
+ id="path2826"
+ style="font-variant:normal;font-weight:bold;font-stretch:normal;text-align:start;line-height:125%;writing-mode:lr-tb;text-anchor:start;font-family:Bitstream Charter;-inkscape-font-specification:Bitstream Charter Bold" /></g></g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="50"
+ version="1.1"
+ viewBox="0 0 25 50"
+ width="25"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="rightparen-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="8.5818182"
+ inkscape:cx="16.208624"
+ inkscape:cy="28.039923"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_"
+ style="display:block"
+ transform="translate(0,-5)">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask"
+ maskUnits="userSpaceOnUse"
+ x="0"
+ y="0"
+ width="55"
+ height="55">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ font-size="19"
+ font-weight="bold"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+ </mask>
+ </defs>
+
+<text
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"
+ x="-20.419922"
+ y="42.539062"
+ id="text2820"
+ transform="scale(-1,1)"><tspan
+ sodipodi:role="line"
+ id="tspan2822"
+ x="-20.419922"
+ y="42.539062">(</tspan></text>
+
+</g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="25"
+ height="50"
+ viewBox="0 0 25 50"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ transform="translate(0,-5)"
+ id="stock-xo_1_"
+ style="display:block">
+ <defs
+ id="defs5">
+ <mask
+ id="Mask">
+ <path
+ d="M 3,3 53,3 53,53 3,53 z"
+ id="path8"
+ style="fill:#ffffff;stroke:#ffffff;stroke-width:3.5" />
+ <text
+ x="3"
+ y="35"
+ id="text10"
+ style="font-size:19px;font-weight:bold;fill:#000000;stroke:none;font-family:Bitstream Vera Sans">cosh</text>
+
+
+
+ </mask>
+ </defs>
+
+<text
+ x="-20.419922"
+ y="42.539062"
+ transform="scale(-1,1)"
+ id="text2820"
+ xml:space="preserve"
+ style="font-size:40px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Bitstream Vera Sans"><tspan
+ x="-20.419922"
+ y="42.539062"
+ id="tspan2822">(</tspan></text>
+
+</g></svg> \ 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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ enable-background="new 0 0 55 55"
+ height="55px"
+ version="1.1"
+ viewBox="0 0 55 55"
+ width="55px"
+ x="0px"
+ xml:space="preserve"
+ y="0px"
+ id="svg2"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="sine-inkscape.svg"><metadata
+ id="metadata18"><rdf:RDF><cc:Work
+ rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
+ id="defs16"><inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 27.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="55 : 27.5 : 1"
+ inkscape:persp3d-origin="27.5 : 18.333333 : 1"
+ id="perspective20" /><inkscape:perspective
+ id="perspective3603"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2899"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2844"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /><inkscape:perspective
+ id="perspective2865"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" /></defs><sodipodi:namedview
+ pagecolor="#000000"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1280"
+ inkscape:window-height="725"
+ id="namedview14"
+ showgrid="false"
+ inkscape:zoom="6.0682618"
+ inkscape:cx="-10.761139"
+ inkscape:cy="23.329909"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="stock-xo_1_" /><g
+ display="block"
+ id="stock-xo_1_">
+<path
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke-width:1.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 38.865058,11.144043 -6.650424,9.587395 4.84375,-0.04555 c -0.283192,5.880654 1.723623,16.168145 -3.594211,16.354652 -5.317835,0.186509 0.651107,-25.409284 -11.122134,-25.564929 -11.773241,-0.155644 -8.472284,13.665727 -9.229873,22.1875 l -5.3268005,-0.0026 7.5630295,10.195445 7.670021,-10.255297 -5.15625,-0.15625 c 0,0 -0.137331,-17.150048 4.661749,-16.80078 4.799075,0.349267 1.051366,26.109187 10.932178,25.79434 9.880812,-0.314846 8.282896,-11.065623 8.633541,-21.595783 l 5.125,-0.139301 -8.349576,-9.558793 0,-4.8e-5 z"
+ id="rect2817"
+ sodipodi:nodetypes="ccczzcccccszcccc" /></g></svg>
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 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ version="1.1"
+ width="55"
+ height="55"
+ viewBox="0 0 55 55"
+ id="svg2"
+ xml:space="preserve"><defs
+ id="defs16" /><g
+ id="stock-xo_1_"
+ style="display:block">
+<path
+ d="m 38.865058,11.144043 -6.650424,9.587395 4.84375,-0.04555 c -0.283192,5.880654 1.723623,16.168145 -3.594211,16.354652 -5.317835,0.186509 0.651107,-25.409284 -11.122134,-25.564929 -11.773241,-0.155644 -8.472284,13.665727 -9.229873,22.1875 l -5.3268005,-0.0026 7.5630295,10.195445 7.670021,-10.255297 -5.15625,-0.15625 c 0,0 -0.137331,-17.150048 4.661749,-16.80078 4.799075,0.349267 1.051366,26.109187 10.932178,25.79434 9.880812,-0.314846 8.282896,-11.065623 8.633541,-21.595783 l 5.125,-0.139301 -8.349576,-9.558793 0,-4.8e-5 z"
+ id="rect2817"
+ style="color:#000000;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke-width:1.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /></g></svg> \ 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, "<string>", "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("<big>%s</big>" % 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 <EMAIL@ADDRESS>, 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 <EMAIL@ADDRESS>\n"
+"Language-Team: LANGUAGE <LL@li.org>\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 <tswast@gmail.com>, 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 <tswast@gmail.com>\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
+xœÁ1€ PgOÁà+ŠC‡¡„©¡¡¥Û÷Þû=qÜáY·pžKÖáÒ|,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.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.
+
+ <signature of Ty Coon>, 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 <alf.rodrigo@gmail.com>
+ * CairoPlot.py: Correctiong naming conventions (Series.py ->
+ series.py). Added series.py to setup.py.
+
+2009-07-05 Rodrigo Araújo <alf.rodrigo@gmail.com>
+ * CairoPlot.py: Merged Magnun's branch. Series, Group and Data
+ input formats are now available.
+
+2009-03-10 Rodrigo Araújo <alf.rodrigo@gmail.com>
+ * 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 <alf.rodrigo@gmail.com>
+ * CairoPlot.py: BarPlot, HorizontalBarPlot and VerticalBarPlot revision
+ and refactoring;
+ Color themes are now allowed for backgrounds.
+
+2009-03-08 Rodrigo Araújo <alf.rodrigo@gmail.com>
+ * 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 <alf.rodrigo@gmail.com>
+ * 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 <alf.rodrigo@gmail.com>
+ * 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 <alf.rodrigo@gmail.com>
+ * 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 <alf.rodrigo@gmail.com>
+ * CairoPlot.py: Added discrete series option to FunctionPlot
+ * tests.py: Added new tests for FunctionPlot new option
+
+2008-08-14 Rodrigo Araújo <alf.rodrigo@gmail.com>
+ * 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 <alf.rodrigo@gmail.com>
+ * CairoPlot.py: Bug fixes for BarPlot
+ * tests.py: Added new tests for the bar_plot function
+
+2008-07-11 Rodrigo Araújo <alf.rodrigo@gmail.com>
+ * CairoPlot.py: Added BarPlot working draft
+ * tests.py: Added tests regarding the use of bar_plot function
+
+2008-07-07 Rodrigo Araújo <alf.rodrigo@gmail.com>
+ * 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 <alf.rodrigo@gmail.com>
+ * 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 <gwidion@gmail.com>
+
+ * 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 <gwidion@gmail.com>
+
+ * ChangeLog: initial ChangeLog
+ * COPYING: License file added (GNU LGPL 2.1)
+ * TODO: initial TODO
+
+
+2008-06-13 Rodrigo Araújo <alf.rodrigo@gmail.com>
+
+ * 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 <alf.rodrigo@gmail.com>
+#
+# 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 <magnun.leno@gmail.com>
+#
+# 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 <alf.rodrigo@gmail.com>
+
+#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"])