PythonQwt-0.5.5/0000755000000000000000000000000012651077706012207 5ustar rootrootPythonQwt-0.5.5/setup.cfg0000666000000000000000000000010012646715536014027 0ustar rootroot[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 PythonQwt-0.5.5/README.md0000666000000000000000000000554112644476522013500 0ustar rootroot# PythonQwt: Qt plotting widgets for Python The `PythonQwt` project was initiated to solve -at least temporarily- the obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is no longer maintained. The idea was to translate the original Qwt C++ code to Python and then to optimize some parts of the code by writing new modules based on NumPy and other libraries. The `PythonQwt` package consists of a single Python package named `qwt` and of a few other files (examples, doc, ...). See [documentation](http://pythonhosted.org/PythonQwt/) for more details on the library and [changelog](CHANGELOG.md) for recent history of changes. ## Copyrights #### Main code base - Copyright © 2002 Uwe Rathmann, for the original Qwt C++ code - Copyright © 2015 Pierre Raybaut, for the Qwt C++ to Python translation and optimization - Copyright © 2015 Pierre Raybaut, for the PythonQwt specific and exclusive Python material #### PyQt, PySide and Python2/Python3 compatibility modules - Copyright © 2009-2013 Pierre Raybaut - Copyright © 2013-2015 The Spyder Development Team #### Some examples - Copyright © 2003-2009 Gerard Vermeulen, for the original PyQwt code - Copyright © 2015 Pierre Raybaut, for the PyQt5/PySide port and further developments (e.g. ported to PythonQwt API) ## License The `qwt` Python package was partly (>95%) translated from Qwt C++ library: the associated code is distributed under the terms of the LGPL license. The rest of the code was either wrote from scratch or strongly inspired from MIT licensed third-party software. See included [LICENSE](LICENSE) file for more details about licensing terms. ## Overview The `qwt` package is a pure Python implementation of `Qwt` C++ library with the following limitations. The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `guiqwt`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`. Only the following plot items are currently implemented in `qwt` (the only plot items needed by `guiqwt`): `QwtPlotItem` (base class), `QwtPlotItem`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`. See "Overview" section in [documentation](http://pythonhosted.org/PythonQwt/) for more details on API limitations when comparing to Qwt. ## Dependencies ### Requirements ### - Python >=2.6 or Python >=3.2 - PyQt4 >=4.4 or PyQt5 >= 5.5 - NumPy >= 1.5 - guidata >= 1.7 for the GUI-based test launcher ## Installation From the source package: ```bash python setup.py install ``` ## Examples/tests The GUI-based test launcher may be executed from Python: ```python from qwt import tests tests.run() ``` or from the command line: ```bash PythonQwt-tests ``` PythonQwt-0.5.5/LICENSE0000666000000000000000000010126112605040216013202 0ustar rootrootPythonQwt License Agreement --------------------------- [1] Software licensed under the terms of Qwt License The essential part of the code was translated to Python from Qwt C++ library and is thus licensed under the terms of the LGPL License from which the Qwt License 1.0 is derived from (see [***] for more details). [2] Software licensed under the terms of the MIT license Independent Python modules purely based on new code (no contamination from the LGPL license inherited from the Qwt Project) are distributed under the terms of the MIT License (see [*] and [**]). [3] Software licensed under the terms of PyQwt License Some files under the "tests" subfolder of the main Python package directory were derived from PyQwt PyQt4 examples and are thus distributed under the terms of the GPL License from which the PyQwt License 1.0 is derived from (see [****] for more details). [*] PythonQwt License Agreement for new and exclusive Python material (MIT) Copyright (c) 2015 Pierre Raybaut Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [**] Spyder License Agreement Spyder License Agreement (MIT License) -------------------------------------- Copyright (c) 2009-2013 Pierre Raybaut Copyright (c) 2013-2015 The Spyder Development Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. [***] PythonQwt License Agreement for code translated from C++ (Qwt License) Copyright (c) 2002 Uwe Rathmann, for the original C++ code Copyright (c) 2015 Pierre Raybaut, for the Python translation and optimization Qwt License Version 1.0, January 1, 2003 The Qwt library and included programs are provided under the terms of the GNU LESSER GENERAL PUBLIC LICENSE (LGPL) with the following exceptions: 1. Widgets that are subclassed from Qwt widgets do not constitute a derivative work. 2. Static linking of applications and widgets to the Qwt library does not constitute a derivative work and does not require the author to provide source code for the application or widget, use the shared Qwt libraries, or link their applications or widgets against a user-supplied version of Qwt. If you link the application or widget to a modified version of Qwt, then the changes to Qwt must be provided under the terms of the LGPL in sections 1, 2, and 4. 3. You do not have to provide a copy of the Qwt license with programs that are linked to the Qwt library, nor do you have to identify the Qwt license in your program or documentation as required by section 6 of the LGPL. However, programs must still identify their use of Qwt. The following example statement can be included in user documentation to satisfy this requirement: [program/widget] is based in part on the work of the Qwt project (http://qwt.sf.net). ---------------------------------------------------------------------- GNU LESSER GENERAL PUBLIC LICENSE Version 2.1, February 1999 Copyright (C) 1991, 1999 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. [This is the first released version of the Lesser GPL. It also counts as the successor of the GNU Library Public License, version 2, hence the version number 2.1.] Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public Licenses are intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This license, the Lesser General Public License, applies to some specially designated software packages--typically libraries--of the Free Software Foundation and other authors who decide to use it. You can use it too, but we suggest you first think carefully about whether this license or the ordinary General Public License is the better strategy to use in any particular case, based on the explanations below. When we speak of free software, we are referring to freedom of use, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish); that you receive source code or can get it if you want it; that you can change the software and use pieces of it in new free programs; and that you are informed that you can do these things. To protect your rights, we need to make restrictions that forbid distributors to deny you these rights or to ask you to surrender these rights. These restrictions translate to certain responsibilities for you if you distribute copies of the library or if you modify it. For example, if you distribute copies of the library, whether gratis or for a fee, you must give the recipients all the rights that we gave you. You must make sure that they, too, receive or can get the source code. If you link other code with the library, you must provide complete object files to the recipients, so that they can relink them with the library after making changes to the library and recompiling it. And you must show them these terms so they know their rights. We protect your rights with a two-step method: (1) we copyright the library, and (2) we offer you this license, which gives you legal permission to copy, distribute and/or modify the library. To protect each distributor, we want to make it very clear that there is no warranty for the free library. Also, if the library is modified by someone else and passed on, the recipients should know that what they have is not the original version, so that the original author's reputation will not be affected by problems that might be introduced by others. Finally, software patents pose a constant threat to the existence of any free program. We wish to make sure that a company cannot effectively restrict the users of a free program by obtaining a restrictive license from a patent holder. Therefore, we insist that any patent license obtained for a version of the library must be consistent with the full freedom of use specified in this license. Most GNU software, including some libraries, is covered by the ordinary GNU General Public License. This license, the GNU Lesser General Public License, applies to certain designated libraries, and is quite different from the ordinary General Public License. We use this license for certain libraries in order to permit linking those libraries into non-free programs. When a program is linked with a library, whether statically or using a shared library, the combination of the two is legally speaking a combined work, a derivative of the original library. The ordinary General Public License therefore permits such linking only if the entire combination fits its criteria of freedom. The Lesser General Public License permits more lax criteria for linking other code with the library. We call this license the "Lesser" General Public License because it does Less to protect the user's freedom than the ordinary General Public License. It also provides other free software developers Less of an advantage over competing non-free programs. These disadvantages are the reason we use the ordinary General Public License for many libraries. However, the Lesser license provides advantages in certain special circumstances. For example, on rare occasions, there may be a special need to encourage the widest possible use of a certain library, so that it becomes a de-facto standard. To achieve this, non-free programs must be allowed to use the library. A more frequent case is that a free library does the same job as widely used non-free libraries. In this case, there is little to gain by limiting the free library to free software only, so we use the Lesser General Public License. In other cases, permission to use a particular library in non-free programs enables a greater number of people to use a large body of free software. For example, permission to use the GNU C Library in non-free programs enables many more people to use the whole GNU operating system, as well as its variant, the GNU/Linux operating system. Although the Lesser General Public License is Less protective of the users' freedom, it does ensure that the user of a program that is linked with the Library has the freedom and the wherewithal to run that program using a modified version of the Library. The precise terms and conditions for copying, distribution and modification follow. Pay close attention to the difference between a "work based on the library" and a "work that uses the library". The former contains code derived from the library, whereas the latter must be combined with the library in order to run. GNU LESSER GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License Agreement applies to any software library or other program which contains a notice placed by the copyright holder or other authorized party saying it may be distributed under the terms of this Lesser General Public License (also called "this License"). Each licensee is addressed as "you". A "library" means a collection of software functions and/or data prepared so as to be conveniently linked with application programs (which use some of those functions and data) to form executables. The "Library", below, refers to any such software library or work which has been distributed under these terms. A "work based on the Library" means either the Library or any derivative work under copyright law: that is to say, a work containing the Library or a portion of it, either verbatim or with modifications and/or translated straightforwardly into another language. (Hereinafter, translation is included without limitation in the term "modification".) "Source code" for a work means the preferred form of the work for making modifications to it. For a library, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the library. Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running a program using the Library is not restricted, and output from such a program is covered only if its contents constitute a work based on the Library (independent of the use of the Library in a tool for writing it). Whether that is true depends on what the Library does and what the program that uses the Library does. 1. You may copy and distribute verbatim copies of the Library's complete source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and distribute a copy of this License along with the Library. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Library or any portion of it, thus forming a work based on the Library, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) The modified work must itself be a software library. b) You must cause the files modified to carry prominent notices stating that you changed the files and the date of any change. c) You must cause the whole of the work to be licensed at no charge to all third parties under the terms of this License. d) If a facility in the modified Library refers to a function or a table of data to be supplied by an application program that uses the facility, other than as an argument passed when the facility is invoked, then you must make a good faith effort to ensure that, in the event an application does not supply such function or table, the facility still operates, and performs whatever part of its purpose remains meaningful. (For example, a function in a library to compute square roots has a purpose that is entirely well-defined independent of the application. Therefore, Subsection 2d requires that any application-supplied function or table used by this function must be optional: if the application does not supply it, the square root function must still compute square roots.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Library, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Library, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Library. In addition, mere aggregation of another work not based on the Library with the Library (or with a work based on the Library) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may opt to apply the terms of the ordinary GNU General Public License instead of this License to a given copy of the Library. To do this, you must alter all the notices that refer to this License, so that they refer to the ordinary GNU General Public License, version 2, instead of to this License. (If a newer version than version 2 of the ordinary GNU General Public License has appeared, then you can specify that version instead if you wish.) Do not make any other change in these notices. Once this change is made in a given copy, it is irreversible for that copy, so the ordinary GNU General Public License applies to all subsequent copies and derivative works made from that copy. This option is useful when you wish to copy part of the code of the Library into a program that is not a library. 4. You may copy and distribute the Library (or a portion or derivative of it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange. If distribution of object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place satisfies the requirement to distribute the source code, even though third parties are not compelled to copy the source along with the object code. 5. A program that contains no derivative of any portion of the Library, but is designed to work with the Library by being compiled or linked with it, is called a "work that uses the Library". Such a work, in isolation, is not a derivative work of the Library, and therefore falls outside the scope of this License. However, linking a "work that uses the Library" with the Library creates an executable that is a derivative of the Library (because it contains portions of the Library), rather than a "work that uses the library". The executable is therefore covered by this License. Section 6 states terms for distribution of such executables. When a "work that uses the Library" uses material from a header file that is part of the Library, the object code for the work may be a derivative work of the Library even though the source code is not. Whether this is true is especially significant if the work can be linked without the Library, or if the work is itself a library. The threshold for this to be true is not precisely defined by law. If such an object file uses only numerical parameters, data structure layouts and accessors, and small macros and small inline functions (ten lines or less in length), then the use of the object file is unrestricted, regardless of whether it is legally a derivative work. (Executables containing this object code plus portions of the Library will still fall under Section 6.) Otherwise, if the work is a derivative of the Library, you may distribute the object code for the work under the terms of Section 6. Any executables containing that work also fall under Section 6, whether or not they are linked directly with the Library itself. 6. As an exception to the Sections above, you may also combine or link a "work that uses the Library" with the Library to produce a work containing portions of the Library, and distribute that work under terms of your choice, provided that the terms permit modification of the work for the customer's own use and reverse engineering for debugging such modifications. You must give prominent notice with each copy of the work that the Library is used in it and that the Library and its use are covered by this License. You must supply a copy of this License. If the work during execution displays copyright notices, you must include the copyright notice for the Library among them, as well as a reference directing the user to the copy of this License. Also, you must do one of these things: a) Accompany the work with the complete corresponding machine-readable source code for the Library including whatever changes were used in the work (which must be distributed under Sections 1 and 2 above); and, if the work is an executable linked with the Library, with the complete machine-readable "work that uses the Library", as object code and/or source code, so that the user can modify the Library and then relink to produce a modified executable containing the modified Library. (It is understood that the user who changes the contents of definitions files in the Library will not necessarily be able to recompile the application to use the modified definitions.) b) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (1) uses at run time a copy of the library already present on the user's computer system, rather than copying library functions into the executable, and (2) will operate properly with a modified version of the library, if the user installs one, as long as the modified version is interface-compatible with the version that the work was made with. c) Accompany the work with a written offer, valid for at least three years, to give the same user the materials specified in Subsection 6a, above, for a charge no more than the cost of performing this distribution. d) If distribution of the work is made by offering access to copy from a designated place, offer equivalent access to copy the above specified materials from the same place. e) Verify that the user has already received a copy of these materials or that you have already sent this user a copy. For an executable, the required form of the "work that uses the Library" must include any data and utility programs needed for reproducing the executable from it. However, as a special exception, the materials to be distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. It may happen that this requirement contradicts the license restrictions of other proprietary libraries that do not normally accompany the operating system. Such a contradiction means you cannot use both them and the Library together in an executable that you distribute. 7. You may place library facilities that are a work based on the Library side-by-side in a single library together with other library facilities not covered by this License, and distribute such a combined library, provided that the separate distribution of the work based on the Library and of the other library facilities is otherwise permitted, and provided that you do these two things: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities. This must be distributed under the terms of the Sections above. b) Give prominent notice with the combined library of the fact that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 8. You may not copy, modify, sublicense, link with, or distribute the Library except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, link with, or distribute the Library is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 9. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Library or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Library (or any work based on the Library), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Library or works based on it. 10. Each time you redistribute the Library (or any work based on the Library), the recipient automatically receives a license from the original licensor to copy, distribute, link with or modify the Library subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties with this License. 11. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Library at all. For example, if a patent license would not permit royalty-free redistribution of the Library by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Library. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply, and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 12. If the distribution and/or use of the Library is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Library under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 13. The Free Software Foundation may publish revised and/or new versions of the Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Library does not specify a license version number, you may choose any version ever published by the Free Software Foundation. 14. If you wish to incorporate parts of the Library into other free programs whose distribution conditions are incompatible with these, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Libraries If you develop a new library, and you want it to be of the greatest possible use to the public, we recommend making it free software that everyone can redistribute and change. You can do so by permitting redistribution under these terms (or, alternatively, under the terms of the ordinary General Public License). To apply these terms, attach the following notices to the library. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the library, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the library `Frob' (a library for tweaking knobs) written by James Random Hacker. , 1 April 1990 Ty Coon, President of Vice That's all there is to it! [****] PyQwt License Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt code Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further developments (e.g. ported to PythonQwt API) PyQwt LICENSE Version 3, March 2006 PyQwt 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. PyQwt 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 PyQwt; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. In addition, as a special exception, Gerard Vermeulen gives permission to link PyQwt dynamically with non-free versions of Qt and PyQt, and to distribute PyQwt in this form, provided that equally powerful versions of Qt and PyQt have been released under the terms of the GNU General Public License. If PyQwt is dynamically linked with non-free versions of Qt and PyQt, PyQwt becomes a free plug-in for a non-free program. PythonQwt-0.5.5/setup.py0000666000000000000000000001312512627660526013730 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the MIT License # Copyright (c) 2015 Pierre Raybaut # (see LICENSE file for more details) """ PythonQwt ========= Qt plotting widgets for Python """ from __future__ import print_function import os import sys import os.path as osp import subprocess import shutil import atexit import setuptools # analysis:ignore from distutils.core import setup LIBNAME = 'PythonQwt' PACKAGE_NAME = 'qwt' from qwt import __version__ as version DESCRIPTION = 'Qt plotting widgets for Python' LONG_DESCRIPTION = """\ The ``PythonQwt`` package is a 2D-data plotting library using Qt graphical user interfaces for the Python programming language. It is compatible with both ``PyQt4`` and ``PyQt5`` (``PySide`` is currently not supported but it could be in the near future as it would "only" requires testing to support it as a stable alternative to PyQt). The ``PythonQwt`` project was initiated to solve -at least temporarily- the obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is no longer maintained. The idea was to translate the original Qwt C++ code to Python and then to optimize some parts of the code by writing new modules based on NumPy and other libraries. The ``PythonQwt`` package consists of a single Python package named `qwt` which is a pure Python implementation of Qwt C++ library with some limitations: efforts were concentrated on basic plotting features, leaving higher level features to the `guiqwt` library.""" KEYWORDS = '' CLASSIFIERS = [] if 'beta' in version or 'b' in version: CLASSIFIERS += ['Development Status :: 4 - Beta'] elif 'alpha' in version or 'a' in version or version.startswith('0.'): CLASSIFIERS += ['Development Status :: 3 - Alpha'] else: CLASSIFIERS += ['Development Status :: 5 - Production/Stable'] def get_package_data(name, extlist): """Return data files for package *name* with extensions in *extlist*""" flist = [] # Workaround to replace os.path.relpath (not available until Python 2.6): offset = len(name)+len(os.pathsep) for dirpath, _dirnames, filenames in os.walk(name): for fname in filenames: if not fname.startswith('.') and osp.splitext(fname)[1] in extlist: flist.append(osp.join(dirpath, fname)[offset:]) return flist def get_subpackages(name): """Return subpackages of package *name*""" splist = [] for dirpath, _dirnames, _filenames in os.walk(name): if osp.isfile(osp.join(dirpath, '__init__.py')): splist.append(".".join(dirpath.split(os.sep))) return splist def build_chm_doc(libname): """Return CHM documentation file (on Windows only), which is copied under {PythonInstallDir}\Doc, hence allowing Spyder to add an entry for opening package documentation in "Help" menu. This has no effect on a source distribution.""" args = ''.join(sys.argv) if os.name == 'nt' and ('bdist' in args or 'build' in args): try: import sphinx # analysis:ignore except ImportError: print('Warning: `sphinx` is required to build documentation', file=sys.stderr) return hhc_base = r'C:\Program Files%s\HTML Help Workshop\hhc.exe' for hhc_exe in (hhc_base % '', hhc_base % ' (x86)'): if osp.isfile(hhc_exe): break else: print('Warning: `HTML Help Workshop` is required to build CHM '\ 'documentation file', file=sys.stderr) return doctmp_dir = 'doctmp' subprocess.call('sphinx-build -b htmlhelp doc %s' % doctmp_dir, shell=True) atexit.register(shutil.rmtree, osp.abspath(doctmp_dir)) fname = osp.abspath(osp.join(doctmp_dir, '%s.chm' % libname)) subprocess.call('"%s" %s' % (hhc_exe, fname), shell=True) if osp.isfile(fname): return fname else: print('Warning: CHM building process failed', file=sys.stderr) CHM_DOC = build_chm_doc(LIBNAME) setup(name=LIBNAME, version=version, description=DESCRIPTION, long_description=LONG_DESCRIPTION, packages=get_subpackages(PACKAGE_NAME), package_data={PACKAGE_NAME: get_package_data(PACKAGE_NAME, ('.png', '.svg', '.mo'))}, data_files=[(r'Doc', [CHM_DOC])] if CHM_DOC else [], install_requires=["NumPy>=1.3"], extras_require = { 'Doc': ["Sphinx>=1.1"], 'Tests': ["guidata>=1.7.0"], }, entry_points={'gui_scripts': ['PythonQwt-tests-py%d = qwt.tests:run [Tests]'\ % sys.version_info.major,]}, author = "Pierre Raybaut", author_email = 'pierre.raybaut@gmail.com', url = 'https://github.com/PierreRaybaut/%s' % LIBNAME, platforms = 'Any', classifiers=CLASSIFIERS + [ 'License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)', 'License :: OSI Approved :: MIT License', 'Topic :: Scientific/Engineering :: Visualization', 'Topic :: Software Development :: Widget Sets', 'Operating System :: MacOS', 'Operating System :: Microsoft :: Windows', 'Operating System :: OS Independent', 'Operating System :: POSIX', 'Operating System :: Unix', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', ], ) PythonQwt-0.5.5/doc/0000755000000000000000000000000012651077706012754 5ustar rootrootPythonQwt-0.5.5/doc/examples/0000755000000000000000000000000012651077706014572 5ustar rootrootPythonQwt-0.5.5/doc/examples/error_bar.rst0000666000000000000000000000023112605720044017266 0ustar rootrootError bar demo ~~~~~~~~~~~~~~ .. image:: /images/tests/ErrorBarDemo.png .. literalinclude:: /../qwt/tests/ErrorBarDemo.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/curve_demo1.rst0000666000000000000000000000022112605720044017521 0ustar rootrootCurve demo 1 ~~~~~~~~~~~~ .. image:: /images/tests/CurveDemo1.png .. literalinclude:: /../qwt/tests/CurveDemo1.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/map_demo.rst0000666000000000000000000000020312605720042017067 0ustar rootrootMap demo ~~~~~~~~ .. image:: /images/tests/MapDemo.png .. literalinclude:: /../qwt/tests/MapDemo.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/curve_styles.rst0000666000000000000000000000022312605720044020041 0ustar rootrootCurve styles ~~~~~~~~~~~~ .. image:: /images/tests/CurveStyles.png .. literalinclude:: /../qwt/tests/CurveStyles.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/image_plot_demo.rst0000666000000000000000000000023512605720042020437 0ustar rootrootImage plot demo ~~~~~~~~~~~~~~~ .. image:: /images/tests/ImagePlotDemo.png .. literalinclude:: /../qwt/tests/ImagePlotDemo.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/curve_benchmark.rst0000666000000000000000000000025112605720044020451 0ustar rootrootCurve benchmark demo ~~~~~~~~~~~~~~~~~~~~ .. image:: /images/tests/CurveBenchmark.png .. literalinclude:: /../qwt/tests/CurveBenchmark.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/cartesian_demo.rst0000666000000000000000000000023312605720044020270 0ustar rootrootCartesian demo ~~~~~~~~~~~~~~ .. image:: /images/tests/CartesianDemo.png .. literalinclude:: /../qwt/tests/CartesianDemo.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/bode_demo.rst0000666000000000000000000000020712605720046017233 0ustar rootrootBode demo ~~~~~~~~~ .. image:: /images/tests/BodeDemo.png .. literalinclude:: /../qwt/tests/BodeDemo.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/data_demo.rst0000666000000000000000000000020712605720044017231 0ustar rootrootData demo ~~~~~~~~~ .. image:: /images/tests/DataDemo.png .. literalinclude:: /../qwt/tests/DataDemo.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/event_filter_demo.rst0000666000000000000000000000024512605720044021010 0ustar rootrootEvent filter demo ~~~~~~~~~~~~~~~~~ .. image:: /images/tests/EventFilterDemo.png .. literalinclude:: /../qwt/tests/EventFilterDemo.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/cpu_plot.rst0000666000000000000000000000021512605720044017140 0ustar rootrootCPU plot demo ~~~~~~~~~~~~~ .. image:: /images/tests/CPUplot.png .. literalinclude:: /../qwt/tests/CPUplot.py :start-after: SHOW PythonQwt-0.5.5/doc/examples/index.rst0000666000000000000000000000115212605720042016421 0ustar rootroot.. _examples: Examples ======== The test launcher ----------------- A lot of examples are available in the `qwt.test` module :: from qwt import tests tests.run() The two lines above execute the `PythonQwt` test launcher: .. image:: /images/tests/__init__.png Tests ----- Here are some examples from the `qwt.test` module: .. toctree:: :maxdepth: 2 bode_demo cartesian_demo cpu_plot curve_benchmark curve_demo1 curve_styles data_demo error_bar event_filter_demo image_plot_demo map_demo really_simple_demo PythonQwt-0.5.5/doc/examples/really_simple_demo.rst0000666000000000000000000000025112605720042021156 0ustar rootrootReally simple demo ~~~~~~~~~~~~~~~~~~ .. image:: /images/tests/ReallySimpleDemo.png .. literalinclude:: /../qwt/tests/ReallySimpleDemo.py :start-after: SHOW PythonQwt-0.5.5/doc/installation.rst0000666000000000000000000000100112626552356016204 0ustar rootrootInstallation ============ Dependencies ------------ Requirements: * Python 2.x (x>=6) or 3.x (x>=2) * PyQt4 4.x (x>=3 ; recommended x>=4) or PyQt5 5.x (x>=5) * spyderlib >=v2.0.10 for the test launcher * NumPy 1.x (x>=5) Installation ------------ From the source package: `python setup.py install` Help and support ---------------- External resources: * Bug reports and feature requests: `GitHub`_ .. _GitHub: https://github.com/PierreRaybaut/PythonQwt PythonQwt-0.5.5/doc/conf.py0000666000000000000000000001557612605720046014264 0ustar rootroot# -*- coding: utf-8 -*- # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. from __future__ import print_function, unicode_literals import sys # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc'] try: import sphinx.ext.viewcode extensions.append('sphinx.ext.viewcode') except ImportError: print("WARNING: the Sphinx viewcode extension was not found", file=sys.stderr) # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = 'PythonQwt' import time this_year = time.strftime("%Y", time.localtime()) copyright = "2002 Uwe Rathmann (for the original C++ code/doc), 2015 Pierre Raybaut (for the Python translation/optimization/doc adaptation)" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. import qwt version = ".".join(qwt.__version__.split('.')[:2]) # The full version, including alpha/beta/rc tags. release = qwt.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['qwt.'] autodoc_member_order = 'bysource' # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'sphinxdoc' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. ## html_theme_options = {'sidebarbgcolor': '#227A2B', ## 'sidebarlinkcolor': '#98ff99'} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = '%s %s Manual' % (project, version) # A shorter title for the navigation bar. Default is the same as html_title. html_short_title = '%s Manual' % project # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = 'images/qwt.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = 'favicon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'PythonQwt' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'qwt.tex', 'PythonQwt Manual', 'Pierre Raybaut', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True PythonQwt-0.5.5/doc/reference/0000755000000000000000000000000012651077706014712 5ustar rootrootPythonQwt-0.5.5/doc/reference/plot_series.rst0000666000000000000000000000005712615225450017771 0ustar rootroot.. automodule:: qwt.plot_series :members: PythonQwt-0.5.5/doc/reference/transform.rst0000666000000000000000000000005512613464704017457 0ustar rootroot.. automodule:: qwt.transform :members: PythonQwt-0.5.5/doc/reference/toqimage.rst0000666000000000000000000000005412605720034017241 0ustar rootroot.. automodule:: qwt.toqimage :members: PythonQwt-0.5.5/doc/reference/plot_layout.rst0000666000000000000000000000005712605720036020013 0ustar rootroot.. automodule:: qwt.plot_layout :members: PythonQwt-0.5.5/doc/reference/plot.rst0000666000000000000000000000077512605720036016425 0ustar rootrootPlot widget fundamentals ------------------------ .. automodule:: qwt.plot :members: .. automodule:: qwt.plot_canvas :members: Plot items ---------- .. automodule:: qwt.plot_grid :members: .. automodule:: qwt.plot_curve :members: .. automodule:: qwt.plot_marker :members: Additional plot features ------------------------ .. automodule:: qwt.legend :members: .. automodule:: qwt.color_map :members: .. automodule:: qwt.plot_renderer :members: PythonQwt-0.5.5/doc/reference/scale.rst0000666000000000000000000000032312605720036016523 0ustar rootrootScales ------ .. automodule:: qwt.scale_widget :members: .. automodule:: qwt.scale_div :members: .. automodule:: qwt.scale_engine :members: .. automodule:: qwt.scale_draw :members: PythonQwt-0.5.5/doc/reference/symbol.rst0000666000000000000000000000005212613464704016746 0ustar rootroot.. automodule:: qwt.symbol :members: PythonQwt-0.5.5/doc/reference/plot_directpainter.rst0000666000000000000000000000006612605720036021333 0ustar rootroot.. automodule:: qwt.plot_directpainter :members: PythonQwt-0.5.5/doc/reference/graphic.rst0000666000000000000000000000005312605720040017044 0ustar rootroot.. automodule:: qwt.graphic :members: PythonQwt-0.5.5/doc/reference/interval.rst0000666000000000000000000000005412605720040017254 0ustar rootroot.. automodule:: qwt.interval :members: PythonQwt-0.5.5/doc/reference/text.rst0000666000000000000000000000006612615225450016425 0ustar rootrootText ---- .. automodule:: qwt.text :members: PythonQwt-0.5.5/doc/reference/index.rst0000666000000000000000000000044212615225450016546 0ustar rootrootReference ========= Public API: .. toctree:: :maxdepth: 2 plot scale symbol text toqimage Private API: .. toctree:: :maxdepth: 2 graphic interval plot_directpainter plot_layout plot_series transform PythonQwt-0.5.5/doc/overview.rst0000666000000000000000000000642012605720040015343 0ustar rootrootPurpose and Motivation ====================== The ``PythonQwt`` project was initiated to solve -at least temporarily- the obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is no longer maintained. The idea was to translate the original Qwt C++ code to Python and then to optimize some parts of the code by writing new modules based on NumPy and other libraries. The ``PythonQwt`` package consists of a single Python package named `qwt` and of a few other files (examples, doc, ...). Overview ======== The `qwt` package is a pure Python implementation of `Qwt` C++ library with the following limitations. The following `Qwt` classes won't be reimplemented in `qwt` because more powerful features already exist in `guiqwt`: `QwtPlotZoomer`, `QwtCounter`, `QwtEventPattern`, `QwtPicker`, `QwtPlotPicker`. Only the following plot items are currently implemented in `qwt` (the only plot items needed by `guiqwt`): `QwtPlotItem` (base class), `QwtPlotItem`, `QwtPlotMarker`, `QwtPlotSeriesItem` and `QwtPlotCurve`. The `HistogramItem` object implemented in PyQwt's HistogramDemo.py is not available here (a similar item is already implemented in `guiqwt`). As a consequence, the following classes are not implemented: `QwtPlotHistogram`, `QwtIntervalSeriesData`, `QwtIntervalSample`. The following data structure objects are not implemented as they seemed irrelevant with Python and NumPy: `QwtCPointerData` (as a consequence, method `QwtPlot.setRawSamples` is not implemented), `QwtSyntheticPointData`. The following sample data type objects are not implemented as they seemed quite specific: `QwtSetSample`, `QwtOHLCSample`. For similar reasons, the `QwtPointPolar` class and the following sample iterator objects are not implemented: `QwtSetSeriesData`, `QwtTradingChartData` and `QwtPoint3DSeriesData`. The following classes are not implemented because they seem inappropriate in the Python/NumPy context: `QwtArraySeriesData`, `QwtPointSeriesData`, `QwtAbstractSeriesStore`. Threads: - Multiple threads for graphic rendering is implemented in Qwt C++ code thanks to the `QtConcurrent` and `QFuture` Qt features which are currently not supported by PyQt. - As a consequence the following API is not supported in `PythonQwt`: - `QwtPlotItem.renderThreadCount` - `QwtPlotItem.setRenderThreadCount` - option `numThreads` in `QwtPointMapper.toImage` The `QwtClipper` class is not implemented yet (and it will probably be very difficult or even impossible to implement it in pure Python without performance issues). As a consequence, when zooming in a plot curve, the entire curve is still painted (in other words, when working with large amount of data, there is no performance gain when zooming in). The curve fitter feature is not implemented because powerful curve fitting features are already implemented in `guiqwt`. Other API compatibility issues with `Qwt`: - `QwtPlotCurve.MinimizeMemory` option was removed as this option has no sense in PythonQwt (the polyline plotting is not taking more memory than the array data that is already there). - `QwtPlotCurve.Fitted` option was removed as this option is not supported at the moment. PythonQwt-0.5.5/doc/images/0000755000000000000000000000000012651077706014221 5ustar rootrootPythonQwt-0.5.5/doc/images/panorama.png0000666000000000000000000045553112605040216016527 0ustar rootrootPNG  IHDR0ˍ gAMA a pHYs(JtEXtSoftwarepaint.net 4.0.6cIDATx^\g ;=S",@R[lA@@DTP@NE[}*0{gerwfgsSDyzzfee1Ǐ%Jϟ?~\ D JU^3Qė޾}ݻ (Qf ~| wjSPPPttt\\;+y}:}F aBT4Dȇ355frwwC84=~I`7(YR녬ӀʊoyŎѱqD5}XUau^= UYYIp̙3@YZZ=OllP[3|ȓԴ1}}W7>ώ, "(W&8" QvNpvɉe8D"""ռHfE,샥tݵ˃=}@HDYYZxYs_]=CcKJr2~o˿Q]Vں`# OGSTTTaa!݉.(( *8 cRRӻw?'ZWSM&-؈o$3}~aF^NHl:ҶVg/Y>~7lrϼX77kk>u VRs1I^An`ݷ|;X-fJLޣ{3^p*򲲒Kˣʅy類n!IYLIi,KX*KOOAAJ]L & s@PaQf[YypFGTضssާWTgR_#c~_4ɡ~-Jڂ@|&Ǐ_~y, fj|sϣ>!#dWm3BmXf0UW=z=!6SfN,Uȓ %BNNNj, $*V32ǏedPoSLI[4w=j^`,#/Q)x=aʎ}WВҐwzԒ z;<%JH@聯CL999>qu3ӓ'Ow/޾}~0svWCbv~:jbcHf*HM(":Jӊ˲}(@@3Hf3GQ^UDA DjNfCq0=Ip%0}hXoM{-GH\a,qף,CXF楋ş3Qqnw"͝}g|`Pej.f"Srd0SLl^Vo#|aHO/++ee8Lj?ffg2_ce'0WJ]LdJLLL=zdkkٳW^0S͗O,%Ágp*ʲӫkʯ}S4b&&<3E3EDn'z%JmXLld$ jkkZXXX[[OKK?"ZA훧/lqusԠ,OE_oeW͞A0-ׁ>U/,͞B1ݛV@ǏYtE!݌u7P8qfʛ{㭛?i1'p@aZ0}47-66,WJ])D0 UJJ IpƯc Q ?gY3y0˾2l -$;.66-$;)2>!)%cAzlRRVz#=/3߾~**HB߿~ONrsڸ}F hƀDBzׯ2׼fD *++al"L_ӛ})0AݷA0R')\E>Fg8?" yKR/f3;y"$ 4Dⓙ(Qj.qa&J(/fDRb&J,(Qj(fDb&J,(Qj(fDb&J,(Qj(fD3ZS"J:gg8)i8(QԺ̤Rk`&>1(Q*8;?{l>>>ׯ__eHW4͏M\"+끶窺yk,O`{M$ͪ$?.e {v c[7"ڊL?E b&WRD/T1SK]ޘ =**GkEsR;`QK%a>~GR龘e;TT. 1ӝqV'J s6DBn L_K1vk/+5nFNB?0ڸEDM+?ϔNx(5Hf Ԥ_b߉t7S"^bt}30(*/}wOC# u9~߄/ʣW|4ӧ/^KvtGW߁7*O1P?穫+1if(?#d_L^,- 'ӊ˞ѩ x=F(:_|-,1TavytH0)1ɅKW#?~t@_7X[cC_b,޸;3>K;֮}*-T:KKRq@iv%}qf f]7&! op?qH"ԬJII544(LZBpv&ǀ b&J~_a;.=% ]߶[J鰜`L{=ׄv-v`]z^r[U^IAf[/mtt6P򰊢UGk{㠹kJ{Ms5#-`oNð?y(UD7dv.өG>OBd̮c;udHupim僛`5{81=B]{gX@+ t~5->ؿ6nI_;w@v.QX-޾SQEEy5.h^DRKb&Jڎ4:s0SyiQ{9O@ =77ͅ[;w `c;d`O]VxSN2SyiѫjgN3iVqn 7Sݠkmf*+8s@v§y:J]4k`?g#rLBz?*+2^CB-37WyfyziڹSD \ L@h.sgN;6h*$PYI=5ՓLBFYpsL^a*+9n 鹹Tm;{z_3=I1Qլ\u᭏g1T"mz3I\^5\1'dϥ i93 [w$kB啖ȮGp8/cD[N"4G lL|3[ZZ:;;߸qڵkO.\"5V~XG` ؠSD%D1%JmGLYzb rsAAӵY%,N3yC".;vhb2^'Z4K2GFLYee\+++.4b'0Svp [ yu5="d-.f J(z鱗ä*Mҗ^,ش}ӷdYZʝ`IݰKb 6x?:yޏS>2y:u}dZM`z ړzVf$_;]gƛ^9L" f:ZVZqYeG7Of$IOZdG?5 𨲲^O<@={+&sssXsAA*))ɝ8qj@xYYѴNQD%D1%JmG8BO bڙn=albC>s4?}یv啇òAMMqMrw,_v>»X/2nf:{r젾u3Syy>6f6XȄ!Ny*wU\Ծ5}#LE(e{ LYU w?u`ݤzc;53)8Oj.UQ>J%/XsC\It9M>e%r+0f+#Q!5P sܹۚtmsQֵ!F$=Tu^P>~#SCFAYZF8=~\Tjav Ϝ1ѹ|NqN;ZK t.zT7h=g5PU=zixSHYgSˊ Lnw\VCCp cݕUNPI=Ck4 SCW}3(5zAYE1 %Ei牸CP2 f{a#F:*O&ӳ`(`UZRd|^P>v> 1Iΰd?Obdұt&mQl WL*Y_8칙ec؂R3 zK-sbᆨaLgϞMHHrww` sKAAt)~]]]X?~o/EEŹsRDb6+6f`:v $w(Sjy 3Çf/ZYe=mIZI g:r䨟_[X&..TA`A99ݻw7Vb߿a3Q͊L=v)^p̔W/LTM~zqBcV)^6= sNWzMYIVt֟Y;ny;.7#e`JK7E1S[SiqI&11Lǎ8;DEEGFFyzxiǎMG2 ~L;w!'~.+zm~DJtR go\= &ڬ} \։`&zҙXA|"1}ŋ^|QC"hu os󪁂2Fx؊nû\+l.uL KJcjo?tդz8{_K((fj⚛#)5qz%LDzjOf<19G 1xLn7?i߇/;dؠ[}Lڈ(fjx&{{{---rppkJ1F5뗑@3*:~>E%eWY7T5.3_g9|i\vȳsX]:pթ,mޖej< , =ws~q+$,"B3a1t* Y%]PmST~YVcX)P+-\ELmVu3Sܲs4`|p;0Ӕ]=UDž!4ik`r'^'qݟcN)b&=ˇ(5rss6ӭ[n޼y/_ͤׯ_aL(fUbensmGTX\vd؂$RB]7fZ%n@ DK Lf5/&<[lԠ +-ZDgB;׳EXafC27r[ ]ɞ6 [ X(fjպ @z;n[TuQ|QP1Mt}< 3eǾԷsG&7I_u0ӳgpnFz}"DL+$ |ӧOǏ_re^`3hC;kE DE3g\lY޽P3j޼yK,֭j-BxUؔ驯Bą ߠ&$~Hf*LYpoa^% y0SWfgn WfʎغdgDy0+ٶx{>Myr+d&k5sW ?!_V0JtY}6.SyF0ɐ!CYfѣGQM?~LII! | grrr( )ZE_~=y#G2-[}6i$333"RTT 6mm^c~/~./s^ҥ3{D%<w mUQpJ~&vf;cK+"zؘα*jW/\$!x›ۏS|렍-/?!ct $%WF瘂3QۄnDR0ׯ׵?\z>WfEF O"?|*OB\P'B%'QQQoP'B%~WSդ~,~/vvOzU~&ŘNnE%~6 'Wwy|bF)ZC@'퓒:t萌ULiii$';|pOlwCss$'}qȑp@Evrsݒ)9VrrrNK+3Ssg:k.#Gdw)aM=q8T2)]t!aH-m.Klt\F1SR0Ç^|uL(Т޽ڷ+=׈ÇYlfrss#Vѷon޼y%Nfܼy3Qz(*8k+37WG?ә]2ʢo=ts'ODX&gDڻyi_ڼ5Vg˗/;GE-LKjr7[pЁΎ'F.k%ǢXX>vv]yŮKD甥.$Wfȧ.ox½ۜb&J-!sss8LlsGGdǏQVÏ Z?)Ç䨨(^LLD735%!!(!LHdooOx(Nf߹s琐HՋ(4A޽;8(7mk.5VZf/^i˗o8iZ^rv'IJFG>]_nnAm:EZ\c:љi-zxE . AVs$9H_v3$F;QD%'8,,,bL#HlU^^@mu(rrRxO o7DMdW(Nfz5cܴr2Ǐg͚DC2SF egg8(0Ը!MCLlҥKtt4QnFTUUN+i65V:VVV32ѦY(0t]vӦUkM}'ȎVaʏҋ3Qe244Ahh(p40SN #j{YlD#Lp2"|R?~K kjj QjZlmm+)))--}!)))+8)--mСDkr2SCLF izl~Lvvv`&R6}HDx={vxx8Qn5z; SrJ:Kj*ϋߴbYx0Ӭ#P,;J>#QLa4 ~5TK3Qj &8mllruje`#bO:2[B^fJKptϧϟ*>t߾;w.e˒& jO~r͚+gpvv;4%%c(lTLTưB~;(p+3?+3ikk+++c̄fj#޻w(0\̄39PkijznN)~g*=`3QfZ1TkQx=,6 (jgLZBpDF7G0Sa\އ GM3t{sULL ^Y N/?~]%!ȑ*.ۿ;rV<LV֭~ ^RW78uLVRLEE'`(=] e^tQ֡m._?p}=dQR?o^̛xzm8\)L7>i&Nf8' ݝZQ|2|Mqu8&35=7w@(ecx&SST?fN <|ɫ 핕"s !|_sAd.%5<(tb_[g/IT"mSD֭p]*(6dτCRbgOEF# f]='cǂcԔ<~-@a.y%3gu4ۀ'W(Nfrqq1>>4%%)22R؂+3GL ϟ̙Kmk0VDSSʕ+TVV֧O?6q8`&6n߾( [Fљ%J N^^^M6+3()[HQ~WT(S{q!;0:}'MǏnj)1B}"JHW~~xe%+ hzG9%bg&nz̉3j'OzfTֶ{'5"^ɟ8exRęzlNLٯ_ dINN~]BBBxx.3@TDECqj3qvV!JJJZ*++З/_vVss:FvލDzecc ^m63Ƶ_ pv GdS:ʼ⋙֯Ǎ3ê ۾=U8@7yy|~zAۛ8k*)9~]S⇙@1^O:F1hD=_2cPPPtt4IIIO{F#6f: b&ӧe>4#*{J*XǏmk V9 F;KLp9sfLL Qnn׬Y|\k2 jܑSSN(QK֮-LA3~I<enGT&1*O86feǎ%y}{ k3dj.U>>OOfbWfz%\Á@FȼA9 qe˖ϟ>w~zןE9%A{[ #,ⓙ@RRRw% IKoāu8"%Jirܪd&..Shh(ѺIII@N%%%hjj EMLL$---JHHU1s޽2őΦM^xGV 1&&E DEE#|tC=|I@Fsra勭AY[h_ѓ'O3א+I'((Ձ/A7Ǒ06.0́Ç8r|||;??~'Zp<<<&'N 9nnn...R[y 8;k%J Np&"823>\$f\XMھ63=L22bÆ]J6Fv 230[?כ222؆7(7P[ gK;5Vy^O % /z3byDp'Mh*#cPW|ELK3tl+F@UFZp{jCJGR6Hf3P0 Qm<>?qN[LHT4*E-ii SܠoBrzs/&Ҳh hcQ'^<@9I~RQ^'A%{cvzx0VW7oz3B]q^jA6]ڢج[7VPyC7V`LOSm6o)iFuؤӎΜ'IZs6l݁HBkj((J Dd0:IS2c:nC:vQh=z$!!A86m46r}jzb\73IKK798߾}(0a[JnLe/[1{D Tj\plKI",rWu-ʋlZ ʖPir|-RTXvhOmjP{I|ԮS߀a-84Wb=.(]r;Efhך>AnˉA5ULŵ=iFΕLVg7чsۻڳI6v‚ vi̔Hcח?') 3F-%?v%qDqqgQM _Ϡ1rS "f6?',C{C')NYa Mk//1?%D- {ۨ,o ҰO㊒=v/ Q+-yu8`ۤ*L2geOx"{*+x+ `7JK.$8nRaӲm&/8VacTdݻDȑIK>E EaH2cmN3 3:#mݺa͚W*++GZx41֔1:P7K~_._Ԡ39kGLA6'1ӄ/j6blYAxpP/xM=9?ZDDSBp]l˹(5xu3{FφA1 'L{`%wðC& 6{\ӼBe fZs V$V#ǁuD]B~̡ ){u雏T>EHm%e>hK$t2u6,J ыvIDE-e_޾Od5œOd)(ުdf>ҸA-^:(aOb`[``b:31=eZ@:|fn~gUTdmJgmjou9U4t6~=w9aZ۾)w0lfyuEa)Vad/6Z2 Jg00${]*{'t6fiNaIUU5[ld&igfmq{HfZ~qDVZ فxtN&SB⇙߇Ǧ䔗 nPo1e&OOOk;v 0$3kvn{Bb 6l.v1*`靅=Z>L<Ӌn].(M]wߧ){H~qiyfԮUP 1t bآH-B663M ]n-3m>l 6*RC>] C@_|ۇ=|Qh1WH 2Su-UC/d9VaE?updxZ:3%:İ[@f{⣷\s 3n?71 FJ >Į/"27L?h(W'@pع4/kIfʊ3TۀEI5#.A '6fzs3/ADú_4sKH&?]9{,Tsdi19*:qg$$8dXIi9 9lT,A*++ }zMHEYaQ9;Ԅh؁.e9{FLy*bx oK3c8t!AR9#ش'$rx]57&ϯ `]ny+ GI,.S]dywT\#Le]~ġ{cTd7۱#߽ۈnj ߵ+ b *Y_Ms>*6f?fVf:nCP) 8uܛ鉓,,,xLKKLW)8NJ-eeyXMnw}pb˰> \CZw:$(f> )F8-7jfjS9bK]!=04r0L@zrޱeV~Ogbc!/HvOT f?Y3N++1P֋xI+ˋ7<(0`| d4`ph6C8!5i hR7ύX}.K%26T'놦E8=mALXGnZh% }L 3R] lCAc ӿ/Vf*OY1y1q 1)?9Hl<2M}0qL1 FFl{˩hAÁt }7D~uXYC8?a \$SXfKTdCz-/΄ׯ5xِ!6xdgNJV*+#TLj t".~?JSϼ;f8ŵ3g@k׮utti>a#1guƵi8(~u4[sUWW矙}6c 4$ 'ZREY].n}57jf!ƪy`}`j]"Y:0*́Њ`AC.϶,DCT.x:FN\)Ԓ\ -EP{129 -b{k=^ FX$B,1RyYl^ȕ"* sheȧX9ε aql,,JEx|BMp Ձ r4`֐5QFjef3ڍ79-3Mkak6$z~}ΙS9z4;aWxŊG\M\kJJpRL\TwOU.Q=yO~8f̘z)==6zoN6au1ρJpJ!VL4 Z|-QOM>A>tFFH6lX x?WNJU$3(Tf!~ M4PoRa&~87m3hDzN q[3^2 5؀"}hljޝGNdV̄|^3h4rzѣʸ2!ss)w"P[uI% $))a%'܉֠.u:@\sslO.8^cJC11}`:iiUlU8!۠șE~\8WrvQEUm7Ή<dz[KkٜWx7V/Ba%A\`TWoߪG-L.ސؾ-Cayylt1ґ/"?~7"D̴m{G9Ha_DĠwlsOƌd%dCj/Sj̄}O,jy 3{fj\evćҊg%2$u5 n 8^+so}Pf޽%nA ^cu灹ek0]dBS|n* p1^W.c"E8mGdMƬ Hvmga--Ϭb1؝Ԋ6#2wٜMvf ;Dƶ`ybY r1 5˗qqq bgWd,³^L'(3s¢7pi lݣaPCmځ\F>`٤+`ѣ?yd/$a0pJL@Hp0~ 8X'11ݻ<}8 '!!8xYt]Ҟ֭Ujmmm88#&6f#@Mp `px&QQQpHTT8@f \Uȁ6ϤN:/^',, 9P++MCBB!ˇ#Go8KHHQdee+Cׯ_}A:C˾-svqfH@@+**???QPPvuO^իW>ボ)S\rpp ,tttdu`Ǐgf`_RtssC+r޾}kҩ*ytqqq!ݳ|` O- 4'b8 N0ҮߣB#4y󏯐e8UVnylÙn٩J9;2Ҵ8Jqd"kJWfs=iz.S" ~yk6K1h &N12!6K[UtF!@,e͔6 !6C!ҌRFpL钘 k]lAlC)F; 3l~l3k=l>FtHM݋m6E"me8C96YyDwVY`Dp0Klb&8],8|-8LeYG/jZ?7xOڍWq ɩ( Nfd59d:yΣR7oDÇE4thIEu0} q!K g >߿OII*3AիWq'8|>0\tAĨ)+80Z# ]ާ*S H姟#3?{DJJ땼<33QSJJJ-τ[IJ]cuHcZA׮ZV%mTGK V1Bhu bsKU15.U].:$(b!ƒƒy>25G*9GGjt;a( m6Bl-FSS.؞?iBja Gf-aFuyԆ4fLrq0,U3U?F| vVH1B BxeάB6YE;ìj ʨ&ƈۇVS=s fm45F)Yh1ΣA)4xu1B6`&+2IC2A>{ш[n?~~rs7n$oƵF> ۛ𚬂h'%#TO4x #/̽bΜ9u0!y\:9<.皛kfjznř vAxrs`Kt;a7-Pnnڝ1bB2n qn*[Hl+VGc*Ydk\e9 ĹjPcnl!ϔ\3ۄ|zMT۽{œRTs1SNJ$!|̔g{po֧{+38yx&$3d``n3XN&P(0X j⟙mn5Nb$3ڪ+Lp Ç{& u =z<./r͵t 7G1d&ph1GX+Ǐ100mq';F=4Z6[}ofR&D1+~^KlK'L֯g3X4l`lWv\S; 7v`7$v -u\aA"l)6I 6vfvL'O-,dކbj;6gl~g9>x}ƭY߫^xRd" QczRD-+2 sGI?~)--W^P.xJ{X*ԩSaϟ?7(7Ƴyj⟙17ǿSLN|Q2)[d{:/j Ιkf0b&ȏ$3gώҚC"Xb ~ dbӅl;/v6m L. fZ)bB!J`o:v\'D+1QF?QMcbN a;QQl} , -1Qh c;밹 (L637bSm!5,; wL 2rGOSl ,V7 7nPZvhq2 d "dx/ q.vgO$x+IʊrSz32P LD)rxI&|zv/0`ۈ2oDn^L]V&)B]Mp+0Sӿd&RQQb&i*ʕAׯϧѸ9b3IUc^&Vc=vcjiFrma9e 60fm$:mfg 0%lnEL?&-ƎbQlp%vE˜J&mNdžNĔLiؠk?S%66 ]v?S1Ӆ:^Lnb{F/yH)h«{~d{zmޯ.Qԡx&L 2'O)$}Аi̹*[P'SΞCuݹ,aYx-[-[勒LTdB۴iM_V\I⚛jF! n~&ȏ03PߺҥEl0ss!4=l2& Բ P ;d4[L( [ ̄a`)j,& mĤhN8s"Xa| ۊDFذn\W|lw~Q?iEEEhk9V~fx3mhF}F?r^ٻ-1`s>OJR h%KKaAI]=|gvv=\}Z 31md^73qd!f77Ǖ@M\sNh^3I\-'--mbs/ɱ=iTWN' ZR9^L*9hi =;35,7GDc>c]b @0s#1-7lo~E11ԟܵA؅?1&7!C_s%؅KXRl|g])Fgl~_{Ju"l$ ENLC3:j1(68L{-ZT珫$%%!.xN&0 bTKO7rs.6u;=_3,Lw}NX.(`^6kC>;g/l'Î $ ت4IJy}l60%]l6[9eZ ♍c˚.V>]S ;u};H7G.as W؊؅5G?>Ϥw떃b& ]JxX񄙗Qq#d,/V±lA,/_5fZf Վ9nzcA]wiƍ/_$ ŵOfj\EEͭ[Nhɫ t/M˖-:WWSK3ӯ9dؾc=%\;SYiF0zm G4ΪùdZ"?6 ㈌ݘPNy㞘GYy 7R;8cxQ̀a::n7D,7p%[60j :vm۳V)!.: 3qN4&vĹ_;t?NOO'BT3eYD)`#Gjߋ2SL 8ٓ(|=zpL9+ssCvʝZZO^8HE,ck9"/2eb9eİ"#5Wrs=*)=*eɁ3%'NU۸1K:TM\>)u@ fJӇCXg9ES97͑B[2w? '3Ik5DCMd&PEugW67(},2޵TZZW;r(rsL#7G[5?ݿc߾hZ&P}s,ƊDؐHlNxF/i;wZ5ie?4dܐi2ç(+7uBw,b[&-M1SV`&^I]Ҳ\VV0Q&G9E-ᵢ2 7#BSsŪѫ+Djn.s1niji+Ƚ W8׹)fb̄[bԵ۪[wĬb1! QD1?Vs'@Rt)q5Iɢ8I3SI[ɓ'ue&ή&>חZE:|C7޻W{8!WXAj|?wk=,r95 j3{袏abt331/fTPpgK1Xaϛ@̴`8a?D{]˩2Sk؁E3u\5fJcL_+Kfrpp#79Vm瓙%7WUYI} W4 "!ϟ?۷/0Szz:8"r&NVU"έ[Ɓ'68 Ёtsq:߾}{ds>299E<vċX^Tާ$;Liii!fo_ Z"7横߿*>>"|{%,Z)00EA)9V/X,&(`OOOaaap~ˉ"jҁ͉@18555 دN-L )L㋙f3SG{*rs3U8-ɎveI[4sRoJeJ\.OdIL{Pԡ'3R5-)p0SSԿ3 |Os81mL@$Lp%!#\ AE2ZA-*6GFs(@\`cW~w.UJ<"+Rtf^Uܥm`(H_+n##{(T+ލ"tڕjv>/[Ag"X3|7sL`]._^̬b1\iҤIA2:]ȋ+*ό9k_C wev׸IT^FDʊۿG{ڸmS{a&xI㉓rFF"s#EgNމ |@_ igJ=p!'ssFFYvQzsXsK͒38y؉7VQ JTãO*(~@c̈[zG#}iD@ϝNx,UUU'NGU.k(f"X2S=˹Bx2SL)* *tLP\f[ &8i]V Q9sӘ.}p޼~mb)###x;KBB(fDe1)cDu; 1~34䟙ᏆQ%4 (Ԗ5ڌo&GL~~~d' #8aFN4sȯ[߾}:u*|D ꌖc*aͤK?ӎ3u\K| ~ع../L.W.3߼ԍ7N6δIYy7.]Z,>E9$!!QTԘ~y+-!҃ee%W^iI 2uiX\µМ0S3LhoSܔCeeel;!Lut|m6\A V3ut>4ULA+ٻOfo @1SUd&>ebUMMͺu8gee[2sHp~ss&&&:::=>x~F5 Hl_gsW>3h4 S9ȏ 3Xw1gL[ǧsslD19"--yf>3mRO1SUdG%@pP:3Ms}7㛟fnIR"o߾;ݻD)˗n" U ANkI7+ԩk#N R_ǬeraI&III1ghAQLu0LWmV㫝pϟy}_/v" | a&~_7 2^>u!cccl9ss %\I kQfS75V3E~g)jjK1l n@t[3!fڼחb6LÆ KNsZ&VԠ02S6lh`&$ok9$LH!ݿD! Ν;u#q3, ~ m9frs"팙矪tfs f2b&ke;7mifM]+)uL^֘Z&RpP~~ B\Ϟ=2B9r)SS\rU+q%%%C`&"x덖cW^^cՂ$ux2 Xz.1ː&\iѢEzzz@0hk923"ܱ3m |RꀂM1~wj`bW}kײ1i۶m怙,k\b9ͭ^f⚛)J;vN:5ef "?"̤RH:LNAs iʅ\ |J3qfZ6:~RxM16 mD =mN"B̄&j&P73wٸ &YYY[\o?DGtTnW0R""$:llAk i L(7"~Rx 2qkࠎMMM%gi5ydv4qM8iL666*055?>vkдLlw3yzz"/́qEBԦssH31 37|Ϟ LI _ף ;3`#!Za:uٳ'»X[[gȀ"8|0 "द:222dÇηo '..Ό!(B:%? P^':RldnEŻwZh LNN$%%#w1fzo TxW`ABBBP8mF͆m^bo '[pIvGK2%(Ŗ.]z!Ԙt&OAl gcp=Z'D<8o<r`.\A󹠠 kvlAӦMC[a`%3gΜ4iҜ9sPՔ)S6ӑZ Af`0.좘_?Uq%@K}AKdcfbmQa Xx^/raE~Mp c{a|`ٛ Lc_T~[&LQ{eGBp(4\툙''K3z8a" ,VNXTVVAXq8Qcz3Ç^mOx8^TTԭ[ X9`5Qϟ8ESl8O}nn2)L^̢'O^mur /_(۷_~ź`-Q` ^x(,,ذ.]s&;;;cQMM 9f\<)%0.%e:u2h:&3'^=GIIƍ'N4INXx6F.˛`0h˖3_0C KEΤ2𲢜7xw~`nyzu %=٫GN6)*iDO#fq#~Rx2Sċ(L<'yLs݆ /**" UWVT|=RJȝZT 18.N'N\v I&ݺu |^TjA^Tuft~c7Uu rmIXXd8SeXm,i -N :wn2&'>Gэx3pL ḫ&\\Y]]u=ң]$O]PfJT7D3\7)Ħ' |9~Rxb:Dg-Aj^~ ~w7Bx)V $F Nt5BUUU¯_Fœ'Oر1%M6 |NfJZ~jCoҦDUTOw7baM:wsg8"qVEvX]0֭{ ߴi ^=oUBBÅ6 Ȁdùl$l~L uA͟?kٲdU93mX}"9S1l:m5= Q`LV.$$N0Wjv%0l۴;̄M')}5I7wF;'$@H^eҖ_Q;Yt2AAUsl6GduưkrlkIuwNMmYLV2[؂fa->\r@0) iEfA1TuJ3M8v68=s7LYm۶M1SeSee.}pxw]\ss%nL;c :+y²kǰ(g[I%\vL''K|SsϗtٽП鹚-~ xO$ Xg8DEEBC䂻tdɒcXoT~>iD9rua\8>e^nC:yg 7񃅡ϴqcI Ḽ$GHQLaxfow$ח"c澿!-#+X)4m̟PbM(l$Z`,ɈwH *Y3#.S߿g ϙݠ~p<Ӿ7nȝJ'+"gN߃Q 4(fb̤m7 wy Lⰻ]iz>ďB\~|0Ss# 3gY]}+ {$R–OqSE +z:G%fL`OJOwf[EŧN^ʬ ͥA5[- Gq-[);ڇX)HhH%QO.K_﷏zu]a^k=m̟'͉IKɫ,׺.SUUy&Q`{!!'׏FKDy_2FC9sst_X_%;9:xg0of4~Ts n}z֬YDLKSWSS;<ٙKOܩ{m?aM[hYj 8f&5K$uNK킜܌m,Y;ѾG_FO-ޡEcXPXBLBb\''33mbg& $oĸļw >n9n?'j`㐄H$+U3lWؙ[=vT7Epj9Vf9pRǏG[5xF|e2*3ؙJg77i[-yM31of:'3q>*..tӁ+8<::t0'** 2҉du`"""Hpt3'CAݻN۷o쀷H`\p`;u߆=D\\\X)S̙3gg/_kpIq|yGGW^Ad3f̠2Ӊ%fIoEǀ3ƃB|T oZu̘o vd𗃇3vMcjG̴v:3]V́2??+7WPPP`&PQQ? 5xyK⏪A Uo{izo 28g_KJJ" sF1ؓ'/K( 3hnNCCcDHp^9~ M7P]գ2G= ;pPJNhh߬ѴLh-3ؘ́3YN.ګ2Զ",Щ 4z|8ö~U?~E\B)u0fr}62 )*(:=>}t Md&DLH"T[l͵fnDv5e:̙3^fshoN\;w>tnL5\L%Rӽ{ee#B )33../_puu}ꕆYssD@lmm2Sx|֯_?[>}8qbjj*QfwdIKKX_L|愅9̵̙3u4]3]/N?hdj4,,'3l=ղxZpq1~RLL㛙a&e{#̄aMegJ^D CfX9S&> `G]\rO NŅ5T]1S9LmTFFo! -g8 rlhڤ}rsm۶q2hgA Q`u2fDEp411{/(𖂂tH\6}\TTWfbc&69 hнdhSτ0&/&̟R#֘9H/w8aU,&$o'7g*aEү`b1Xx>l93D<,z-'3>8h~Yϩ\5ȹ$3%#7/ ss~\~#fZ,}'J5ݻB 8YYY@Q)))fm67L]M{575|8/ [$7W`mGre&]M03%N&3ݹoL'4\U߾oFyc*juWy/Vuyt8v5ݓG >6(_9Sd`񰂨n.Dc׶^nf{ӭ*g5{a;U}߿3 k̴tRSSS8e)rSֵӎֺ3_o\?믩vt,}wN۶ˋgRsޚYdG'B"\&vĶl Ҳs16DDbB L-b&8o7ٺvUp$mmmeeedZ/[$ʵ 6b&[WSuu 2{N>z\L 6f2k[9,vcYTE e쁯w38O:88a>K"%: [%7>J-jmowW2VRMنǮ/%e1nt}$ɉg಩}IৄZ3;ARt( 955XMпv+grOX=_Vwn?v;&c 9'3\ ceLt=BYݶ'f;럵Ba8DEE!^M[8SjZ>zʸ{b+??L-~:SHQM?3H&^L_L|`O'!\('F;3Pdղvi\~G?SYqq 'S^~J*vLz^apFm9Aظ 'dkҌiI78hyXI5 jϋ `е_VC5.3ό[0$Bͭ 3tnt 95Ȯ&D@źLudBs۷ ;"ڊ'7T׫W/!%\jⓙ7 ݻw}8  Hɴ]d?GlENb߲c?FS&nXjܻoHfXSM6(nx ؘ ,2wf*\@?P-7vc؅^︹>Yq$x'kEc%qb>6SϘfGbRww3ۑdKV_e۪hQUY\/,՗XbeAFtX+,w85wj: f"O7g[bT*$5#^sD(d[9UU)cᏣEm~z}m>)?=]a#C$"ʮܜ~/?TmF]m3alrud`u&b zv}~1_f2ps9-,o?#Uc1vLy#bѫ- 5_텳V>}P/a&>ssu3 7CӂΘ1Ba~?:pj03e{c]gq<ꮝ{R)=%@ ~={ד)Psyr~:3c*kTTT,X@AAn=_2OMO%;/ޤtJܺw_~Nk>|t"##y=8A|F^N|||TT x9 rQ9Չ@ՕX"&- q-@UBBWX)` t10Sll,Dbgju4WWS푙}Dp[{"2~`5ޯ jKZwu_;zAU5ߴ~K.%FGWdk=kr!]NLߦE礪?.w.X Ī&. 땥`2:N_}uԤFnS%cdX޴9V{Ed.+TU_fwo_DQiykw"w_z.>'w򢂼¢CX]o%g?F>uxos vfk3RjS:xiwDզlmmLU.Z0CFؼyӧO٪H!]BO$..٢J|e^س+;>$)iHq)ȇ[Mmp % UonI WŽcN"N>c KL/_& ]iL6w8iM 4vNKtg~46G644$<D%б u;i:ϡߤ8gnNi.:uu\i.`m =3==Ad8"zbX33񺚾H-=;3MRδAjЋk-'FY;хP+~~Ƃ_Hpܫ|E8>Ѳ>Y3y#ՙWnT^S~3r99{mcRiZn2 3}s 81K.C!g3DN  NKp„2OPnh-:UM`mX'u&^BE[n(Xw/3iF>Tb~ |.sZ6@킙 Vyޜf/3^cwذqDX0bٳI./>o431e_cy/!w2S;>G:5v 8R;g C^ J툼iA/_C ! Lׯ/lŹ*x'Og&܏?́dN:fnucSLL Sg>U4 uMsߺx@7'(xECvgBi|3m׶a&+X퉙&Lw^8ŠqzZ4AW|m>uHŻ3dF|36#t>HD[Tnqn=7ǜh@w ~ NDrfBB⇙Μ9sUp6=ş8*lp6>(0uy~&oj_떟RFsZ6N31ofW^$|†#LO,V@ĚL3񧨷.=T=ztOU:{W;bŋ$$RiA6L9L5xMNNW3P-R^$K8ۓzTVVֽ{w8}}EJJJBBwD]kl[޽fco|;PDEEESS\ŋjgJ r{.b&mfIntި٘ ^{>6B@mvc&~3egPN.1$Q#i ̴gO0J 4̤AD[L@Hiii22240}ss\ -ko/؄owr ?=^|[-шfSy3DNs+3_z= )S83yaB:ڸ~{"3ia4Xn􅅧?M퉙-[lP2;lQr0Sw@Hg㙚l~5bNe˓}WXNd]u썸ȪݘyKMvD4Ԉ)㿄\\\gbfn\YYѐdRЮ]&@N;񝤹Ux$cLj7fa'K26m+P^Q~Q@N8}^(Es1 7v'5ET?L7}3S͑NϞgN^A!iTleT>iɶܢ\s~j)6.ݽ e+c&@-b;Ţٹ-[`&9ŋv !*֍W1IKfj9'3#8)= d&Oj5񟛫`U}?~|α j9PݷڴiD>u+3.LaV`Sd1Ɖb&5zrfgȐOh 8ˠVv. IsKz#3-_>~4?`Q•qL(x[b. ^,.]hY$8KG-^de D,]\tU呋GCdѲbU1\9LK@[HDG@K@K[-]}m;FZzMt oCm14f޹uSG3657ݾ_̉#ŋ#tt:>n2wn,ءC23P``"&0aḿf&~Ěk3pի\MLPs/z D2gδ0e϶Lڹ &k?(Z1݆ϔl 5hZs8fZ,jH3cHF}ѮF7쒐 a&21̛`i$dg&z?SIhabFAKW.\Lr+,^*f|xELZ zwK$dˀ$A o14jà+`&ZEA3-X4:3m;w.Ƃ7ou|hfrq l}fj׹9Xss W^P[%%%}a&sseeehz\b3׮]# 9ѽ>>>̔9gD1rsLk3sgN籮l8̄a;um0ˁo`^8Eߊ0T6dNK 2n amCQ⢲r+YbN㞣G9ImM15pE,k虵 ]8h+ 0HhʨB841T,ZLNe~+"Y~CC[CN9niBQ[@-m=pvU:ZLt$ 4t Y#7U.0Z!4bSiI!l驝f~("+++&0qa&.CLbcGkeGpo=3ҥaϵk&Du}pX(-99;3p$|0ߺ\??>_r:u;pΆ-ÁwӧD t\HHHEE DEE:@qD E'-(H^Q˗LHpufFJKK夦:߿'H'&&VǏ8҉tfNb&INN&8pD <9FH||<&Lp]Ls7# MLLk3yvrsUX#"&1𲶗+ζcׅ߲mӐ|.N͝L!ϯO[X5ekƘk }lIiuEV_B}IȔq%zM3XL3-[V3eG^fx\IWvYkv*6{9 ^PA@c+ R{Bg$`E{$$T_8dfvV#ĎL+Wֻl tݢE\%K6m &3p]}p Z,x2ӛ7oECh۷o+*3Z\tnO0@ K+c #XQDߚR0 o7ydV +34˞XO?tVRRRSS#Rx3Sa׬~#cYG1eX$AX1D{Vٱc><1^o`&zy9Ql!fHuբD p||gʉ]0k1 #: 7^SfXS[o)&*Lfj|ft̤}ٯ `&gdv_6,>>K II9QP36jowۑ977=4W`qR͕%6]x+Ʃeul>5"wgΩ]ltY]]]2¦Wom8f"#E0aS#cssIQaIRRRBB?i-anO tL]+x!V:O!d$$/Dߩ̄$炔t`^IENb;w~Q=SWQ؜ۆik[=_dMb1 0[9*d+WgKK cѢx%Ʀ%hɒ ׮='rϝ0cu 3 ϭWmLPiOA{>AX"w^ccc""T1_Wh]/ؓ_uՕx:xL+pH33`4]%=7GH^^={pA73%hb󮼙S阭X!#&W/G3T|\@Œ\$sfysw..^.vOn2G?ճgbs@B)霜$x,gі0)nL`)saS'0\]c;fPiw:>\{14|f:b_,u#U<7 0@.nSV6xf-Ӛ36ڳgkOMcǎ[֊+[W^u9`})&㸮^z 2t"A|Vn3u)=kLxPݰ_6LZ$e bJ]ple/a=# 4+ӑLܶ`Ab_W{qYvm8ݰNMν{=zjt9hoN~P߿3f kJ/pbt90&yyNfҤ. @yPP9 FyYvv |fbZKI Gf2mBDغu͓HdZgWfQQ^Inv|fⶅ nxJ6|utF+H$*깸N[g3y{{'$$dffNJJԬ́XS(JW)II .VavuJ?S+ا4L 6P}&g&D,wYle;֭7W:X{1ڵk':$1lۭgRU5"^:[yṕXc@z4 cLEEE@Keee̻[k9ByyyDܦ |IS Є{y&ehh8tP cs 10א p!S3ج0([' ڋv5h{V[ P}WWƍGsE"TT4I^RUX$T&,-tg1SAARA~:rl7QFIKKYLm sJJJs B[[[SSn164i҃ -ϱ9ҿ+++艱EHHph:6 1ج0xaS{1SZkb4wn̙3o5zQLY6KWޙ7GFljNg&*Z\\1S m;6 <ёLbZ+dhh8WKkRp6 2@ ˰0>3MZG1ӚBCfu+f&>3Fݎm/_e˂9NP҃.\+$QqDf?B&{,--}-VÇp3!#mgP>}dF"##D84iL+!3l26VڋO`k t,3oe@{B1ӉdWuKC8NХK/=vD19A33[C'/|#7]>{Bmgw{ QU`:^3|j{pWoԏ{j+mV;Rod7omcccD84iL Yl&ñ]֬9Ы;'Ψ(}ciannCaT:1XDk.kZGwLYYYwvf:z9թRUd kx}҉$%ͻ3AF1 {,+++((?ĸ.CCCcccaPP*7oV+DD 98)*"z A >v C%%PYpppxx8\KIIIL,sffmles=~|ss9;J.IZ"&gQFVV^AIۣBK6s8LnvɩDʾ ^:U׮i.\qNyۼy3Ng&K[$MMoBB Żw?NP4q9::TZqHOXUU2Q/X2dfk}QdRL9} 8x ∶HS9666>>\NL >31%4ےflleÆ]RROmiڵ&~zZ17)Ǚ rNE:f},w^Ų!` o[@Qf$oC,{l"x1 թRR>BBэWS|yG&)000== q `fii)8o3B133$666ΦRڸ<<b8@nI9DLrU%Zҳ+""7lnUj:B}NlCĠ'yo< EuC[oݎ$%旯NՕ+b:q)ĥk4/(Ҥu.3@S@NM -++\|LIcO2h[\))2u6hxx&Ls`bc)6Lo4GE޹#Q\;׭[/^{lˣGdR󕐐}"NM9v 9~ :9-Lj@..)* KvLCQ4.rЎfvliWRV^=nYfJ0ScsN&+qscbDWC[鋚7͂HϦͅL>?<6Wf$'F-{ta<"ȑ#JXkZv f"_:U.tp8il`S  R|grP]MsDO+@۷o FjZ"2CF ;wOGHiHwMM&'wII֦ R|t46NM-rdeYDԥ`0怜=<<ږ<dz*;t A3WGF _Kssկ-15ŀ%%m'RRR獵ѶRىd,z@=N!\eOw -,6o~Lj@ A ,\rpD84iLAκ2جG0X^t86 3u˵Ev?|F篽,ᵔyb&hwt _:UxO}ǎu#f f<Tkuu5| ֥D ;$$n)OpEFF貲d8)j`@FݻdzzdaV_gjOkiV+?ʕ 7n)'@KSV&Pϙ3dG4Մb2ҨҎ+%$c@K.̄ *ϟδ0%Off" &AV3MˮcL[:/_Sƹp{1Sff&4ci$ O]Ms@r߿  |)#\111ph:~UG0nns0>31{g??jFwSusn׭4nLВ}V[[ ~ꂂ%;00022ÇpQ(TTDt)wIѣd2ьffw4*Zv{==2ќdu}[͊DӧJ J2ިhTU#/OpqFPSx1|>3MZG1jȭfI@ؿXf&u$EV2Ӻu,--j&I]S%#c“^G:<_Ҥu:3|TXn,nnn-qý4jk99S"K"\Zʰ1~ODA%CΎ@4?-!ޑ: d{QX[2BD^LJ| bBdN,6-*#}|%%#F~$R>]J֫-%eH>zEG!\dD}y ӧd)>3MZG1SUD'f=kl7 =zؖ޽pۗ2qյ%[`:U[64`D:˅:E"PY2MZW`o߾*ESMUUUp' qww3L DL(7BWWa'RА+(HMG"ժ:q :y 5Odri2EEH:we[/ mmyȽrzh0Nr*fy3Фu3~(3~VB~Qcs,>~b-nL+W 0_ݻ턄xdIMs+^Pƭ2SBBBf 4DK2_QzХ23?^ӧV 6ʂzzd >MpG+!#*'BBPZ| 0 WyyōO7+)Y)@2DP0\9Lk!30ج'ͱ(]:%0{;zV c:W76ҢE᪪۝m(kR8L[9RCp햇66:Lb12=S31b{,oj 33ZLOcs,0`e˘I@@`[k;zV`aՉih6…׮i~:FmLm(vfxZZGft_u<3ؘ n7n((r "csCցt'EdYOg&|֙3H, 3'>.)^櫳h9g0AsT1ٳ#*Ҹ-_g 2ѱyʀ**~30L4$R^+=4"1Sdxث}; F 'MʦD.Oph:n2|"+?|fhυ W.թx~ef>}ܶl=8ӡl{vv6a aRdImJJLOldu,WHH:TXRWAAG)L!!"22 7m"cI.B 5[LW. oڳ]&Ĵ2F@C49ϴq(T {,ڈ1?&mFdW"aJD]Fz^YfZ~U5`kg鉽 *&׋L@"NZw λ Cra2qًE~{KCdDR՗2+"A:YlEVBּ@1LOY2. 0G|)>Zy==#< RP3S'Fxdd62ᩥg=h:>~kyl:ؘI?&qgq+!RF֏6XEmWʨQvV#@و#\f.NG~v b J fȨt$hB_NG@5^Lf8nB G/F4ђ@B.{#ӽ/;R'S5G~OA^*}9rΡzRk~4怈X BȽ~yA6A39l'@,7!2zҷ!!!fP D`&sŁ[2RgbJ˱XA$3:*SBȎڽGTt]fY<71ye1>&❵2%櫓$'wqٳvzVL`|fj}ժLi-g{eNDsٱ߶\z#C/ Y bd PkliD2?yJ"ȗߑ{0镇F#غ=GH@10Gofb*ү@FkF Unmਂ{d @=E#A;)vr<|c>Sͱ1S],2i=̓~n۷5- 3 󙩓uĭY/"aLKxd&0>3TOog:^|f"dԐSȯLx͕?# j>ҫX( 'CtVEz"-.6Lg q_d| $yBlz|f:xB~BEB]ROHbf*M(tZ qzʊt/!OLdqe^`fJOOOMMp7bٳCϝS&n:Iǎi6L@Kǧr$񙩥dJ=+]dDtG#3=|g3DDn ̐7^$nBc'#ŋ4rM9ȑ@d9R XftLvȈ iK-gbD.#}jWqHlu)qހlNG(ak:""Șd$z(RY45Oȟ>dA.rSdT 6LAfOOH/*(󷷵O- >hkc F=;ܹkd/yyE|ΜCjnQ[1(0Tnk[۵G,.yrppagD8{Vl0vٱÔLmXOx2IXy2gL=@|f"c&^E~\?Nwn+V s!@qHov~5D"YU 酽2SAԹz M~ 6ϴMj`׬Yg?2Ҷm7mzCؼy/^&tUpf:VugɼfKAdҥ={pwgLWLW& yi8Rط |hs]⮤9 Jz7msgNa%EܻYC7bt+/?-*Nص'/-}3]yJYO6%jlhnؖU3]8ܠ'e͛ V78/!|ѳd^%#swb/2r֭|f3SU3SRRRa{ ݅ԒԔԌrj*`ë6QyԒgf\񠭘itkM2Hos9739ՃߩrȝFfU8qtҺu _k3l}ȼʕ@fDYuv񙩥3SU3{j7YUR\Ho5ymL4ssd3 ZnCe_jnL?"K^^i\7f6aۮ-Bf7OgΨᅭ$RVpi)mn|fj}.---rfhѢEd-[FHYp!b ˗ G#9RϟOVX!&&FDV ey6+WмyS)WZE!o>Qzj S)0DEEDcǎWRg͙q;z}כw-MLn[[2g {"%؄X`]_Ga mnnVu&pG &#sHY؛0>3T=+vZ<葘H3Su52 ;w 9^bƞr §Nϛ7oDT]]?y$'N!?NpwܡșD󯭭M=zBMFd)YYY2TQ"l``PVVw8t9z!%!CLIKK'O#Ӣ"ϬpeK~:I?vv60߲e5GہtG"Lǎi-7  }`ju NPoN<͖,f68M/8}JufC:AHP֭DJ^mn|fjz03>MCQÇ::>3^-b<2T_d)0(,$}&-b;#em "|f"HŞY_ELr + ?3ӂ3gΎӶb؎C^đ"3"WD.&8!77 K,3-;Cfsɳ MNTxE2s ΘP4dӧ>|,ԉfe?7ۡ[4WJ1ctŎp`NL:t*PS zuv-\g3.cV OD Z1SEj>3uLe&[ ۷m9,nt03͟3c+l g[1шӜ=q+om[ iӂ:y4ӧC۷'=}=p`9C""d^tcΑ6Ն Fڈ SO\tG09mOH'9911upcb"{IIU2צMO+9zT,bdJ؂|fjz03A>9*w>ppk]աEԹj%ӹjC:C݋k⍺f&'''[[[֚fS H-\8|i{'Z|#aDIπlnZ=Юj5Ha9 PCJJtbӡCg HD3ǓG=sg\wܪU%$N8M aq %ڸшU{h'NdCݺD +(ʅ,iie8&ǰL-UϞ.%%z~Cu]L|}cۛRRRĘkZX z/Wܛa,m/JT,$ |hDL!ZtʕL,r?~^ɓ.}?ȑ`ٳ}T\X;wޅ8`1kM҉Mׁ?H33{wXi͛N~D츳Pd=F۷&3@@P{񙩥a3g(|8H[L|$ ?tDXLKM26)4Zi-p#fH'n5t+Q_' eڴݻp*:i_5;Fl ٷ{!&0>@$,YyCfM@'Ty|fbӉ-i33 99%vY=zfq>1JKTTs֬]zF`ӪUٿ&W;3>3T?̔^!!-#SXqk鵫:,{R©QR|:v$;ȾvK}|H3S'6[>3u_u 3xKԃ t9bGg L@q.L=3YNV䇊Qhi "R*gT:cz*ݱ`f:|0g͓ll` Y|r&GMHMIQ5;݋}fuSp$ Z߻QGa4f@kΝ Q 7 L͙y=UqS ă7ru?~vJszn}X0a;aS8ܚ4ϭ[؜t?KnwY՞í9̴l |rN g8_G4%Z.L8b2מ%$eJ:d今\`'W"nLDOSɽb b߹vރٌrmj^+Tc{҉L>wxavl޸{T"CT^vEvԹ"jI"S;6oڽ|r'TxF| [o=vdv+I=qߥ,S=Rbxݮ {]{Oǖ<ؿ]:ZGzt:nOR^A9ӽ GH,Ve]<ͅc2a[[4ubKpp ]킉̯(>ʃMF]tˊW\Խ)ꈬQx/};́2R.ݴe)NQ y:TȉI^I?{%~v\%CK)Of;>ՃԹz1coUR5n7:kN antm%GEU a˧9{(/L"6-xbSm9HR|#7;?ZM+r4mZ3kvyncˆsW񙩥0ftUGǿl*+̊t7vFCВ -][z55.H-.&rFyIn>֯d2ͫWnfRԯ_ nw+: 5ToSi W |Ll҉- nw/3WԹG5.m J蹁vD"0ߍ<5Qjtpa;ΌQ_p]V ZE8qΌɾ@aklxDl36Y+H^nY,Fg_0>3TLt:ŅW ѣv]~AI/3H븈up^98{;*kۂ{Q;Nׯj\UΣSo+i=uृg>w>D)`^<}`orU?^wJ" Cڝ$>w4_4{'w펕9^R׬ϤܟZ?WtxV.JG^#uv&>3uOLٳjH֣?Ms=G6'#v}n( |:B}oޟGX0d5lD_10אZ0Q',Օ&HEf-;fbkWF]%lW\nDEk2ٴQ-؛ayÍvL78$_ers0ACӵRS|u 3P !k컊2K zzjbQQtF~^8)>>1%V^31K %!1-'/?/^(Ĕ̂쌄bTXUW̠*)qZiAQYA^~nVQ4!!;ytZ)*> [0ܕUU=p;a 8o._KEo=sζcGXBMrtt >V;1"'j]]]\à}vc|9E9{5|kjϤgt ]?eAOuX6 (jG.6x8]R6k~eU mHgGqS`+D.y~ {.aP9a?t&zρkMxisWFbž3p,vyd^WR3SKtka!0UPP~;]T*]M["Οw&Cm!>3ut,}njO{ݰKuctFEya߆ lsxdw] M^N)i Zvb&G]%BJ$3%$$1=$3ֵbpi΂ӽepd5b`73^P}!M-b:\0ӈB:|ߋLdpPlV_aW^Tt&vELZ }\-0>3TL:qŽ 80啑6ef:NV7:b3S`/1:?f$q*=LVz8`2:MvQPjT;1SVJJ1< 5 $7VEv2 < '6h=림Y:l4l>/Lg C{F607rum÷pJA3ygj9 oju䗅]}uM{W!+i3qj=),,3Sծw )ir&xwo:Y Ĉ{DN*2udãS-4ڐև_]YpkL )+,錳gHO20LHhW𙩓UVAF:{)vi&L :WyvԐ._1]>iiH=F f **K~\|f⋯f~'~)iiL6ݻwחpidK<"#\jǏ=<<mFS7772¥F >{ٙpƎdK|ꕝR#Ɔԗ5R#uZXXypiǎdKVVVdK411!#\ڹs'RRRyvSp[jr*' ᳿Ʌ{ŋuD5_LpMW{^k^rm5-[%6J2%OOl2%qqq2%ooo8dK׮]#C\Vxb@@@jjndД&''.5Eh9aaaqq_ %#\jΨh2%UUU2ĥ@>)_pdk(ؘL\j|`Pkr`A`bb ϵ;3W⋯64urjuvRbjz ̔R?} 3˗] #ԓh7n460Gڝ_?">3i'-S(n'Aczf56WL&|Z]}۷uu(J'ɤ몝9szSnnsdxLܦb2ڀKR#sHVw,233 2PT EPE,?3sdQ9|o9>35WѶ2YFSZvا+=(BǗg6\9bDFZ W>|wuhT3^|}L|f⋯0ӥ cTOC(Or8<]BLW`k5K'Nؓf`xLTȶ˧vaSDKsb]]]bs]\\b0E$3q dز\󵓋_hInJd26ձ ƙ ]]\<#= KEom9tw5SWW,%0|y~l0u EsE$w[)Ԯg/8T\\l䓙yJɨoRuu(P{H r;ڃ?0G(66իH2m9PRTO'VY}x<{n+ofiNx<*:К<z}/:D;lv )a:޵O v!G^eW3ӁNDg3SkTѺKpBR}[z1ezyeP5+ *|/|m@O oˈ7K`w‹7|ؒwNeŹڳS@p[|Fo5PSruuLQEāG?Eṯ̔Uiu呡fK^މ 5OL񙩞>|>a>|~zNeD=g e `nyxsmBg0.nRQ!)^** HL-Ƣ؜/\]]_h.q[ڿϟg%8uvqqjI kl.DZQ[ bkLQN7SY^#smR2L$tE{vL=@AV&#BL#?o t cra4ɨȑqɉqڮ ?}kԐ#FOO-SFأ'NipQC3Uh:>$TwQt;{)$RE 6~9`&1אZ7<Ꚕ!r6o6IIiAT*ukHź?^>35GrlY8]"L/.lL6{f7^+yݽ fr;z()yA6Ǘg1T v_‹WN11^;wNۺp]8D)*vK'Qk,|;W V*./} ۸Od;/VM{MN{unn+W'PX( p ##)KJ/&LѾK7wm?;o%C5eN?3%0E՟ʈmYF:<;]t];6n8WJ3^("gͲK/ ږ^y͒E[߻f٪.b(HZTP<8'Rv"EEr|f5gMB&?;3;ϛ(zm`cǰ6EjC/"345rwmq EËKc | Vi BOPdO]\_?}:3y7/#0<=cD]\|fnbd%Q{cmfO/PزjD#FU|d&~nQ6ncGɚm5x9Bny7 y8!mmеb`PFFQs45}y޷;3AcLdp6W; ur}~+Ⱦ#fo,*ӕ A6\&dEysί=[$jqpORBa8C([GS3lQ$3e><߄Ac~jc&wBc\4bc)P#2`uZDe$?=ېqn y|f +3SJ l 3OGΧ'w੎;0?93b`PnnMHCB29>tFyURRFFj]ttQXjrI:@O| h\W}gTϫM[wnٹ?$>NFƠ[dG'.gBR~y^u¢fݱ}̱`ϗ+W̾{|AFbUB(xqc͆&z.<;vݴۗ]AR]yܶW_^=0={d27^2 0;G"٘ƞ-bW^䤄n^of~cYŅ7nw8=mQ2-nݲR^n(>6ati[usHo23è#>̺S(ĴHQѥD I|J8@,~:рCj[fZn5>4Z/`~ݓJ))Mݽ;!MM_L FݿT_#@"k@pmCIQB񙩭U~cפ唴W,L1ah# ǔޠZ=LSJqƠ Lg*ma]x]CO> 0fS*>|?^GQ["<SKJgfj!uƆxv2***p?ndjQ#t#nd&8 u2ዯ(*SB6񙩭EKIHk/bkInt4lDGFFFy1XeEY䫾r N^@uŷo!uu1JpD%2 |8dRL^O(wAA׸s#dˁ]-e&qL:iFf7DG˵|4ܥ¦2͛z-R#H')⋯⒒&>3WQ">>̤WW/.E7830(CA%gTD110sܟ0ہc; ת!Պ\}HAɓjj ic HMGgܽkrrZӋx9TH'勯( ر h_]GL}TħOӪ}f\WE5(:E6Eˌmg#$n[ˬU uL(:EO=<>c[y.h5z}`e1B7!uu|BMMv2VںyEP9t!3ƲHOOf/zઆk;+++;; ȉM|f⋯.`aAA|4zڷoF,Rǟqvy)UF qXg0P]^N0JS(:E ޽v)q0L#ѷnAN¦&PA{x.4N&B[t9vqLlm+bL&"D_=QpmMT*j"ƙ)?֌j I9> nvʼn훻gI3^as퓄Ҋ[%es :˪0(h[f*Uӧպ߾ͨ#:oM!3<-! 3PELo"FBؐ&ωe`s5"P|@>(: 5T 2傠] F]Y fBKOp72ƃbbrUU=_ GC3)ķHAQlV$9{sJZjsC< |=B)~hFv72C=<3~>~l,!!5&KNWĤg&xsᏼ/܃L#*uۧO~&\W7_&@E$CPtNEѧ(C|<> (JfNi5 l>FpW8~.A$KO 90FC dk gYZg~ݖk:9%16káLM.ɮf̱٭Fb"FK5>$fLMM/--m3%$$@@ӎm2S)_d&oUDw=ro>im`g6VoqYA9 *_9s62c.eO^ypoOo@7Ki'K3QCն7}畗5Ă zȩ{wOܷHiUS;!~FAؖomò͌ȟ>v޺?45tlHv+Luy",䮉GtK& g4wɆŦ>Q;vNw5C.qhX\<1(hHf*uO|0hi x4&HO( cpㆃ%OxpUx j[' %/ T-#3h eeV/Ԑռ7WKg7`ի@z4񙉯/`&1%%%0SQQ`c;ExN5qMSWcrC(eQ W-%#'${{]sMHٽPDaYW/uK_=2P2v )1f-{@98,<.B"E KW|M+@ʹsFe'.aϩQʿt3J*׿YXQpRV3ZQWoϟ5,͍:qZ%C_oeCZS@ ùj> FE՘6'E`ByȽfoo2״{OLcj+txNMͫ32ĄL߰ wJ`f HHH1 _hy;(s2C/ө8I+~qϬ%.{$)O2+@=nXT`opB)ԊdNOFﲘȖ^:a)M;d=tMD^'/<,ņhnF7ʽWH='S}̢R{}E#o>o,)%uB!ɋƷDEj;EPOKH'V0$-֭{ɋ 9xUGHg%+=/{%!&{/_|2]޷z"8 +@kPǙi&9bKpfMC1x*eΧPQ4 tO!9Aд42 Afj+{[:0Մ1ӗYE)Q94F%Cz^ׯ_ܰ} WBFfDEEEDs 6b&1$;E97vqt{{NfjY9".z>5\*x~ݨ}/:NLZAAZTO~F}CPt2ލGN4#9n,~h%;ZLJs+**⽄fLpX愱ispo_8 楏I TEG |l-( jqXxo .);;VZZJ )###33tL|WC/.dJAuo{rD5x6~DŽ;W2E0@xY%DyTP@'#us]G3}0svfL%zaZAŷoa.Ξ0Ш_E&W~DDqz&XjAmL tdTG#{c}y6*JJJ Pt͟E_|R/r+/R]}۷uuG~( lDIE Q4 ('|Esax$Am`A\/s=z39ˑw9؈J-+cU_>{y)?1CMy}bR^~FZ2<4EE9!1|0+1ԉTYY)`o:;;{yyJR\\\<./:K!!ٷh͟?W]R[[VW4|x.Eolq9G0<@'H`Cc*@% ^IÿJ 7ى-\5yjΊEE<^"7=8#,"';Z+=٘),),9:I&gPt$=6F$q=؎T\\LSh>|7b*$3RPPPddF1>#Äd+ٹ7ٓڏ| bπ_%:37vg뵵ZZ3 VVv4OY <xbh.NH(jN$"}WƦ4A(+z]"MB 3B@BՓ$:$o@\ԜṨ/TfLIɩ6v6ll:XY;VWcOΥ C(Znb~PYU]WϺo++?'_|ׯ_>*B?f*a 65RIffSlllJJJB<)-{JjrjvV+03&jttCq } ~e:L_|J-.3,L>?ߝefM͸MuuqZEQ]o&[R|e@9x5'WP4̑iQvHOٲ0d{M>>CC܏NӅ17p E]PiV e9W*++#22)'' ){q/(7)ڇCJ:ʩ:*z?|>vI둁9 buC D熓=uGOܿękOuo8:}ĉTyy3ho@.j{O?'VXۻROaܗ]]]o$@ӣG,-- /{Ok7\2>uKɁLm@v?69&x6S[O k$)WQV}+,""Rm*3`47NynRa"RXDTҒ>'{ږ瞲bk)O(#~P.VKOػ.{gW4<uwG G_Y[S&$))dFkc&P+9L|ճ 78wǷeeeT*5:7{Q@5ǏքDT}Iv9-:K__|W.}48rSL/_ʈ xu]N㡑fx"%EW-h5&GPQ(6JTߠZ"35MȈVS(kF= sd.K9kC9xLCD"}GqD( |? C\Pd //2l΄ݠaM( 3!֔7^qƊK 7a"m?S(XQs/TѺVX 3mFsB7fNĆ2GP`' 6GV 3&,ؓY?>Q&)E}d|sV"jp4R(Xn OZ0&B N5pf*5 GZxʹ99TG걵gř*P4g#qKi0 |^++' %KN+ϺzE>!,‚ϦϞ[]ܬt'22R<}CH/O"Is3SbI@$|T#<{О222m͝e%*Ȃ%Cr'6ll7*Rr;DpܞQӗ9C_qٮߛt$%wdAϾHn.a2v)7Wrs .;v?lM:E_ȢM= 3@|f pMkz_Rso߾ׯ?'ePŹ!d)Q?.%s0;-$>,sD2EL`KOO LKKiOA{emm `gGGF!7ޠ@EdXNKYL}!1ys"hd[ nM0+?[t0>-npA̽e jX2v c)oz "yZ!JϦ !#B?{"al͝)2ɯm&oBv#FI#1t%db,9lBO.G?7xYy$f >O0`ʮy2X6x~"ߟ돏Qf0PцdXg,3 `SE:Ry+@9<ݎ;?{^VϘ/69ջE.(8?Ɲш4ԆV ! W 2kTHvӧ÷_PP[h ppp} r螢bJ|"T%Z;^ǻrU8*@$WJFCt7,fTUxʾ_&UU=j`Ux VUͭꏇOe7UUݰ*4uK,RQ窪UU"UUUU7qUU82 EڝDQ!4[lի>OܕH$rBbB(>mh=+ DA  9rmڢ81>؁ȭgUΉ܀ ? ŀCUUze'qeUYU˪*WUӢׯwzAd:ۖ8- Mf5"~[͂0 r (:zKU2q)K+{,3nb3gP7B_=WpAfb!u}UWW πMpdq1ܦ84_D*///)M+N -Ņzzz~#eccCPRRRHHqdAҺ5F55^8!>!f.VSsŦbPzO55"B$sbkT3fnjj&`5QS3-qpMӦԨ>xt@M ɚ557as55kjjj6,X 5LO55kjƷe!]ۙI55jjF)k4bj>`8r,|B:9gv8l7D"<tMYj$۟f ݟ1ODEk$ ;551V)f51۷J`ckkfΌBD%} [NGf4t+/=ӮWTCžEUo7u]@H,?(.2tFmVGBø2>GQ6n]_VV= ::s%%n%]Y|f*+MWb-1fJzjчQԓ흚,!kH}y~"-dS_??z6%!)E 6ׄo]RRؐ< U8>U'\1rN 2w:W.l5^p'NWOP/Zff:>¹HܬI=x8C@݌2^N>}|9@*? O_'[/cEl:+-NAzm؆O~POE+΁$O x?9(7}شxG޽v3hŹ~Xz/X5=Ff(!QMRZIo\& lI^<Dq0S^NvTGY9¸Ho7$3F2U˫&zD )G]PY5م^:ITh72/+\S&[Mvwl/qAUWTo΋QMzx/*~={}Ec=PكU7L={ =*䢗[Q5I兹<+@??uVWtvOV/隞۹k]yyy% >^ga]vkC^vW^* +[ֽ";o #??sB:::p=kT>H Qg^R(hy9zzuH*SLdj_&m`1nfM݈;l[a([f)=WfuIP:L-msib73r(x>A< 6Yne@q 2s("Q p$OwF%dZwF,5$ /E)S8\,jxwXT;_  V8jūUbg%\`߲c\1<}1 2k@'8;DA8#pZڟrViyRy_?PQ{d+T{|. |&l>vq&3& X*Bpf&P\\5kNg?ffzgSj=B5e вO(;n9a-((.7Oea mG#}5ldky/{u(w{w[( 2S^O{|Dq%8Ysn`Ʊ mkF!nR;k1Q~U> Bs/Y%f*++&)!;%-=<0f?M2 o_Qz9z>Z[f~ɨ)G>T6~q"['O8X2dńW{Q;#45}>}A4yVfp?.E=EkPg4+ eM J-RNhMacIJVqkKRld!83eGlqvb|jC'~ө)G++,, bJ__ES)Eg抬8x|D7nHDxRga-06% 48 - @ (73C+ˊrK@#5.,jGCpx%S2PS}كC^2Yd*"AXKIAP,b[?78p'& x't<~z@B@U& #'Gc &AlYBߣwQWd=l}88U&9A 8&pWD s?;j؄$Z֍3T(wRf&p!.*jkweXVD\?9"'2L?8m㩼~؏d#[g`Z7.=A\R+ x )>ZP=sƌ%;d +unCnQ[jǨa e+Ϝ)rJQAS?Jp1@qFM1cvMEaD+v6S N nB0;h2}k Z+DYEcًqr'X=u rOsRySfcxD0* m\E.HS~ .)Rx,g;kG>]s9T%s036ᅯ^Tpn^!#'ΘDp = z;De1(gθxSwY6'd7~8);=]bJ:N pINI `3Fr'xT2`σxzY0QB7UeY/$tΟȔ4Y0hz#!X DWa5@_ICVN˘5\Av"U 僣a$)/+C شEog}F(71e_mRY~y9jY>$O716wlԏR*%׀ H=(GNxf"Dt8=똕eb;1Sc![\`me}+ cSMp{@`آ 5op49xʐbmSsd&|s讇^ȀVjsy( Y 1.B>|' LR#h?h@oݫnWx?6ʃ{!i1^)5@Vڀ}DoԻ嗁#*r=emg,ٻ?'3hȬ3gg{h>#z_C+~,=3gܫiY3T/ 5!c&o Z_5¬tp~?!W,B{[zaۛ}{#<nU[6L9)LYLԒhԳXSc1fWTU`|NKϦro+-)%/E(G4!ۇ4|i>DAѴ,{]IzOqY'n潧U~Nɮ۽kGE|XHTl:(ЁE҂|삄|*,Ħ1pR' |'%a_5f.8m Ƃ$E*0\hXYYeox1NhEE)2:lN7yc6@_-.K"?'oI;YY,˕Ҹ?mxvJEI=7v'$AQY)O*b84nUX(h8`qc[-.0uC2"Q1+@r[j,>`*I~†؊ gӉ_P *xRW4* .'0|3Ъy³vQ LQ/?a& #=B݉Q~w !} tɃEM83C{,~|^sKj^VxT<43",ok8^1 (7ag& ['eD,!ziۤ?yve&Yـ}풹3qO3)LAO+h}~{hvICcs3{&9=S>Θ1eH"G܀F,UN]<F=1?2ȢO]NzGz#m0ӈy;4yȈyIE[&gogs866/:Bʰjazڪ&ml.L3!cJ+ s޶L-qN?i%c=7L|f5}lH_?bؔUHF[`OƗo߾UWWJk|lh Ym\޼FI4+6ZF(V? Od,@(hYQ\(yN` '"H:~;;jd3\'"xd̜6giDGTCAv" Lk7Zf8X|YjoA lHR8<)dž|9y_ _ND> pEh*8$ ?eb;&{l N=c)8E2W&8 (>ᒃ3O'I_Ga[}îⲩ`rB`^1}QHx?pE4fy>̄ >8c +2wO+#eRsf*wu-24KPQLq3iH/DLɧf<`8gGP+* 2,bHWI ]I8 sAR`&i}o(K0>6GLOFL-++Ɠ93IyؘTodND~+ 7>L eȿk4L #\BRmoo43;g#kȽĘi\j=f*ڷJ6b& 찘Mu(/U2Ao~&`dϟ15UWWzfjx2@K[B㍬ vaJF bQ捗5CPf"O>B ^̜*> ohNEw4{nik`YMGx't2f;3 ?hEL۔FӖ$KB!Un8xXNig'zN $xf bv m%8<0WO`(Tifrq&!l&8nS|\!yU@'K|D]^W3SUa&b`6Iesbc1%=Sa3g67cwaUZ֦ ncl5Lw-^(Y;atIjL9s?NYfgFC JMpΨe{ȯGϜm>)3<ӦN1,Ch! 0(5|!9s_V _v 1Sr9 ٿ2c߇MF=' UM8 +Kik3^zLϛPDˍX:CGΘ=JkTm9&ia:FLzzz"cYڍF"cmOm==`[Te@KD񕸏8>x פQ4E]}ہ kT=M LJ`m?V2qt Ăx> u ۅ }ش08n>E# :~v|;0aΪɢis[bY:q_޾% r hi:Pce# s< '8[H~p{ C:6% PщRo V\cRaμf \!7"{fe11hS3SUa✔BBYD+΁dL4Jh@@$3PXd@8  psu UDq2^0zl o%M!2rS _ g j!/W@AȂL,<Sxl"6DQ8M?"+9œ1Bz<9;9()x]Q"!&!s%b@Iˇx/ , JũEDuY4 AV0P̔F 'gM|k%0COdwGe^W~76WӮ'ݻz-H140dzˏ6NZދeP4ق=om3˿1Trb 8c.:r8@:q1jX)P!T (śINfiz;`7B8A4مOl>J`}( > ~ף§Dr eU(@1_W(8gMԽ6a爵<r"! zt6?=u@@,bx <>>pgc7?'FbUdT&g4g7dؙf3 k$Y¦t#YfYǐ@|u)q0SnNk}_' Ϻ>?qVWx΃j\3;kڝ_|8 ]JI6r#}%P)˲L7,(WZdPf8N9A"+ ) yP% ,`P$E6i$5, p3ouMfCL¶fx1xLۣcй Wtd⿂UbӰoLMkB"w˜W^ P!zGj*+6Fɩve&ZinDB,?;^T%Byv9Tq[` D< K|:Rq~26 i4s87l< 2 g/bR.6;܅Xç=f"_>y b:UI0ݍ7fS.`V.2q~-G'f3x^PL_ 8/LKq;Pp]kh2L-}rL1TyWDed'^[ O/OI (L0haI)ia1Y8ħcPE5sw' 2 ĸͫ|Be!I9Z9Ԃ\(\d'_Zѝkw w(HM N`)q'KʼnˢeGGG%f䄛S1 )*$ghzfbє`*耀bZ|=i𙉯/fNOqMJɠ%9iii^u{jw-8<ꙎjFP*N3)=| <@L"(-PxX,2'?lax'ٺX5(E9[2ɟu>KӖ$!%Z2e,vFσ!ؙI`6XBS+Y/ky':$Vo?Et̓u߆~$ c7@"#hie:'ZAKyL 'cSJgNUo]=o&8k>roK;y={em3:f=W]705T;kWr=|֞/TNOq[ZC7<:~'\{rGT٠hpP[뤤3^{|I3=޽UhƢ k$>{{5^;c2>_/\]]3SRrrrjZ&L٩e=rxI,'5L*K J+g%e2mYm\&t.ʼn .fPJ'=݇$$Zsk"9\-6.kZ.b%p\O4ܘxyX?SL#e%wd$zv*M(0 f[< 5G+Q+ -tf*A4( ȉ[ QMp]7j]nHr/_LYy”GnƼZ詇կh\9}_``L)G?LJ:2\=s_@`dxf> y%4<3;^|T-4 vS L|o_ոG/S=tRC x?Sr2 `UUpPUPv'Q~GYCYkO?251 {%V|\DxnZ*Z VF#kz.v9y LwU.Aw7ɵ:t謮lM}ײD.L8*yTT$Zz5;3<7KV\RE9*9?"WuB4;~Ow?iNkx>U ahTO< fIJ =ޮD{kpUb#+w'9#[mǑ>J^3%:n9x]û/ ԧQ~JgyE․m`Bs趏T$K({Gc* *520L2 37s儘ā;iH<{vy,m$<_fl8\{lgQiycژ^J\RV\>+C5;]Q4uk֬YL,uZpuuu-]P&xpM(@w,@ <Exv A"pG"YObp':X]VvcWU QUFmJ1#Q40S#*gըLߢTQBt'vFՏdԮsYRU.{nRFkjBk%ؒy:8QŢ[**Fe`T{3)S7P']OP4C#j\r})ܜuִS~&u@8B&7AK,* k9D,e ,EA: <̓7({őm7GMz#!lnxt[%uOGP|M_<ȕHBDb/&^*G{V}_QEX.6 ޚLGi}_4.i fK#vGۆLD##qc`Q"-|z ;J.M9=I;wҾC`vcSOb GC^oC;]䴞` ~c%@ѣxCӬ7mp0SRTKsd4[+LvK+߱xwJQ^AXL,:F';3 Js }!ߎwPL@_[DXԶH7+0p#t)ğbhKiP2"lu#FghBIPxk#|8 |lTqxVvb ["od 3 ,T\t]J, i+Np 9RDӊj}a)j.5B?)G:v5(9odTy}EKp' x41Mq0SiqacH\t*3bC=nMK5k=:{i2}L wBQ,~֗Lx @Df3`fSO=u6i <;HCQ;yIfZd-'Y(yScoDM"lNc+#j<'dgd$a5e_FGHo~g. Y3QdC2*Y7oCv#o!8шֱw@$#t( Ja'=mZeS<ʒ5IIM)+ɓ2R_**=rx$OV+ؘIMLOUsB4P4R|ܽ]pTty8tǙn8u)X0ghAC5zTTd<08"#;`Gx^\ 1UMgoq䕂xoOG&qLk5$@}Nky1S #b\o]prJçIo$:f @S(X+9YZ2,\X65il7d9bκwpY[m{jڪ]Zpo={ T {7N?y·) (Z>rpIM.X527G%'ybvL„_RcMjS > Or5KY:U'L?N mz~iQ9{`iAU&_ 1_ܞ=lӑ\s3&Feݯs4IQYylxv.3͙[6TyEihyM']UPusQVkֈ].]^X`*3SjTDDdddlB")7;3&::)=~]c'ed$NjNXo>fC T3A>)(D4YA,elMuGѴDW'S;Bؠ%04uߖSu/C|kxE7 `nKu359=i4p3xcЏH-xd#~|z9*ql!y:ՇY^p9ߚdޥr0wS'T_Pyۆk2~zېީ .5xK!/a\RȂB./T\2S5WKtѷ]ƺ;S]؜+Rb֙Bߐal%3ӿxRGh/+\⧣ע9:wQ)05YlB̪ ~}؁ۍ~ }$L@9ҺH6\g;CنJ@lOF1Dl4Gܞpp|MJg۞ 6r(-/6\0)22ng`L9`\zwOsMSқh_!7*i Z!GL9ɕ+*se :}ĦWwg 3Ex[; hJ?~ln~A+35ZIxuN $@x[= VW %k??Y e}h-nEO(5&tk'VB!LrcLD }d#}"Zji3U§1"0F@rFVI8w]){;OG'$W/}_)-]MRGRNޒΪ`;Ց$Ҙネpd_y a'mz8kS*-e+Fj}[U.^T=ڨ]\=_ Kijl怫$i&<_0bGwCHii*mRf&!ڵ51V5kg 3%&e&y;2SӫdL !Mf^3A¶I<뀜 "ЎNC41e|1ɛI=. !Z?2IK7/ħI<>8vAۤnR?>.+_I!NHHN@$΢q߹G%DWIUNO #M?]ǂzG]4\:]뿶O6XS31ӄ dm"#v,&X3q#Rwzwȴu+2EW&3mf~8<|͚L^?tȂ@ K`&cqq+rs/{\b Y͵7LwXp݊֨W C LHi V: rV: :C_`l:ԥw{=_=_z[gJ;h1ON~{ձefv9z^gX'U'gRiZv hDMK$HL<|RCm{qi>l.3<=IżnPR_PS:+zy>6U"uv]7+Y8ߋSfNR߈ƋsUs0S愒O-V5~V65ɂ3m8g$o{ٛӓr0Ӷm(7-//޴82Tls8f*,&g}M* 23-Y➛;|cx]Mg}M2S|~V+༂BRrS֚6ggZ~doϰ#,XKje&Y '!d#k|>E؜!3(=MNxDL'9c3Kԁ!"i௺w)"jh48ܸIhO;ek>ekҾnm=v?_E|()M:04''Yʹf?yCl?w.m>벗ƲDQ޶HTp_Ζ{~' 9߸"|NR;XigJK9o=>1 ~UTs0Bwj/hձo_h.<ڨ@s4֮p[?̖#p۶T3ib2_4̌͘ZLS,777<#diO4bb,RS_g'h10ʝ=Qtfݻ]E#TXtZfL?'}/2kO@z@r;-aJy7<ڬVfZpz7bVՃA\Rf$229 fBC %gE>aӁ > `˯أA'tDx!^>Z,Ű OA\؎YouzHqTfkSāy;—Gq{^Ez}XTz>|/t/K~7/Y"dB\ Z(w3z^"b^^|z'aw>ǀ,|hFM53\PhLaÎBBh7ov42jUS~~!Z(5 3[ 3]o~6i-ضPDѧUp&5df:~s;&3x)8;OvCW3wgC~dmE[;<K zNԔ f(ૼu7Gtlѕ+lI)*@S\cWauSؕ*x fbb5q2զ.'<%03j|՚-vx)ؕd bCWUeL SFo<$I<׶Ύjf#. OD%{Y- ¼]o|v@n~VfIKd5@6CdN%3 ӇFIJNT/-tI^lT?C}tN4i=ՎcF[ϥON[h 1<˜Rͤ>qVIMW0}pRsouH4__@!{ޖI޶$ԓՋ3ڪӧۨ6ρJZZ'2p&yMHZܪUΉ5]6nts5As۶y|jf ̔gMWW%fڟ~Mϣں!=] hAkC@cDfLDKUYtx 4Iڱ2QDȣ5̩qT-ؘMJ+4eT/pS.T]44!h2b,aիLϴxL yb}dEAA9bpCLG]Ʈ\f^Ǹ.q&՗L{Z Q2uڸ@w/{^˶Y Ҟ^vҞȸzu:ZLοZxP[[kP~vlvL3oU^*e7_wNE.=n=z «' Xq=WoGZ V+^}rG@&9b_ep~ʓa)qQ9 əRV;4iIB234ƺMLBW1H0 W9gl u=Y]c`^+3iJ)ԩ3ax$)5!3 r10s4`HؙF_9Lxp-iKx͇ۊCہJ%SW'}Æ|yQX,u*yۡɑG499i=[,Nk#OTDkT`a5}˞~UjrfZ)>1t-,nezzڵTj!pݺ U` L22UmԮU60'T-ͳW*9)-ԯ Ҵ'3Pk-7QJ36e&CCjb&x2H@N.]<l?33(q11Ѣ @Y31꫌,`|l1z(Æ26Ub4c&~Ԃ󖇼|T6-dl$ea,2]`l4*4.JAXN?O7 ʾǟƮK +]M8=/8l,hy{lO9J+ k1~Aa69\4hfsn#3 Sc? 'vߣ.4L8zNiFU9i²4??cUo\ϊ;zZF~IufjRCgm>_VfAK5:W3 TI'SW3: 'T$ar$Ӕʋ̄1y <ɣz7A򹎱fȥ瘤G]c]|f~$EvMH؄|ͬ:MY7pRLiE[70'T-]bckx2ZkSk4n9BlGE6[j32ALsAM\xh̔v -s.Gc>`Wr>Mz1J;v!46EmQIØI46VVQ3NݺaUi)J~Q4v,wB9+ H뉦Dt*1SzjDh(q Qh򚖹ÃqfVļV@I LύFȽRv9? B^K wFAj4¢9Y L+n\ d+BExjAi/]Lje&MW-Շ"D~;LB@EbMd ".d)'aɝ:o^?f2zZg2i)!1st\ܲF#Om6fԻ]BBRvkqڌLP0H1U!F2@23[WiE[1ӆ x>jWLAeeGi I; Lƌ]b!mc>(/JiWFfzZe^d&fsiiGhK1h́\370R52R5m]R,Hɽ|ƱfR+3i̴@Lgw@Nh,G0ņH$"d&hN%ت(RGsVl_R84m{юANxn;sML(Q_$ m~)z*dj(3Աw<=LƃKvv]kI:BGsΪfJE3ص niZLCjn)?_0?gI83͚]!qD17~|ʮ]bgo 312}gDLtr47jd&`@%NYEOM힖q 3d\Qi h(f\`&8%tw 3G_ƖiYrwdC3ͫLbUZe/'OMr~3%7 r e;{9>E=wJ(x;NI :_פG,61ww懂&Be2gfSw_3KMŌ-]]u$t;d'\TmTVTTڎFEynLYA~~ lF'*K?x7A͠ژir fڰAx+F]fnj303MOg'NdoʚCJ۬^qڢ&3e YVד**Tff T\2(ddJKLH} MeNjM%#GLffc^2Ο_l i**-_~^|[u3Dtt fr:Ů `3Y;_}GM,o2$%>U2\ܺ׏Y j2SFjŠVfk z0S$-t 7ٙ: BCu#|&MfmY0 }Ď?:2Zj?L23:9 0> 7f_<`+ǥtvWv-Msʻg2B%'go^lsK#ҥn]s@6U&6LYƁźL-pd߼ݓƻݾ݆{S\čNj%$$5f歘I綾8iƍO8R$3-[n2Snn-ڹjUUGI#F3vmk&ܶnJ.%G2ÜnL99|n?ę(Ԃ$#Gt.]YVL?/h2">F^)9!&Ypu8[THf`ʴ+7!+3]69>6W_NN"?V~u/iy)J/gGxuGs[\vE_ƢV9{l w璞){|81Wm)̻ppf œY?0%%EfR+3U =/LƜhųg);Lb&zR Nׅ Mnˌ}EF]2P)&L&@9):X^.~ yiflΕEW89%$0q/{j bO ~`TM62 4"psbiF.WE!!{ܜ pDT۵̴Ig=S;+/uEob&CCW*+1^%fTق<֮Je4Lin24,OPL 'eeRP532;#*UI=%.f:{ED3oB_O/M=lj43ɿ2ՌOec+KbcGR;_i2SvVFWp EF8Z^kb/5+SM =UXit).H~<ltj8YvZF^QZfxGvZj޽x0jKtkv +^i)p+vowiŁ]=[LwX3zpB@49m#lNi4pkBT4 9?It df29)hquhθX,:fvݤ. ŕŽ=::Zj?;9;Lkָn_e#1/>5n&-4%cMcj 3AK۶yApdQXJcX`(Z،,:Us.rLZd^R( \#jGaZlt%(Àm^n)/f^V +)~lQ`nɍK {/sșCEIxtAaAkf7" qYYFALU$&3 ܩ`&IBbI<46(Mo:@_&?IMNՙI(@Z.SL 3f3pDRǧJ]ͥ! Uܰ# d: Zhfl@_ea8ljg%Y 浾W]ʕN Cæ20p?- ݂\Ϟ(JvӚYl=Z{ M3{bVg&A<=E `DK603 v옜 7gl MrbS;t:4t$`SQ#Տ&ZY)KK.]OKO1}N^C$!޽23+CL 0!'*ϔfHOHJo|pO]Q $o5;-nk)Ax_Vs=#04̒Š4״ x<+3Lzy H\Oppxlfzl{dr^nF3ƿ\.^8jԨ={2 3uGɁ!08)X cC3ğ)۫rBkc& 'Rفq3Pv4K9J=ic|)؜sLK4ZI QC""Ri؂Ru#8QBFtKfR++1Tq.3ӛG 窂<YXQ]P}OxJ7nр}+WF!6nӆC6YZb$()r /s>؛^NUR̤im7;LLл`Ĥ\"Y^WI~{s Rb"ئUCɚ 54ژ A6'! @j:ٜT#3Ao}Yx: ?Aƒh׮}BIKP&345txAs-Mf:gJH%…T;>HՓ'<:M' 7ԟZyHH<(@bɈh 3A J@Nz4E O<իlQ`ЙCUUD>/Uſ'u1tW+F ̵(5 3ɒIXL33"/>b:Bta&l_ f*(`/NmjWu>WQI”)띂6f)oLn3T̄_yd͌5dkbzު3ʹTbb{vnZbR{&I]^J&7ud =\ `ޔi%iS=TWU<~@TWe:X nJLTt1SZZ-['Ԉb&t={Y rU k*fڸQ;R{__?F24DLP')i֐ l,̔=`v揌Z0AL򌌟hɫZʤF:II53޽uf&kk:ds{i*tQXMс./Ƶ8f*nqZv2TqMc 'i )8(;ux *.fmbl{ŁVB#TT#3qrJHf@L?0PYdJq2fBk/sw!6L- Mw~>꿸==053Znٳ{t쒍X)ffcYR||U*] tkL9tsl-3&;"j1]1ȞhfBJDtkM 6a'd(՞T!BBTZ>&~L%}*̔jggrruuc&cְ f-&c~RTRE-|ZX{'gմIDZA6| v\K/ W܇ڝy}uPٕ e*'-!tE'䃙<ϝHONT}ޜ/*T*7O en`&{[&3zT1vf5mg샟b$Ht<3hDwZs 2Z]ę)6"01#7NJІ*IL gfHС$ccxx'g'_OUI__L3&6T)$YYqifbfV f0mM^O۳cS ΖP HW&3IL-tG[U{؄UYm[s9t)*5`ZIną}BϜU^蕓Δi>R^aFzduִD!Aؽ{Lh $ԀMT-^ Nh@KUa&kB708 )52xzf`b6-Pr3ww|ݗ-]Uqo ̔/Y9|/񮩻B!&Og-Wի) ۵x45)0Ӽy*f¯@gk)ܻwR7sٙÜΰ|q4b;tA'L\2J77QTq~$_)z[`~LavN"e_FkvqPnqY_&)^|1ag\VlnQsؖl̔aժU3A$ 5Ȓɧ!Q[#IF%y>u3{!LBTч䉊c&H¼œo^[r9㍳9s/>S6ntkUL_>:?v̐%V=щ%&56N3Ʋ_3==3$cczhѭI_̄Y^q'SRn`,&6AN0…**~d;=5l=cK##W,] ?-SEMfoR`&ajdGa~bbݻXQZ`4UdMCmGUn3#b3%ņG^3TOgN˟+YsRz ]S n\JqJ(.(OOqJ )ơƍtԬ`.+eYɊswpg@OǬַS]cs'\Yc MƱLF<՟  A)!1Tfbp,̄_HL4FŪ*_A,_W5͸}~9̔7~o. [Ϋ4Js/ohsg|^^/fk֨WkoLi 3M̙I3iݑh`tS23-X( fg4 f**^jZ8ciރlEEeH22roE,.*.5ڶm3/Qh봴PJ7 ßd,)Vpp`HY;3iDHCyo.-!pj}o`R;&Iy:yۛEIcDQHP'1IRuf@f/NO&0 LI3SmK-f}Q \e3~f [Kf,9ڨ4ݣ2.n74LSDL4(33%P6f\bAK ,"SbMCK.s@Ctfҧ һ=1jl ~&9IP1xRST1>:tЪ:3uCDd<ZBy3C/\U꿠<=?p̳{\|>avx?|n)bGTYoj{=YrPm6ՍacXl-T3ȷ`&Fv8^h431Xf"k'NTU3e <U33ݜtKf5^53IG#ol\b?{.ă=#;x1Va&pLXTe&PI0,"3SNVFg_)3%>4$|QqգMtV(- ]pCu{4#csNbBzhB6[q7H#khO\QM RuMzy闃H iHV uu0cv!f5k\BC+cy{DDj*=!էU{ڙiuz4$0p<}^~ G3ccŅC2ӦMy7oCHDlk2Ӗ- `gWf*.Kx3/Z0k7JVVypFՙ)/-+ 513!Ѣ+`S?rSc3kƗT/L̔F*<*0'3=4$$Nv:_- vjv¼ШBvbtXpphZN~fJ|0)&9#c dyfEGuM] jYU4 8eHE >Lcl7x9\4{Nw|4Bvdpz>E:jTj*RiEZD;1ן^}L8#N'lK23]]g&H:3A*L33AǃOMdeZ,vcsTj)bCBjv(tŊh֯ige - _Bq̴Q)(hfCE{UaF34!D!+zݖ-ŊփY Pufjfo^FB)&YV)aamG ?ĀѩNF RjaS`hwym\/>ؾ-cW :y䎕&]>NڞW@iqxmU3Xy k/2[k_91 >  Ƚ@-sh~=p߃ls79b#}- :R f8qu4B@_Y]ػp]dw$c3WufBf&\˅k.^TYy?,$3ÿ -T5 Qo׭< ڙ usؙZ$FZvWl550I ԏBC-v|$14vBvI$&3%'3 9`b)LД"v9-^=·(p25GC f?7+3MpsՃ`i4 Kvgz&DzUDާ#vۻ-i2[o]ʾ?ktWf:s$g|TS*=g6ЖQA #SR֌eذN 3SVVVZ{m3~˅Iv"\᪰CSTD=qnR 7c~] 'uj't(`B霁3B(9W֒)u~g¹GPЁZZ%K/6k2aG=/#+V(TZR2!U q#)V=D#y^HءPVDC )YOWĨÇ¡hak*TmB)U~Z^%|QN)Q(n؀`]WnP;QaAڼYѷ/Ӈvpc\D}T!|K*,,ԙQZduɉ &+x)?+lrn]M"6X,>ֲeLVH|dy*Fx1B fue5q$s:) ݙG'qF%֏S?|!8\RX*`Al cosROpSQQ9̎< Xi};>H44yٳ܃4/+'~&{0kYQa?mOƆ^Eӧ KlΘAam[[5so6%KT;S(#=̿#Qv-3?rd[>lo?Uхε?}Z+6wW5oS{) g L1!~-\JAVVgZTv]!]RIeK5|n[Hڼ3ɑwg$6*6#(7a#g6\~>kKd\(ݽ_}^>g#COdl cm{ۗϼLdB&7?rf16:yL^q^-}#>=h&^$ P) A]BMa 1W>y='%gc㪅7ҫ?BU$RM̤imje]I=*kG՞={sy|&&$ӫQEƟ}Fv!hk] ,2o}2`Sܦ`@6Mȧ<b_it^!lmݑ1`G 23 3~}.P?2&Ea{kE#CS\ɮPOMLS*Cs^tJRوWEAVV-HP+39§A f"=Ŗ-~,}3zmfWBȳ!Ty0vؿO˭'37DɔA̋N_|m{^ghDi:T, *Cns; ۿ'9 /iFt$׀(;wXCЬuJ"[G=EFOXG'`}@rZ4y|c{v0]#nj?( 71&ZO1?%ۮpOeUp&K&a%4 :LpSѩ-t+Lsx/,.*ZzshFy,ڤTT'73GH\{mO=8/O6 KY-rLDDLvs6umޱCY)|cfc))Se'3^@<ǣ:<-W<=jh:3UV];?#pcdVݽSLQ~! A2-ro,:jp%{33ofE_fXx(:%峜 6g?K`7QNDͻ4}b sDG=!:pƘL=CIX=cKtũ#F6;k:sHGZe3^&IWSv:Dکe:U%2=%)&MLSx:`6B=$ϠhNTOM&9bKJUc8>ِx9gx/ts^ L'iRpHXHfIn"`s҉bR !R-@%V;`l`MLZ63Uiozge,H@Rz!ch_KTך &z߿;UIiJXM+$f= X4Ix`&w̄!MfB$L&,K3Ahl8cW: p,` UIA=Ur#h-A^Rx۷eXVFcs7>ڴk-_]',,8v7z㡸=R8Ԅ$$|1#_]H%fJO Yy!!aQw{s֖xr%R|J of2{[̢2XŮ_f"9gIu LO=+e%Ny%+gعI25/3eeenM#i,QjξI[gcj6u{4`)$w8~C(}JО PS@oke4D :?Ǜp446X?LMdLҢ/ӁN^P[75ሌS#4,I PA\ ` ?Vrl#Z%@'~Qȭ[kee0Sf16Ɋu; ._Wln|>_dX@KG|P _ˎt]Aʉh^Cl> y^G[LIY_4x  ]j(LV<*->xpKL *5*`A ţ||tli.lFf&H2 L2*H ﯂'!4TILs]\&*߬XhfRWPɭ 7$3459y 7%f,{Ҥ#FH}0XD]wgek |7ݸ4TZߛ| |%K&3~%]cFëT~>Joo>w>o|+ :D2Է# i[7|n39J5.mn*!_yx '?y}u7 #AH yxFܞ_”4'wɘ3wbN3ƉO8tbA|oNDӌ+ہ'jcs]{hS@XyArdTC煓`&Խ*BLhRRd[ry9D'_0ӟm[me˴9C'^gr.3e }N\8%39>&ߟAcsZ2s̛=}G9>F69gDp筴5+>d|Q OoȺA/Z6kԈ9Gnhfa4n*,*Ҽf\so9}wn7;4J50SQ_[G{QGSşZZ:Z5'x.cXhMHf&q"0L*!(MfرJe޽4R23\zi{Ev MK۶y>Vﯭ[mٞ)*h>frp3nLJF&So_3=df*,w=tcw.\}RSkUϮpb;Υ^t3iG/Z5+53ӆ%Bna~ [Tnݟc_\nA<#"!>""9y__&F/иq#盪̴*$R ..V|h.H*LuVRԓpGdd&>\hfb"O` CR\BDZMLa̔̒bJhT/٣[QF)0bN9i~ݚƧrݾ4|m)RUc **}R+3|fZ;erggr|?)_hZZʔuS7^a|BJ -W&O/?>I8 dE є1b1dyCPDZӱ`ÑdG4"f<OKKhW5 \f{hJؙPȏ >p}>P27?U+3Y??osdM223V3w_vɧ=#btL>'l/1s8uSywYrv򭻭q!kevdaOI˓"7pYd}le,,7;x`lCcwlѴ3 -5o37yXlZ}zc[f؈''0 ''~QY*_3Hٳ1E/5T:}rrUfZJJP T8b &&gK2A<ɀ3=yJ b&!\ r%TP+KJ*ZA̤w fͿ泿IS ^|QI\~Ҥr;vf2ugesH2^Loqͫ*oeigp\qIczN5ϪkeFTzNaܸE_ӈjmtF%(/͍_zz f8n̅مZ&3~˙IBi ? /9BUQ zƧÓWİ]1LJRm6+` -\B6LJfRG%U7נbb ,\ϿG|aX i): ıbNӡNNJЀEC pgA(7ޕ$OK/8>$ @r(PR-POfB+˪bX{4 ,%kwZ3ZX+s;\oY|^AH`XvF) +;l ?ᔅmZ^Qr{@qKOSlrO9kaad_~,ԼVr)RǤd+BCO[XXu+* 8oiy>=;1,:%ԋLEaA -?:%q0;14*9rG_ǟ|彞=ߖj,HP4*Qҏ̇/0τ؇(IsxU*+* 9 |W|ȨTV! tkfߘn42Se{7 1CFFTf5rF"' a g SS .\jf9&/xvD_w 7·&ffٝs&LYꆸEIsfN4dW˶(:K8veKyQfc3qҵ+Tj׼0SQ#RRnIj3YʛM'HN;VпZr,^74 !&L"c1T' :FL? FyB'Sƾ7Fk3pK Ȍx3IuC812V.7Џ{2#"`/g74LZbTJv~BӃ6Q̴lM2*5 3iд&`&Yf >A3Aىai;uVbQٹKho]zs-CQ+Kg\8mэ fHLJ(,s:*oGkYݸmtl>+FJΤ$Jn>Wbv;Sz:_CBC'r;~F2q_pY޿2]?. VScZ@'Q4U!Щ/RjH?/Hߴt] lj =COSp r A{@iDg?A<ʓD6.0"oUX8~$-Lk)Cqpv4C8]'bBO 4bՇ 9PSLuK,Էpe1Srvi8!+a#UgTE4p o> 6~)՞F3ė5L CU$/֮}J**R Kt9;>wtn#NxC b&ajIxL8Ѧ),# /+:ʎ12.f,w)3e;0uHI8fR]7:/iWsRwvƥ] )kM*tlVƫS ䷈j+1FO[[?BBؑz--{Ho~xЁTNKsI/("ACw \A܃4wDQ5VbP $+2, B-\h権1 G6CV+l&ްK|X Cpj@?oX]*(zn~=&E.|9a( &=~ W[ | $? IdJ셽E;+p 'NL>#۟hn!\H OPd:tGU!*+Xl1S%ee0EXPvJ=ks詤ۚUmv(|0S"$8kK J%,VaG$L]VIMb(XZ|5@8MB;+ryQީ.ªVfjZQfRDL $K?.vFcsa|o5Lkܜ ^ИfoHCd&vGDܠOJ$? sB+ZLgj~n,SS3pW]&WӟŐ=}L-J-BB%ż6Uz~lD|gR} <>8` |P=@>P r{2"@Bd|B8 OlTH#@ b)vBp^꽹䁆\x4)(DbN }!hO'T*!~tGW!y) B:DLzT&[7mn8[NJ}**re G+G˭Ԅ/a3Vėa~>YLcHL6ɉԩ Raeˌ:4ifCL4&38P+Dk3&%SJ%ZKf&@Lp53A{8;*-%GVfj4frvfқc{8M˫/|M;fp 4D%w4S Oϰ9%{ZdR|!zL6}$Op#D?IDB`9"d CWo\A̭Mw_tOgWnBdqRȅ"(\#P6܆ mi}X )U<F(4 $$ 'kra"B;]cvuRu8G4Qp4}_vq \tx)k: wšLf?muӌP/YJqOz٢P35|2އ/vHUn`|ϼX~7'q 53@{{KFzggS _*CЈ 36-() e[iE1+$3~Ϝ 4)H 9 k@`5$^j]f:t]U~իM أߋt_Ry:!~rwKfî4'M4=<&8y(1Sn.AӎWOZpxZOt`t<`G~!RTC ~ 2J͊@0 8x .i, 9&@[pt;S 95 ~: hi֦IL:p`Yӥ@h@BLp!ᢞ5(X!L?e yѓ-Jȅ<\TO*QXB6۪hFAH<#"4 AHXHGp[;Uh:`4RRyp_lB-nC}}+5 Ae=ҸT5[vTXT&{"IL۷va'yVVvS4O5IEF;L<fO*}OWE-B2D7di2,x]a]jj*f:v/h_}Ll@Q|&#linctfsؕʯ;M2q3 )Yu<`ҊS΋e}P055kd`*hTcDzgcszclZΊ3̙-fJD?|3-)h$S敜Pd&)[-H&aݻЅRRR@Rō\eZAJJ۶dS^Aa}dl[l[e- K)ئ9Y6ץeMMQ=NfÇRG; bfdp(.0:*7.50BVreWC,PD2ܖlk̑nޜ,vͿ<@G7 vC4t,#³*`_ڋ{S"8d  g&8/;#Asmc44hYQ~"TBqfRh h!oU .ӗWC&,!z4hBG Oh4iu[}v+^rreinߴvWQvC &I˖Zr)7ch TKtQU@e63A1VV' gv;XLÇɈhfj׮\;wN U.goZQμᆜF3e&nGWL?<)?B.t`e;2xx'B|Eh@( 8L+FԿx'hSFsODdz?GOO TWDKQHV!h'n{ڬQh5Ӈ۔NHޯXNJwk]:RoeVesul6Liu'jPS0 lV'С*f WEj43UW5^ yFL:.Ωκ+*Nf>3<ȗl f!hGU4dF;+14qb#E<6Lʆ1վ}U^b[HMfI8̤) @NɃ@h/R4J%3%/~@O(e`asBe /JnRtPdM: 5 p,AT ".!Dft4bu"{33PLyiFjx*UŁ ((u遟OȀp8`St88e'!2GyOQ-v!B/d"΅@ʋRC_AWp`JD]!\|Wr:ppjCSN8iH@x5퓴-ZĩVfM8fuY@Nt'=V{LRl#G #z2[;0"}FfZ.wf6575 3A=t{̤BKch&&@ vZ56hfK ̄?9ymjsSRoۆJ+35?fG ~#X?lRs `<__6ExM0kɄ~,HIeCEz6Uz|ϫ'z5i1ɛWO,4-}b!HmYzb)oaow"pAQ1 9K|(W(dChwp]V@H8 PG1mF]O_'F?s(4DPnps ).*tb '`xlq Zz6Ñp\)pȌCrm.rD J>N4Pm FNd8(ljpBgmV-jeT˵32%hߜĤlWOTԘ-e;8$83tq"M0[9pzR9_k?D/0 6?A=0*Û bq2m2Sbi!c/7)H4&=_QFbTVS0(Tܐ|Y͛wm KBP]c&uêxl\^^m5l3t̄ciٳ4M( J҈-U3oz&p0"%SۈݏΙcGsßu9NZLFѝ$j8$5 eR4#O@p $BD At- `X˞%>K2p 78`/LdD?hX ~Ky0H{&@j`ɐ@yVO%I|:tȡSM(ms[4Fq/;!f!DؙĒQ&T`LHJ=*+@ 9xEp%8OPm: !q e !WD9GW=)4Q!"ڜ1+j`%?{Oz58>퍫P+3 NO]Lzjǭr馢lԺf`&(ɛ#qaa^:BTkӇ\/uOL|ȿ5}zCu ;VmnL5Dv 7VJcc[MIj{G 6G,4q!صq4][]je3iJ5ow 8Ȣh,l<5@؅ꗯS7 P3L`eR|IPp`2+1ui GPH@! F^%1yWB@N4N +hJ;/2 $@!ECmƧb:Ԟ׈m[naB$:M>$A]C=c]A$(eJÑ(81rp*k-uN0SG6TG=YN%!_ː^ɺ Fe2!B"#?/\33Վ[^`:CJKKk"fŗ/ϋ^۞)W `'n3C11$q8f*~+SgڣUd!Lu 2|Mă"*B}a!H%}fj^m1S%M~Q%eB zNml+35RefHk#І+){UQC4+GGv*!"D"_l᷍Wg|13͹H?K?*uD`ǯBq0 ~ld3W|Yψ*^3T[^FhR5IE[.iOb&ܻ(uOH9!$TB'h4G1ke .<p.8 T,q%'@ O.wQJ Sf6( Zpud rZr-k+ILx6ť!pf{4qJw?thɓ8aWRsrNfjm̴FQtVj(3U>Վt˘MLH>>>L7S"q k*DT3 fzT8le?굏ӶY]g&矯QPm 3m:f1"ljY Ogz4ցGj3)000666%%%I *U#MfJ)PQf^NFlLL"0-1KI%*ՄmV8܅giEl uA20@Ϟ ʔ^vl{[+c1[.=" c QA3,8̠l3;N#sm1K].I~n—aԂpuKͤ;p~2@ߩL )d? EC:- Sԇ3NhȅG:'DC ~zUJ 4 ÅEP+#~y '>Td6^'nzA*&gE|3u򗾜 3@ԑ{k24#&-n_k4=hxNTb(瀃:]( \(Mf&8aaB6zb L`B+Np/|1/hMZ^C4bB'D?_8ʱdiOi p]HY8ъ6ZCWphK{uiqi{bU0B DV+j=LΜQb*cgUijg۔%D>jdO-MfU%SJ"b&f۾WWl"h 35 Bh+st6;6ߧ80-IC#XS2/_r/$w86nە|)΂1SmmmBBBbbb@ )wO=LWdeG/C|#=m~=f>paf_uO0p?B $`aO)щڑABO+Ee Ǚ#܃0*N?ε4np1eۑ(" '@`a*Y; H2yٔj*BCc0 x#?ZnhY*&fs)qv< >Tӓs&@Mӯϧ)?M p16>!Eat 86X áCV, 6h^TH$_ M^;uJA$֪[?Lh%b32dLLM-MfT#3UQZJ3>COoߕdBm8LM&4h0 9:Uڷ7ηoLB:N}5V^7{6~=;eܸ$KK#],jfMrL7=L#dgGM3'] L[n"lKa#Rm^3ը<0(Jl-΄='a| {ҕ(00`<%QR{Üu8o,вy,*}T&2hR0T88| e B(pFJ }*"4v8XBCdF].QAuɁrLou8$ ,MJmbWVM<++]vfx?0Nq oNHTmʊ^6J)$)"Ly;m|UѺQ1&G".: 0CS7uO3쨣|VS/@?b-zMFKznk??3g27~$ l0> Z'!g;yfMI`&TBIڄڗ th'&H5P O r&D?H.<Ӆ`xR@q,~KHd~"C= Á~G:Db@Sp8Pu5ylv]Xrʒ(%N' B|Fi5d^PfB_U{j3wuUmTSGF2]vR<~`*츫̄(j77Nufei2z>clbWG6<ў@U$H)y3Ǔ^qҲqLMM fijE16ԵG@0uWl2ܱ38<*Np=&I{pw@\tD@@`lBBDXDhx4 S"JRF&EEF{^8=0-#?3A`%o DRp 9TwQҐxIБe <"$L1x 5pi8c<]R50o@?I\7nqQr(z]܊pAu¡;QWW!TxBQt(A9<ظ9ǤA&ֶ Zh\B"$V\, EE*ضsu!%Y eLc__F574nܩSe܈PZNN\FpT楹sFEZ9uK؜T.3AxJAӛ5gB-6QƊMc!hTZ3WIDoOMfk-)q}Ƈ_-EG:w$0*ʕz0SknnblF8)6̛d9KN՝uy M7Mm:yξ 3Z:r겅.!VGyL3e#-[8gړFk0Xyܱ[Xh<ݻ\|3Gr8l#-f%4%IjZO4&3}SzX:7{ V={`}n?cL t zh rzeGl@ )d8˄Y!bE&+ptՎv" FQ  ?/d'}Iv&C$86qɂaҠVQ>y1ӒMp">G1Izg[YȲYzLn $/j\jq`&+ *in, -zv4P-hٳ*iŋKT}ЭO1m۲lOOQ?:ut0 uwxsa&FFE"TIC7)kLݼqvyy6_iժLP\;s&}aE0Ӹ;JKр45uE LMٙkJ%OI`?t>k-?y{D gJ0I[8L}ɮ-+\#/L_ r2߶5q`۶o7?|@,EVV;&G p2St3yk' ;ک.ůt00"t%"S|͖:@@.Wj'/iI#^<ڧwf];$ݮ":75)j(13чG1CdLN׽LU6Lhff&訙ѹs2l%L)SgU*QSTYNNMY}F)pfœS!Ӧ%h%G**C63A&d͏ݵfB3~SSndڼC&1X?]v U7ko{sG7wѣun]38: <܎8ze 7rC.GpLQFڶw3L>'=c-t}`&cF_t߲lGdIWL8l Ccsـp"40|TF g'GF~yPћC?-LAv/AS%HjkKg)9rt:Pdqi1{p O/ B7©ed*f!)/YvQ>1ri05RA)rJ OHͅ",x- Љ]p8V q\)YHSJƼf.叿R+EzcY9Jt/@ѣJW+3Gw}pwILl++VǓFPvI'T)3~C Iu+@f'tQt¼_amR?BxDpf1M'fX4g1lixo2ՠ){Qeg3`5}IWp)T_`ďG%]~eln6{O@7 2ڂ Kex:Yd9ЁyyR A 1CPi.;h$jl 2>SΏSN͙ ~Q! 8ů"a7҉rˌEXAU-x,rnO>M_]!Y{{dS6*e20hSjG57TPXe+;վZtoAWΜ103Cـ*?_P%~O/wНMAspL"f9A-S'f);m$K*)AC <:AMz{f&\m $ y:&K$-$Ο a ̄񉠨̟ȩ4g)d @?'e14A+9FQsj&!p=Bs% 7 }HzlΑB^% z\H$'p 4L/'x/ '?&Ч(# KHA(Bh@4 H`42[s=tdVFT t|-O%Lq&\ue2S*3rvFdWk}j̴V*imdrr;:f6^ Ҧ:XwTxvquETcsƭTϝzÕL,Z LP=K.&&&VVVvvvh<<<<\\\#T+3-Mu ~#/##XO3P^ p/ `vpfC؈L 鳡@},c-5$glpdQ[g4xtѨH52LyF2ֆ֫ 'JC{̠C̃@-{]%J (O9{/[C 2!2|5!9&绗CpQF\Ըrcy!9=ob8؇+!cbNAV E@>9kD%Y`&CwwPR?x>*+(+gvj3M/nbc_"@|NZQ?s̄K+._κI_3:ȑ|͍]wUh0f8~}pk&V\fPz0S7z[n 5=DLy/^Tjf@hT?6xex:cn,LUU33㉫a|4?Ԓ鰟wwr'{UWfbCYԥVfje<.AB(d'M% xD|` \ E/2hAc.D]I$>"!f  䖳?34W p xSG#n8 /QX;3f|G_ĶdY91Չ5c/eW@3}yE[=IDATcr~RnTA3O'?o֯I0!0 L2ӽF0S~FzMg&-[+~ڹslzD)s栥:4gRDdj[33Ct)&&h̙R= ;d |ɗƼ@H@)~FTĤ ?Nkvz&agPp ML_M8rE?؀haGIR}?2󨅙[PͳGbq9}22 ە315 D/&M$TBļAٙAsd3×qWz$ۿ%DsTH6/IQje{W f;vj<3͙[*3m ._Tk31A!=XEG] mz biD23KNNNIN(ve$ zRz"59>Y  s9~RQ؜,yc3ёZ@ [<6}GR"<ȂyD<3ΡB{2`SKq(et  |/~fR !)d'=7ҜdOiSޣWJ529q]:7 ij3mz\~ ^4$ c2Nt(5JᩏO yjKcޥ3)OiO䈑/MQ44Vf:IUy0ϴ"$B3.z&Xd 260d3t'+NWW01|v"m* BEA V QHsB Um1ގ=w}B ]PX Ɓi-KH l~FB|Ih ^|fе~<adյkÇqzyy^lo[n#S wSUox[zKޜ$T4Jޅ8]Ig5V5t{w9 -4#LK4H_HDӯ6ul58A qHz-9E3mPqF7 ? `#B\bBI4,919O/m??ِf y򰆱|fL.4r'SNp/rKxݺ9dYyUbIpbRIC-hZSN,ho4]/'Otqq脄Ii ;vyliJq>Iț@YhiX[-8Iϕϑ͖Δ&Ot>qTE6ZISD9U" T;4ӳ+ E;zΉ&<t2}U|j3Cs` /ܟ44ؚKF&@#'i LK fH,(؜=؋hp3N)! V}}} ੩i v߾}߷oߒ_$ͭ''ͨx;_;=ysS2 L$OcmajJAu>':yu`N*Tn @yP#4hbH1YEK Y@ 2F@ Z%l-a`IyE(cɚᰉW1ۜ1×I#n򀃟6p b''S0{7&I߶_M96 %yIV+3ݻj03խ;14rųdr6yA#(>9[E# ĭdHK$0K4g3P?F Lpp$j^' $$YPH .CqG4hCI Zʢ14H'dAB$_0^vQQQNsm/TG@Tڈ-Ć77BX KlN6%alF?˴Y[ekWвsIJG-YZ b HV'Į'mv6(gb8Y,I + L 8Bk_$3u*"~hԆ?ldHZ=1?9_+8xFW >Op$RpqhS|>ppZP&5\f||Z]x8,P"쥮YRlK-8{tGZ^@ҡ{^^8qi84l=m~Mr8|7POwR$ EEFF^:<֑EHNpg:5a Fl aDjabN<8I(Ds>O|V dW|"EI3ͩBys>>~7}LDjQ9ٽ <W{,d£X\ M)T -ōtpP*Π7 ~Ұ&(܁Ot%[|P!XzŻ <{<֫O .X Ǩٔ*ib[z o R}^Al; rEF3BSm Z,oR副 8e?N0RY@MMNa}rk#kSUUS}BOq==]jhhP7:}rkcMSSNaw?L;whNGJp 88',@BX$dG`{Cƙ{Q[ bRD Qp[*bu'᜼ !'"׼41Wcmz!%]A M- \jB?:b׏ϊh oZ65@m 8';!uWw=n83\&ٙcccC0RYАNa ~]A&B:)]\\j֦S---:qss tfp}xcx9S*:{6TNa~~~÷'3eee:*)))LPPK)Ak׮Y@QQNaw>RbHH={gyyy:ʆl:38o߾Y@NNNadeeԍw4q&cBIpĪ V ƙ'8r#Ųƶp'^+k:K# F--bOڋ q /xƙf.wΙ@SKKK:S:Lffft #555SД;1He}}}[h:8\#v<|{ݥ$IGGNa{.I`-w&)ܿ9†KT$)ׄ~:R$%w02223iTběTZP!L % <" |pVDJ?J$Zm?doK[QDGk:CE',_A1.<4 /I.G+030ҙ&Hg#i"t "LDʙ&3M;L [l足I\;XԿdX3kjj]qD ߀ =̀oT2ʙ0N/ 2bAǫ[񪜂|UigS#~~ < Ӏy8ca d$]XL<@Moӳ8XEM$G~B/#u1lmrPA[lW-D-2+ݎmbjm\{%Yd[,nۏ(W8 l *+ts&P/Y~IߋMy!.߂?)S!, 8pa)҅ӵx<^0AJ@trLpUs[e| *k \~fE| goJT1{/PQf?3^=ޱzd,=.G+!nc$$10Ø>4msnd%m=❭Ħ04PPO$uJl "Xk*|&;erP?(JO2B,LX,@\p)iN3Q "c x' l<$LvO R L?܅ MCش~]/,$zHţ')&n46>Ӷ3df~@ (q8̅q&{LTۙT3Q?B8ZN{܃κZZ~#1t+xP% 0WGhҐS`Isߖᆭ%*O{i^@nhUrGxKp[!s1qIVhpE_ ;ng`[~llJMۃL%h#X=Wb[ܴpԌ.^6ܟ t T NuvcJ|>xk05; Ȧ dy}K,PҎXvso;ӽ8TMu)E&.#=ykKknNѡ-⪲Bfu3T*ZAZi{7$!>zU-綩QwH> (],5yKaYE1El[e.`]C 2$=ʲ]tn̈;V[(˴wF:B.uՏaQSZ)ooɘp}: % @V}3ijS6'Cl:0[ ς `Δr5uCjYARt\a&W-MLƙnڍq5WgzIklcn[JB3>O rӎڝ/0?$p6\R@mu|@>5郪k#ʍ &]D Wy糑sn**?F1eHP*ny[y,8E!O!o~ٌpocc;tp"l{ |p/j7 :L3q$9AN]2vD"L9De*2[W6~wNDx zXmE߅a'۫v_S5qG-,9Uu[y̱_ p2pK]=U ;TZxI<쎌ە"ީ5l|n"zk-TCNV]юO;+/`ex58SAF'5:lf FcD'љ7o IN|!Ar+Q8,3vS'sg 8 o6Ȍ q4٢vy9|XnLB'zvIg&Q >UIrK (*zEۖp:6-_\& Q{D5ܨhO |6JBU{(`$!gIIz]Uԧ` $Lgx&pG=lp ҕ>G\-$,*A"_V%6Jy{pgL3ueh vd%cGLœXO c x> gP٘y|nJa E uzcEإtG[uRh/ 9<>< c_Q&x(Uu)*$!Tkn0rsՊ?KQf>\'F'%szzK6с~RF#9w:7xSQ~,ڙB8p L;nmg)?8Ʈ/K?qD[' rxT_q@ "g[_τő$K؞Qfw˙^I8mg0YR$,zzzz7;6:? ~WYr %j +9$1L3M M)t'qnf;OӓkS?2f12} EdB1u77V2gNwe:, 3ݞ Ijx9Y6mkpp).L io<ؗu=p4^5A&(mƃ*pUS]}=ʞy=ez^_5@dwIL*́ŊK@e4@qI{^B-lDv6߻!־ݥ),rjCIs~Cy?[_b0}of pgϯ?]?(,=P Yn_mJvkS? $"Щ^6|sQ]ȵW9r䮘B~ί[Kfs%F^$ϩӶ=ل( p}lxeם M%ࣰ}"rxi.'Bw4LCsc\\{M$3a|ÙZJOikiUӧ[|J|~*eB4^gWTq@= gh6'Ј`ObLi7}zWW"i2x=_"&PatwHn dgw=$"#ڕr&8TUI/`KPYL*akM(jl!qIYͼAj}䌼I:=Հ/zUn'wFrL'+(4FS~gjI#Ue= :Kcݜ8X:ɬrWV//!snۜ~aцz:QG]z\wn!L]cבnY-'6N:IM:2[ 9}UvNr;mT 5tS LL5Vv׉8y'%IJ k<}tcEMgiVV!1gϐN3M 8`ɓCsnxQL^ Lö9j"-ȇg*U8*# W'^Q;R5--@bRzrĪ&;OoǕs>4ZC(ęm1{T(w9% lQTz ~g,N4ߞpB῿~3 Lށv {nh b$!g +!M <ޱ.QY>=$OS=^&~~M\3Yu<<U[;;{+nV^B@ӹ#rVs ;(p|>TOXvuvi_dWs)777܁# Hq&45;R*[Zw]ꓼ552s 5rIKs87.x:=3<]] 5EemEˆ16Zޛ 4{|+2sǠFLܶ #=~Jl[MLLtH} BDd|Ȋ F7MR%ZMXR P&pbqX OTXZN?{U9B-G{V.=~voB嫌{9tl$RK>%s]x/i) dx[j2'sjpPE_M:JACl3$:SxoVTQN-k;lkq)Sn__MbHtaRaz13y;Wu8`]LCA'#E%}X?%;MzH/tp<u uqE :j \- NcʮC9N^PϨa٨(nS>}49?uU2$t+u7tSQ ?+۩(nU:Y]U]*FGG?1&}Q8D4Y;U-X[Ҝw}}}}>FK=FFzp 5-\;q;gy~/(r3'tq.@6{r&N*d;ig#FLp3R70=`' nZ?gD LKT+/XYMEk ZR*%  Dҧ+,v !_\ҭȮU?l\rφ~Pj܇UK򡹢jTBChK)R#RV@}^ñ;<.^w[/K|z=]C*[/W9_eBBPQ90&\ %L{pP* c>7 qonIup~'t3I%gә Le3FΚYXrein\һ_;z/y}5Y}eۯR?0GGI[:yhAkqgz{(iᴗGtMؼqm-xy3Ź0=/oem s<~--pXOmMm%YӆWLh`i4P=)g:!2deV%,T8{NG>|tC3z3R^ҏ*(!f99_d0Jv{s+'B ЅKH a5_ǜ֟ <^!;DQDùcKsr2~h3o"J%~X%^+؜$zr=&8#povϦX,!O.3M[9Vfg-,Vb^ß_bGOo_7/}!C5v6#Uz㛏> =o˟m}v[{G=jo^RF t]תIjQ Tx+2!6g;/x7 J e{`ޱ\ӌÙn4|nZ~p#BXg΀pqGr) L80heޅ,v$V]QL%(; HּP:|. wZʿ *Ϙ bt KdiMhC -;9DP>l&م+O}Ǜ)g[)3~͉>787#P΅?t'4g>㍌3g#mq,ƙ3Tę*ڋ[(g FOv⵰˒UGf|ދGo5zd .h|.g:JV>5)51/d͋o<97C>Ĵ1 2QѦ_BxmeҙwfZbV +"ђXtD&Hã|G}|mC͋NȰN0mۿ`{`# ( `]AGy2S`[rzC >L#6 i^j&m:Eca򟮠QU2˽݊3݇4fusa[ ƙ&JEfw! Lݐ9[Of:DC>N& ޘl  +ʆ xmp[$*LX S}* Fx٬ȽkB߬>wfl*;LLD{KAw0kHӾH" 4Gދr&w|q7:.B7 h^r|q5YaCK^Inp!2´yq{Ð n[(\HTnXgzГ"G-_>TGrԉCVd妏R03\g6|k &E1{ꩍ.LC&љlq8LLC8xޥ 'lʿ_!K!WP% ;Sn5zu%bnss?.*jH5p&c< NEhn#}T*L٫?OK+ ZkyU2h׍ڗf<G,7ޓqai8ý$:Ÿј 'qaf(CCg+ڗmWgnЖ7,A;/%Ct"{m6"g!a^5ܙJq77 6Tյ(gg`9xEo}clqB(φK}Q^d׏ 㕋  ! B;}3=x{9r6:[q_oWiJAdܠ}2M3 ~A]N"OVCH%U20p3}VmgIP LӜ2TE|x |$=MZRgdAHA<8NϚ\<\pgHmB+CsK:W觗k`}"26 {\B< h:O_&gz H G7M\Ug5>t{gb>Ĵ1ܔ T:#0TS`˖V'ELʿB2~£(I9@iSʹ䗓Mr"bZ8bMJa`3*gtsO RkHY|+xh;Guz1*G:S04 Ϥ'DQdc9¤#t;X5#L͉Tiag8ic @Σ;63qM]u^)ؐ@`3P!CJ:]T]w&|>Bo-poJQ{}!r@>E?W:HsڔpgR# Q K=dt.x%r|.wW?IR{me+:CdsҎҎcUw&2<p"faI UG1>9p`3~厼R3o E7lw: p9< v!J?Q.$Ǚnv6Y4i߰aL@IZֶ!EMuÜ -k0-^ũ">+ĢBE=F@UΗ`NTT +OvsaIp]7w=՝ `8Hݩn+ΒE8xkf]P+M( %9Y=|>$VkT,1wSgz?m']jF(\ڙx%> ~ZTjZS_/zX,=唈{J2nXMauEǯ@ճ>8d{5Kؠv֢sƙf.3iL{+M9v汩ꞧvq<iYX0rSwHxANM^E7ێ͠y&2sOëdvz;h *:Q]˦NB ~s$@9堸9,L dOx={۸&dr]U4aN@LvvEJ3mg켻q&m_G(VT^!Sn-B_w{+X r kTw-;8 ,E7w9'J/OJbEr&! Њ<1]UǓ}T$TVvN=\yh[r<A[#_pdUL wX/Cs'7"w|JI(2ms3MOʇ92GdHUK$]\ zPu{dzq2R}%x捘g# ܠ4~zu|f$?zM w/ޕMMCpg?k[v m\ʷC!a/,L[&)g0msD:jZ5+l53RJ'OfH:Պ{;WV^_-kQ^~~ IQ^mQe,v|k:u9M'7lu~-QtwKƵ~3qS#ϳPaKbv* w$ػ L N/+"kT l vd Y^;So~7p ] ,\DvMkJs)wP~~~}}}WWt&xWۜ.6p{; ܫ ̓0#LH ;_G^B?#)=Mon.jlD9gUz; IL!^ƙFŠg"t=]4-aib144д  ,GtWgɘ7{R"--0p/y>9^YnNb{+gLttݙAɆ:J||h6@ D_d⢨8PδTgr&p)jkЯ>-{hK&LJ+TJY롗}s}d_o ʙ$$ ))F`=$\A}I,;|vrCK%eJGo4ӖmI3OKH3bz%M")U^SSSUUwRdee9QftJ H'b'b?~kll QQQ]]]tF EttHi0r;v)1mmmtFp:9z( ŁP7ř(vw]+$δFgz;GL,TF,G23)g~Z"k?Iq_%IHA/! }ptp&6Efc)ʮ@S]rID>˿FXEjK"&>>>''𙀯{?|P233}fpk3+K0Bn&Pt}(SL`i#رctJMif#KT錘t&Ep)g`iR^Xb2":$q&(8Q}ɶ*T4듿8rM}LOzP.iaOę($=߾`-38xUS&m&*Tw'֥O#;ac&9fmliyJvv6|777=,\_lVmS;vE}q*0'P CN1UχX,I^^h:H 蹩3M"#Bʙ)'˙DB{QNglևm Xv :) L3]C-GD)Cę}7/z0}+!6L ,g:jjtf4/_S@II T.^Uݹ~2|D._|r-w5g7:% ӁvqjtRq-QtL,kd8 2gE)ژXZFf|qqȶ-@524ҙ~"bX\VFWt_S4&-5d!Y%xV- #;2Q4qÿ0&gsHԦ+23So2E_[aS@ii)T\o~Ts8O tSoohv4%!:$4*}>!**c#}2ҏE ]QXQ^go |UJ6ˍG+𚩧|tfp ٳ HKK)++f+7 ::z(Ds>%_Ve KBq1ƙ&Ho/r}ϓ0 [B95T7ze g-//X8S?J% PAXZCjR~oH ;xmL`KNaL[W$ݽo/&.HESv]$F(&OYTɝ6U]PVihS}Z'CCWtʹ碟֓~֭na'd{~AV6GQ'6*JɃ;"Oo+8dhVnaPZJ[M(f7oHxzzBbIaT-:x#\.U% SL=7sxC555ei"TTJb JʙvYBHN?s,y)ED}0WMɴ3!|r&~R{czx"Dig=rlϑH(%",j(#_33s223%'ML73TIj [b ҙ{ʸʦĚ kA"ʃ{SLYYYԀIqU6[\]du }'{jzT/nᗟ>Uï8}-hbo+*LruOJx332ϻ nooNJJ{ ¡$)9 ?>jFW0:\.|Άr~sT?7qk>\vV~r.>[V8Ӥ 88+#˵R2kMn 8#8?ْpeR AL)xkl_mEmG*ZroǸM&?@DER&ՙDV nʔ8SWW_{ jtwZwy \7W \%'av:2[p.&ܐlmm %g2l2%nrt C25u EEEqSO=('q eճ%zX+˘GPsqIlևd,*,\6_8tGLQCTu43MƲּ{vJd2%Dp>4g>=/^~ee%8ܟ<}!m'*չ(VRۤaF]]t&8M300 sӧOҙxl W;CAR$rHX#[ +lܒ3RT74>[i^q~iVL3?pӂԊ]{\.b9LpȆ?8-g:_i(((㘖 4FmEғ)M;әP~<66MB0,:?IB_(!L23+/#+|A 6*zvKuǖigF.R]v%<,hTjG۷DLgJLLӳŋ)1t#'''UQ^^^GG`錘;***jii1#7SRRԤ3(Р3b,YB(++1#7Y3bFnBg0P3b.]JY% \tF}Έ3%eܹ`7+U,_NɎ;茘pt%۶m3b>C:%F__˖-tF̊+蔘%3<|_eWGLglLnCd`2rr j7iM2dy'n,~s{VMKA?f>3nmm͝Nʙjk3r^KUU3rzct]Q!=A茘eddy$dff1#w"tF 3Ɂ13rǼsL~~~vv63 .ΈˡSb}8}ZfK :)///E>-t%tF Ǐ1tJ |*:X:#LN3bk锘%MMMR _Q:OEnx=F)KC$yƙgܙ&4te3M.#irLMi3M"L wƙgǙg 3iIb%$$Й)>qqqtf g:x $g0p8ӭ>p:iҺ$gbwL100}?I9# mL R8Ŵq&8|E6IVJT8}xJj,I? pP)$P8Ѝ{SՑH#3,4b+oL<6:33aۨ/#0ގ|r@Qz} 3xLtN|HϞt#M V}~,N 67Q.Wu ېb`a^5@m;5L4bmSL09}M9fl2/1Q>Vw W۲:SsmcK'j0l{UQ̚-㲙85G}8\$'SNb53iX̷ۦ_8ӺͲvޝ`Qk?~u6}gS]aMmM;l=ǣ5]ƛ ՠ rjuW~ =kmUSu=Fgbɩ85۰~s35D)to8ýęˈ>Mt&;L=mͺ {';mj+:'],L NTl[ms6%'K"%& hQo[ND~kD~}erpI/`{ۚF3uW'GR2&`7GCnAFaoBa0aF@NA&F,I%NNjSBG=c!ң= Nva:u[ΩhR9!C׵83&]ZlmTwmOZ-|dN8#FVxL3 0pO_F9'N?~<>>>.. f}v6ʮ74)O ;U9(PI'\ L5)j=!RJv 8_T*+es܎W&.lk^n~3$6GC͙Phϸ#5u=2V=N$;% Iq^^YYѳcM%]G->'EٚVUEUyLv*s=mCNR1Pt@ݔeY-J.A^|X]ΤvG8X/5ٲ.m%n_dh\yQpZs>Tvka |^kU)A)q?x2^IENDB`PythonQwt-0.5.5/doc/images/tests/0000755000000000000000000000000012651077706015363 5ustar rootrootPythonQwt-0.5.5/doc/images/tests/DataDemo.png0000666000000000000000000004023412605040216017537 0ustar rootrootPNG  IHDRL9sRGBgAMA a pHYsod@1IDATx^]=u r>\( (‹0R I&v00B0X $$BH|߅8yIlvkժ~~U/]U]Uϼ >4;;0v#`@GQm??HIZQ;__s;X ??k;T5M_ǫJ۲}O^~~_~W~K_˧|O_W//tIhdO)KiK6c/WƮgNXw>|^~~^~>7cG،#;!|/S'ۣg?_Ǘ/?w󳃱' cM_wzopo\޻r |˻w7h1':r_kG7v }/S'˟_+ȯGcOScgMTʯ &<} gl ]A}ijd\r7d5jfMN~_G~~(~~K&. ;+S<5i[эM|kf_՟o߿㗿 }O>o}p9ӏ2dq cn8[FWw?˃kicws7y~G7FL}[4y/G5a~||^w\WGOGo׷uzcߪzcn?v3zÖk'=h0vh2dܔe~泃I{s?|+e4~t)O?޼L#0>|?4)}k*犧[9̏د9Po4tPes3ߖϟcuOt!Ɩ,E-\+mZbML,nM d=FmrJ6Bgc7Yocp䍗,*q2u?>U&7W|Ƹbcfc߷(gL̷۽~6%C,$귕/\yqM雕2rY̍yu8q%27%Cfi mwzcSM6LjU>qܸǦ(cV9dɼ9Вs<օJHon39c?S̅kS8Ed6:єI:)u_Mw(٘cT{zDZLc sh;Jbs/uᾃ$TVgX/.*pաyEuȟq mڸ#KRgRl[zs6ښog:wY:c9kssRrϫ6Ǻp߫N̷gP03idQJrb^Q&^Gy{͍cF嶱2dQi٦$OF6pj'LӒ?(?s4c2s:c]UIY9fͷx$H¼ijKnnŗ-}ndgmد/Jo, g$j3h-Rsx?\qg&d慖X[uϷgz>`Ģ5gsGtmq,m7l'R杴Qg榙o517yTh*~Xϕ>nLQ죬uᾬl^˕\9ߖϟcu!h6:ӔAM ASWoe!'o&'{ h{!hJǏJs?*|OoCn A\! #! #! #! #! #GF # >820v!;`;h?>؅`m/>؅`1``B0vz>o)ct`}0vz`㟺Z!##G/>؅`uh#>؅`Cn%{ 0v8,ڀ o`,1vK{m ywry3 XGCҭdcz 4š}kIMVb0O7 ]cV؇{%ZVbo+1c/rؗڅ o`-H"bW4pұ 1F ]؂tkxqTx|}v󶁱 [M!Z8@G)![9`Nkq[ jmI>}Vtby}Y5yıOǜ.+ف#<<,V0mo+Q؅`4D>C [s6R` m1twyZyHck)O{~5QeH;فHcp筕Od^],ajۘJg(_. yٝ=̀ɼ^.oY>Kkcߙ=4Dd4^p-<U}}Qߕo@u}|AaHL O62d}tc0_v>[01T4? cB'8>uPOt|/cz _؄ q:> iՙ?#)NdU؅`{Úݣ=,NV!11cNӷm]ƾ1|8=N$ 9Ƅx_:{,}Θٶ![Jc=/ʛJ!q|7=o |8XeӲn:Qa)/9K۠f_>χ!cf}3g3O/,I@0 {uZ֔@Tҕk/-6dpee?]'eڞ}x[k.C}ηBokf*Yp>eO|zO_/Ϯz{yJv-A}ηBokv*QeR}p4YC^v7v-"`[i[>׳U}C}Ή-DokT$MCv*Vid{4hcׂ7S;'}9Ԕݢc{t!<{V=I[>S_oee{>͖~zcŘpK)E)TDYĔf1*Ud|3vRx󦆥FLȧ.}C^l1&\ǒjnFp *}bNY74hf}@3\d[w^R5pRٵmaߩ+ 1e~B)=y1>!P?~1X;&k2kNþS%V.cN11;ؗ< YfI#B`혔ꮁ˔ʮm#8 NXy#-0v!P?~1X;&k2kNþSEVNF)[aB0 ~zcvLJueJe׶ ܄'otG/=JcNkqz ֎IL6pB7Xz ֎Ijʯm#8 *Ĵ-L Kz6؅`GB1)]Ty-MF-wk\<~Ibm]ƾ!O}$R5Lؒz)TIy[d˰ctyh?k=4v3t|%deR5Lؒz)icFmV#hxs*=^02o_-dM]v1[78%LjSj`)/RQjc}n׺үxk;]ky;*Vu5upHKSnK ='ql@lL+Si/e,u:-5V{|V.c4FZy{b M 9+ {N|؀ؖ<-4vO}~V&nz/F8c|my申X[~kX}Nt؀VTzc;Xg=䍿TNug[_ey:V^wN9j_ʳ5up{Z{v{hφ##%}$;>cds*]w4{6o oL߫ ᓂ[^U2q>ZX嗲P[X*~{dF 덝۴\T*OzkΧ9gŭ]cO.bN=+= XyYo䭛DWoi8 e+g;Ŝ{Cm#c|<:}zYqGϡTV^>+n'8{lSl\+I[*4&m_`7v isNQNK6L5N:5uιp;ȘOs(=׫ϊ[RylSl\NiI.iYρRe*=q﵏f|{/7S `t_mpc's=vL˧9gŭD) tKiYņ63`Ej\i`ـcYet`7#>% :S'Q=&^^ UTs'܎>xt>RSXyU(ni6qhOELriN?eC-׶ e+g1U/O܆>xt>RSXyjŬ869qpr0u#7$]myZ e>~o;_= P>nT|:NC)^}qbVtM\nn[}9cT{!{ ~*agmꃗOs(=׫6NP̊iS$v0u#?$v|jcϿ- bqYuLKSya^>GϡTV^8A1+GN =ZM6nn#ˑU{Px[#Ftxc*%|/2q>^}23kz1at7 S>ZJUlcA&8nq嵰yeנk2q>^}23kz1]􏔏ߏ:Ziqll,pSe쥏Y8aq嵰yK)]w-2q>^}23kzƔj 6YkyM۞w >aq嵰yK)]w-2q>lm}Se|{zƔcy]rnw}]]꒔-XZt^[`g(iq[ ndޏ(c.Mj~ps5W&^~W+[Tbku{mKU>/ծsM&G Ҏq&yxz# mcg(K2^RSE+s:}V|VjΉ|;zyy?bv$] c'Rٚzנ9Sq>lm}^>]5Dm>lm}^>]5Dm>,]C-qɶ{iGH!^7Go*GzZu\"By"G.3akD\|>Wuq=֎G?Dua5"qxYȡv4ۏ؇3eޢ?J3\W~; {(·2sD%cA8qұ$t!ct,((K"J9"tڱHv4jq]z`@AQU04u矊F~Qe(K"Gý?(K"J>ݮ q<Jǒqұ$t#;VX X1+hy?'؇f'Im9"Jǂqұ$t! "Jǒqұ$t#t,(;ôUpJ;Cl|ޏɆ@3/@lq_C\KDy?O8*t+Kc1cX#zl|\\cDua5"HX1LYǬq><ƾS h{+?oԝ?V~xMիuM[:M^T\+[STYNV0|^C 1i){>XTio/OCf_6vYzL8ӭ</ͫW:$u#NWΩ:22f:t:#|Z>;u0eXv yYc:ooVW1Utym1}\WΩ:^Se:x8qnӴv.+[STY^s//3Ès32$/[f췏41ح|:A1+xiVVRDLM#4#8K9UVL0ܤ\=Lភ"coH߰<xVSuYXeJ4G^[i:FpLsNC x\z^c\~_}of״}n+Su>.+V5u20Z1yV{nϳ[zԑll7&޵<xީzJN#S[^q+TY/]#ՖYxt!)yYg7uRo$ +MZkizBu^#S[^q+TY/]#ՖYx[s:cv_Pƾ##K%.+O +VRY/BS&9le{]~S}D߇:(+tmUn+ng.:NṾyih.[yy9^kdG ]4k)ҧm~>e2WVڊy42r0-{<=/S2S_+kNXi:2^SNO |+xe=d~uM9f*]2^@s~{keJ^ViJe 5הXZn }s/XqWi525嘩tɜz8<4ǥx̗@x8=/ƞ W{r5,y\Ë3V*'tZM9LM9f*]2^ hbeZo'|^Sbi-u;8cŭ2^yBՔԔc5n9mT=MKeJ^{V/J_̗_ .Vk>/Y~{as/XqWi525嘩tMxvty9ƾw |W:v=-gQ\4|^D^V/YT=ym3V*'trK/):u+\>T`ȵg1uo쵚muū^XřںJurYK@m]' ]R"G۞ئOT|H`jY|}Oymjcm]:_M9ӬrK.)^iܢ =/;)6<5`7#~mmPHu֙:q8ӽx-z`ceJurYK@m]' ]Rr 1&R%/{;~z/8>.zlUPvLLWS4Gױ{ !y/%Ҽ8J^֎xނO@m12S屦2أN&t[@< euT2Ic'-6ZqɴSG /]-e:h8O.3ރ*]^׎R8d#t;ZxyTn){ @O4uG>TaB0 vm#eCץrK٣N4u]K]Y¹LKNl.ŗ@ulQxF+p;ZxyTn){ @L ayp\~k|ΧGnQ: 1{[=o 0rmך<}KǮTNǩDM@ڵbezƮE_{k`M|.S剚<떗yXƥ\JK}Kh؅k/1c'w&jGcMǹu@\ȗup}Kh'~*>|>=lP0;S3>2=j"tE: %4fM K:˛O]&;>_sx0ޚxH˗u:}Kh~{Y(φXew&֌7A5u9p05rHK5@^v cG{~g֌7A5u9p@u1:Jͣ-8~̚&輦< (^ʵKߣ-.c3kƛ:^ʵKߣ-.c3kƛ:^8޷S;uZ2V%5MyMxt/e}K[yG-6v\g-KǛ)=;x\z6`B>{ֲd|d:px\z6`B0;d|d:px\z6`B0;d|d:p*_{6Nii-/+d|d:p*޷ 2ߙ%#9A=Ӽ޷ ά*1$.yo[}ۀ 1u.|8{@iOio4vb0zBm> V6Qv'4uQO";;[`uZη} /C0-g)}zy1r-b0{u=@}x[[yagqpNԎ !ŋ޷0rmz֚v0ñŸa8)a[@ีEm æ/rc3[}ǭ-mN fM7OXkm]֡Y&b0- 8nmo2vGUo_@b8!q[xN'RB^֎{ ~go<3NH\`{ɊD[`gI k?$i=cߙ- C_*n y<'.c3[7GouxLW;N8#[7GouxLW; o`wf3m@^uX؅`p G$wӭ]_]G#wC,y8 HniΓc{޿:a읊:qGAt-eQؐ}Di϶;ҭT1 hXѹ_< ]GMe0 Ra c?`3߾c@>/0v!  :`3Dfŵj󑶮mc_HgwOm/=c#<3}!=g =G`3t}6/G}!=}V+<6y֮ЗT0iS=gӦ0 >CDAm4Ѿא|O/!ɚ)ίT2gzZ3}Ui,X?0MTߖ^ICBLBT[RGmV˛oJSi-$5Iί SoۚR/{IWJ! ׍q$r6A-/vjwiϦ1X3wǺZ3]Z# $=zӴ׼}*Zݰ7#j.HvWh_khlǓh7}=nwx fvS^I?cgyJiX.{I ZzJҼn=2m|˪GenFsp Sifmjz Jϙm՗Y~`%AԃܜIsݠH,Vm?Zk0jDZ]Cs6Lj_twP;]Z# }d 7k2[Ƥ'A$? &<Џ}3 p\*5~6ms/kdϯMmm0z1 ]ѥ5l'&QhK 6UB]6tsKJj~4q6FN);=RGVoZŔxxk%;=Rso[K;&,˪]7Rs%W{_MFzGA(5~.s h!ZkWCKclٴ|=Qat/[{_l̓=1Fvq2A~ftUCo雯h+_y_q}@mDc9?[GIA݌}4Lt˓4 Ar 6T08A=cGƞT%ƞI "=4vy?k,c,;=Rw6v͹F~Y} r'c#;=R6vq?UtYN}ԓy}I-^YW0vzmט;a[}%mf}3%8xX?/ښ5W>1_MXAнcG$P*14(}OR}ce5vw|yVcY?`͐mPW0vI-T5";ALab&A : : : : : : : : :eqLT/A}Aɟ霱56O uAnaY_,EjdoVG|fs=?㾈?;g A( N?z*Ic{M}0@xeyQnH73۫c2ͩǽ,;)^'}~Te,]0v+]_=Ln~-t O<æq h^7RR9y/gڄ2N 1SuBJIee^}cef8EQpp@VJ8߻?_'ݏw_~Կlp>>8w~:!\~->|{wnzzZ߾/~߻y( #ZF*=|>~sۿ'O}ypLx]iLaz7Fp '9z,ݞ~}n0>( "JY9,}QWu;-[SV$gA *.<|} CxP ׅ(r 7//t?o~87o>}{31oM}ݷzL 8EQTeUrZ!'9|Ï/ۯ!'A|A[TwwN 4}_w)|gCQEQUg,ȬrQt?˟`A>{{{wpw۷ȵ䎋Y~3OJj 4(*"u 02z'2䳟/t?|mO5<((*9|'$ '}((*:5p쇍÷((jCQEQ|p((8EQEUW(+EQEQ(ꊀCQEQTuE(({ PT||ӽycN[>6q9n!dzk$ L,ly䇀j~ 8x}at^["Xnh Nx#ns[/D%[s9u~yCp.8}7 :8X_-޾X-$n݋uBN8ƼopQ <vvɅ<ږgzi<0<<٠g^:|Hzv8[>Mcί "=F_c56'ͩP1p~"]H.]?ЅL}].߿1>ӏ{pPaok, iwMP6)?/ŠIYaC?5u(P<քc/ΤXh99S@M8 OIH.vCr;vCh>i5O=6{r 2'xz$w9s (/^0] ̴ ^x1Ry?t1H?_?ŠQ ׬;hakTpS@8vA{\n$\5C??|RՉ:^"ؑO8g ^c/{SHS@8`/^‹{eyqE"eoE^/~!~g/c?GVp̅)~p2 pP#⋞>Ss_L 8Ű\0R>H=8rk#ض3$Is 8rC0p/CoP]⥿:pWژOX2m( f9779}m%pg%c<ҶzUA#{'ȴsA 8r5lZp-BU~<H߮+Zy!O0_2=ml/B5@9g?~Ҽ~z Ǭ|M{f\ic9E8sٱygun犬h1d_/p2Ɋh˷kH?"=Ǟ'H|<7|Ҹpe{P;8e16C#!K._pg@^8[aJJ[/ޝ 9_lՌ)fk֖!uA:❚},1 fʱq̰N By\ic5p;>|#˪˗&/-Az-yc.}a+m !{v3+Q̰Ϲ/"{V3+A̰5ε5X/7zΕ6 䁆5up\yFOxJCcpҍ@CZ>ǜN=!JCɀ# )pY uTDR&K/|1cpjh43,ijGB=\icp\Y43,a"CO+m !S6 Kx\b# JCcpj8pw&drk42fi/k-t\ic!!5\n+e|}l-wwc;3}y'"Cs 8¬\5ji[seMq }jn{&½;{X*{ݲO}XhZn—@V8OEc! # ekqN;O-L_+m̑y1FK.T\nŅ0hweܝxz(k7JK=RXKZv ϏpvƖ`=<>&Τ-ป]VzظFp5l+mluY ii`2-9& ^[TZ!x&<7q^)!\ |r^h˷C , װ\e.wz?Ȅ(Ƕ'X!Tɽ~@d_p孑-G{r8'zBop-ɾnwyu.NTqp\YQ?u5#hG.B?F_k/Ɩ0ŋc3r z!x+Eޞɔ4㟸438; V6ж'~,djR#/=~42m3\icmp -PbÏ)hU߽#~r8fEZ!KJCh3 8 F]nsC_Vs_p!O}Rz!O)S 3.oArq}Z{+m |Ύr 8)'(͌Ӓ;'OWBSZB}!]61L91if: 8/"p27j2HC{B1[-ye 9i/pZSVz+m !Ss"95}^p}*AGM eiNj 18F-G95)a>op \ic5p!Yu)-Yb?"%ްz+m !5A##Ԡyv/,JCcpvo8=v=Κ'*y[Z{+m |NН.Cwq8Zodf֚+LWB/ā'՜C(qpR{+m |%ܱ9w{+luV8W 8*/*NL]i@'iה:O1זPbOp!YI Nl</*~J 18vҶ 8{H42fւeG 18+k%UJ3] qC__)=6 ധfV#%WBcC3#䢆87QB_p!d 8mxy yɽ/E餡Y72 SZ42+fV>9WBӮYj}ʹ/"k\Og/5ͽ/XWBMiH1Ӗ\OjwqH/X#WB1 84|jTۼ-zqr!D1Z 8Ff~jQ|pJk+cJCcphd(XՠVc+JCcpZٔSvo8C|r:xgNiMLȬ] j_P+m mNƻ=v#aXhdV.wϭ7X?WB;/āgvG3',pr9 ^aV]]ޒ_jW)P\+m mNpFlNCL~FBCpV1M s*SNe̱y.R&VҏcNZSvp]icM 8;) ~&4qyng)f孩X,ñt!4)t^ Eejrg$H0j4(#ܙMPcVR G+m pxcyPb-^ʎG2#yWyuy̦[RK+(>B}>1Kbx i,8˸;#j:[kiW7pJp-o/1 37=f6G]ޚڎZCy\]icv̷rAyd6I]ޚZ;CP=+m >\߱q^ͥpMRcNKG+m gnM|W`s^| f-yB=C|ȕ6Ӏspqm>$;B82.oMxi-ΙB<{7|Օ6ГRbOY^ 8KN\MT㼔KkG=7{:9}~ﮍk2Έ+m wpZ@v#kX\}o6'~ * 8_\icpApBf3-iX<};fGeZ⑝_WN+MLȬZ'~/~nM#v oQ-ɀ88ϘUc>E-͑{7ǜ;8ԀRs&[qOh5|~xq__]icp NzǴ>/\hqn4}zx}+icJCcp4̮Z=cZ}vJbJChZԔbD_h;[WB۟~G~O' 3.oIs| 0+m i'%k>p91|`X[WBe~}V''W5 qMg,--o GM_!@RLv%!;c1{+m &u?eNЖoɎG˘L 8Mx&"LG##h[#;sǼ 1 8;B46CrǞb~prQhkhdcO 1?8(8,֛ٚ,W\NyY7j2ػ>!:uyKd'hkc0OZ}6wp hdǜk}\icp <72V3 ي5G|Y/J8üEg%&/%9ogq"gv%ǘ39|_p!ZE3مNЖ׀ki8cpp|OE.TFvtcZ;KWBXy!G--/ǖ*β}6PByrxUf7[#;B[^*1B^^ 8#2> 8-3R] RqL 8s1 1NM&+:+孑-/ 18{H8#,Ff-$,TOp!D18j9K5=q, 8Ka.-\icp βhdlOǴcHI|"lcpG3# p1WB˩;,MaW:tt?fO.UFvc /pqD? Au=6z8ˣYs^8~0WBw':#aXN hd֜fg--Vv8^4gN!VuykdGhsqv80W+m NH!& #s6 B!ƾVZPpcv%p#l;8)%etZpcv5#xM?Vqs3땾JCh8Ycr8cYS+m |ܱvZ 83X]q|^?6q@:5e'숀>5mcczl80JCcpA#RV8. g柕:]icp 6hdVj#ĚsL80R+m !A͌ǃ3J1 8Ґb8g=fW[#;B[%5X1JC;8g[43NN 2ֳJCcpE#55q %gIٜt!D18ۣpr1 ,yiKWB=5_npü+m &4?NYȎЖ}o-yp Ff55ߗe1?9JCcpA#֒,54G]icp ~hf- 8cZ\u/#A<檥UWr<+\NPioޥ; Y{?NٖNЖ/l-e0_u12 86Bؽ9Dؙ8YK_x}ck}LYsVJC(߽9t]Nmhf- 8a>r!YMxoQ ;,ؿk[Z\icp.Ck;68pB!fXc κ!WBIsSpa5#bnD?,oȕ6Pf 4q5p!fŘi-Oc1\icp}x$Ϸ ^@̒-n1\icep# # ,6Nqc#uyKd'h˧`?ZK1m0-WB>=}á.omy*F`>[18v8ͻ890g;kepC#^@ 7+m !AikꅚfMo"lJCcpąښzfmng{-qWBMiH1sh孑-`}-sWB18m^_ۆAGsݕ6 䍋~p|w!D18ym=p7+m !AokϚ8iq޻j2HCC.om9gp#m6]icq _/AWkߕ6 9G-p9JCcprtǾFpʹJCcpEݒ j\p!d 8y3K]Fip!}6Xq>^9;ǎgy_pg%ic-e\icmpl0 1Swyx~t@#F6m#kE.NK6ж{sN둻8{,톜xeu rfN^j 1v8^ ki,ΜC)WLo,nANLO}6jr:'u 1]q)BpaVC+嶭q@j 1 #! #FVԖ&$־JCh8(3O?LiY,l;不|\icmpGEejR׏7 |56X6ʃ5u8/8Z)$ {h޽#~83P]^)'֖."䭶JCh3 8˸;S&s奛] Mq@^j 1 8vӖ8=-\icpvDK-lvhKvpPK_p!D18uymyIJN9j 18>7_H[^R֝Sp!d 8u0U][P[9[}q!SҚ+O-YIL)Oi}JCcpVJ3[s=ɵ9*i]L+m !A_lі礄upʕ{_и"6̶\/y1myr^1rm\ɀ# )pe|/{|O9S,gh\icq %zȋkӺS\z3>18=94AV@[a.Nr z"6riD+{8R^ە6 kfg"+-_֯&N}>GR_ӕ6PGR'6^Urm:[:5\icq @̶hKW4:sSUxy]icp ϷYim+Nj@iucS}WB&4spC3{KCi=SOJCcp0dJcghkDiuql18 4~ChS+uckGAth˕6GA-"(8Rp%WB@*JC rJCpCQEQvB(Kv!D!pfߞ` u83N)u䵟xzQLy,pfsxxq^uSt8^9^{=.aŔ"g&eMñs-_Ԃ3-_1űA-83űs-_Ԃ3-_1űA-8:PTC!Lu9u7Cwhc<+/Ydz[<dzqۆ=Oc3]p`$ĸr 6 4 ]NomJ_ݞrwu/ ?vި<ҭSY8S?=xa:8)wp"A/^ X)csSʺX๓GspO8diz~Vn_=rlvWȺ^郷Yy8r[{W~{wІY\l)~y&cOx{[8m~߮4i،v]}ד}~Sy#RfeG&LCobމri/cJ>`O<)i.4) t]E~/S'mϱR8S'Xx$'oJ'K3H]X,!(?e> r6(u}8c&u浽iɗ>;o0gDnR؃ ޖužjnd}`s\3of7uhYŬkyx=ïK;o}>:u,Ic?;Iq5E4](v^6o ڨuz *u] ?oTy#}>rl:AiQ^ҷcS:Λ|q@AiݢJV)M8o p@u8:PTC!؟>=7zc ul "=`"r8z0Qwu [1=ojA(zH<}ۨ+HD NU;8[T#o'm>d `IϬ[T8p\?dln p@u8:P[9 0IENDB`PythonQwt-0.5.5/doc/images/tests/CurveBenchmark.png0000666000000000000000000003635312605040216020767 0ustar rootrootPNG  IHDR02/sRGBgAMA a pHYsodu/\to}?赎`.9?g\0Ĥ`^◻{k}7ߝ`kW~!@`PPDx{/?}O/?g ˟;K?`w^&`Z0, /Bv1şg? g??,7h:w__fc`~hkݧ ?E~ 8W^W/,vxlɘ?<_yǚy<=f*4?w:t`e8 ߧ@ѕܟk'xǀs0=ܝ) /1+?ٽk?绿?~'?[f?̿I]}WH^.{[[{/⃎>^A@6&OPrr~RrG)}Z 0r`;/~~_}ݷ^o}5`"~qgAT>D/!86F:롏aာKəϭ9cEI!)Pdߒ;;OLFb|B?%`;)V%9dZ @s`90h 4A8o~󛈈6l5EY ".\M0XK_*RƎN䈈MB%bF_(cx #GdR`qmzڄ:X`#c 0 zڄ:HYGy-M[zڄ:HYGy-M[zڄ:8`yѶTyxx$\Hu|ףsy> ض{Sx ^zZmOj`/Y_{Ovx;K,#Z#gw7@ ''o}mI]q97\@ ח=uZr ]yt;&K﯀m??sחa( 0n|*zSYoxk{o@_ç6mwum7 ж~#};韤wϟg?n}#覟ݶz??`D_12~F9$7 ~rxW'@wq{O/_< N@mOlG oqی f1.TCp~~ 0zêw?G?.'ޢށqKh/=ا^\49՘9Jxm:?9wm6!l 0:ONqU|gstm}ýg(SƍO]+cdDzs'b1y/y|L7x5^PۆG&YC^ل.SG`Üb?nL>qoQcF`6{N/v`Oҷx:U/<طqb6?D pW 0NS9 7~>.] =׉ 0Co1n|(2F0s/x'}"ڞm|6m;8?kV=m;C^/ 4xgğ/7- &4ş[|~wNROS&qI_i}{[~%sz&q/ <&_=m©,||w~uZ_9ds5.p1~xHg 0rK:Gn؟zXz74y׽JD 7b`qOc 빼񙖾7waOÜZw~Ò}A p4stZ׳4F/`P |uuZuI=ciqK%,ll]`ڂ.s$6غ$mO=#fEr+嵀6so:`V$rQ^ h>G#fEr+嵀6so:J)э0R=y∈s6 A @s`90h 4A @s`90h ܔCwx2g랎NaQ,Vntyvc (蜚=γ_`܁;ͦV z0pSr̸+#1imm} P;#CL L@ ܔy&-j`(?YTM*?pۜ?ڑ0ԋZ M`i\ӟ}l TM2,Ͽ (C ;@27 ~ *`ax(P+q 6㾱CRENcώNj"`Ba%J,F^Tnum+E)T9$jݚ@9A%) 0 -y'0m #`ūp0pSt Dy970o^pkH1ßc^TnʥLOxmt(F#sB_lwO<'0h 4A @s`90h; iB ޵@YЋx4xZ!,;D"޻0 uqP4 ġV0 @!B P7Z!,q(LC@jSñu T>݇p ^19uC^6{  bZc 07` >Qg>G\0  bZCALkS1-) 0'`2! %iS@N!"= cr -7` `3x˜oC\V0`˜ ĵjS1-)ۺB)Ɣ܁q`B@ 8Š,u[A\V0" ޳0&78uh!=hS1-ށm ރV0`˜w`1v` X*fp˜6⚴B@ 8*nk BD{PthmqMZ!@AL cw`b`pZ!@AL cFzu?ĵhS1- 0Sk  bZĴV0H`BY ת6vP6{w>uo{S`VAu#tkJ>lSyuC ө ь3 `=m -vRȷ~! ^ӛhB תn3*D-Yxmv8") 9EִZ7~Pqze0d|kBu{.X15K [j  U+M"s}HN! 0>{vֽ5 1Zv?UT`hw7;bg?LbޜFVzۏآV0B_8ηa_۾} o!AД/m *hnA%Fl]+m(R`r Q,|r`6bMuCݵc}[JF~~ s *:auk溡ޡ#\)<w߁!څSFކغV0P M`BO5iS1-ށIk  bZc 0*چتV0`˜3Jd3%`2! ak BLkXvo>6bZ!` 0RYR0O =bK˜%Cn#Lآ0&7XפLZBm=bKBI6bZ!@AL c&G I+ j0CnSA$Z!֮L9HHIhm=bK˜.[KآV0`kɑk  bZT LĴ0&7MvhbKZ!d WCA euC+u( (Sk B$A$趞%a5C[[Jq;=whfMaxgGk0CҺk(`u4`æ;՞]'hFS$!d[7DDt ^CmvoQ^uy܆M`d]ϋ؂USq -z=%4`vQ!mB$9(]+H[fjBK؊V$xoX-+Ƶ܈[35 1>d[,z/bZY] ~7S= -Fݚnhw(‰XV0J4yL|"^}լТznڭF.Bn#-+xI-/B̡W8`UaՅ L)> NBs/C []n_7BCM/bZYm߹9;"@,A%FVn8w(~[qmV;0#b *?7bkJֵB) 022JX` 1uc*|~ǵC[  bZc#-ADCE+0ia5 -lQ+ j0Cn.[zb+Z![\X۟faLn( z[  bZc#?jZB)FO@Qc 0N L0m}?bm˜9FF ۩>zk#֬LRx|5 06nXoZ![\谄ڄ1uÙNoXV0`k)Bۮ"֦LĴ0fN 6bZ!@AL crƜ9̇xM`2㫙 0{ܳ9qkSzT("֢Lȅ7Wxe*ڄ1낈NXFY+m[ftmvݹSۜnxTy? *ڬFFHk!`Zi kEA+MԭBx-D2Nrqkִ\7r ml/b-Z>+w(PoPoRS:|<:b-ޜ  -GV0B_8ηaϵFfs72d͵C*hnĜ /NaqɹE+m(R`r Q(K?f2^F\ʺnL>tK΅k#[YM&4,i fj9кk܆B 03\$˜ܺ1 bam-v΋(Z!jqnW*?*ށCAIHXz6bV0`dL1e+K`0` ۝z}jE=&mDV0`,ʒ˜9F)]_8j6E+0iaLnݘY:+Fj_ q( b@}1Ej#ZB) G{sVinjYr D1C[ezLhXѪLsݫOJK?Is Y.u7FϵX0fNѦϭҩBm=WN{ 9F !CN?VYˣX[0Ko/Y*tm)qnY.uj^B)`ny~{ٗ}ֶ,c9, => crF۩m!\S9Cxkԩyu &)<kqԾܶ,.e97Ǩ犵}n/ұ.mX%JejS@n! }zܶ,c!)GK%F˼ 3˜9F_e]on?77-mqj\<6.L׶Ҏ2օؼts 10f ]L[wm}Sssͫm\F+1,s…w]bs su_i˻w_h/+v~_R֧3v1t<;o-K't _9v}1='L} ]sz]>uߐ~[-ː1sڲ25^Wi[o۪f/M@Ǧ;՞]Fr Q"Ծv1mQ.T>YP_r>w!vJ?9SLCǓnY:N=?;kҍҍJqSTJ?Wݺޮ[ک}vɘ9mX?K[2oRsu Y2ʅ|QH{͢$ߢotJM׬1.xh;c}ڲtr{xk[.(cu.>Gv(q~uc8߹sݘR_ }1>nڗj.jmt>mj>_OmvH+0#o1 y<6ȶ][Lݖw35W,9/vǑcc,mY ji},mwli;ݼݽߵsGuD-sܹۡ1sک}1ZT1Z_o\ֶZq8|sϓrڢ\hh\cd?S[o9sn\1uۙ3W-wG-?ޗZbmQn_h^Y{q1nhui(S~SŜb%sYss*L_&|^}H_Nۭmm㞫+.vKe)%Oͥ sq}1~N-'g_n7ooR>obe"\8_^~dP;?vO\9m|qKi.ֹbmO)c-Ri;S!Ot)\sn`r1z.}zPݞڧiҶ,Cir,v9\8%k;'w~%sک ^2oͿSRr??w2fJƤ;`Rlsc~s۲ ߗcʩS\cs۲,UeS@ƿ>ev%ʻ2T[wD,Ǝ!LJsjeYjkg񴱹`Lnp]~;Mz\O[-ː1~{joO9556>-RSse_ͭ-Cv}zX;twqʧo82ƿ 1Rydߧ1uC_ o[mXmYZ̙+=kZ!BJ_ɲ4Pv1x.y Occ`~iCeiQ/5qZ!d WS{~nLi ?9^XP^7rL]L>Gz36-KzjS@n!ʽhK{N?מ:N9NJ)sM~sc"TP;el=W=O Kw_h\_DGa.Ɣ"h-˩vؘܶ,Pk:B)FW`zK̏ wahOϕ3^+iri/5/֡L 0.L^Ͻs!cBf'10LPX meh K'Kp$}\9˜& w}t;elLn[`2㫹U5\1{Y۟?>vKB)Fϯ?c*C&F,Ɣ܁IC(~96\`2㫹V9Z\e^ ^B(:mYlk9E+r %/sJ{\Z[7sÁws\yZi#lSyu~PfԞj]7JtElU+mUmT;'_u/M[5 $2%4`vQ!mB$9(]*<KFBxT ϱH/D.V1Rj 0:6bkDkuCzY2&LVBD{fj.2ƶ#̾ .xߤ 67~XV0B_Tηh_7'}+ 28z*!}s"֨L)> NBD{]n_7bp߇؊VV`wngbj0C[ $ONZ;0#`^1%&-z^GlI+0ia%Bcck kĖ1w`t8َتV0`˜ 'vB A+0iaLm(6 A+ j0CnZElQ+r Uc 0N (PִB)ƔAŵ툭jS1-@^p-Z!@AL cJȈX@qS}[ &)<'_u/FLT#L('<Oc,&'>XV0,`th!Z1%w`pz[hE` 1%F -\V0`K $~ A+0iaLiqmYmVB@ fnH`5jC9On 5ġUhcEo'ڴ@yT{vFǐt*6B4 -DWoOuC ^G\V0rq6u/<kzs`pmZ><6B$ےHf/?C!S0ioMuC$T`s,Rl([cSj0C͚ꆘT064y&y+x~7[0xޚFʜ`تV0J4㻉?ƛ{OVCAL{sbNP!ڴRq {bT7'}+ 28 )s bZi#D3[0xMu7T064`wngDyW݋/ *놨INq Ziu㫱ן#}0Fn$B)  .XP!B)Ɣk  bZc 0ZZ0f` 1܁ALk_ q( _Z0f` -DWa%Ă ׬LĴ0;0i` 1܁ALkS1- bZ+0iaL ދV0HXqmBy%hS@n!" c,F ދV0D~5 c0i` ( 0^^1ĴV0` bZ+ j0CnG` -D:,! B)Ɣ{  bZCALkS%H%#]CALkBx4ġV0 @A P7Z!,q(LC@j"ġ0 uqP4 ġV0 Eu`h 4sAyq-u{V$+z1m2R 9ᒯԼ}΄, \Mq}.5@3>p.kK+Lϒ`%R 9ᒯԼ}΄, A.ܭ+\1u}ws1,`7Ő3\wg8CZ|Ax#\ i1u}ws1,`7Ő3\wg8CZ|A @s`90yYf=7yv}<'|L}>I?˜y(8p;CFƞ˜k LL7r7)?s?+Dy"|xJ3܊cucucLń^(f id<qc]]✧ ߞys\:Džy1ϓ|KyؓyP7P77U_->f Z_LCKkcpΡux_u @s`90h 4A @s`904#a-ϵn 4d_9=kS+;ԍ5@vE$ԍ! pߠDE@ԍ!@߳7wmԍ!@߲ko:@ @tӮ><{pc3qk9c=HIcx%07̭9c:UwƔno E}n2q_Sstw 0x8B?8cÀwxX6~0>"z?VodN_Ƅ.>À2&2öC;x ^yFH?Ʃs8o(~&_0* !  !@s`90W0Kpm0h 4A @s`90hhADDDQqk` =IENDB`PythonQwt-0.5.5/doc/images/tests/BodeDemo.png0000666000000000000000000010107712605040216017542 0ustar rootrootPNG  IHDRͼ5vsRGBgAMA a pHYsodIDATx^ uWU޻QT[(5-ZV0jZ((/7M (`"B W \(FM5o,' |bCx|lnpۦW{?ƫiohxc/~jc$g/|u/,4 $_Η.Mb= ~pf{6H6%>{lze~xM#j BcSX4f,C;{x~w_yo7k_-v{wK,7<ӐhƞqnS~9#%m<>p~{ޥzeT_2wFc3)hd<q\RѣP7ym}Covoa|pn>Aw-w}{7grQn?m h<X`.xu-[۾ drFK'cNmX=ޙxf3 vG~Qik_pI0AU1xrǔs O1[mW?9oܹE|n4mQk<5cۚ<_aα> |{^O)v9=G {R/G|ph<۷UĊldd<.1僡ϓ ׼';ۓVi_f~3x/c$x[G 9g AxX!x7d¶ܧ1z~鶉 ]Ǚ?n9n[m6 !w.wT|`/j.xTl_ElD.)rh,JOٓ$2¾l,K'Gv~*Su;' uuۑCu,sFja;.>u[s^i_ٟF˒MXO6-nvdZlN7=k;2XD(u-䞸#/물߲6+۹XOQ'Fy4?wܭB1U[s^|R öI&qdz~><}Sۀh=@vQ y篺Fܖ,w8JO'14"7ߗh<=|p , nQ3W~IfF 0K5]>u5r?P*GJ>FmLxbm34mZcHCr;,ԍ}<8YYwRbzF5ۦPEm~_jk-Ny1;q8Z Ɂb)V}upq/>j;r_X9ums&v1H2@M?vKEt0Oٷ缪篊F˒lz~>J)P,Dїŝ{K%9Ai޾\|1i"IgZ#m2#w%:5xǓvO2!-Pxu.V>YնRnSc_%,DrƐwfEMbx'ML`k~IKꁞ.>ck2޾ŧ;zcyRnc-;ǻv.Ó?A619[`طLz7m)Os@ʟF}yߗI!p|c=i&ēAx??Gx45˿Hxp<9 O49])spvR$9yZ X7?/ 6¿u,<։5=OAF5ǀh¯ļneZ=դhQךC/jbj' nh݇?1wSw!։/'kuЫ'̘/<`ˑMޜ}\oƣ|P-?.0cG<8[E͚u$W=]>sկK-q8Rfr[O#܎t[g[vMk3Yɽx'j׽]rip0)C/MOJylZv96w٤čwb ?YΤM}_D~\;<&pcG<8[E͚u$7nxk6? _|(Md7\C!M$r|]N1oL^4?n) Yiϔe<&x̠hbk޴YC[ 0}5^{ֳͽ]~N'e'Ok4ذ4.&'o;Lj}W!ka?ē$5P /kMuXX5;<;,U,2R%\@O~\3`43q;YAz@hbk޴YCK˽͟ K‘j<& D eRNS^6fLv/ T@XO~%A79mʏ0yL3)3 qƣk_Tl֛5krh֑<\ܠ7/ǿֿ6Zx_2 nG8w0ީ8k_Tl֛5krh֑<\ܠ?y=c2z˼xA~\3`H65gQHz@/7c;i_Ecmђ@/ nG4rSA6'˃&㌇G<8[E͚u$7?06ǿxĭjmAH,^k_qqjR^k͡|51JO7siH5R3YwfY @%O)NhiQ&f }nerK8S@cĢT-Zsh%_MS-3`4k:k8wH~@%O)NhiQ&f '~>ͅi&۪C|\ܢT-Zsh%_MS-3`4NLٝ4ql兦>GãKRzӢfM:[Q6o &_of Z=դhQךC/jbj' nGǾI4_Tl֛5krh֑<\ܢ?nzl>D,xV5>^j&EZWT=ap @1ãKRzӢfM:[w.j|4#=դhQךC/jbj' nhw]_hyݙGvW&EZWT=apLz{x̠hbk޴YC}p_]_h{]_h[ooOT=pv_E&EZWT=ap 7gκd;{1h<촪iͣKRzӢfM:'nS"ڹ7yn[ݓ4#=դhQךC/jbj'x 4_Tl֛5krh֑}g\Fƶ&EZWT=ap 85t,h<<)ũ7-jЬ#yEqlnadw'cue9~-zI5{Ѣ5_(=rO3M8˾vL<φwk?n G+c͡K>.nSMًu9&F驖{&ƣL;ܼ<7!4_Tl֛5krh=3\>km7<%eރmK-#=դhQךC/jbj' niB^88i3$G43h<<)ũ7-jЬIga;[;\4#=դhQךC/jbj' niB8֝θA1ãKRzӢfMy;i|GJp}C]=ؘIO5f/ZԵKZ gtaȮ񈚊Sx,vѺq%h<<)ũ7-j߇ oq #յ/pÕ[VYxTjE]k䫉Qz0yD<r1ܜcy5 Z|mxx4~SS5ZoZԬ16X;Q\ܢڥsϽnğ!#4#=դhQךC/jbj' n¢8axlIwZFcrm۷2/yJq*FMFc t)/?Xs {LjEO5f/ZԵKZ gewe79{~m/l (Z@%O)NhYWsj&g[pg44u%vgYo- z&EZWT=apL so oe.v^FK/O?}=^F%O)Nh} pR Ce31l>P>[Y⿯EC@1bSMًu9&F驖{0q<J3T4_}hUӚG<8[!jJ,IF}7okdg=xXTjE]k䫉Qz0yLc:(]㱋arp88; Nknwi 81CTUMk_Tl֛5krh֑<\ܢ?97ͻ wÚC|\ܢT-Zsh%_MS-31s16Zp:Zմ%O)NhiQ&f-}w+?ʹH}Z=դhQךC/jbj' nǤZ%O)NhiQ&f-+-WЌYZh_κZ=դhQךC/jbj' n(-BãKRzӢfM:[Y{uo-`sLjEO5f/ZԵKZ g1j<ڒh<<)ũ7-jЬ#yE;< m˘IO5f/ZԵKZ giRKkvh<<)ũ7-jЬ#yEOa7ZxXTjE]k䫉Qz0yLA%O)NhiQ&f-zOrM=%"Ӣ5/EO5f/ZԵKZ gș>5+-yɻO(ãKRzӢfM:[HGa{t[5Xsh[TjE]k䫉Qz0y&&sN3G8B}h<<)ũ7-jЬ#yEw} wc[} Y@mk_qqjR^k͡|51JO7OڿuEA|xWA2/yJq*FM594H.n#/(3{%ڎj9~-zI5{Ѣ5_(=rOg/UMk_Tl֛5krh֑<\ܢG _eQlQ_rk_qqjR^k͡|51JO7Ϝ (z}5^F%O)NhiQ&f-z/_V4K.KoZ=դhQךC/jbj' n9Z11%‘h<<)ũ7-jЬ#yEߺ _QlD`wB۱a͡K>.nSMًu9&F驖{晓x.XxgG<8[E͚u$?sÿwQlD=m`͡K>.nSMًu9&F驖{ˆ癘x9yn/P\jcG<8[E͚u$䆯({^Z=դhQךC/jbj' n9)yFx* /yJq*FM594H.n# ǟg?vc͡K>.nSMًu9&F驖{晓C[tx*;U†>G<8[E͚u$vp%QlB{>W5/EO5f/ZԵKZ gN.nSMًu9&F驖{0oY,T4O6`%&EZWT=apzgΞdS %&EZWT=apL|3ux̠hbk޴YC=+nxS>mM%&EZWT=apL6/wgx]G<8[E͚u$v3gGT/lRϣK>.nSMًu9&F驖{l=53C1ãKRzӢfM:[HOsïz[cRϣK>.nSMًu9&F驖{uلlg{"`s%Jx̠hbk޴YC=Ҿj|leQl kG|\ܢT-Zsh%_MS-3%9̦_☾mťBaԃcG<8[E͚u$/(FG|XyZ=դhQךC/jbj' ni3 1L}㱹1~{|7:i = xx4~SS5ZoZԬɡYGpqi> }[Aãk>,/9~-zI5{Ѣ5_(=rOo~{e4_Tl֛5krh֑<\ܢ'_ΣZ {x%pU{[sh[TjE]k䫉Qz0yxR:0a}cUq;+^F%O)NhiQ&f-z_V 7]p5/EO5f/ZԵKZ g'BくLwXãKRzӢfM:[D?q?VÞc)Z=դhQךC/jbj' nC>͒s/hbk޴YC=&7]j5BsؚWqqjR^k͡|51JO7π 43h<<)ũ7-jЬ#yEOW gt[5L9G+&EZWT=aLL<';YqF_R.VE3]T^{6X? xx4~SS5ZoZԬɡYGpqh/vR mհˡk>5%&EZWT=aN<Ov1_xY |X#Zմ%O)NhiQ&f-zn_Zj5*Fbr,s͇fmjR^k͡|51JO7O99jI\Jz@hbk޴YC=ў\7K#xx͇=դhQךC/jbj' n9s|Ba˦H?\LGE㑞vؾcjZhb6m0?FzӢ5_(=rO徝I%&x;<8u~U^g6>.BݶD#~sQdJ4>NzҢ5_(=rO<9LJxl^aOYˤ/yJq*fF6ƃ(-X&rk {0ƍULeCslXLzD㱏j*bTXsh%_MS-3Ǔx/90_j &bo Kٝh:<>GãKRٴ񟑓xlc\CP[#k4F$D9LG}ЧxD5ugtxKZ gžg!fRY|LG{t/hb6mgdύG8Y/k=L<ێtM<ф<ԾM_:GP xc{:|%h<<ZWT=apL,!RSPjkhxtmxx4~SS163#x<_ۉ zƠXsh%_MS-3M!xL ||?T4'@%O)Nlψ& Mp$xf_ײ]OM!N܎(OahCEO5n޴k͡|51JO7 Z'&x̠hbk޴YC=>SpGZ ,_vVُu9&F驖{á7aE, :/팗xx4~SS5ZoZԬɡYGpqh nǗZ q|NrIO5E]k䫉Qz0y&&s6zq2I4_Tl֛5krh֑<\ܢڸohf ZůBwUfJO5f/ZԵKZ gh<h<<)ũ7-jЬ#yEOqݷق5/{nCTjE]k䫉Qz0yL(K+)xejZhbk޴YC=wꆿD7r _jST-Zsh%_MS-3` /yJq*FM594H.nSm\i||n7A&EZWT=ap P4'@%O)NhiQ&f-z׿ _SnȤxAZWT=apcyb'4aq4_Tl֛5krh֑<\ܢڸt×=.э7-Zsh%_MS-;sI.@|ٜXx/f֐h<<)ũ7-jЬ#yEOqϞق5/Nn;RMًu9&F驖{hv~G0u\_/hbk޴YC=}76[%_3>miI5{Ѣ5_(=rO/yJq*FM594H.nSm\/vc^F#x99~-z}v9&F驖{0Qx^jAыV5y4~SS5ZoZԬɡYGpqjw_놟zuA1bSmz/m`͡|51JO74Yw[ DoUG8끗ZzѪ5/yJq*FM594H.nSm\ Kt#hT4O=mSxG<8[E͚u$6._d7nLjEO5fNXsh%_MS-3Mœϻ%{s!\hcG<8[E͚u$6.jRMoĚC/jbj' n = xx4~SS5ZoZԬɡYGpqj?Y7jRMuoÚC/jbj' nhق5/EO5frѢ5_(=rO<&G8}$GV4v~4_Tl֛5krh֑<\ܢڼ<>.u#ᇊXZ=դu9&F驖{ˆx w0ZӏP_4ƣk_Tl֛5krh֑<\ܢڼx34#=դ?bE]k䫉Qz0yL(RKLϏ߻Z7|1 UzpW]Exsixh}q;<)ũ7-jЬ#yEOyٿſe3m<a͡K>.nSMن#[ԵKZ gDu㱃: RȾH4_Tl֛5krh֑<\ܢڼ?"n-Xsh[Tjc-Zsh%_MS-3`Ac@%O)NhiQ&f-zK'.b=ق5/EO5f{E]k䫉Qz0yL4m<ãKRzӢfM:[TwlY *FbrXsh[TjA~k䫉Qz0yLA%O)NhiQ&f-zi.bxXTjbFjB`]_/jbj' nhۮ|51JO7π 43h<<)ũ7-jЬ#yEOy׻$fLjEO5f/rav䫉Qz0yLA%O)NhiQ&f-zOxyI'6[%&셮nn_[Qz0y&&sN3hpsޒ &EymS5KZ g4_Tl֛5krh֑<\ܢڼuOs$@1bSMًuۦ1m͒&F驖{0cG<8[E͚u$6/S?KbxXTjE]>v[m͒&F驖{0cG<8[E͚u$6/7&1h.nSMًu9&|51JO7π 43h<<)ũ7-jЬ#yEOy> 5?T%&E;o>X/孉Qz0yLA%O)NhiQ&f-z˟H7$fLjEO5f/ZԵXX/孉Qz0y&&sN3h.nSMًu9x#Kykbj' nh+c;3-z"~nsE174'ᲅx;On爗xx4~SS5ZoZԬɡYGpqj/ xN3~"mG$xÏv믘5LWۼ9)O^J{HFo=x=xQ۲υkg/ gi8B}<M!w_Ϟ);LG%O)NhiQ&f-zEO>Q@"a'k%h&Ch=?xUI3}@5{FZ֤MKtP0y(gB}B G8ΞN Pt@%O)NhiQ&f-zE?Z7|󣘁i?E|M M##nĉ͈L6y[6o|#MUpz̄3ḾG{7$K}ŭyãKRzӢfM:[T]7| i?vG<>N>_C3yҟ`O?e8΍wJxQtxLkLrEMk_pqjRi͡K.nSM l0p_┾FښhhQӚC<\ܢT-jZsh[Tjh< ;\po 8p?\ۂ?\0p?<-h<7x`o@{53ѷ9Sgݙ;{徥~a ߁4Jyy>(#8x [ `aW=N}w`s`<@Eڹ=p'i=}KB@S@ܓ1mX-w1e=5z}fxh%X-w1e=5z}fxh%X-w1e=5z}fx55GBo\х}k<ւCr/'@}Xp]g;;x#ऀ{ 4h<Po);-@|JP}O4&tV xpX~ڌK'#Gn?{kSp@ c0xH8讱#:x)Ϟy;om6h<}٘=< $hߙ k;]`&#ĝ67\S-']Hgsl<#4jxqۗm3/n3yD0`<1Axf7E$9=S7Ąj<8mFx̓rb&oBfb[Kۛd6Y!uhftш0:M%zmEu0yڎNo3TL108~G#:x9Y~CP&Y#&rb!'m 4k^^tdo7_4yD03M`Id}k ]ͷ)AK^_ZhxxvƄ?&&mw۞#޻yD00 x%Gt40@1h<89`сc 5`сc0xȠh19@{ 4h<7x`o@{ 4h<Ѕ뮻nC@xPAfg9p]A#;@UpF{mПv?EwJ:IENDB`PythonQwt-0.5.5/doc/images/tests/ImagePlotDemo.png0000666000000000000000000006075112605040216020555 0ustar rootrootPNG  IHDRZql˓sRGBgAMA a pHYsoda~IDATx^ =U}_1n %,aq@PA yȐ@ H ( Q0Fx/$=>Vw??oT՝_;3S!B)Ok `0eF`0 cb0 )h `0f -`0F`0 cb0 )>㳲[S/ `0h/y(/e?pxK}ǯq^k_S/ `0h7hb^Wza0 h&Brz^v*]ԕ;h-o9|wIrxÇ<z+ozxz^FŇG/? `1fF%FmL5VF'/}> _'}F5V4Z `,k5ZdIDW|=>+/<<}x/hQxۯK?F`0ƚ6Vs,hzg|? ;=zFk7 |=%hLҋ{x?<#czṏ<苻Vmyo6q煡u=k]`0 k7Z0Xs,x7F{~gyqx >5Zo~ӹau49?/zjk^җ o8UW& _Gڷo!4Z `k6ZlE׽ [׽WWGu؛(IG>iC0 Z֘lEy՛oy_i7ߏi7׼Շ^XD5~UG}@zoN!Og$s5WƊF`0Cm!9)+wzg>5M>9=ه;_M/unCN>#L:$FBzњ͏ \ an& Ș| FK}WK㛛 TZBM6?+mlh7-B|o[I<㛚D멫5 ?>=܊:ھ#hON״ לS={R%cro>9#=|e!d=uă>ZE)n@׭F1+xM}Fgbkýn瘛ThuajU״u0O}?b 4Z,f ~_bk^ZrG@ؼm2]G3zXtLtt+ CuzB" `>UzJ'cO.r#.4ZBÚV- oh.-)tk0\mhX4ZS4hV-Ct!A$OWrB̋eNRqMsϻoOozfd[V-Ctf7 /15_-#k~b2]&LSbk,qz2Z1uf>>?qO@e!d=uă>ZE멫œL*nh4{Qpyj}|Mh=y2!dFBzXѪO T@;4ZBփ|o[I<㫚D)!~h Y(8ο׼~䎀>ZEC !4ZBÚV- zl~Z4Z٬ђo|]OE6m<}z+!dO Y(8ο׼~䎀>A"duă>ZE멫Vr"dhM} F?4Z,o~_bk^ZrG@A6ǯcD|nRa<Ć__kcok7S[_۞_- hBh=ṫ[DsrsшtK堙#р ܌ h \@A@{Ah78O=b e%V:ikN4'B{ut}3̽Hl=ܷcݮy=x=kd1cn1ME6Aim' EAyA{C;E{8=iprSb]"9>P \ř!uĉ3ѭ߱Ϩ놱o5mmr~ a¤ :\+S}|" X''Z pX].aUB>r@\{k;n*%5%w6 Tq87TynTtv7S\F}Xt=o}x1o.n`ovҷߢ/OU06s{~̐x뭱|'OVYduy_'659-_8@@-cT@ho%Am{2Lm9dJILxg&hLF]ςQS>4ZιkelӚϗk2A@SKh==(\c/1c>+-<I Oԓ!:cn1^t_kY)Χu-t:sZrG@-cTq8o8Oe(1fzy!ׅF1K}kc,ϵala|y|RۓK ZOet9or7q r,9?4= 嗓] e\2!dFڄڹ%&x;is-j"mmԒ;o_D7Awh ѱXuv &;k߿RP|o_D.Bh ѱv &3ؗ5_lnsuk}h==eB!:N>+]IFb'hviz*eB!:r}(ƶ[jÞ敚s]e\2!dFǶ[jÞY!R _66S]& h*)텔䔚TFb柲')m—/ϔ9ĶZrG@Eh=y2!dF6ES )=EjN)kA@yh==eB!:.)/|jÞ^Gj%H'}M\+?k~C/;4Z@<:R#\RJh9{s͜嘒l%w2V.ZOeLجтPkt=hS/55s ? ZOet2'Zh )b~nrm{f\RIi/A@ih==pw9po!$!:,&{]jreqj%4ȹf.r;?v&MԒ;Ow T˛B#$-Ct4r]^79ԔY{]-\3Ch==MV{m?w7mp}Zf2DGo nJyIh @81p]ε%C r#9KD.o mB_;ƩV[ 澥H6 !:X6*KQ3cS-E(P]L1mjo~[2] bN-=\9_ߢ}sڴëd3h>ĔkKo=->ZE.:9h BcS9\;2mjؿ /inh==er~ -Ct)b2K?%ƨO\{} Br?T_3䎀>~_ۃh=y2!dF L©OecT@ !5Zh 9Sj%X"ko+ A2!dO 1"?1e1bQs_;c?[L%w2V.ZOO@wh  .}cO?)5+S]& hbD>D>rY{{gTךߖ@@A2!dF#!b:׎o%־DČS?%w6DCq!զpwvd h#of>}I-%n~OGgz=3-CtT?=Rf5pAdڧY>բ"F!$Sb%׎o־T?ČW>jj~o2]5V:C.h lե~R:6[ud^ZV{K_b>z*{QdY??z9w7krmh BBl_r#9k]J~JE(V*`%/1A@~h=m{h)S-h  V]*GL; X25/61[m@lmQ+䎀>A2DG7Updz8?_0wt.s\sw<| >bm3.Z%w;wT˓I!uCeV}*n_Rv9X}]( 3nOC}g CAdmDV!!kF'7%9t)?9wd~Cyp`W2P_3mpL2D'$쩠uz>/ƲP?9^K9Ӈ.cیGԒ;h+2]& h#mK aJ{KG̵86S{X3﫳ΓxooH;4Z@,\ЧV} ޏGKG̵86%h{/0gww T@ !4Z茉z.S=K;B ߵ`Ϳz?96%wo7DC !4ZHے k<{4uc/5->~[Gh==eBFsc' rAei>epx>0MKO}-X A@-cT@ !-(?p& RFt,5qpXmr*s\kڣ`c`VX}H-#y ZOeLXR:R؏* ч.'گC] c/1>~kA2!dF+Vb\!Xh{S%Cю{:v,;({9B{O}Z[H4,S]& \h*t{-9q9_{9.pmךCoJQm_瓾V{/5f_˿-c{Z㞷븯['z*it!{JFPc讙 |*^?W11]*KN:J5\{h摋/ǿy4R!2!d F+d 1Zqv@!1\RY"wQb=GsIwrs>{}|Qǃh==eB@޶D%穔nӇ㭟P~p>K󞾧п_9b~~M\ku,o }F-#ܼG2]& -Xy~]嵝LXgʆv&K>ty.u?.&?íeԵ?rí 75D.BlѷGgNCr幍Y7c#/9{Iǔuv;pb=|h+L0$%pe>ys*,+s-0=}},B2GkBk&w c[;ڨ%w{z*eBȀmb47i:}"w]wX!̝tL]νc#79^@@y ZOet2`m 501}cIǔ5a s!.s~#q y$S u-Gp ǫêV9Ys5?ecvJ!bFr\|Q䎀>7DC !4ZH71r!o)U?Ɯ5ƞa?>e1GO1cqGXh==eB@~ᅳq2Z!WU?ܻLs+w2){:>\ǰƘc.{}͓h==eB@jߨb8^}Ӱ%%w+U#esM1R%gREs5H&j}}h=y2!d h~CF<]_h%gRM̕;YԽkSErX>1@@y ZOet2`nwΟ⹹I71bH75oa@\ojނ5s_k?S]& $LO44ZVŰf5cZ}"uXsM1P"uXrs|Xc1%w2V.ZOeLH7Z-Aܧ:G81;(;H]n}=D9a[XcIlǯk> S uLпoeՍ:N 9}"9z9r')닽ê15N͓h==eB@L5ZAaՏ;)kR;[?6v=K_qbAwX.8%4䎀>~mA;vqI|o܏t 4ZJl0}DZe%eyaջΝ mʞY)< h+GgzCt{{5bnr!1Zvרp`ђ>;N̘c#e.SDz(S6e,RƊ!:uXm4G3n-Ԓ;惃h=yr,d-G]LYߖF v~wluS;N,S|5s'kê>)ukʸ}͇z*{Q5^#XOhHMhFj8)XCE̽`صݫ1JcRWj{c'h==(ΞJ 2~>ΰ4Z'ivD pXb#61bTb, ?vmcjرbÜp۸]䎀>~z=d=>?|s`ԁ jZجr]?hYu)ĎC0/9zP2orNbc);^y2V.ZOet9 X%hдs;BnrXu)ĎCbV2orNQ ^GV=p+1A@ ZOet9:1Yb}:BIhڹ"7V} zиc)}nf5(4tkk?rXuČÔ~b玗;v Ԓ;Âh=yr%4Ww˛T@ !j,=un{FKDUؘ%49;5 I%D.f3fW?D):Ba8?1HzHnc)`L߸1Lޢmw,z֡ۉs C#uǜ2%w˚?DC !jQdݸ .dp>|t3Lc5ۋL˩3ĸs7/mLL1Z/M-u 6Zz>[7ߘL~99^[ӱԽzY^;{}S]& D- e\iF~*7v)׆fuǝkkiC֕7n cy2vm7k^;ekSK4DC !4ZG:T0oSesG|kۃRL)06w=9񋛏 T@ !5Zb1Zdz֕7v S7#5A)kc帨Scϙ^@@A2!dO"E[Ǝaʵ1`r/:S.Bh"#uD$CຜkSV|%~r3|kǞ;=>ZE):BQd ZxgC/5v ץb^b=[n̽̽.gs/l>&S]& h=^kq;R:qvḲfĚˡA^\bVY5I-#_<%S.B"V'y|BPrsM.J8[ZKk"g/sZ =Ryl?D.BhW7c?pnNcȹ&k-Xki\e5`-ϩc n>6S]& -MzHcPwhr\GXk`}Hm#n5zy\ZrG@-cT˄ po^w}$?]ނѲ]kUܵZˡI~< yz*{˄$u{.n6u-M}FUh!=~ K5r*Zk~*X9sc ZOet2aOn.5݉31_=~ Ku-aԆz"e?6)}t֩`עGW@h={!:BN0:X:%U1FJےt+Ӎ{^Kn9Iϔ%qy\#>~v ZOet2mPw2͍c i[neNku^KK)mK-F.[}S]& DuE1zMl9VtZսh37!f/bŐO::Q*k0osf@@|B2.BU-8]d\|ĴnV:j;7{n kR;v\ןؼq @@6.ZOet2aw7;uwV\!p>,QYbՁ6s5Be 5lxm"f?&n.:WΥ,qxq0FӨ?mRSw>ÑO~Vh==eB@“+Ccuu hY"47Q ^fN:6XC}X f?cڤ giscOv}k5Zn ZO%]& $݃S'Z4ZE@.V:fnU:We {z:6K2%Vˣ|1!_4ZBRy?} AFKꖤ[.Y3XCV% i~ V3XcHwH{ͣA:BN\YO*DrehY2Z5cuKҭVK|jӵ,؞zs]Y5G>9Δfh=y2!d h Y\!hiU4jՓϚxXK3cuKӮ)Zr*|(~ʇ5mC}/z*c2!d h]'i> Z{Z+VYNkk(U4}>ƾ_vFr]/!x[|s>~zAr45Z6ZPzo Z+%o'=ACy rz*8jwmU,5mE>'nU05>>Ar5Z0ZYKtM~S& Ԛך}o]Qyiͧz*{QxBj%h j7ωߍMe(ڼ6צT^s  jOݜ9Nwܦv}Sh=yr5ZIzJm hB^1֧zjm{D5|~yoDov-y$^HUMZ55Qy՞'G#BQxBj%huoE2?uh(1@_zV^n<ԹkXsRs./rB@?yZ҇.:$ h 3BƐ>tOп*х]v*i` ꯁo_}Ejǿ<=SC)JF.h Tr^_ YY+V|;-U;儀>~rAJL0ZVM Z>G>uKӮ)ZrSޯ,I1/jVq}SC !F j'XO#=T+|UptXm- ֪| fIB{_ A@RAJL3Z;)VaWFWhп&Nf)\/S{TtXm$z!/6D˄u{_pLVњf_{ڄ(5bՁ6sB.L^CV@.V֏ rGgzt!{'h> zjufh^\a[bՁ6s5j+86K11mF֨=T'5wAJ?LrDU{mB#͜5v.[jn.N:2V.ZO]& D[[$)Sb}=6!Jyi3' +2-Sb:j77{Ŵ/ ֧=T_h>+SG !FkgCFVp͍c i[ne-_)Qgl9@@?yfҏ.B/-mCAre2uЗ/)mK9\#ױ[i~ƶ+ F@@?yVҏ.B"VG4K +5-:ZJc)171RږDB:vsbƴ/qu#Om>;SG !IFk͸B'ZeXC;)B-^kEܵھGXh=~t2a6=A_v}naܘ)mKѭHw+UܵھǏo>'St!{F$:Drί#!?O7nẸ6J]e[Vn7'n!>)t95> TeB@}fp`ׄ+|U-mY\ǐsXz-csb}h=~t20hޮWhʡ?g\Xȹ&:Z='?e?sg6TeB@ŷ'[+kqC}L5~krBT5RbXKWݡ΁;v,ץu}SG !4ZJxHZs?D5X[kgq^^# ZO]& L2Z5Z_m?F59t+g`-V}=R}|JAJ_L0Z:OH\ђܬk[Lon1t+`{jܱn_w>_krK361z*}2!d`ZUa拂h=t2@ć8qcrmY#XOߚv0>O˩׏16ow\/TeB@|B@UWF 7n)׆fY#XOߚv;1>O˩׏16ow\#/ TeB@z87p#߰+Z|\B$pՍ;1cSسΘsJۉ06n Sa;" ZO/]& D-uhBuE@-cTeB@p{m7k!ta1c(чKhsgckH5S;./ TeB@RP lͿ^ϵЂVLjCcP _%ۑs7D>\d8z}S_ TCu(50<ncmX9ƌDQ^(=zUD.xG4_D˄H1<e\!* ^V83f10#z߱uٛJc~@hx7_D˄$f\!hka3sK~nup+5)1w᮫ F>x1nva xe\J<̻wWG[3&dŋx!Jk!@Ȭ:plV} 1R/M̝tC?R1C@dAJOw_ 3rORZ}[BHt~aEFCX+vn%s'zкbS(QUkDٴWOrGThz0ZÓ+yv:\ 1f_Xg[\r3V,悱x%^+Eԇ>]*3~nV7C@? ZO?]5Vz 4]ZD#hB'Z偠Yuآ?XbƊD_V=(7zƮm>X1(՗INc>6_D1S)A1F~Zӟ#ۃFK DӪ[Eƈ+}Ωdޤ+9J09z5h+Ӈn;J>4Y jkaN`-u_?5 :6 Vت?rcNJaj_VXhDٸ5^Ę,F\5<=8ƂU"79R rf9ygڦE}_|ijҟ.|^5bĠ?8j(4AYNjWre5aչ[F\^eJL"]8oҭ)][V}8?C }AJ< }΄;zEEGf)V!|u&%Y^V˱p$?eXr<͠=zksY%>#e\R򑛻&uX5_D˳y-B#hWDS#3e}wrXcVo TeB@Zׇ}BPѲoEEm"qB93u}rXs5sG@? ZO/u(տ}4ZBk[[Uo:F wwUc^ϜMǹ0Լ7-SS !FkW;~tSd$7Z[u>WD0koq<=ǎEJ]aon'êwGnWއo 2V.ZOO]& lhH'ZׄUxEXcI;6_͜kscJ)j|c |}ҧ.B c_ <<>o AJL0Z0^dbu-|GNJ][b2s1'5=SS !#F FraՃc9aՏ1g{F590<-п5>}ҧ.B e kª q2xcr1g{F5a s!ˮ;1F@-cTeBȀi4E5:F|pۀc Ck>kuX~_;o P}|Ao;^5I<Z.գ7}wMیwM3X~=yaՇ}|h=~u2aJ4V0r.hɪxu\ԝ_]F2w{]өr=~ޒ܏sN~c@.r,د׿/rG@-cTeBSibhA)~Yu{75K*w~Xnǻ?wwjcXcH8tTrG@ߣ ZO_]& fZu6?%o#bSgq&},rSџK7w nXsϩfJclSpT?w7\G@x ZO_]& \VGwϘADa<Ć_ZkuηFڗk'}_~5_M^Sk5>>AJL1Y¸ AMO)#G[.z -.%r+\kKG?'6D)7Yۃ[?8wy5-:V VXcO&0F{*cswZ[Xa}mR4?v4R!JLXh?e8|Ãxu~LXgʆv~a|53.cOѿ,N&r#ֺ\oCjJ(wݛo TeBOFh?ahGHSio7cad{ZגݽCjJ(wݚD˄zWzо ).[K(w6D˄z̸B'ZgɛE;Œ[udֺ^oWOs}S[ !7Z|2) Dݪ}a՗a:2 k]K7a)A?L[uTeB!:KSRPk;v}j3cXkܭ>;]*:G\[u. TeB!:d:!aOjw Iu-޺Ws-?|{:B!:d:\.pJ_/rX_/%p;77S[ !4Z,MIAkBЗnw,nw)*5)1wК']*m8;?c.e\JߺL2DL'$).ȟ|mּԺ[}>9r}C TIS9\ جr]O5/Xs.5a]Td:{(n:9.]>sAJOgz$_pNpR)|eҔԚwjKnTN`Y#%#8armc TlܿҖ}Oh:2DL'Fc"KOmuG^S)Շf?м8S_qf|됦!:d:>.-u˵'ֶԺ#úeGXh=u9>fJ*<{u]\Br2DgiB"F VX;W)JkD{,w ZO1v'Z.bT!:d:ZdXJCka/vdkAҿ.g+xBEFRQ\J9ږZkߒ# ZO]gOKL}Y!AeҔxaMlsPb>5(1w_1kC~,]24DLuykԙۖz2DhR\֞^/~#Rc>ZE˳y-BFRQ\֞^[ߩ+D˳ o# Y 4Z,MHJ cmbcq@cL>DG@w=AL2D!V(G.k/-?D멌˄-FhݪAU?7k/-?;D)'Z,f_;]Nk{\`b X{c!B}/?h+2.Bh !eŔkKO݃kR# ZOe ]& hC+S-ckh=1t2@eLډz_}̵s3%k=kRbc}ͽ>PSs}i^D멌˄-CtH9bbʵX{{`l9{]is-ۚTeB!:Bo1R==Z}e}SG !4Z,Mq]ڹCמ5)1P{] 1/?[z*2!dFR^W9l}}|B ZO1u-CtH9 VAN.9lC-Doi~ SG !4Z,M5KK5{&Tb%77A8L2D%EA5sNhs}SG !4Z萲>ȹf.֞ o9 ZOe]& lhAd4&%Yߪ:fIrrIy^sH+}.c{ D멌˄>2D%Es|<-k߿G@cAXL2D%US/Uby7wohY2.Bh YA\#!wBAJNh:J=R;ImoY rzt!{FR-VKJۥX{[&fs/R@@}σh=t2@e)O]ebz 7_ ZOe,]& h4)VƄ_G;_kSjuIJ=IJ[4)7kD멌˄-CtHyb? 5aĮo_ ZOe<]& hC+Ebz7ge\xL2DgiREq>?6)sHi5J=}A>2Nln1m\7:SO !4Z@C76bo5W7&SO !4ZyP?Vs[%vs}UͿ c]Gޡ2Dgi$7ր[k3[cތ՗&uSE@eh=t2Yz>ZU'k` s)=?KoiB9揀>ZE멌˄>2DnX]-_FzZn9 AʘHn-Yu1gn9lsOcl"g9怀>?z*cr>A E;ڛk+R4Z|kd s)k=񝯍P9s@@Yh=1u9iAGji'\hKEF2|XZ%R'Kr?KD멌ٸ5^^Ӭ5hȵ2Dgi$77vf|{(1>-A瀀>ZE멌1<970SgOmѺnFHyh !qR{s˵->A-tQh =iC"dL1}|\_h=-g}߃h=t2P-i?5 AMO!>zJE-Ctf7 /15_-#yE2#dk x'Zl޶ΓxGXh="OOx.!ۄFk:o~(S-BTlRK}WKkg2]& hCYk6Z>zJE-Ct!Fk:e\hFK}WKiw2]& hCYk6Z>zJE-Ct!Fk:jo]Gޡ2Dgi$7^;s_Ւ;3T˄-Ct!aFGXh="f|z-B|o[I<h^D멫>2Dgi~;s_Ւ;ӛ T˄-Ct!aFǟքzJE-Ct!Fk:Om^D)!~h Y(8ο׼~䎀>ZEC !4ZBÚV- ?yu]Gޡ2D{:?zxܑ6 zJsE-CtfE}pyj}Ikh=y2!dFBzpqSNⴍGBۧ/>h<<5Tbx5Z=z*eB!:pnVӘJy5Ѫ7z*k˄-Ct!A a)lkIyFm7zJEf ~_b]ݓ>Su61TZ?WYU#?yS:u:BhCYm6\@@-cFBzњ6͛h=%2DgiQ/15_-#? ZOsmnwV!FBzسQ-czZ[|֋Fl-Ct!Fk:iDiyDl-Ct!Fk:e\Fl--B@XZIƮ`02-`ihB! !BLhuBr9"T5 os\!aKɹp18΅R$ os\!Ѫ si:4Z o{-BVf[FCUp="dhB!!BLhB!!BLhB!!BLh]_ӿڮROwVj)=4Z ҉Byuqk6ʮgho27ծo"ux#eڸ{< 34ZU}xxxs`غ^I~Ѫn Wغ5^{>^I~Ѻ&ru*k7ܳo~]Do{"h]F4|iÂ~.Iϵ wc!a]7s}BZhi:wh'焐@E!24ZB!3AE!24ZB!3AE!24Z0~R8]_J!BEv`\Rͪ~%Q& ^G!d4Zdh*dj'9!,=~US<[|E!@Ev9G= ?U;{R`3L-},oujB!"D+`_̍u6axnw0(mܔaܶB-{VNSf;շ4HoUmzF!R-{2Zu.-BY-{D+aOCBY-{'M3-eO ㆯr !dYhQOMLau P‡6'p 6w$8`(0ddbh CBBK $pl_pg<χc٫WzU]k{yǓ7?/7!Pmj &08 *J08 $>JG?Q? >D7gVk܇~ѧJbp|A? ˛7ot6䙾ogH_6=7?no5W/ͧ|<џnop_oyocg481?'mp? ?7Sw?67o?lpapp-J58gd[mp|Qkp_W͟?m~W?>}Q{08 (kh[mp |5WI___l>o>'|o0ޜn[o5o֩ݼ[;_F|歷Ƶj.{u^u'tض27!qkp?_mr~~Tj.ojif;jN3\;c0_upbt&blrJ?󯟚s;OOgJ|1*5kf[CּQNnpjn=M~_i O>wo$ovٱdL )|E?ʿZkr\6wDD9kHr;_obϻ0/pNJ59# ?i~~O^?G=G5_o;o`F=e.Nhۆ?[0񳆆@yl3|h/|5>_nsNMeI hO/Tc Dr @< !eOlpۀ@`pP%T U@`pP%T`p>B!T08BU C!T08BU Cyza=s^yܳE(ƻaOiA$&2ϡ[\?ιq,.D!#kotҿ~!>W{V:ݾC6m=c>kvTbdJ^bඎEhOdM 9/ZwX/8//ifԾ|{ k>f3Õs[Emy.m 084dcWlptjx{dpOfTJM.s2 Yh76ƿ%3P~Jz_HKk^g }GOL)-os|wH,Pvaphë) ϑi%3Y}x_HK+lpM= k?[垸|onkg0suX]/"6uTU_^_H3RIbH/s-b61>똤-7Tw" MIiDn^ t 7ϖlpGD L-4^! !PBU) !PBU) !PBU) !PBU) !P*{=B(ZB*)48osCu KIё DZ&_cĽ*lt +>Qɠ$κj!Ǒ+gg%%|>ہ>Qଜu#3C#WϺJJ+6辋U*ke0`#X:X*Y9F=gCνs+R08y`W/װJE̓sﶣ/YX<9 n;28frns3C!7Hy "F9`/Wl*s^, k }Zs!ǐw |f{n<4O/x5 ':Mmi~ŋW|RМ.:Fr7ߞYiHs 4B=)s!ǐ|uȡ9<66KTepG+}(vlXRȽm˽ #&oNv+g<):y8HMӀ)S wpqNqxhowW8.U}-(bKǎ)s1Wo~=ޮHJqcȇGߩk%u<՘#Ko4! ncRӎ oȎF"C\;EN7V;A: n zxj^R^ N;g :D\s4kc`v>LӀ{n us bǖ#6fE<8^2_CscuNw1seQ}el2>CN1,.48ilugCXvcG h|95@^v@hov!f{J'& _pwTP~-N[: NWƷtȡ9%v{s] NB8n\|A3vg@k V}{$$:MA˒2""t{JCsO\O>K\%Z-{+c>)rhN.ϖ{$u.ߞUR N܂ / 0+cm=P;rԹO<,FU&{`FEV1]5>Ӻ^C%74WSgӾK%bD fT_piijC%ܹо\%u.;^oN9u` -:!eg2sAsmԹsRМVOUgn ߣ]1]7>}RC>6,iԹsdס%ʉW0ZP!YijXW"W-ҕkΥGи*54yXnYJA;V]^+ HMe M F ;,o}pDܡH_{],z]|}dף%KUwl1 rXz.w[1u4ז9 &m \fl1 g-uصLlǰ4ז; .m U&(l!)rzcyy.z|+CP5Wz]w}94Eb׳_pqIӄ|Fsz5H\քcaeSj0LI8kA>Wh]jܮQu[P!IӁբ+ڔ#~u}<*7\K=c"|l>C\t}n ,5i0 4P[F]Od>wߺ%[+q XXF7tXuZKdHmZYiŽ [kɤ/;Y*Jǩ 5BjQ6$Ei*`pwE#DzuPz JTep{'-9LQ}yZj5 [c?֒IY_p+b`Rq- Q}h0B`p+R2Ru~2Q}4\2Kkq2ErBeK1peS 26d@f9c=Z) j(dr5!)rhN6ŭͥ+Ův0D8R ,.wX@6> zz ICsZ-k*#iW*Ek< #mmZ-.7G,r4^_Bs4V_pWB;U4삯z _ک nkR.8 0zKTepk%īW8dͰ WcocS)|/8U,h !Xi3P ·o#^s#$E)nP˸1 .Wd;UEE"Ndč7k<yi<=QϾ˒k_pE}q>=ZH-Z1δz~l<<5/7D;% nm/xԼQ,\թu z{h^y+ 67oa9^ڏ֡'Fk)Hy3D?Qcvr= ή"Nӥ `OuO/1hpS<=^'}ǝ9ct;uǷ18cc3eqgxl.\/-"ܡrm{}@C;t񰱂m!SKMuaIJ .\cph7E@o.]XcDkҐu}T a[(!{{|؞cp^}<v_n/̌/nְ:{xzsǦK' ܡC :|q #{\鱗-AW"% PǥK`ϷhRNGC;xizKCB ݻ,r4u'?W$sKi121j- /dː94dm, =589BIICszyll,n ?ķZ+=8d{ .Pb4y>Yi$Oa !8k󙹤䕐R` uHn2 7M*ksn k\!?*=cI) n (ۿ޷;>&qbc jk=67tC<3S4I/Hn P[둰6 N&I7] yrLA鱗-'EngX_p81PS! Oc"N^%C!'}Hg}g*[S울5BGls~3Ro%9.w\PKyPΫ'xýh QGP.K_TU\jk>RbHrX~/$jw4%g[ qP;RZK |i:3qZ T7W3?U\ gHԕ! 5sn^Z֊61\ vERKǕEN&7Cp 6g* ZpDLJ\ecZN:^{Wd/Ru/a$ M@o2lnp3Hl*dsP?6pnΜ 3R+C&0ľ$81~ {+>xXFR1U4A9'٥p|Ump3H*l:dP76PgYUAP9 HMRb_ CMc{-08 ΃CS6~ٱݵP's(3殪/8 HJ|i|ȩ;sW1صg3ܗkDM59/eM5>{/e~Al9(9$ׁ|mQ3 /rt=cbsMm:)s3_؛7ͳoLd{kp \Gr]ge&{i4cy' =_28 +:MP\B}z\!y z{h^8 =38L94ܸy)5ĦSaiʼ kx A hov9:!q18Irit9)h WuSH!∄B Ht;^"%Uԏn (!yIT}~k`ڕ b8P,>$S9HMSTepm@awn (1:Ph8>itK7.v ,Z#PN$6Cr@rS\ws@9:i\dS) U4؇|W5P¡v"Mf/CE(p^YHZcRSHxY9k*cQq{R_c^{kR ލA@ٳ08(^@@ٳ2861=7)auw/Du|AܳQ`U08] 8|UU}Q]snZsX[)TeppMn 9Ǿ ,0"^DZmUe F9+GYn뻵EN"qc|A( nAHS0 հF)f!h3`k#7UVAy-7Y9jr[{!hsڰCX ut6>ncڿ@'3T4}/R>ncڿ@'30GX`ֽOw|ۯYb?=ؼQ[Kĝ/8(s_f s}?hovzQFqeS s+E)@['9r8*r[ֺIBRМ]Ӟ"NSՋKJb:>lvl'Xo*`pP,o4ZCr9*r۩9*.C/ֻKwXm# `c p9k b=`1G~l\E)GMUeplc8*r[{aS4c;Qظ^+y/j*%|AH#$E)č+@r) Nce18Z&\|$ȩ+ 9j/861!aɍmpdǸVVJܫ28.R܏` =08n$%c\2861w,5qzje*W_hIc2op/OU gߘV/Ӄ,֞9к!nal\gZI=?6oq"g읒s rnHž&6yz硯sJYJ爵õ}pMcC`p\Û'{N|A.lB:֜[~ۛ$9b{n78Ii;)/l/+1W>x58iW|zqz,\)XL/=?Uz\Xc+w;u݁9jB g 5Yܒ9_p,1=֟+ڦ-[ϐ=>NVHڞcpy쁔[fZ{!)rhN N}uX3.Þ](Gqz-XxlՆvlmj`p%}\a9_pvc< Bl83M N~4?I<=`+A쉔T}i`m,yX{kpm`wJnRYrIʎVmкS ֧6+U5Z{Pfc7Uq_ qkqljR"a_j$%eTe6jQ0$E9 /88M5[ ^jQzteSTZf(1Z4gW!28614qbo.A|9ї%MjpѸ:`p4VؐPSINϸ\n!Fۣu+|/*5ؾZu+ jueS Tep}1K^ϸ̔֠ ICs ~ĝ/855 k @JcppZq/xw mp=RC}/\֤^sT&0 `-"N)կ%qc3@I`p^ /EiqsXG@v PUnFؗ{@C;}RxVRbH;_p/ߎtv5ICs P+տ~tzEN&S |THMӦȇc|,_?؏ ɇ*ogb( ?@@ ه*uܚf.S^j!)rhNIb<+k:8[Zr\|iՍ_"u|:@ TۯfiZ!R#e.RB qF}s!v猝 FRyH;_p `8|J%vg $08 J!f߳u( .%B^j"q+yBϥ w"Xz٫lbY'yj`L5%S+ybv2861\!Ęe$v%B<=}d.)sG=wp Re18ؓKcsץgW/861wYN|b3wXϽy'%UPcQq7pBcܮ94Ȍ5Tq|c>)rhN.GUg77qOH9M59CJȌXxBRМzd6C닗lP{AIƲ֔ݧ8fl* Q7qn|>E1U7Aڟ8flmhw22*֔s)C_@s\  3cJ0l۟# n>,;^ N߱^pq^in۾vl'Şck|RPO yx㎵gh _pfܰ!۱\k\Yi@f|+E͹eOX ξ(`?ȩ$e,CJܳ?ԼxH|AMKyj%\apP3>TeprqO#1ACJؙ\ƔkZv؇ ˘r!%|L.c5@\~ j⵼[Kyj/8˘r!%U@ 2\ 3>Tepdp =5Ƥ ?3kLH`pϒ 48W(R(R)BTap!TQ'v%B!ݣ\sD̕&Jsz& .$JsT\G=P]s\i} Cw'b1Ю"lj'b08q"lj# *~q"B08BU C!T08BU C!T08BU|{yjIyAwy+zh^|D/Ӄ71EϏ71n%J?wN{pk(&)ʇfZ-)$_fbm b|?B/n*{-ʣ7pkcS4T#<>_p(RRD Rx[Ɨ|y_ ]pIݦM37}{-5O#B/ܿOW-̚7'+bfE)% 78QW_RX=KtQgx_\G~k7/Nj~/3^\Ľ+08tܟ ]|QzEBh9 IENDB`PythonQwt-0.5.5/doc/images/tests/CurveStyles.png0000666000000000000000000007765712605040216020374 0ustar rootrootPNG  IHDR"K92sRGBgAMA a pHYsodDIDATx^ ,WY}!𓙠2T&@DDLbO%!A0@@ƄI%Q( >%yF3gT۽jժZU\O ]{C!BȔDADȁADȁADQ >6_ܑ#GF@שmԇ?rU>wL[LݻG@שm8H2;w}j{s^W41F ` 32Aܳ/3ݺ~җ3_"^fA4M5N33-Ss^;njXƠܸR7:uӵ1t /7{[wg_qrA b>LGQqپ_hhnӟnd7=sg_{3o3я~t;w_A2(vffs8ńNP=!rSʷ1fek={7I =wo^; 4 _́ʖ̠y;gIs*WU7Hjbo/4J2ΰ`KxGþ c1Uc֟3z'φY;<6:ĵܴ\y?z :ٹX{a;Yl^wT[]pϠ{^I9oLZ6񛪖ʏP%c,[~yu^#? ף[ߕi)ʹ\eֲZWa;Yc9UPl{l:{gsiP{xvK1-g"mˋa{M#?oY6/?X?Oc˛2o#y=U;6 b ;0)xO˟oc, z}~€^ =ϖ񎾇}2uӖ0?n>U--?opڏ5ѳDkou?z4YyW5>3RO:Zs3S@ m1`M9\igga?c$SVwq,_|FgiSK߶דhC`gAG\// ۋo .e.&jy7_/RX"cۘ:aU㮯U2-H{SYG~_T5";e/3,O :Z~3K~ڴO6M GVl̀AqHwq,M Ga춬vPƒl=ҢcZOqѦAj?Ұa I;[֞>1d?}2eSGU<"ʍu?oMq:/ҥmjf>0EqO/ J6 cAr3-?dT3eMF=X Xm c~,(=}cYَ "NUM˷"/ yʗ7lj0.P>&?ròaűL*,/+^B_Qy2/q\~[XevT>yٶ]lj0yWT;@b㞶 { bLUZ1CS_?4:1gfɯg*o &)KL{7-S?0\`l;z3Z5VGa{\ #_PȦ>cP<{ÖK,mOOPpycK>Pdu4Gp^gi}`YQi BN!V'\Hٳ6 | 3 ZB_' hłLA4Gm^?>R!W΃u|̏up,uƟ>*?Ww)#W&Woaery+̲댻8fc ow*} FG*lKVwL >Ɨe5 bKlXkퟶB0 hi/F))` Wst{+}`nZAT] ".߾i`Kg0fG**;3O_AVab0 pq b0 b5~Z_f~/مA &<mvu 6՗k" ¦B9B!2sA " @ " @ " @ " @ " @ " @ " @ "Llwk[ܖ-[Q@ ʱ}c-3lmiL֭Am}ˣո Z+8 A}aA' "Aa[4``99 "%1,8n[;DoӸͭ[Lk? P XR 0Jf/ķU+D>HK5y mh1glD~[ၱߑoE,E{ckGAw5oow|ר4(qr&r͇`a!AK&A\3\m Uj3A]e\ƯLH|[Y0Ҍcj0hс A2rf8@:am% Š3DA61A|CY&tcSm Lwdб cD zG1 ͊P\ bIYF|[Y02LL_DaPàX4F bЗ)0 _(/~57/ocķރA"rA#\G.fE`[<. 1o37m5 @ " @ " @ " @ " @ " e]깠2CK)`[ BL'BQ*o !/ts[`[ 8? ޑKGuO0BV*_9w׺[N::w!'q~+ b SG'AꖱN<:};GuU0BV*l٢8ܢ#j?p^r/hPEsR 6*p^NYp3 Z-Al@U.s喋/~Z4fA˂"vl}*q( b (CSҽ8*5B4s_|KFCJ؀:SR0 !=A:qnK_BT*`Z-AZe/s:+R 6j 2x;BT*(PI5}n Ź=~;GL9T0 8% xBewϥI9T0 hjyBe&wͥR @2O]ꦠYg>4BT* yBv_q;҈suS` Je 2W^qF.Mq׸K>8PGJ? ,X62<2CmYvs>)Q-+uKqn;~z.{^FC#Jq[߲Ʊr[󿗡@:4VH>QC-V˧qn׻vZ.tp{hBhJ1l[/9gH)XfC-VKqn{41uGt nX+NUN۵A4?2%bZ--ƹ];|LG1uHAq:o`ADZ6}s>AwwΥ.}C-^ V^z$R'pJ*Bղk#?C4aw!T:o3߷3Vp-ji4ܑ.f:|ꩥpF-^t ,M.{L,YPDINM6|0[qn;r[LGpw^!TaKQ@-ap' "ouƹ-oK3p֢y+^DݷܶNI4SUBh2]s;owGO:)f:r[>#!x3j 2;zq4^w!T05P AD߂fqnW4ӑNr'y+ bNI4SUBh" 7F4,!T0 "Z"58ߝOIg6XBhJXP>D-h i0{J,!xAl@ba*!XAQgY> DVKPn=1~}w!x} r{OSP} "BVpӽ7_όckBW*=7K_*\Pmoe=O/7YkBW*6:a%ΐS4SYާ?]x>w;i:&QOq'8J ^ANNx :/Ν|n֧N]~y!4u9@OOfGO>9.ZR$1 ~-OQG'Akj{Om6BVz;w~;r^GChyJe bKHpOO"ui0O[tDK.y}reGB?mׇ>}~:q)^A߬]zv#i ^NYp3yt\ڮ= rR玻])9ToKQ-@;O[Qҽ8ũ,?ݲ3~w׽ȥ~˻u9TV 꾝ඝ֨8U,L35C҅8www\w*~t7w )>8?0Hmxin׻ߝKg>o~3'8xԣ ) b (C PY+ht)Y=_>a1Ǹ_j./{Mn>tBU* *PY/(R q}啹=y;K]J!v^u;z[y88 2s;?\ޗ|{8b 2RóovEjć?yri|OǼ=;|{ӆϾ]rsI o(}_nmMJ 64AbS'i;G=~u.m߳?o);?i;?wS>!8P{JQGA۶^qt=Boh+p *R´: i88,e/˥.3q_ry׾6;KܑyFC=y|Pwx (Uݟe8K㉟?*ܮ?wwpݑ[:~5-oɥO])9fP*+icurdeYAb'"Ҵl< n^O{G=ܮ\#qGN9%{p}\KtBB3(^ĪK/Y܍1}8U,L35mkh Yl?In _KCl]Nփys7Ϻ'3~$x䖷,!,Jq 7k޼= GƔ#ilȣ%fq}{^.|vo;3+_qGoz\=3q_>AtX(߆sh3)DeIlPu$KoY5XuUR'i}睗K;=_-e_8}>0|7 osw+c.u;;5qm6XlA;ODz)m*uUSvgf{qqg_Nw/uWz̿;ru܍nv\};t1Ǹ'\xm,"ΡͦTzmuN+-mS7pXf@t)_ϕՆdR-Qt="izdws{O,Νxz0ν{宿aFěNw(lJgC1=TrGU?MUiGQPA|CvCqug>ͬ~5;K݋q/>q6TZE2gml,зe(CbcH, DrgU"T?ZۯP4;',z{GۻQOG_'BU D= OQ􇔃z'7\7}Cr_G/~?.o{E~Y\fLO8!,<8fU*s6ЩG2 bpȕUPP8~`]L<.ȦAѬl8m*~cYr,]Cw;?,Oeų8^?v?w{lFt_? qRA,>aw_Hba) W^%ӿWvu2R߸#x8w"qN*+qn#:I\:lzt]7pXfYZӼ nPn/<;p ⿵_AeT|X'hh9i~2T睷>i[Qeu ;eg"q:9JS/M{,e/`S4Agmuֵ,)zz^[xOݛi?w~#8sW~}˳068ہgnl+8deH̡ j^8 *mԅĹ/l0&< \gۯ{B=v8w5:Ks F8WMad6:cR^نpsF 'L|vU?C8.؏x;Sr`eAgʥ|j[i?; {10_rɓ x{C}Z {_z?N>?Ki׺?\׽' 8ovݹ3u--R%1ˊsz&icthheӇ/"B)w;SKϺiyǗ4e&C?38wܮw;32VQ=P jZg6? ۢErnk^y17;׋θ276l|G 8K\R ps|r|cqNq!q.0c~{P)ǹv\}0ɧl#ic6Le;Ȅ;hGx#{~i06 CiYYM'}}X|f uSe[ ڝPG7 daБ/txY?;-̓;qMpqƖ 1\5_'t}@\wht,Oۆ%\>V߶=h787:ۏ-=>N?R~ܗzF<}ҷȏMqy}m^q?l.e./q_~Sv;yܱ+3mĹMk7%f/<ґ շ\RoM hV`= g͓veSUYc>Àf7ۙ ܔf%3זnw X1Y 0ousxضyIMQ=~5㎻2PI X1r?8<8*8%]NџT4uawfQ Z6JpyGoq c{}f钍ou\[N9qC?s5w_g bʯ>={L cq' !mgtwYןϓƍDfó|SO͌$F uN=޿M?/[3sjBryq^qN eSF{r`9nxc?qǿܱ׹?x{3uZs31e#) roNy;$ g,';h-wԍs:h>7?]^x`?^ VdzvX~z%&quƕAl8ͿֱzltyV78wwswym6uڌsY0"xCpDiRt컯yvĨ 7xd6,ѱłoLOe橼v 3~M+ꒄvP÷eGZ 뇲<- ;C;#`iaql2ǩmn,壃ض%ٙ?_+uu]JK ,=/{=?穜kڞŹ1lY+sm{vv;jf [ߒ߿6n8ghԥoV8g_wSܟ'=z 47y|a,q,,E΢.|Wݑֽ9;sVQ]=4: EĩNǹ9\+Y86A?|i LHgrFG̕)<蟶N7KI$Meedi;O;;f'$vgٹ@P\V_c5㬳`3^B˓t( X_߶m8؅u?gЈ̓._#15Ƥpvs H/xCr([6P ϋڶefO %2PĹ];|ܑ.;Vۻuxg={8Obֿ5?oqy%qnxFgs|\]W?_䓮cwv?w븼dAnc?eX;ƻGz!qk7wz|̏stϼƹ֏ˎs*ە8[>s &MǾ7نoG|̤#Fv,1Yե[h4,Wm ˖9K;hy}C740v8ն7<{粼ՠ/>QU_|S~At8^Z\~@1)?9mmuOYp_7(R zɾCfzǜ˿q97 bt{ٳGsX;׸~xj髷6?/++#kM D:3ia^ն,dz3s7̣o'q.a!q; Vs<\]2Q<ʎt6(lF+l2Ϟ5MYGi~EG HkCխʋ3 6_Ds}V*(PAGU/+zTYVD(&I$M=CP<)OLk:|Dz]+Ź◴sOJ؀Ӥw_yVemֹ3QeYYPPA ޝ-)/7]z粕3,DM[..3gM9) bjyXZʥyhDm wF7=oirO+ ҳW*}ssyqt>~ 7)mMgJxUH>]2@*5]ss{/xiهS=~;Gќ-٣n>[shJ 6^r`9 ;>{I}jΑT\?tX^LM%.UwS@?,wq,]fPٽ;Q͒enE8VEt h9x+GlXA,CGkHq N\vkqNyŹ7=?u ږ -C9 JQ0 # xYhXvbuy4-9݃xo1Νm;7+s3+ױ7zPKz^1ww3jT:g@9 n MgNuWVOXvbuy4-UsϿ6.>2v_CO2IDԳ.n?s.!L bե|>NIntV^4!e8 n^Ĺs߸Ofen?8vN;m|K#C5S*7HYa@<6YOA,ѹ Ciw~:^wwϽyŗcu;BB(]t ,M.{LbwNP'e#Xz=mS 3v4!N8w1̚I?.<8tGt3Vg׿;#?ugM:;v!!TaKQ@l~ܔ)x,]GB(Mݦqȭnv~g"9z_8o?7N7-:a:BRA}8u RCcj]BKҎ|=lZAҥu\twΙMlJ؀)x,]GB(MP$%?U=Nկ{{:;:;C;\l>B:BR 6j $ĻܾsI7܍nT,0'=B:BR 6`1NgPsA$xg9kn:ɻZ뻲:7;wӛC?Sn;9N#!4sޫZ %pJ*KRjn8w)swܾ0K!zr9W*6:^d_<^(jNӃ8wo3auғ yGN9ݮ}GC]kWPSieS4K]AD.Ӈ8ww~Wg ٳ@BޭoMeg!!ԮRA()8AD}s=(;̓ qn{w7yvʝA_z I$%pJ*K!SCӳ Of v{E<ݳe|aB]k8Kord= "BNӃ8wଳܑ;!odӑ;1{M0lIӳBB]o( 8$29o֑$-h|0[qN?0O;?{8'~¹B:B}Xe*RtiB=nĹOxs<=F60 =DPJee ni "Bҕ8?w;|Ckk'B+>8?R+z;@,ERܗώ9=̞uOT;tw{j_`k@ʧ,ps'|⧹N]~ymj*L3U!N_Fܡ{këʥgy;rB+ bN9lV9|JuHChqJq97kOy{};jr <nhpmE9ToE&=&e,K'̓f&G uUs'(TaKQ@l76!5pnCCͮTznAjuK0BV*H <n CCJSuG0BV*h8%P7!!oAl! ts[`[@A`!l.0908Gf!+X eZ q Vjي^dYRʊERVr)e"K) e6Xd)e"K)+Vbm XƒRV,b_JYRbYM)+_JYRʊU/Xd)e/Xd)e*RV,XnSʊERV,bK)+_J6 XBV?ez8vۃe.q`ghez8vۃe.q`ghez8vۃe.q`ghez8vۃe.r` r` r` Ol[֞imm%6FeLFy2Ƕyk[F} ƴ_i{2:9,.c0FO:3X4϶o.4~RmDcZ8ڌsްͭ7yM̳mюöΪ|e͍ Ae Zcgs[Uqˀ871ĹiqSopg>c(]e˼ie8yu[-?XĹז(ĹT"LSmmmC)d>cw~Wઙ51es>m{,l]cb"lۃ87y% 0FQ6}-0IưuY̥ŭp9`9,jߘ\&Ε2X4b.mn l(=Rܣ*sh;yig uʴ8.yu[m?X<ĹvĹq#Ía*3Lem'o-7po,,z]Ɛcm+}t9`,zߘ^ľjSֶW5aBINut+l{@667َk۹yWx"uY>l{"er0%@Lm=`q%U?%@Ձu0+Chk.o~ADȁADȁADȁADȁADȁAW%e/?%^6~K;@!AD(z pmXfy>lsPѴjX9sP;8U8q0%9kk}:+q D{stDmk/: "@m q 999090909090909090+5eOn(`ض:_knc@"DX9Jz`0_ HkO$NnZ0Zlpk"VA oךJ$NnZ0ZĐwp9k˔ƚճ Gܺ~- ĺI*A#ʎQsQ6SjP :,}FTI,˛}ڞF:NV "1s;XzdbrGV7; BJ#@ TD,$y r;X$qm Š9L<'}T9h/UlK*ƱX:NV "D-hBQ* D#(ADhJ b맆o"˅=MA}0[q!sܑ|i?OEBt7ε%BW*6:a%ΐ:3 仟nw~jWq.U9Tzm 0p*,pGƳyDHJi;Bh~2]s"! B3*~5S/˭mI6—W7ϼ +MOD z)8ĺ'"JQAqIW:;ugX wMODi3-Bv+TouDmG# U6U'"Ҵtz歪BiJQ0 # xYh' bVmͳme87oM?q"ΡUU*+icurd𬻲~-2 bY^,* h.j5-K9ҕJ/ bե|> qeUymHҿi{Lu>k5Y9ٔJ o.y{P+)GXA%d+ښUj:uUiU6\߈sh3(DeIlPu$KG͡|f1<3=ʄmüY0U W9JGM8c< D&s(e¶f1|ЍVMݦqjl;FTzmuN+-mӦA4ug `:Vo˾<̛Uj:*t9Υ)V&y]V8bhJ . od2ߏhL/׆W#tl]VWPm(.yZ}U6M .[9Tl%e(Y .LzQNvq=OR}T^]6U?(Ull8"e1C^VnĹ{w,)/hÖT^a4 ԣ~Lh8{g<6m> |އ [uód5}2~4m,@5^[>%Ytj\5./o+|Owvյڵ-̪-5gc8orHO}ȰMڷT*﷧:;Wg|~XnqQ֟|,٘$SyvU̦-mg%fmc$MkX-_e4mctMhOև6nyڴҕ&6MҸYɟg* U(!F(+cUNhU6m ԶiZ2[<'ömJS}Qzyξ⮯(-M-#ט=H<ĨA唯vU^U'_q*M38m<*+8/hʳeO+MyS6f ,px56J3c MFcSy+gޖ{fy ԇi,̓3ۦn16N%(3hc13.Mۖڰ<*8l).vmi$\XF*?׷}}'8A<۶'i^ eiiP9266Y-TV}tSi Դg,Tim=46-婌ebGs8) roNy;K7@VF?`4V?cuҴ!״g#0Yޠ m'д6bE?Iw,2jWe=  8hDzTN悛_KmHJӘ|vbF0n㴱24g3-J3O ,x]yV_ug8l|~Y_iZumlT^~*64-gHumLu-O}fgԇƯ |i ![4lYƢj}q6s_` qnQҶiZ\дJ.}yRUOֱ}T4S1M- w)b%mqiT}kOڧ̤X}ެdԦ3O3ʌi꛱4Yj׏Q&)!?19_V|Z9}HֿU;S,$}W9+OC[09 hW3, zFҴ]l%Pi۶iՆ&eSƠʾaTOy<_kW1svtf v&ہO i+f$c:$ͣOh5V)7Uז>-x8,0ؼ?3cjGL~T̓tiYkO~4>[6O4.Xl(5<kW5NkXlyHejSۖ__mjB>~E}>saz(ͧ& MK_|Xh9*#Wp*gi֦ kO[?Ω4#Y Aif54-IJe(X>l:#s ۺqNLI1R<ENigUiy3#X̸YiէQ Mf%mL\X?ԟ Y;[*k;9:6엷I|ԞU}˓cQS^h-OJSwY}oHYA; ؔg,?K8otϣ,ԶIӒUW}jZ,ڔ4]3j`H6>6lfU?6?mEd%3ןmO!_/?4p[E_~Yl|X,nn#bQ ϳjS\in|#kiv6Y׉̓qn$Ky~}Sߗ~r,=}Z&)-O9̖fmt?^Oׅ8'|m7mLUYX]?87TqXP>4Sq*=܈T&vF'4^`.mj# WoKiehoU3sgl'R^l'iZ}-Y0I:&[]=F?]e5p<秥JmSl(VҠY[l{^[ˉvz2I}jeRXle8|-X=uL4mcyĹb-- b2䛪:eي>um 7Ԙ*Ҭh' _J+3NuTV_qfUS1ʩ} eyazUuRƹA.D^i>S_ʙ>:MQ8H=R.X_Uy1SOUK" ]2~obGSU%XU^v1mֶ몪ǙTU-Ϫyiށ-uqPKїKv?m߳~z,&礘yGusˋ1uq`k@ʧ 1UXb /I+MmYƴ=L/Ӽvƪ9mYn ŹeIiT74^sĹ>* b:NyL0LҤ{g U.(6~SUZHW ~8:w?*N1U'Ρy( bhg (U.?i֠UA5LGH =ޘsJ1x;@ErknRjXϔA5ߦPuI{5(ͭN8h-#IU)E ns/:(7@9 Mce. }|Ueiրoi rE˧qꚷYDChRArW]ؾZ 8}7><]L`Uޢy%YKqW*&jޖ)Z%y|Pwx j T,MG^jޢ E-@m-9_*)Z%9QpSpl8curd𬻲|DMM71Uy]QͫE*q.̓J9G3U^c$R'pfçg*4!\˦q.!W*7HYa@<6YO# ӎrѫ2UyZ:=s8' ToE&=&ѻ' 2CYpԑt,jgA;`K}UR'a:RA,Eq XIgA|Ϲ<E<!4]ݦq@JQԹ6ug J2]su:RЖAl3!\P-R @2~ 9P ugS4!\Pj) bf EfsͦT0 XA=y 7#4Ȳ b]`k@ga=?!ԎYdFCAl@N;gLDAeDL9V!T0 XAiN0!LPdQToS_n?|U_ֻLGHhG֦0!87/j_ Tu^p˴Φ&*w"4uƹy8|J hm=vt1/>Nl4sq(^D4 J+ xY bxBt9-R9fS*2Y>3 gݕNJ>6%BQ*+w1~%$': CG]q!k8Kord= "BN8RAY\h7lHڿt4M>D8RA,Esug?L`UBh>/ˍseuOA};m;Q'ps FOj?0uK}eq.f-΅*> |ޗlFOASRn 9AkBR* 2xg=,wgeFD%(R'If 1u[`P7p / "-AqNR쌳yn( bf= "-A "ZJXP>`O "BBR* )p6} @h~"ƹ0sO`P'picnC'(2kbF8|J? b;H2Cdo%<<,Wo$)UĊ NbqJ2~\#!4p>=迎jvf UQieAGg4)"} C] e b?H=!4-suqZRAԫ`10p*f 6Y#X-UsDChJs1 8kE+VOQ7oh6Cq+^A2%( IdYf4cSOf\s-Gt "gf)7oOog@ZάɣlxBY83{9ToE&=&e,K'̓fdȣl:9=?ZRA,E ؄)cȣlꎺM?dsJ b7h8uG t9G Al&#k#q~+ b NݼK'BL8P Rgz <CCJL<CCJ q`sADȁA96 !Xb-{b4O0K RV"ˊERV,bK)+_JYX,\)e"K)+_JY/l`"7b/XRʊER"mJYRʊERVr)e"K)&%ȍ%Xd)e"K)+VbŲuRV,b_JYRʶ q,e,vY0+;C{,e,vY0+;C{,e,vY0+;C{,e,vY0909090}bzLkn(9v6*cZ6ʛ9X0L0N^1̱` 5uYq4Zx9Ƣy}#dw6:m&JCr f mn}a 70kLgmhvwV},-kno5{ *^к,<ۺZ?XĹ!펡|_%MsĞ}cmgU?Ca*_L ,X1̳mŭrlq ΍Da_%զz.0drnkÜoGL!Ck' wWͬy- ica2\,ogĹ+qnA6RFh]H"4eE=`.m/n]˱aQT6qĢsi{ugdDJVC;O;c8Uu95pΣrls!΅3p_%էsn She*sh;yni ?wce26sh[^˱eT"}8Wq?Hr?ŭX6gy9v\{Εȓg.+,\8gj+0+6 ?.o>A\tBG ]cχu }cu`] " @ " @ " @ " @ " @ "@*){/iB[ q Dsmƚ7ˋ`s@r0eMUA0Q5"9A(Y[ XsP ݛ#j^s\}Ձ8`"l[W܏S@!ADȁADȁADȁADȁADȁA3s=!B@ui @ @ " @ " @ "ζ-nѷmmٲ:::NA$o 7\fYpG /p,a~aos~>A$28, oqX[s·fom$_c:،kL[2;LI43XKv̿ 1~.(3Ga;}ǫ1[^1EO/Q񏾗6`* "$D zAD g:} SfBeNfzX6&Fʺ}L5=^SEҖP1r@_ @17Eb&._|hJi騩\bD6dǗoьfLQVHQj }I42fP|6bd2y*HYݘ\A,o<&C6p݌/3Q-DH" F JA Tse l"!6_-Ä&#ky~`` lS`}SQf@ra_˟= LqG7Mw8e&nF[?W0\ȐʛP ̃If=K*V?_ҳ%&hDQ=J7`+7aKWX:DX2DA%ADȁADȱpB!l` @ " @ " @ " @ " @ "(5!Bh`-!Bmn "B!BCH&IENDB`PythonQwt-0.5.5/doc/images/tests/MapDemo.png0000666000000000000000000014145312605040216017410 0ustar rootrootPNG  IHDRZx?MsRGBgAMA a pHYsodIDATx^ێmͷo<~u eVa0-$nH2^>b1 Cᒱ% $( v?'(\*m6[c2gfFD"Ƙ7GjZVպjiiK7ZM4M4iiэV4M4̓FiiAt4M4M jiyˍ?WwSiiˍC;_zwҿ}G7Oii:,7ZUB_Oii3nI/^={o_o?|{__W^O\oٿ/iy8+36ZhTljײh_?w_7_7ZXu4M4oGm@P=ˍ__/?zSӗ^?}n?jiOGn6VjrF?^s/?g7x4Z' $?{/wG_-?xs.ꌝˇ~s}M4MӔ|F zTh]?ɗ/߿?FoeESZh+I`{pržiy;АXyg{f)-+?_WG/?lpW-mhC% W{h.qv,M4M|FMFߛÍ_^~|o[@4Z/ų?|l._:ƬAu7ZM4M>r5VjGoh\hͿ7^_+/?ṉ?*v>g4M4MSQQCfk?=ӿyiu?__o_K?~nv(o@xj0Es燍U7ZM4M =xQ#={G2J?ϼo 77U_4M4b?Qɪ?4Z7M4M|,P5Zii>h5M4M|^jiyh5M4M{sF^^v~?7|_4;hy|^3vU˪V+(__@/_~V^ˡzو@ϡ>^(VSwWsҿYugv?߬Aؕ6}lyU=xϪeՍVUt/o>2]| B|/%i~g(}?^W3^ր|gv?l.{ ~ߧ3\ڰZ/9w!?WU7ZV/݁’%f>/^GKi%JI)U>tgd/˟֨"9<5[sf?c{-~ϪՍVJ_C;D|o칼ԏx˄/4I~>{%=緦NK?s}翸Iٟ>whpߓ9w~V/nZ-_^|!Od}:=j6.yK&=kR/]~/;}J5Ygϻ>'u>=??| {?񽪿YjLYHpW^^ȥMa<'{]ßI峿?gW~7|J+d/aA t>L+y׬YM|3OϯngWFfт9{߃տ7Z_UhZ_LgWV}ԍVԍV7ZVԍVԍV7ZVԍVԍV7ZVԍVjZփԍVjZփԍVjZփԍVjZփԍVjZփԍ@VjZ(ZVjDXM4M4GFkninFii[FkninFii[$ϗ㷬vj]i7ZFi*T\?4͛ӿ޿qގG>Wg7i7j/yyC݁8G΅›ޓxy&(;gPxMӼ9h ԍV܇xK 9r.ޤ33A݉8KΆkRrn>z؋?Αs&$A MӼ-Q7ZMEo»8rћ^ gg»38it%Fi_SMJd%.Y3G g::i|Fy]~VԍVE?zGK|[ի=+»q6]eʓH4F>FiCri8]8ƒ )UѾqoy(;Go%Px&iL7ZnvF/a E6bTjQx&wmDW o4̓FKԍV^GQx, ""Q$-wmDW o4̓FKԍV\ =˨<^E$*]Ľa{mi4@7Zn:V_5AqgaJ-gpߑ ކ!i;ҍ{Y7SV+(iB7Znf#^K H!뾋9rO((oWd~앳D-+`iIA^=Px$r iЍوҥۉ6=Έ{=`oCIVrޕQB4 {rO(7;#꼈b`E>9 Oԡ 4ً`*WD64]hj{EC$}3zO(w{XK @K+=Xx=r4w-Q7ZWG߱t_viZѐ}(A}$gIf}+^jict%Fd/Yz׼y}T s:PxR0k-}9=Xg^= iG7ZnB2w_Y0(R| 4wOUx0x7`R.g<V~'/XYPxw5Ms?u|vF/x/^* mO΁b@u,)sRyYgPJ<=dxf^Z)Ű_9r]M܏nDh5 yn d|Sxz;!й_U3=>kmÐQ&aİl^ֱ`oEiFKԍVY+`\Pxۆ@kJ:ּ>^tf1dsi1Ԟkvu||sCD!^D{^O mW<, *>qOy(N_[vpQ}@/a4]ҍ+ֻj>*K/ψm=/;\_xa.`oC9ל7b%=›۶ij<:F]Px;b;8עxk.oDh57Ҥ"M/p/9PxBDv)}PxSճ,a-" oQtv1gۑiCE^4w-Q7ZG'ޙTߛ^@k^;,(r"'`"suFU[{*۵(WGLAۑiC"K:a+7Ms#hj>*x)RJva.Mϣu^D1uO3P #1Gsv{G_kEEdM{ޡiFKԍV rԫ2NV>p_5Q -lQx7*{_nՌ0Ǧin-Q7ZGC_G^<+֊^56T [u>= Fm:eLy.a*!è4Q5MsnDh5+Q זh s ׼b@ȀG)gQxwb*>+>gb%q~a!@5ML7Zns1E^`l5y"E]"@|eKQvk}:gC$.Qsy,NQxM,Ӎ晉w3UV3kk^D%DtjyѺQ_n,ʠe}熷K5皋gkfnDh5 mw\x]^yg9tN| $UY*. ڦC<C%Dtk\{]=5#42hjf*k̫>dis>PsT>`|/ ۺy0D3 /!ZsOGI|ict%Fy&N{m~Q`.痭.N . kUdgCDy9{cŰ_9E2dkz:4:hj%*95/"9י5;ꗿk}K" O+P#byZÜ"g}41tʥn`/*5]y2D0U)1h:W9^Dۦ)G΀ƒ skq5<:ofNFKԍV m2[yegUl܏f`o $sb]5gTy R3v@ ^:l9ΛӍEs_=RyNކ@ 2.AQmDyz~V>?(Ymγ":ۗ՚ӍY}idY1̹Em8EfD#F^`qSY0We0NGc.JgVsi1hjޒײltT_y{_Oߵ,;}AƜ"/SG9c(Q 9ŖAd|&V~u.FNj(Hlp}]_7MӍ"^rXSDij|sxNŠf֮r#<#<@nEކg2ʾ<]D1lsd^4t%Fy^fUU@@޵-Dt X.%Q :0]kmi_߃%#볷éXHNEEp#9ɼi.FKԍVso"K,[Q}j" o'&s@)ň<שf¯`)2 /!mM3DWL.9h⿺'}e5%󚦹-Q7Z=TR\"pbs3:ċ^,'Gψ(ŋ(j{ks-(ؑhM2i_t%F *sdW>D3xeK{|>b9Ob#/kT=(ݛQ {"ag$V@G~s=(ؑYyMK7Zh\ZF9t8:ux|b8ي1t8Ny!&ضۆ}S"d+&}$:8]D1/Fϒoun[MYyYUϠ+ etsSRfu:z^S*_L" d>4#rb`$^V1뙷3}ցAy_p>*%Gބ&:Έ /hrϜ8]Dކ<4_nDh}^V^^ YxG?s 2KqAR1dI3K6<:].ev)y6Lan5`x{+ૈ{:μ<(ؑl]15g-Q7ZKKUsu}5yBVjENRQx1Wzr*Ks{^ 6:X+Ѝυ@#՗F9;h]ʣE| >ůB-8sG?s | J3}׈d5(#/" Ϡ^gU=.Wa+>EtϫGA6WZ՚3ҍCe B_k>Z[]3<[ֲ:=PG80깩x{RP辸<^w{ZOקz_oJ7Zn _(kM1?].DtN43:g>ϸFTY?//,7³ }9`R׺A~Px἟k5ʨ4nDh=7+_U~tԈϑ1~9qkWe8z ׺Dg"G4?SFS9Nj($1٭.2X{ L]{k8_{{)#25g-Q7Z W̹.vM/N69qkyh.˺j沼{9Q)|Cg>ZW~/Gg%5=Pyv^" @ 構~Ũ4nDh=+_U_'[ {`੼~RGk2ry-ˎz^=#97ct׼5^v -4sM2=]N kvRz#0sx~%QyYMA}UR^ t>{b6J˅yyEH\>(<ې25g-Q7Z*SPbg-]cqb_jM옋:\s^)Gp]iYnUUkyCl C'8oNKv$69r 6jMYҍ+ֻz[Kq)x[y^D1u>kvtQs͘2W쁀uQ@gFޑxSYrN.b8Y JU 6Z" oG՚3пu>ֿlKz¦3"M/{ (|LJV"`P_X"uш"B=e{(gEMܣV`{<"R/HnD E]WBQй35g-Q7ZoO%/o__Ė/"[^w_;Ű/_Cg=zG%ˍW)\맋 wڦ:3ft%F|2YHᇆ'DEè&r`@dms E׊q&||frsr.e`/5~!U;Q '[*rH|MQFKԍm1E] 漈bv)XNj(`M]#yܯr q_29gQ)YMWe뼈bЌ ́f#Z՚#Ӎǰe๟y`xpƋ9j"s+ q`{P ["Oa_3L esKlRZH9" .L W\SFt%F|qfU=1Ev"X&s"[_U2?ּb`}A34_a\Gι}fj2?WF M UGs >SMFKԍ/،*(We{F? d^Cc= =5^D.DHN*Fu枑ΝQmxA:fKvo<`Y@Cb~ZT|)4nDh݇/,P%@3&1.|}>]TgеH Dt~y8ERzG sA1l勳{| " oJ I5G-Q7Z/ٗ$3 O=0OQxw`[\= ezݣOfu+euȲ*v/Q@3/KZƒ ΊՆ8V">"aPd^|TtʥnYbdc.`r2Ws^D1D8طz2y T5ύ3V8:}!(d[aTNl=.p<]D1 j`MhѺKQ[7>1l O~6"dlLGP|G_ECNQ"Ë(El+hi /I t}fx0}@4nDh_fNQx;1HluElSFV=ul>F5{Jꮊ,":?5zuL ۾T9?cqƒM" ҅vdzF4nDh3/ o'RۉTs颪I*eػ,w@CIdkzYgި"!|qYb8#ZKtQE4nDh1/Tv=)fEd`"#9!>}`Gj(3E>򁯕YmE>.1b gMzf"YxŰ` tlL7Zn޿`O3(>k-?(؉VA^_ d>E*ǵהQmW&?gs8%r#9ZV" 6ŋ( Hά4nDh“_k J <3 F1l3XV}yD1] [+j*Rϋ( xkת?].<+m(a#2ig-Q7Z .vŽ8th3*gl{*3|s#:,Pxw)+k}k@lI(4J7Zn.,^(r)[VT5ϓr3|OUF{W|^&Py+d{׼~/xnY~x- |p>3?tQ\i-Q7ZK _pTqױ$^VYM=ɭk(5]$3ݛկgi0̬NXtTCb8iRz]WDs.?.lg_.kxNvkhzi:" o@lAr##rfk,Si}/2!2s$|m}F d_GA ({Qk-9Hv[Z_YexפTG9W֪qO@+r#F{eh]GP0j2Gy:U מ}d-i*2nizPvETyacӍ2q4?ykGd|_'ZW`ڗ]#|9>zj3?kRdVGs$<h]V }zRv4lr?|*/llz<|} zqOuVΝAl/>gue d>8Vhqפ͌E|1t\(:X@=e P)X2fY^ϑl|hj|UWh֕ vNE纖쬑ܬ2V{QvV@SYkRfT#5=T5]xpGxM3iϚf33C3Ur8r#}xF+Xh?+#ė."[nB,v(k^Ν3F{Fgk:@S߫"|su.E=]#tƒˡRq_k k"a;]#f9W{p>G>G:ŤBg}U-ç ?:{(Y-g1ҽt^@SGS`)hƳ<GO6|%ZyfTtkW4re (&yU'{p]yZ9/"*kz;+[9z#>cUhz>kP̯gdQ-ܣ?#7-~?wFܛeGyqȲ@ך=_挲k]k*"ʹYFZrZJtQmӀk5lVWVrn=dyF Qu[36ZGAr3eފFd0gg~EufWs@׮#>]g5=Pg2νksg~]bN_dXyZ,G<[rPYnS]{3HG֙d>磬U$Aۧ{} s)YUDkF`zY]af^^VXͯjC+f-gk ps>?_@tj癬9Ys==dyG33:fYkGYFFݣ5ՌլF-W-gH+~gߊ[ь*Wy.kG<53Q:rpl*^D  Hv\h+}*}ͪqmvfk9z#ٯD7Zh'1`G Tߊ{9"gGue̲7<3Q~6f] k6t.u&%<^D1DbV2 \gZHӍ5Zr un`ߊ>32̫̇38 ds2g~VeggR*_am%juj^t}DJVs>.&(<؅G"Nj(MD|hʐYY9{%CdQ4FK̍wy+<"3=잪Dʨ|5y֊@,luR|T{3ܧͱwQO\OLJVw){Tgʑ,#=HFKV _.hvEMŋ(eGݓzvͼdtgUZ|5RjΙFx*jDk,OZs95kfFfuH\GJFKL/T4K{Dg'(؅fd{T$Ȟ/ޒ[_2)Y"3eWsfz.'H}]@C UMEܯj@ׁf9 `.SŬ~_vɒkg4t%zF_B[0؉@fd{T$ dDrQ>33:e|]st_7+4"UMHsg"Qkdt~/~ɒkGoM7Zn$p "HգzP=FLJVϔQϨ4b51ڛsUd5߫t$[W3" o'ʉ@C^Qxތί ͮ5{pGӍGe/#`[,Iv?73W{*_ѳF"GkЈ3Ud*k}9#DD.k_Q +*W|{Q2d9ogk~u=FK֍{˫U#QdR=^=8zGbVϫD8k߯XT^ᆴ+9ty^,^ H9^D̫]0:34'[ᚳo-[5Zo^_d >Ո,zٽ{O]ѽ뽩UUəՁgUPj.c`"D8ehv$~AR4UdY(ʯ8Utѽ3}4 hh=UGFdy#PO*gV=Tŵٌkju2\$@&Z@ŋ(<uԺ5ߓiz{kѼrG|ʥ|F Khs/~n!;O5"˫MvՈ,O)Fp+C\ܣdFd!gV'Yfg{WIѵ9WX.Hj:/:+ T[T><%ۯ@]뼈ƒ}@#j:}P HVtNtՉ8wUo> djƑ{osQETzF7ZV@.^tUGGUeU&G&A;3T+YFz;#{Z,'Y}E Wp tQ)Q_|xNT{/W4h޹e/3ݫ լGOUeUג3eT3:ƜjJ:kTSVsg3:׬e?~B1tO)UٚxMYWq4ܺlFK~zU[WUeUUZaeM3QN)^׳ύ+p9RyujtQ9VT*辣5eulϳ⟅ZhGΈ;:Y+*#:КqxjĨ>;gVWd,HuV_QF@6WO@6w>l`Ȟg?Ǩ#g~nDᛯ 3s,z-eVwfGYGt^{z#ELO UT>8yAU'ڽsg?Hʨ=p]h5V^dϢr*s5l+g(uNQcD8?xU^؉^ gRV>:ϔ krVFKԍVLoӟU3ƹ8fdk,Vdg"U#!"sxU^حh̞jPx VQxF7Zng M(r.Eלg9J#װrge}^d5J9gQ16 QDyA| 5Ǚ':'=Bާ/fVċ(<-Q7Z3L'Gϩ\΁3Wܟ Z,SDfdݫwν \xb\&^Dہ"(EYMV< pt*hK}d@^@ :Abg|F Kh5g=_9W dHDU]t-+geb;hj*/H"2*l`E2*gVw<_Cs3,du |@lMV p$F@sHVs]-{ɽ3эr ѿLU+0Wl'YT#2VМw9Y:rNy-[sZȨF d>2d5蓬dJ V_},<#эr p/UµD}VyRtl2*'FTl_zWkz@6s#"R2_ukp|du|U~U_ 9:tBvJ }_"B(sU}j39Y"\s22⾮8=>ˣ t%FWbc%¹MvL UU#)\*HUsE2"YʸCsGnAzKu|U~+g1SeWqkR& xT⹑^6Wxm&%@/ ">遙>+ejFu|U~+g1SeHcŬ>Bzٹ~ߑ@*H>PO}udJsS$@4U{E;5JDZzK޷ЍrKk_9*f<`ݥ>=3\ 3̯2?|_gxƳZd5y4³#)Y-̫dJߋ/hripjr3+ghʓ}{>X2Y~U^DEN"a+_fEZ)#QJU|2_gJpϳMFKԍV942[9Q3Gyp2݃\hYnT{Q#й2)Y=S|L=GJ C˅8*Δ %S`^ ʹʵA7Zn5_b+03z(nu-YTF哕f":W<"sp;] j)9 pT(XR9ݧ:2E8:2?/vM/@gdYJꙀ΁3_#QxoH<ªxw'u|e~Tse=q֯Z%KeT>*GQ]5"˻@_EF $%CNQ_Whgṫ΁R" ɈDŽxw'u|e~Tse=q 5"˫ ]s/3::Ws2BK,9^ƼN)Y"x,^D<ub3gg»hj2GW9̌8ʓ3~HYM>PڛQe K/K\Dźزњ\J.E / Yy\8;э+s{ʯ(ggil'lpgk`=ѵΨr_(ؑ|AC-9IFKԍV9]ldT}nu77f=.vb.5kYG3-Q7ZWw(Z[8ʃ>|5{h~șJedWY8|5{虣}G|/gFH+s= hj__+0UY:VYrkY=8Q;zϿ_8 -Q7ZMs>F#*K_*Kn? <G9 }G}V_' -Q7ZMs>F#*K_*Kn? <G9 }G}f؛ */@|NPxau4}\a=Ru0z#q40;wg&ɠ"Go-Q7ZMsBs6;C*Kn? <G9 }G}vhn-Q7ZMs%l:VYrkY=8Q9zn|vtʥnȑ::k3t0z#q40sܦoDh5ͱZܟ1cTp8Q9zn|vu4^jUsƌQ ½>h>;GY{ܦt%Fi$g/ժ,W՟{}|vrdsӍ9,W՟{}|vrdѳ3Ӎ9\yf?;u3F50? knJ7Zn r兛 },399{MYFKԍVl$W^`njQ -0lr=}z|%u4/ɕn3X1cTpgh6Eth߬4_nDh5Kr兛 },3+g377W-Q7ZM\yf?;u3F50? |FfYDMFKԍVl$W^`njQ -0lr=}z|%tʥnʑ셛 ,f8y39i>3-Q7ZMqשjкQ -h7#۟ڑs3Ӎ8GQVkvF50? |Fgތl5g7g-Q7ZMqUV=,f8y3|ҍ8pAUsӚQ -h7W{9i>+hj#/ T5G9LTϺ4̛ל4nDh5Ƒ |j`V&99y3|ҍ8pAUsӚQ Ľ>'3o՛Ѝ8\yf?iΨfg^󙷂՛ЍKr兛(5_;՟{}Ng owVoB7Zn/ɕn|j`V&99y+dgdvt%Fi~P|j`V&99<0;KY݇ڑs-4V.w|eV_jUsӚQ Ľ>'fg)X;rv|F7Zn/HV^g`͘՟{}N3RVvt%Fi~Dn븿zٌY<,eukGnH7Zn/HV^gpz3?g=99<0;KYڑ3ҍ Y|N3RVvt%Fi~Dn?;u9`V&Fzsry`v#g7g-Q7ZM 57Ϻ0?g=99<0;KYڑ3ҍ r党ܛgGgt՟ѳ\fg)չNv,4Z?_o?~Kjo?^w3˄~{g {uݍVY}AP{31z֣k,:4_F뢙3K[tn~~ݷ/AQh5/V_+/ԬT>՟ѳ\fg)չNv\h&JoMojG\yfUgGɬLG0;K4_FK5ioQXyu uݍVYyALڪSdV&Fzsr#TfdjơF˦&׍UhFRvڻ̈́u7ZWg WYSdV&Fzsr#4_Fk[?:-o.j:/HV^ʪ|'31z֣k,guovj\h ch7Zq5S7ZMs =GɬLG0;Yݛݫiˍw=7^h¤黨]n$r+/\ezNY=ɵdvѾ^MXhC[4m&Wmih5%/HV^ʪ|'31z֣k:3lsr#UMFKԍV\rdnZ]3:ƵhsyduGoėnXލVӬ eeWq^V[euGoDFKԍVӼf(,Ggt՟k?' *GVkB7Zn5+/ȕײ~̫ 0?~Ne#h^u=i> hj׬ W^^+>GYٸs`M/AzCMYFKԍVӼf2Z]9ϲ~2?|`M/AzCMYFKԍVӼf2Z]9ϲ~2?|`M/AzCMYFKԍVӼf2Z]9ϲ~2?|`M/Az㹦jt%Fi^rdfZ]9ϲ~2?|`M/ۿr&Ѭxihj׬eVeW|γlF+3XI4:kF7Zn5+/GfFYU,['Q= +g깎эyˑYv奻sedV6]^LY=\|5u4Y}9^d奻sedV6]ZS\|5tʥnf8zْyY٨wsxk*9,4_yq%+/,['Q= eUfg4nDh5M Qv奻sedV6]ZlޑЍYyA23ʮtW|γlF+3XWM;r~|u49+/HfFٕyY٨wsxk*-h@7Zn&g(*7YQY٨wsxk*ܣi>hj#3 wR'Q= e#Y~5W-Q7ZMrdf]y||TdV6]χWT4:kD7Zn&g(TɬlTϻ9}h^u<4_nDh5MˑQv;YQY٨wsT?^_9SѼxihj#3 =\0?|C+g*Yi hjEKV_UFY٨wsT?U}ͮy |F Kh5q%/ܪrslTϻ9fW:hjE䪚rsJY=sT?U}9=ӍG/Ze%W|oKV2DQ|WU4{#hN7Znfr(JYnt.Y<>G_W񬞭d*t%Fijf/GdlgG璕3Q=sdux*ճ,4_nDh5MQ\y&}~U_ųzЍ=8г2{4dlgG璕3Q=sdux*]==ӍGi\@<=pxO챴>ʲqKV2DϑѯGG4nDhBOψgIgg{BfQ\y&}~U?W=zt%Fk#^K ) ijɳB1,̞E,k+'ѹd%LT;Y~ͯ{Mҍ+ֻ>P [B 1+ef=qKV2DϑѯGG4x/}*9`{ܥgCF>G2|xګdygc/t4ܣiJ7Zn6|j\ψ=z{sݻʙ̌G2|dj?՟yLGG=zt%Fެ(X"Oss˹DMYFKԍP= > Qo[S?s VRtJ)Q=|n9hٺi>+hZg@ ߂1~{rsL?'U(f瀬|T2O՟yW>s˹DMYylۏor2^~|c㷽?}ԳׅF+c.$'Dd{p{x{Tyy>edu磒yʬlTϻ9F?[%`n :7=hxU$4d8m~|~v_XZ#q3“ V~|ꭹ=s<<}*fuuJ)Q=|n9x:{nmf*֣jF khhbgi=e!0)%󜬮|T2O՟yW>s˹ijٳu|VhI@?7V^fZ_B4]h!.`N_|h->p3q{`=Zav/{ϪuJ)Q=|n9x:;;}F7OϗF+aܫ3/ Y%wUsQ]`*Z=B\x=oޕܰ5{˳@u+8c}>ʢI5МJ)Q=|n9Wlާi>"OB6JV7ZOƋ(e{[HU\{h}E-W)d2?|s͏v}#FK+oFJ OuLK"euy~(ؑk,PxF ^D1#evGYVbV|T2O՟yW>s˹Ggw>My\ͷ7^6GBCkf.GnY`3&1xkf= }C M+avGYԎEd2?|sWs0[7g䁍5H G,#h=>o$zk9习v"뾬6sp_-q}wUG~$Q<縞V9л\EMFKQ- =ADoE ֪to'tᨊZVrϫ*Vk=U:|ճ0zYoljft%ȍƀM[5_;t'b@f9zNWTQ9{6۫9{PwyF{GW& ;}#ҍ5ZJEo/{z{ ((ЈYVY|>:3":^f{Y}z ܻ̞}lj3ܧi>"h>R%=E@³P #1dM;_s ]hWtQ["; U,)I=UfyGr*3{֕f9V3Vέ2Z9h#iȗnX-`Wr8XWʞ4ؑu`1a,SY.µת\ŬQ=+TsM"-GoŽq5h~},?_yCD٤DVsH-9Kڧ>5"˫΁Y-gP=r6񬮫99rt%,ъkG3F;Ry9AVq+! :-ٯѽ׺8V~3ϩ2=gu=#iH7Zh)W:sUxv1{KU~%DŋuA.2fd@5:ӳj>ϤY*hٺi>hh#d2/Cs+ўљ+cƳ֧Ņm!EN̥[ŋ(R bՁ>Gy٫lي9эQ^|)R3XU=W=`ՏCҹ*C~\^@ 9+4"SuFV>k'gz&_,j`nF7Zn` ;*Z%ۣ,Sz>`{TJr`71*^D1xN"YMk@eue?OdCs24_Av{5G-ѽ-yF 5gPx7GVhnHY < pt<5?t_Z0<+K[)c5d{<ǵf)\QdgO-SŨjsޫi>hhKoReQg.ċ(;ǛFrlO9Ufe/2ܯ1x6tJJ"{H,S{JvʹUE9վ Wsp{5G-#W)Q+9<Qed{J3]&B[[e\jsޫi>h+̵"q+ʑ̬N4Z!ˮ_9稔3?#(ػƋ('"YUJVF@J#5=Qd_sLvu{vT#ӍэVC t\($Ȳ Xr<2}Ev" o'ʻz|ѹyj(g~ pQe sTs0k—nX~MN\܊1l^-j5Y>gp?kƒ=/QMќ3"Y푺gf92>]{ d^|7Z{5Zb_T Z^ٞk|]ӌ,GF5^"a+@˅/*cVW4Rt9~9Yfk9gߢ¬":'4nDh(՜{{ryR{I`r@ d^g33.<*Y~ R|4nDn@@L$FhuoF{-T>RFuִι{*e=G8]NV\^*{"84"_t%zT5W7|o;(zdq$9q׊s~YdZe:Jv{(cT#zqO`}FKF+(N*nEʤd~= 稈΁ pֳ;9u*s=2ݧy5d pB#JSUx5ͣFK-%?s&RwsnjQgCHאeT>Z&g"#:/'娅95뺏5F|x-3s/{4~O LJ2L\oy/݃ܣjG7ZG5Zגϼ ϮTU>К׽Za/2k-1cV['b3D焙V(eT<T )#U7ٙ5 =dg@3Vdv!;稚nDh9_[f]gܫLOXױTb@%SE9G료{XtyV}3eW\lVw)}#*VslҌlQ=z|_Bc5ZNP$g@*W  *'uAy{.GE*/s~x;Ѝ#7ZN]̻?kY>SjL3Fy]{^ӼwYutZ<<<5SetFQ^}Tz,GϟiĬΉG7=UdV}h_t%Lwv̻=jDD2Ṿs#ȼ[X=Ks.gj[3Lefr 8\|Ҭϫ{N\kyG)#R;DךZT&gm~{Ou.b}}Fˉ["yrY?"㚡ߛ)CkD8y}\hu2<=su3dSvo<{&'T{}nDh_T˚yעg=JJg2z~5Sbpiex-[ϽZS^efܺZx? d>jʬxZWgWC,ٞLt%F˫3u d~QTg_yb#EqN:^We5* ?Ru93Z2kX%;"YM5bVw4{G5p^+Lj+<FtJI \-Q7Z;gQNP/^= =8O?`UzGuUs{:W3n9'#;>J{^ׂFdn!;Za[tO3}[FKԍ}?>EϺeV_^쌕s29+ Q Ͻ]אsT9e{#ӈ*zotʥnG̻{g^g+UxF!^D%Dt1E')O| uU3>7\sVg=C9y}Ty\9*^ ϫIvxoDhςd޵lehγ^HV;tՕ#'ċ((΁qF3 {(9O2WUVV{F*YIvfd{*Av3ċ(w-Q7ZGd޵YלUO},4Q x£xŀЎEmxN1^2V2+9Y_eyZzEYVEtT3zٽQFS!OͰr_E~)w-Q7ZAs"w UU_k #ˋ(HWElWi+:ƒ _ Yv4e=+UsWUe|uͨ΁f)'>"9seT)Y=j*b_)3c'DdY t%F9nD 喳FesQ EB(z*E8Oχƒ _Չ4:Ȭ>CTf#Y~U&;aYϫɡc{,WQ '{Pxw9w-Q7ZsC̻kZV'U.ּbQE]@kyr>]h]r\^FiʬGϫ2+:jGG3}Yϻ{"b^t%FT ڳF{Veȫ_2kYl9:POkv_.B'3,?XٷzVuȯjd"{]dg?cLċ(<#bx79{Wu?O*yrYU^UF@j]Y>CGYj#VdHqOc3D33(8yw\[;bz>lfމk~->>.yr,g4RwaP  ,Y*S1\JFe2tYJh继#\k΁> ٪{V-#)El/މwjo?^~{; o?IιQh}n=ȼk9zj>ep~" 6c"&XW)\gKZGx>c%Cx~gT=վe 3<5c%C4s hMHrϳl7S O2Px4Z,TG);gj'Y̻ճV2uxé>WDך\*yJ&(gT1ۧuT{*S܃h6_ߓZ=>-{\Qwc{'ޱђh_B4]h}m]$e嬕yݧ+" O6 tTu^wVѵ+'T>zrNG|=bk1FL35:gk-; o'4>o wX V4du3Mm*i T̻Y^j" ψ6ܿ pZlMOGIּkJ(+{&6dxZWg§{+Jv&ygΙ^ Qxi@o?~KjԖZh5Y2Zs|u⾮9W|A]hz>2FODkuFR=GϨz%d IWut{ }$sxm(;}߫i[г֑Fkɂjg59YY-֧˅v_5(P9pd^4ϑr2QTR̬;e'Td'>y.ſwȨFQάlO5_n נBa6V7Z5z?etE^wg"4JVY뾟pz[B*d21i^$) C݂óܣ/w~@1 j$ʻ_;3N5Pք﹘g@7Yu7Z5d_ w:.`ʠ9J>WV֙2ZQ#֪z ffgf{G5PSg陙wo=w~@j"kgVfޱ*k=Fh5_%y9bI p,":\gJ=o>>K#}@Ujz3*3=Gdr8Q IYyF Fy4@eFŰg{Fr!'ըdyh]q#8+=gѵz3}=__м]{]˳3Qjנ10)٦)GwT7Z[/2ɼ +f4 bQ}kB/sV*pj#ɞC=>Y}泽xg==гy{Ev?T>VnDh5 XT ^S<c(<&7{ kBͲ5{ ϡϒyDTJFL9գy|Gߋ\_V1ڣ5s)G-Q7Z3/8]D1*Yݩ} s^sT+O2h/7گz4oq/ޫp^ݣR2xG-Q7Z3/*2R}91E'ε"Uu03OeZ5{* TsB/?CH>@s_kʨv-z?|&u|Ť_P1;oܯ@ϒlH5w3ٳ9>sQ#VYk&cVzG 5߃lUs2fnXލVQH+y}\>g5Y$;Qhѽ+yp͞~ʙ5 ћe2j ϙw>D 3ui]oDh5|QD=:Wzy\j*ի9#\=[9'(k%Ӭz#.Q߸<1E]L [кG2^?rs^ݣ _Wd9=:3кQ ?= =FuF f;B3hj>#M}rU=9r^gr2ϩ2z9ʵ{=gd VRt5P#Z2-=]$AJxJ.2fu|F}i#O}*qk|+ppts_ =kN +՗_Ss9 Tsp"!FW/s3VvtsU6c%C\\='d{}+6}kU^V}YӃ\ҍ3/K0驯kkt].#f*97#E[>y*+4?:s?»q6\-y!x[Z2+uRe3-4V.w|6Ӊ/:9+{#"\3^*y{gZ|~5xwqk3Vre9fue}ެӿu| S QxwjNe>G8{]zY~Dg0tO=ùz2d9?` ۃ z^{Yh}7kt%F ,v i.:G_ZӺ{$֘Sݓ|VճgV W9}{}kW嵖eA6׌֛ct%F 8uiy9P=jDY z(Gz(*6ڣ^V^2:h5)3zZ9ՁzU3t%F*_Qf9|%yX=Z{xnOϮr^{(ssΨV3UGert=5e;]TJԷi5zu|4ɨ5st4Ǒ5=y##V StOX7Y}WW6?ڣ]xŰ7Ӎ櫐}*ZeR<4uyV2*Py=slV=G|) ݗe+lֳT"M/n@t%FJux-t>\`躚sk@ok9~C#IUxmG}*F[XE?]ΰ^e;g#|ۑkK7ZnP/V{FQEC$60HL%=޵ ==}\kE0y+ǩ[q>^DDyWf漈bsэ櫡_]˜_T [ |T\kN׊P ^?:jS pzn3*E1U,r~\h+SFKԍVlEuϝ}L.Ǎfet6˨@]íd{ӳϠ_I`f/HwZCKZbDj\QoeNlOsu|58ÿ9^&|P1l :k9=_gDtQ9jaN̻d^=79^}`m5OVsٽʼna+\kPx,abIV2-4V.w| b%}ۆ Q3{GAmCxFb.yװrNWYEϥ#FI\=}wљwD@1 c,ekg=SkGFKԍV/ r9W)Xy{5B%U !;';/jjJ^ϨG?^Ã-zUׁu]_7oC7Zn~AW̾uU9֎}~/]FMP #qIU ݓyPrnsʯ`^32T{1mpyUse}ekކnDh5_Ⱦ}rY x<ċ(jo6t%FuEVQGs9/"q =AYu-7ózFuΊ?ʸV\Ge8/vM/|ZU'\Px;Q92Ӎ+_</tHv)R2o=ZV ѳTZ;I#ؑ=Yhʇϋ(<ېckޏnDh5_jx{~ecug{&+;#dgd9gz^C!۟y9ՁKYѺs֫gC$^>Yۼ/hj:e1g" 6\+4"&L_ee/GgQeuN=йRy׵ʰv2/o+hʏ2~ ZbinDh5_/v9" 6ڗftw<9yg[fx& f2ܟBxFĶiDx kh{fP?˻G?#s͚F4EB\K̭ϵg3a}˃*>E^BDw\NUS?V{2g:Yc~|0/?̅~|ݷ3 6}ׅFi^˂,"\kM3~,NQxB ݆9Uw߲zDSչ0YĖs4Si|R_YʨsFkkM/WM?QF o?9vՍVӼ_K*)sEy,NQ [9ZPxWGmӀkAC w߲zD& gD,8+̨F7oCBb#Xљ||hoiZoLht4&{/g P ~&jX(X/[VrY`sta-ċ(<#b בPxbɩFK+oR7Zo6tu49B͙.J$o ۯ/u m,7;˟u !k.~-jfn ׆j\|xŰ3k&8[ыS5deY+d9黀+<5:].d?O;,<ػ9" /!&j{㷤FmkՍV ZW#xUQ [b/Z6&8[q:B΢_s΁+}Tbdsst͹v+" $σ-iJU5Z+MԍV.9r^*?;<њWh ,4oH6sje֙p}O 87v$`~e(3_gг4~VsܫrDY, oG@Vt,jO7_6Z$m6@[ӯؔez>t} ]w4T/XhH6בvN_+fQ0Ys^hyF5/"[޳9wT9V^ՍVӌ^ek8lft|L07;ڗ,V2ѯey꧋ʍ=)C!^D7 lӋ39bORɿ{u4cV^,z\s^+"Ë(t~ӽ+ f\Yh3i9y"a+ETz ݦA{k>֓HF/!=l|u^DEZ=3\{>]T |9:k\_tgjgDlQ/PۦA *iu4sf/QhFGu F:QEC$^s]CJQENi4>} XE`@=cK̭|t%Fi^,4H_׵9:El9Q>r_(ؑzeDNdlSx;Qަ ׹DmqO90\\dt%Fi֘xYG#tSz3&GĽ r ꚇۉ. @kkEQxF 9_y,d2f4 FKԍVӬ1z#1Wx\h:j~ʩ |P1堮y(`z+>pEN=2ZVk9پFҍ+ֻjыHGY" oS|F^H9" D]P [Ķ\ p8Ku.2YK9j׫윦hjuF/#xV3YMG囷nTzY}Dsy}!(G61d9E^mQc!gBDyQsUG9M3-Q7ZMsKGd~h.eM[<=h(ɼk6}zq@xmzHy{^5fx43u4QG9^ҥJ.QfzFeg"(UQxBDv]SEC$.J26ͭt%Fi1{~bFތ`\ssy?t~ 7ENRfug˽`"V߉/vl>wXךot%Fis%^ 6ڇYۉ6=N寢,GuEb`=|hFN >D BӑGE$ˁ,4Ѝ9NRǗV" o'ʻzPx;QަE7onBΛu }b`}"3p_}(n{y'pV|T~B7Zn:5_j ,^K o'ʻNT[$#jaߊzp~s\Hkre{qov8=H oS.sukeTk[FKԍV\腆yRۉ6 dpʑ|aۉ6 &Y("&YiI7Zn:F/5ywQ Rlc #q1"BVt~-~Fv E_%ي(ǚf " .k^D)3sJ7=FKԍV\b" oS._΃by__s3=3#7+O]d|$-r>"|MlFJQNJ ZGtsAUiM7Zn62ӗ/^" < x>[ӻEtŰ/${=!|}&ؖ%|eQFQs͝%\y QMVt%Fin/瞮<}jMOk@Y׀ٛx9)EV=gxNk\" `Pj7-Q7ZMs;|e/9V|gfD7{2=k-n90VYɠf5s+?mӦywu4\S39Gd(E\(5=s,\Ũ4A7Zn>KQ3:3G>Ž}Neq<5f{(d٦yu4Ac9D/^:f>e[9-穞 (,xaMssJ73Ѝh:)ɲ)1gLinVq-euY^\{ԍ4VQ?N^Z7Ulq? ~Sˍ8~yNxa}(q(}R֨i?hF 4nJvG˞ZHmXc$3s<<n3 1rh|xSت,5Pf]#ټnE CcE6,Y[5k,1?oZeSsQ3rh|N7SS [t":m'X$jlYZDjaY7=y{MQ .y,b F s tMآYyn;Q"YQC'S<0Y?l8+34<1?oZeSsQ3rh|N7SS [t":/l獁fq}|KVѹ=̶u fI5s ϰOl}.j004ZN-ϡ7fjj)eMg+wcq} 6D}8<}fdJy3⶞h9i>MU$~d:n8y ‹j$Jehϲq=Gy,ހfn h9iݬML aϲȶeg+w5{=Y+SyY^[L@c&dYrh ag7x{&`jL$.3|E_ÿ޴<:mIéY)ż5ƌnrBͧuŢ  ',qJe4w~F-834ZN-Qk2㸱4m~Fl#9E|\[1Y7#=Y-i~F ࿧vr,f/ h05 fƗ'˱\}&j6 n۬_[--q#VD[ˆ4ZW4Zc# 5 fFs4ssD4X/dFKSBpZI+j=2lThI#O0Kh}^QMzъh6 sh}^Q<(x<[ k/cFK%ihS%85qF}hѦ5Yf2hh }4Z4Z4Z4Z4Z4Y[BDDD|4ZoF߳g12i^`7?^ߗH-XMj,I-Ԓ-%ٳ v{zpԟgcOY~3S? 䄣,?{ȵi^ 'gSE| O&i$"""⛤BDDD|4ZoF Mh=Z˿O9D?sco#WԼXzqrM+{7{}Q?{kjvkKy,r=>tF.pEVYr^cvCzycoǾƞxv3>DzX{Ozوb߂4ZϨѺ=T-+:ه@ۖc;b[m{̀5>^35~S$'H>UT;W5oW~+?ƱR@۵oiX|{gۜ2_AE4Zqֱ;~7>U`>=DЬsw}!jO纱1>_erhF]0i']T-Mjte.yz ZSNkn7#S_zS{OqLχ8w}ѥ԰t^̥zxv$vA+b՟X񎏿w˾ƞk/|wE}keiw"' W۵zŗhJ?h*i쿳gzcRf}Ct7Csvx틣緯Uj_(h}rbr'e*I0 cw˶Žﯕ05wդu]oqm}qќڿ{ώ8oA-7־Оmp}pri=gG7\A-DDD7I&i$"""⛤BDDD|4ZoF Mh!"""I-DDD7Iho;+(w!yBD\o~)/"bF q\o"b."b wb/d"]-DNɺ{sE-Do_GDh!"e G WHJa/!"&h!".:hG [h!"""I-DDD7I&""""_{5IENDB`PythonQwt-0.5.5/doc/images/tests/CPUplot.png0000666000000000000000000010020212605040216017377 0ustar rootrootPNG  IHDRZ% /VsRGBgAMA a pHYsodIDATx^ Ž>~nnĘf11" *DA]Pq=*]w=$%*D=Ssjz]SNϙza'^(y^baaaaaaaaI0hTXXXXXXXXR* Z,,,,,,,,)-  KJA%NsZ$<{奰NqZO>uXzJ KrEm%A w]L}uV,4Ub)]uc˪ C-'F*VeyUŠbf Z@FrZRG?Q7:w_}//{-ZA 5=_Y&T d5+ bZyкUzsXXXXXXZ D[:قKrI ~j?u[o=-xؒҼ9hZBȊ~Si--A1AyK΃ K-  KJA% ’RabaaaaaaaI0hTXXXXXXXXR*|r""""JQJR EDDD-"""0hA(% ZDDDD)a""""J QJR EDDD-"""0hA(% ZDDDD)a""'mGeQ3M*T;}-"r=hu{:yTvA0hǠEDNyA9: vfW֮:a5| o9YZUykުmP\"Z ZD:h`TѦ;VPX{:?0LIM?GD$c""'pd!@XcS0f<_t- Z/I; YVr|k*t-SuחĈ9IӉ:TقaM`c.A~ %_ND9I'hu=^x 1>1hp," A$ S3A SQ݂UVs5h uZ""Qk/"D7v'$`DD0h +a""""J QJR EDDD-"""0hp4[B  ]:!D; zAB>"j̘oTedmQ}*~aM5a넵_mF͘w_"Ց:hIu'A?H΃nRkujf@bbТzjߨ Z&}UE݉.Rɠ%mJ7}mkWuAA.rKzoo~yֱ)S׉ \k5]:AK|-Z^w\?*}E+??0|coYk$i)G/Q#"Ց Z& ZFx2Õ9X iNu}뮻NqSOUsUS-RK,xOr:/$%K8I$.[.Q"Ց Z Z8XJeLc{XtI-ӧUXC4HgOZ[֪ǿkOj۩_g{eCƏZYp:AL8∿qZc~<2NFŬV~uz%prPj\cBo-/P|O#E?P}oնgׄ)o)tЂP+.w/H }$=81aC85']̺QK3|bhR~k:#p9i7h}W_pAM'jЂ1cR?t*}st z=sq5vj]' ~u ;]̺QK4h!䘧 ʕſ эU c:8OM+sA7 ZqV7V۞xbMr'haw7*㭷dٸrwZ5']̺QKE#|bYz(3|VC0v Z{!EܠZٙ`$ö_q˟d) ZԜt1F-S>ճ0hYD Z7-X_ PZj[ oדx1+m$3owZM*IFŬ,VW' ZJS|%hڳ3wnMx cJ1ᵥt#G>^{#q=)YO6BңUl\,Z`'v؁A.fݨeڢ@A?EؠuE7\ Oa$`5Ǩ+J\_JW,gj $%qq͟ye' ^ZŬ5-Eؠ5`-+SIw|U .?+Q&5]̺,\֢E/I!hxf),`"ٸq_vf2^o2-j>u#1hn烂ּyj1)kKV]u[u 7Ԭ;w⦅2h5;|N6%eK/EɞQt1Fcвp ZsU STI^PgyfͺS|- mh>p-j>u#1hY9s樶BA LQw+jkN>u NeG;>!瞋~ FŬ5 - נu5^[_?/:uaVZz<Ih=5]̺QCdw0hYKW/145Կ|꿾lb P'r FÐ 'F6g7R̴ hH(u\w0hY_|Q}?SX_c 5z :LѥAv믇#ƍ>h=5]̺QcВ1hY-/׋GmmxjsǙ݌^. |=oi`Т棋Y7j Z2-0AkVcOŐua>jD<@G1zj5כcY݌g{;k2ӂPaA!3]̺QcВ1hY Zw{%(콷ZiW:yOV\U<@GG:R\oC ãk%z249v։(t1FAKƠe@3ND Q.;Q UVQ?]i}O[=CzS< f?4^- ^^~e3NDyY7jv֮:=A?EUC?\ R6k񏽐{g?\<@5a+jR|W]9h 9 ZgnA.fݨ1h,[~[rbo7'Nq+Xk.kRŠ8Q)ڐ>vX?׉(t1F-ˠuIOc=T{g< ^!-Z]\\'<Ŭ̂|-T@UiKˉ)aвNPN W[O5Ak1W<@0d^V~=Ks 14_ DyY7j*֥VE Z0lX oׄ,M rxvַw,[18̑qV7 2m?Ԅ ٷH?s2hQŬDʏy ^x*wTm -(h N~jjyj|T3]W}N5VַV[ݫvІ {PǿׇuС_eoԨՔ)? }ƏL\^ B͚NDyY7j"$ŪJJ?ަC4=V2p˛7@m2mZUw]1d5zRm71deh7Ԗ[~)~c?W;Yr/ĉ4|r5s:.fݨ%r*a2TR|>lnZkJWT  ,Jg 7<^7D|}J嗿؂A+/35rr8 ZwÇ?z>L'먶m9'..UG.~VC||%7Mozi9i~׋(t1F AKz zA0.(eUeE' LQ뮺яTbZ6TZ_j^O5TZo>խ*.~6&déÇzE.O c7fL)KEguJEzAr(J)PU.=Pi-;SuYꨣRGq:E]nb85Fgj?+o 'YmUcZ0:KEgu#,hX^V@Z;hQ@ ZOJ…ߴi{r҆Ӈz.fHv9 Z1$NTeX4Y]S't1hQoA+tg7iR3B^DyY7]RU(TF|CR`kV!Mf bNϠEZ#;RS0|&]5ݎ;bh(Bn$-T?$(P1hQ .x}rɥ-~f?}0:KFgu#'hմTՆ$*=(gl~GL=r[$7K?GWu#EE N2~]+cТ 7TRÓNz_Lܳ} mpUŬ.?A pGU6:״f;ƗX2,`Т j K(5_<Y#+]̺JAkH(5Έ|,~f+l xeqJn$ZhHIp^qOF}L9sk 5=(DyY7]s1hdѢW޿dc~vءm~ǠEEn$;- -tt=$C߹#/L+-#+hQCh֏(t1Fc2 X3h\y[ C2CKi=Tb=qDyY76 5DT2,Hr0h>%KMXh:ZDyY7&vz|"Š?PͿN==:1\ nOGкu$+]̺1UA91h8*|Cw2?wroA.fݘ*8ߕ.=Vfu ZJ@1h> o-CSM7 4ops޼7u$+]̺)\y*pJۼ_s]\ޝedA˂A${5rd+j!htRt ͜ӗW_DyY7 Gbq0hY0hd俪ѣ -E]'ŋ_4_0W-'Q^b֍ GuNny1hIxC|CR9y^z^rɟ$+]̺1Up*}JPw/(GgxB |A#7zTq q>?f 4cТ梋Y7I0Î;ʕ'5:<>PÆ!h͝DyY7Iҙdz??]vO:9sד(t1Fcв`"}(؈|,~~7yij/ 'Y\OŬɎA˂Apk<*NF\Mٻ"Qp s֬j֓(t1Fcв`"G ZyHwӚeh>|9S>IWu#1hY0h߃ʚjP\쮼\qiլ'Qb֍dǠeE~ܳA+&N,f/JnoOj֓(t1Fc2 X3hmüCՒ%/V}v{['nd#Qb֍dǠeE~7h5tzDž gSu4}#`]wk:.fHv Z Zw o&S=Js{]-:fLi^=&M(_&:Qb֍dǠe~ajQs.UUgʱR>S.pd|}nm>(t1Fcв`"NzQ ĠwZ]|s;ؗs55t'UQb֍dǠeE~GJ*lT>=xj>mѣ?Pm(t1Fcв`"Cyͫ8ͫ?#>H6ʘ14/ŬɎA˂A 5d'bIs/zك>ӽ~/5<ŬɎAˀ`g>ϠE{SPTkM8U3's{ψJn$;- -<] Zyx.-\ yogDyY76DgjkkW_i`в`"߻$_4)?X(}f4SqFCкgDyY7rv:v|uZÆ}&N|̦NhW ψJnL-D1hPN>󝪽BApaK4)?1|>36lD_[nyf?#+]̺15oShU[{gQm_ \4)?C@Zਗ਼(t1ztvZIxV*JLSU6V-Ko\-J.Ǘ*LQrxn;F?iq_##]̺KXIx̰N+YзyZ-2}c11gu/3tӿ~V߈HnLMUx©ܫ߫b4qr٨i̖ Z W4UCK>OW\񜸿.fݘ J]\nP1.k+j[)jڠUIs]%\|P Z##S-7ԃ|`(t1FkҠfaJNF ǠE&tZsϪr}(օ.7<Ŭɮ)VG!I*͆18`"ӵ>ˠD Tr@s.fHvMڢ -2`fѯO|-\zY7<ŬɎA˂AL]ɫH_uB|./N?}.fHv Z Zd)-J3q##]̺,4wR5p %^7<ŬɎAˀ`g>Ϡ0eGW̚.fHv Z Zd:WfQL߈Hn$;- -2rk^(UDQ`3^7<ŬɎA˂AL2$ԩoQb֍dǠeE=|[ XaEz[߈Hn$;- -2Mzk-JV[}vq##]̺,ȴ{bj>&+oDyY7F@mbI;'oDyY7%UDQlMT߈Hn$;-GGu#1hY0hp!%n̘?{7*9<ŬɎA˂A'(x.fHv Z Z͟t1hҔ&wDyY7Au]~s6C$A]^ N1h$>GEn$;- -fx֫(>P\E~ɓA+VgjiEC[{ϠպƏ_6=$թ>"wy0}:Թ<]̺آeEN XQŁ0N 6{Omч[n֡Y7]A J>ZljwĊ( 2{.6=5l؟oqju#%t2x,`"m-?%Qh):pš?}ɮ#ط:]̺ZE5-Xi2|5$Q\2G58aK.>]̺ ZV5o/[T+3h.~҂584[<eCn$!py!Ke^G5X1h|B􎿂$Jω^Cݢ@xiY7]JAKsm Ad$Q@M){8ze֡Y7]bAK *3pZo3N%u07Vkڻ+$Q {?^+h՝5+}(;u#%jCoBJeLk;X\kx*Wp.y3h+r(i {d^qy>IŬ.[|ܱiH`UjT !-/bТN&,{d^qhyk|Qvt1FK hO ='`T>uX}cq({AR>P;5y!4")MKCn$|h-˳i+g.f-J 9~kH1c^֡Y7]A+L-r˘r`+V 0fjM3hQjpQF28܂ ֡Y7]"A jVQ)d1hY9%e-KДF28aV'NKCn$ĂVC7,A Z8G䱥šǤub֍d3hOk$_W0hybCwPšM7.CAn$;- -3U<_q%CAn$;-CoY5yִ~Kt0|%{Pc]v8]̺ ZEoʔѻQ&}WjZ8]̺, Z 4bnC\l&PCК;w8]̺bA`eޠJ()hZ`e-CC4]̺,F|û*Lx#Y 6CuqQkŬɎA˂Aca#!J;/kAWC5s3|Mu#1hY0hlŰU_()a]q:r} GÞVkw!-pb֍dǠeEKQlqZ0xj]_c>{AKn$;-UBpq45I \u#1hY0h7/: QP]|"q̒C )ѣ_W[lQ{яͻH'huJ#Pm<9ĠE7p!JҀ .7;,S;+y 'ロ\p9f1h|(ߡo8 Z,xps9QPcƼ&>v!l޺JRs0~_TLY7]Aб\uVW+WG71hWkOcgou C2běslWjb8.OɵŬ.U}骠U]5aТa;}!CsePӘ_ riǴu#1hYZĜ9}Q[n Qj͚u=Fs5`,\&֯;o4w*}&LY7]A|P Za7I:۝@Dq:{}E-c U_~8M=\r7~ 7|G".'ƠVW>ZCNJ1U`A()l:}Ѕn@عɥ4g/Ac6aZ]̺Z]%>m(b֍dB*)f蔡ƠE3g.*V>Aqt l4Nq[(1VR ZM;Q8==uC>BW53jτBZ5]w}»BMVn$9<ĺ3h}yW>QhkG}ŤIO+ʷcmyA ~&{<&.? X?s b֍dנmSZF~`=ͿZn=6ܰ/=8Mnj7).iz z]mkhAiB6Ŭ.ᠥ. QF܎Q5M0h85yrHn0M5jiq틈=:kP>a6aZ]̺RjN { "\5ɘZ Z4n\S:Q&NJC~Mm2q /,%&%q9ԜТ b֍dRN +K -ҧdQROF1Eeet3^K[?Pk/u#٥J*  Z_/ttܙg. I4Îs8.-ܚu1FK8hg!$SU[7Ri3hͥQFX*tRS:jXkeQsq!`z]̺Z+K*6d(.\5lX~Oݣ:X)KiY\\s^n$t۰#SBNDq Tf^]Ռ>u1FK)hu ZPpz}]w}jToûV!̡,j.9LY7]*A>UvZˊ*+!ka Zt5`aPi?љ^/+𮃕jZ8$-}b֍dxЪ L_(>N1NYb}ͿZE]UZ(Ih~XYzYQFruqwˢzCu#%Z SPHaXךe=1hΛ0QTA#`aG3X#W\5Ŭ.U=w ҢT~.v0U64?xĠ:z|" R /~hsjN>`Nt򨹸0.fHvJ~N?ҩrZ5(N _EwaǦ:jѰÇTCK><"._0.fHvRZBhbAZ˜9!e-SaGt?[ŐfY>u1FK8hzf %>?hBbjmgΠEA?Κu1qV qH>yt1FK8hu Y[bu)=.QjÔ<:̅~ 4}^s *1^uHҾdƌRe>`>={KZ509̣Y7]AnɂDZbe>4%V|g?_,˃# Qp WJbw;#-utB[Ft4}b֍d~jb ZK&|1q<9su'J.AƌYR5XÇ0v׀:e9IŬ.U^+ӪzI0hTNW|y࿇QPᡅA`X}DND((]o$asGn$3yڰ,v 1hŇ_WlR[:ԎD/p:;SV0/cGTCкpWIRzY3A+p0.fHv -?7r;oVį:LV|6l{v{BBSN/Jt*-ut(joK;R+84~asGn$tV9LUFԭ]:[Wyl%pƥ XDQ<$5soCkg(ja*tJ> B09̣Y7]A SwSft 8o|0PAb6i_ryt1FK8h +/qY1 4oRi֝(i.wܝ(7*zGTELZ.eo„To]d>yt1FK]̺ ZXgopp!T\Й8̯=8pCEm:@%Z/ŹZCLy\\.e Q4zC!`>]̺ZKG+w\3~GaΜ[ߡ,?}(AkNoK˂z-Nq+Bϊ˦l*C띆{C|u#e/ɛ,ց.Vz}&O?s~Gu̗,(; ?gR#GQ\6e 3?|{8>t1F,hu%'4ͨީ @+F4Tz{?jz>`vSzWH%asOn$;-v`\hyrWdDI SoxJљ^Z6e WjCA}"M!`>]̺,ֹ瞛=^<(L^(~a^kFlz~4{jי( 8lCw!7? Z-SvX&,UlΩ^*~:yPx c>]̺2 Zݩꤓ s<}39}&Mz@\F#{5C\g431qoۊ3ϼ@\رk|P}vpeN;y ٳvX\ ʥ0.fHvnvաj४vdNDx/zqh+ܯ=8[ q<~"c>]̺Z!j,:'S&|9pJ̚5ϩ"}mHv#e ZFz\[wccV48+|G2/-NZ}]آSmqt1F.hu;lX)C MhMKˈOtKB-|ч y^\F#|-Z}V6|IS.>Ԫ҇T\+?Vϊ-8[0~|قBm̧Y7]AJ+hI 5>&qw^z.Ul×}M^HӦDIApr/\<#-DŽ}E$C?8↚Sv6p/`B \Ȁ `>]̺,ZA 5N=0imRk;W4Ŕ)K/J˩SoK?+KMg} ?A|8si0Ð~E@(wYD!yu1Fc2TNs-)C kilT^J 9]т޳Fk;+3$u?KIn¼u#1hY`}Ϳu {g\TܩZ'.K2wWw/9(Ӆzc46 Qsѣ_ EI V _˦p-sЯ^wRkrCu#%uT{|-:o~x.xs#-K$ ^́E'i߉Ӆz vFl3!$> nDo%E\6ՇcrE_l>yu1F,h-(Zh.LVfzqU,  DXAH1uǧhX3:e5ʠA/zDiAe$BkGIjR@G{Gv5;G̙q Z8o.>yu1F>haiϛA˵U+x%iy,bI,tp-{~ TZVO|ol5 ist+J;kUн+BҏhC`8#AŬɎAUk} 7lK2ri1bFZbKZ_{ |2>J5,Pt!i0Xr+#L7˸:u#eJ}U| Z.Zu<) _so5 cn Ր8uwRhw&EoBOCR}Xw=k4o I V ~:`}c~ViC܂ -0.fHv Ww %3/G#}Dw?(;ZA?7eVU:JkŒ9qRj_x)`2y%_V6(ͥ0.fHv-kGNfNo\X9S<2J 40lZ6O_Hi4S*<E&/R$)´%{_B(&2GOz0^YlK+bär2yq 5,o.>u1FK8hٮڌf9X Tcǣ+?AZQσ EܻPZ^;i ?/0߁(44H\BME9`Z\:n)D@1!0?mhff}, 7zCu#٥: xuZV(})4/Pp8]{II'Vz~7fܦDY1#o8cLϕ`ӦóD:SAal-}aOL#?0.fHv|+y^ԚqB7 Zo՚)đ#7M;p\ovQQs_n$tNiNz!)`֬ZeE>Lj!1iZ2C \a ,I;>GZ^#IQ0?&=GxHrR C{s5T&S@f+a8l*}b֍dp:eXӒ sZb*-߽>})ٙۂVRw0eQ "i8i:y?| \;kZ\`;蠛+bROX.LzR@ʄ ]9m 7Shu s@~9Y0?8DÌo.>u1FK)hIH9Ǡ;KjanՊz)2.L10awhX \;0i;HI&O==J-D#G>^=:4ލٓ<嘌u@.ia~ӧoGvih "x/?Nu1FK8hn]ZAAPjCJMr5 -@dQ3!h+ D:pih_o$TtA(M:%s|ߓ};u A$N!pZ٣﫟iC ;^JKG!u1FKJϻ{Ŭ.NS<՗F-45I[ rCEu`R,\Y׻5oWtN+}W*`-(y:mY.t1FK hu.4BqP`eg7&9V(,fҨ:|1j,Ep,AW/&jD9?p鴡Nx_u#%tkUӂbvP#eK:Mu~ b׫~G)MUR}uZsk/>3op-886R>E'FZk+>Kn$;-F-9s.?ZzCׁE \;uܮ~8oCKb? YmďѴEO鴡V>sc.fHv S[9>u蠅/q]S h >\w:h;8drQCKjE(W^7#u1FK hCTWcgxwhYA ? 4ׁE \;r,Ĺ*Qwc.fHvO !;70tQoҍ~&:2#u1FK&h9V:gZGIchi`wf`QׁK~.rVrR""u#%ʺ24Q,SPjk{.7Nu'aT^[=|q>mgU߾8eW;ߺ뾪Ǝ[\i{Ӿ".kf>E\vV6pI#"je8Fb֍dxjf5Ak^RÆ- 0|ЦMTO[F-ׯȤ嘶Qէ:˩cCϋGDpŬɎAˢAk p'k w8¾^-Χ8o6p©Ⲵ0dG#.; w>u#"ju8Nb֍dǠe蠅SXлj; FAlƌiaZ$89sⲳpa.DDI]̺,p ?v閚?7t5뮿m%U5}%ⲳ0me^ؓ֍8Y7E 4!XϳtH=yL3f\8i }!qyi+gaC"8Nb֍dǠerQgWjꅢ*t(iJ,WZDi%{},L|3Q'u1FcвGzUrEU`=td1 7D hq:ꨳeFi%:M2_\~F7<Qw.fHv ZyZ7EU N0qoG Zh Ak;gaȐG;qRn$;-|hs^U DAFyLQE~eFm%3Elxy,h~H.fHv Z+?F-2m - ͞-{u`QAOˌJf#F/.? H.fHv ZyZolchi8wgVͧXC8tҜeƽ HͲpn#"pŬɎA"Aa14 aECfqovzMZ/""bЊA"Aa14w aELfqo?_,L~)u#1hY!h FzchiFa܁E[e6,7wo 5>\Qwc.fHv ZyZ蜍`A14L#0෴fqo[u,˂{`"" c.fHv ZyZ;덡!hI;Lb`Q1̭,3hXٳOZ,swၴNDDĠE^G҂wQM;fsODԝXY7E~Z^UT Zt$E82h%Ӱ#,,=uƗ։b2 X7.h-SS^.chAX72h%Gk 9 {]Zu#1hY'hz1 ~I , X$Z4M~I:ܷ+QRn$;--lu -aR}z@$Z4,We*j#"pŬɎA"/AKwlwCKC@0EovT+`{}]:->fDDu#5aPr6ީTms%s~wQAKu - $i9>oI^7euN۾^SV<VZuwwi5:NE1Sn$ ZU}Jº0nd/WsA;Lr`QWzO{sQ RuNn֍HBDD]pŬɮ [~I,&)=io٢EDL]̺,=hw,W!`/i}[#"pŬɎAˢكZJ;Lr`4!hFf Jw$ڙ1Sn$;-ZX4M5sU(-Æ-5>Q1Sn$;-|3-}äo&tN>P5>Q1Sn$;-J;L;iB8:ʚP S L]̺,CBN{}]M(Jډ'iөDD.fHv Z!h3}S}>]_mٚEDu#1h0Pe& `lٚEDu#1hY0h5N߾Oy LmmxA:,"dY7Vc}N=)0a"[ VcKC Mg2O!5(9 Z,huB67ԩiK Ҵa^ol!$M6 []4} YDDaЊ&7A/HuBG6գ]u竔V`ǠXZSƎ2@UW}]29"" A+&h(PAuH1hu+Z{u/:KQ6iҢUjw km!A+ Z8"loUh,s9myuGWDV~Q 4mh1{{+;Bڹjogu' Z0hY0hErPS?x>Tm6q:Nk\^SbIk\&N_ntG$I$Zԝ0hEàeiC-O?WtmB=-ԩ??H> ܘA% ~lբA+-<ӆZia=>]붅"ZzG'vV :4qZk´j=PQ1hEàeMӆ~Z蟅1v O |IIWw;>tҴIG+-V+m-u!j$h, sPC??A{o"H4O)Н׸\>nWU]nԌUN#j$h, {PC(it?-t _Z*!|RKWw#4OՉ@ j:n#zpQ1A+ - ž6O? $IV{Od,ܰzK%t@/tDjgW`]ǯc ~E+- 6<ӪWa /4~qOAE <&CYقo4qyYH'1[ =?ݧAaв` 'iCl)Dd-t~IWZo@b)/ߏzPkd?zzީL<'cd^vHs<+GcukZW?o4^Z>s>ɠ |>͠`gFe^{!4k(Y=_yJ$nc'-#4ù}Sn_TwIN}2puA%h, Z/S ~_F>nOcֈ~ZrϥOy08*L_X ?-.' h!^#),+]$A+-,ٗńWrݏ'kd.քzP9ӒIGVTX~|.}<̃[6#$~㢽p iIþc䯎B4oK?qA+-/6ѿ+ӆk?zQFy84_?Q-臃^dzg. KZflnugJVH9>7c8V4 ZY-_8ѿ+ӆO˵?JBkiœ Ymz?*ɠGN^k?8-} ]WX͠ EA+/K\YJ}~Za tChȪE i0}:$dzJ2Str%KÞ+V(x_),[|_kJۋK %lwyT?xCҼP/|D 4h,-|)c044;' +y(~ZضIGHβ) k_j?leE%-"B8. OߟaW_i_4h_lWmU=zT4#-?U beiEVQ_YJ' ijL?;~ZL 8>y?J5k+>&hYTm=TwGAhkW5ӹ-)`0uONyWuO&2_ո`҆ntl%Á\N!fiV믆LA)N,j ZtfNZAALrGVZ^;X= z\wkVz;ϥ ے?( b, 7)ǭ MqegVcЊ-#XWHAAKٗ: <5}u޸^A Z:TA mWxe!">5ǠMKh!HcN5hl-BDTO!RkV3cЊh ZF9"V6n%=G M Z _u7h5+hYҧk-""jU Ztà-""jU Z0hEDDA+tFDDjtH1hE#IƠ V ZDDDD)a""""J V!""jFRG1he,5ɝ&oDa1he_v7\ppp(,TŠդep n'7NAI n'7NnZV n'7Nnp;&/n'7Nnp;vj ZDDDD)a""""J QJR EDDD-"""0hAtBG[n'7Nn6rjLGQPsԅ np;ҩzN9 S}FnZVS)59:w먄_^ np;"fٮ_L;_Vn'7Nqvj= ZW2ſ UoSzV&vr$ R8(X8dq_j9 ZV:_+S|Q=mzۋ`yN4S[CFfm/+R jJ6mrXտ+ ۩Bm]Uq;iV?\(fy]-[NVn郕"*VmTͩ[T[_n?|UJ9`mvj Z9Wii/'\T:hU:|A]W$*DhO*;)>]A0h5ʗr>(%%[jn!-JY1 ̠Unv߽Z&F,nnAYT8OPۈ] (VA: Z+S}<> `}cjB_Alre}Vv: ha]'O~n' OԅA+'k}ADF߆cN5jp;vrۉ,뗏V} 囚kW3SmNNnp;Q} Z24ĭȿ*E?Tq;q;ۉ0h5Ky_U7IFmn5:vrF縝ʏq;p; F3.-տZӫ>M{M-+n'7Nnp;. 9p;vrۉ ZP\i\Ln'7Nnp;v"Vx<_n'7Nnp;Qԕ$,u䗓nۉaJÕ*/*;IVp;vrv;qI(I ZգnsNnp;ۉǠg\m]/#n'7NnNFDi`J]_*Ww ۩ep;Q:R_>Ɨ n'7Nn(>JMW九vrT_6n'ۉƠ/xM|At3=n'7Nn꫿ۉۉUn/`c뺮RrVORp;vavv,0hE}Ikݘҗp;vrT6NN J]M˸p;v eA+E Բp;v eA+O$ Uvr۩>n#7N2D/dNnp;mۉàEDDD-"""0hA(% ZDDDD)a""""J QJR EDDD-"HʷjQMw9c񦹕tfZwb"]+((tGQ֕yA˒xA#^3kƺ|t5 Q ]*t>n>0heA(9 ZDQ-YRUGA  Zy ED1hEU R0 UZP1TfFlhk4LfzUWU,-RzꅜAc^{ݢNޫ؏y$.uMàE$-̠U&`byb*1̫+A[+OkJ~+KcגZEח^_nNbLg ZE5d.^Ъ1h%A(UPֆ,m Aj].%k\v_vz_xx8AKg嶏Lk~ND Q*2]z{W%UOmX k+Z ZuvMk4AЪZOU,b"JQҌUKT ӕ+ 5UFҕA(6-"""0hA(% ZDDDD)a""""J QJR EDDD ZZԁG^$C2("7GMYyҖPjX~{"""ʳhAˬDq,*US7* tۃ-r&~UPʵݻqn UZ* K,鱮׬XaURIokV! hڪNjlӗW]/6UZv=<ϩf"WyϢ걪,B=Tue߇p_oe?^܎A!uJRWp+QYwj+C+kUZ^S]Xд *,٦ǿ"-Vx<޶-tTOz=eWr/~zY޴]j_^;`]ߒzie,NvCWvlFDDRU*-r*>s:*RE_Z#=c._z-\ loqiiìGJxg׬ǜX<r+Ә{|MFx>GX7""bhUU|4UB.syAU5MJ9̲M24[,oCfgSkAxӕ)ퟦcoiZc;ߓ~N(YrE]u<]r[9.<+^cJV4՝K ^TN+a5OYs륧)+}>kOSja]-!4h4<_(.֍(VbeV}JTJ2/wkv-iZjIKӛ/XN*ys:C}}WFWr=2^Bc<ƴU4A\orp|}a݈ZѿYUg Qzf KwF U""ĠEDDD-"""0hA(% ZDDDD)a""""J QJR EDDD-"""0h(i?3K.IENDB`PythonQwt-0.5.5/doc/images/tests/__init__.png0000666000000000000000000006552712605040216017634 0ustar rootrootPNG  IHDR#DAsRGBgAMA a pHYsodjIDATx^ mU}[ߧҟNw΋$1fP1:5vcb8Ċ&AX\ƂX\ ppm@_1]oka=Sg9ܳ^o]SB!B#dJ PY"@Ćew|S=gF| ꛗv7/zԽ+ǩ/}ڿطܭK b(YoVw_/7=WP}y_}ޝǨ{:A}TzZ|K}׫o_sg9Y=s'/SOy%9叾.ou:5x]TnW=*>^JeL>SmhR\ڗ"wrTQ'7ѹT|-Ę5]+]Z3gwTx{ 2??$}I?~y+U lIR2 91D.oo[tQIu|.>YuVZ m+m82֌Կ]Z6k 0"Um2Mϴ9fdcԷ47B7]ſ83^/'oH\51|(5麕Ӵ&ǭcW ٖL;h<m3p|fk0SKeъ7eQG_[/9Y/ nC}kOUsoS7f^Hpco&ԊBD=ΠNz<5,V_cJe1%] P( =ל|1V^m ԋvz_O~[(ZRS:isۧ+3 YV2[<6O?޿z<ǡ;.<1͢o3lo*uR؅0-;VSw|j^W~D}kh%&Qtd7~YܪX}5ek#|xƌf[' >φ}Ԏѿ;X]Rv+Ddڐ1ɠGEwYoQw_ujgY-ΆeQ=+ۏȢ{Uzۏ"l E(,@ dJ PY"@D!8a'B!0,dB!Y@ !BoE dB!Y@ !BoE dB!A`fFx,准BڙU{$f+,B!ߴEOΫٙ;,me/hv SH!BH3,Ȣfd9iY$B!NJ9wh YL ^5?[Wo{gruc.(Gk2;@ H!BHBX9AcY ePV&b~/guجBͶ/UcI{-. "ydB!fE_BXcc*E}DW]7X\99' B!7ɢ7OGŰ^FYЂl׍9 n#TVJe i:'C_87Qn9AN˴)O!BȔgEs@cPS_ ZP6Kcc,=;P I:E_ryL B!-Drd$X{kKmخ_.{=8i͐Io̦b/;D;+CtQyR ;d{:׶kݡv5r[;*ơƙebK!BHEdq gY;jE#}"d|/{swLYwy;JXyImLjsK;ѹΙB!q;,&iEs*ib_ė"$2fWܪ]At˜l'gKe\cUeێ/K!BHwA7D,fI" %6]F򤱮xTY$B! gse1N]g mTD6ԔG],ʅnCMo !B0"LeѬxK:ɛwǨ}MRr:idB!L""LNeqA!BY$B!d,rH!BȘe>4OPoE0m9fEB!B/ENA-}f;Hv-H4,B!2},f+(IJoDnضLdB!xdѬ*U˚lRnc- GuM{k&R)mU ^x+0.EB!B/Z nPsٳrUi_гťA) H!BeW=lJ}Nڼc1$˕hj߶_ "!B!ӗȢVfGF}y&*ܖj,B!2},jM6PA \i1"u!bHfjYn_#۳5H!Be|(n7Dl5\Oq'QAaF@ebU¸A !BW;I47JsvVI׿uguZ\\T'ꤓNRӑןt)jB!d}s*Iޫnfu] /TW\qSQ^G}zuKWS_rSf.+unMB!BR] f=w߭rx={>{M«}m-_W{wQǜpw]+9]nFu+Rs=ɶ !B*,Vg3eQD/n-vnS~7իMqdzT/{n7^;[]5|ٲFB!,,YGeEQDp๷c޻KkNG]U8oM=7g;^^s|J-6dq\9AuA*˱QԱNM-i301<Չ{}~Ļ^MX²h3L!g3dQd*馛ߜ|z_3ֻߒ9oڧP_/S[=WO׫'З/ڽFЛ<%5[D$[1;p,J MLet?\D}~&6荵t>Vzn+}=\L&1/V>2"tv'wu ^߼_y։JRrM 8g9%B!d, Yǐ'^xUYusl?o]=Գ[WoqIin/;avMLXH>sa&XتbE+o []fzP1&(Zy=-o~٬ٟ9-t%~麵is2NcDch<%0cYB!Ӝ1bݗwBĉF4IŊ)_R6Px9aiӾ~FFb>ݾv12n\,}/f.1ƄՍs$B!d3>Y4}y7?ԗw5u}ȷwfȢ|_Wh)ܭ=)z1OC_yz}Qk;Ro}V?}'C?{Yi!u9/O&d~V&f.u=1OKPdэM痔0d2^lBaZܳ,VcMo:7,w΄BO$يƽjdQcx@l,/R},$Q;|Us/WokW?:_~CsC~u7ml3 (X5B!Ӝb ZxxV `02e eAqjHc-cR~<S܇&hJmfm$|If!'tϩfI^=z__)u3.T=R]sꪫT~-;Cȹꡇ~P|2>vaNœr;Id|rnN؆'c^|cOtv -`%K9oZ<_Sk]DS|< ۱eS:\{LcE116e BH3>Yl-,:a"H%ˊZuXUŶiߴSX>闋]t)zfȢݮK>~Uשǽ{#v9\rz W.X-/_fd'9E.TșEB&+cBٴE+QY|ye)ڎVLČN2|_XjuJ^y,竊fȢ*ze?zK>}5`-{yaGgD2x;Y|iZ/r(@R;tz?N9c׆oC%d"WqMڮB!dd<%$E z:JIYcʱԵ)xۉ6bx}E}g3dQBsޮ _}z_~Β٧iӢwiYܥe|u WK/X]}Uog~YKv:Rgn!B֌I5J^ >ZPXV 8a_ow>vy,um vf rE_d@ 7!˗?O_;jzw}ŧE1CP]R^s?%-W; '_gY졗G<<מ.rTB!9EHwfqd2%'+>-mӔl,hp\Xjr?YY':;{f/??L$> s{q߮NLu窋Ku۩> !Bs+0&b,}l,~_WvRwV߫~ȏ{܂z΅Z/65~YSвx:O\E*+OH=mhܿOB!Bdq*iEsjlc Y| .P+W}V=e ꗏpʲz+R{¥N=)"9/=MJu*ݖB!YY({._VN=eU|G#v#.Wz&?䋍$>W_NuԎ+^U"wߝB!eo)FYa< 7V'zt쏫<#?S_ 5x)gF]GVEB!Ճ,FlYY%BSϸ@+LN;"s7_!eSB!,F,SRtFr?aN2ײOIzJ!BSE2EB!BH:cŽ_8yu~8oR6gVԱ.M&H!Be|xzT,jz;7?Tq Ir EdB!˘d1[QfE)Tu%YbEB!B/ռ:^?%jfX:YvS_Զɒ_Wl~QDfsuǤs*K\nuq%6UcJȻ(H!Be|X+"ۙ2ɞU kfu|P#}2ε ꤎE_ֆ[5b'}&tE@ !BLbx̓@DԶ]y.5uj%sRƣqubN\),B!2},6bZҒݾ)d5[K7QįkY ڗֵEQWu!y,H!BeL¨({j$l$P=f-DmlgcNݱh[=uhMY-tkX ~aH!B&9xfYYno}o|JIdVZQ/n)mU˥]Lrin&m"עuP.a:+Y$B!L6dlrb,gZlc# B!7TAm`Y$B!A2Uhou_u"!B! H:,B!ߔd`\ B!7"tH!BH,Bg B!7"tH!BHRMfYR_{-4}h`XŖ}:1B!B&fY;f |[;'Y0` ] `EGsY~p ="!B4b&Lg,jn8QVU;fQ'wQEB!2F9K`0ff^tv|2[!%d1׺zK;/ȶY6Rb,U]r_Y wн,P禎+yXv ;m[F87nӗT?MuZC!BHiZߖמ,y42eu_鉶#KO8}J:YWa#xwFʜkO'XQ͈̄}!\ 蕿CmI$q[&UcI] B!B_֥'iU<ƪbuNb8P绍ب,'sB~ٮoK-4~!B`#FVbY"% 7Y!Aj.J6bd$_ERvվ8m0c!B!I5m@iʞɤ)k))@ t\sR.q06*e+ŷ8_C(b~ZVŭr[3|;i:ކK~!BX' 8\&ML"KR&73+T6lm$0d>'k#>5Y,6ەLΊC?|!;vTsyKqN1D~!B@+PA,em,"!B!d B,P߃m,"!B!dqSvۆUǭH!BY,B!2}Aa B!L_E8"!B!d&H!BY,B!2},ڧ{ٔ'z}nEOH!Be|(e)byo/"O B!L_$يfYHBЌF8,"!B!ӗbVvz0U33/VX:v|2Z͋䵮ޒ;&8Ҡ*16SסWvB& B!L_' -=1YI0ی:qۂ/auqK8ΈcܧTŨ/~n6CB!BHԜ9Ԭ,5YLKY,E 26 ?л!B՗^&7"۞Y4nXEB!ҷ .↨vۆUǭH!Bi;v$A7 B!oFYQIA7YܜZX)GUro\Qߞ8_]sEݺ?uB!d6YEIE }} jnqMݷ8,N#.w'D.Ӕ[w;dS%Νr6C\jū/!B&iX8"bd1W/%"ޟ>6UL(u99Qeכ!2L,6A7l'Y,(IƸ#"?faEˑ&xtnC).uWݱre y[߷0C /㖺MB![%&Etq_zO ~d68򭈤}+B!$en9vnϕ2YWWbˢw~ֈ7 R'Ȗ0mki,gWx=e;"0hB!,4ȢFnEJWdH&MjY46;PלK->"IԵUᄯ^,~,B!e\(62R6vnϕ2>Y$yp\2i2T'Ufڑ㙨2V侠ݨT[Jsғ Д H!Bq0ysc\b3) ߭vEZ\}I#"Z!%mc\*K]<`A;wk,@@]z.m S%]5@o .[ced,BgY\}FZݟހ, }ŵE_ԢŵXWwI\_YQg ."tF_e1ξֲ{O ."t֐5\lW!ܹ3# dѥAiٗ̎]oY-xJjzB[諾r"fYӹ/_[Q .CʢF~f1,jZ}(n=djTpGEe1Bz4n#5;`_~&ȼޒ;&}mxmڈ#, >uR[Of4>Z{O[Q .E_6UTo?:%-8ٯ [!wvKoFScI{ bz) $DOEe18b7SMk[o?Z41׫6me<WNyɪb+,[dexY+1_"zm jmES5+_hf;"l &)\wLXe-Y-Lݶk[.n?.Y^vf rE_d@ UUY,E6X7s,6Ҫ511'4lmT^!9NZ39;d ۉX8ˤo_*EȢ X 6Tl3 " dY,E{ m#Ao ."t}YtA3,\b#X_՝w:l.Ȣ WY\[ Qqayy՝FWPXW+;wEL<'f wM'c^Ydd}4|_BGe*نhYtA3*qhWm&iV4]g*x:~Pm=#} ΅Sq!ƼP,Bg[W4 }ʆ~Մ.L>Vczr66r֤bۯΰIg%qMS1L)}ɜ'~8)mj_b2tx}9v㫧2u_P_WڌS15~KT qsTpy?,E'Gd"E\l)WF,Fǵeu󾭰Kp [bu*'4î My9vdn+e+ *<^Lm۩si_QƖVSb׏?@>eۆ WO_kvF秙p 륯NL*T .ZYtm#H91jS.ІAd1gڴm1ijۮ='Xss * ImS,L,+A;5&ƓԎ=,-ril'Yҍ,mOjk۲ \]6}9\W,=;PB|." dѥ,ofˑok$zue>c٪0m1֗jf9edefȊ[d 0};V0FoBMű|Eŀ,4Ȣ<~ } OYy|B7Tъ~`lUeUqNVыYejѓB WNa&~2|jMK+#r<igeEh\l[to&d?0(-O#c)шvоXQwh̵is.+o_3xo;>7<vz]_8Yti!]CG>21կmD_e1 ⰷXWBIw\GV|{TLJajNS:g|-@ RѺ턴 _j#!%YȨ5k迉5rɦNMQ]蜚>?U۶_Ghߥqu?ڥ~^ܾq\ Ȣ 3}E#dqE*&hmxBPX&È#Q?w$*iXZ&U5M_[2aH]"$}}S"JS6}.2>?U۶_M8x4ޖkW|$˻>۴{LdeY̾\>X[y}Dci+og^ niJuf l/OtnuInQuoͬk=]QoC+LtX2 hj/Kja'i!j!*r2v &Ur_q xKRzOR'd/lu-_,E':0x4B .H#c*'3vk!ڎD.'rVN!l\vKɞǸO!j/' f$SLe1Jr!}j"ڄIMUuRs"vvf rE_dgAo .-d1S=WHe_W_z ^T#Ki;h\:{NV֞ŜcsAo . C,eҷN7" dY d܆ڟXEȢ 8Tb~oê㴂,@@]E d,BgV-95WdQɿcPlێWw P>, ŵ&{ltB8vFQ,}_z~0'^U-rRudn4hKΊZAC hc#o}6W;1|>P, QWdE/!lN1VkWVKa '9&2ULvsL޶|o551m >o]?@]E^␷Wt$M.M[ +Y Dvʊ4? . XNo~>3;Yfy"SIoe1ڢWkFM _yn&ԭ@$h`ipjUe>'Ωom&NS7>#cAi?*,3g i*瑿?{7x}φl~,i_ȯqtM<`$Eb#ÔlطoR,޷:DoCUMrf"iEONv,&W'5KMDYVS Q~K&B{:>z|N|!\MӾe'tcjվLPZ٭(CϿYȢ ͬ(}9޷P(OMFɄVfܤoۼrZS]XE-+k>6}Yil%Md?cy4UKJd/MQ}n]k>ڏ>>"ʲ}>E?r>Yt,g,1]!e ޱR?>V,vyg۵'ݒC\0V+;߂rYxGʢD|UQR(L*ېRzRMC1u쾦㭈ǐ\i6`zq[~QfUhŝSIF*/( "Iߺz?53?a$^=Oh>YfڏU>Trdχ]3u/5ȢK Yt&8Z$dBem2eKMGV)_lG4Yt\I{uI6>bU*'tͫh&i3\&a2t#^q,8?:Mzusm~.Y /.W&ƚ1Du~*T$,Y`ڑ6҉, NjR Sm^;e3_v/rvY劾bɔفR< 7Ebp[YIspjrQ^ܸʛ)HT\n[^qHr9Xu9 Ӈ|defbEȢK,NxumK g }YtjY4+i=yiEsj}Yt2Yn-5.B,kR8 7E^Y~,@@]Eڢ˳> ."tFeqB!~ ."tFoeq߲ZXXN ,BgV¢Nq Zޗ I?T?!*hwۜQ}{h,fΩ}>o6ȢKOd Yb\.Oxz-m "lmEbmWY,/̩5YY .Cb.XK"U٭gVr. -2[~>6B:ыeeܾl,qBPSn3k#4VU>ˢo8YtRXYq)nV|1ڰI8^v6԰L*.m EǃuI{-{SOe1 .ï,ӰT|P2ՉVLI/qPV\5 f,uOy,ḽ\Y,t#(EV+\eJR];SYIKo@o .Ȣ*n4eB oC e-n#\؄a@]EZ-/WvO\9q#\z"lɊ\I_I_@ K??Â, }8<&'fj&lD1&G9Mڳl-?sNv b)dd9oB>QFSw7|>ʫ/gFo~uف_ׇMtfk+]_;^.SIϏl{׸j-οXo#zrfkc`Ed:cKeVlYғ|j&lพΦ&FrY4I7Q۶Ni/e=AWŌJaFԾM6SEhdH~mɽ9z 5We1~i2XsY`Ed:c+Ȣ܂7=OM}!Ul v0eX*~Bb-گ??=q/{g3c|> NXKEP uoq웍ALȢK,ꉘPB-" dѥ,Z4 Y2O6Co'!zqe4ݦAo .CȢlF{XNSƫ[,hYY'rQ݂Trqjd{v'B d2,6?ę[EU ɯ01q5lmT^Ǚ!+|G{EȢK,X䦕qo_*EȢ X0Ns*d,T^ۇs.,ksjn.Z6Ȣ&"lW*Aq\\^YtA3,V>6Ȣ WY5hoA,,`k, ŵheqQ-^YtA3*k hy6T /;/_`)5OD5_oy"}@]Z⬚MaeFK>&^WY,ܢZˏYti%Z53,j@_YlmEV8WM1׻Ԭֵ!d__,.ym11)&mPn3k# <;}Yti)C9cmY|9kINk&Vȝ2:RBԦ_ά:[  7Eֲї#_Bձ,vS^U"f um ޶i#5-{屄'IEȢK{Y&erXr9%D/MYOWYBd2,ft{gp<[>Stl,gum vf rE_d@ OY, 'FUx.Rr4#m^RBvFU< 5ÜG EȢK,ngb,}," dYAm<ئ>ڢk~rZݟMdZݹ[[Ed*Y63y,Vr-⸰ϼ"L[g'h;5[bϠݟނ[_3|"g)_i$q}eaѢs<+jkc}EjW0JYY7}1W-EȄ74Wrۜ$6; aޟHF>#K}:0}"=gߟ?l.Ȣ WYWE՚~Sje19{X9KOjxaƜ—EU=~Ymt+KmmIlUugv5?ОKɫԯ jXՓlA'1Q+Pq+v|rkpiխv8>~Z>UuoՕO\՝NCH?}~ic|ꮯ=W,>ܚ>gX=m7\?[/\_@,BgVu2AfQB, m͊ZhB&״dyE>0޶eEzz×x(0$fW3y'xv#LRޟJM#uudG WpFSl"&CGF^QgB6+|^a _4 Ĥop95}k⽿r=* u}0mN&,BgY߆cГbkYbogŔ$IBZYWd+`'#g& @[Ln!=&n^YrU3ضk#FşVqmjk8!GJMXUiɾk2*V⺖ƜPt}>_[?[`zA]E茭"7ӸDdQ U#m#]?hpsD.?Ly= ds}5ڤOFF0`j>Eq:5zu)5 YV+1GEBڸs ]4Fj4ퟟ67~_\}?H=ȢK,ʤ7\}*mهrkГjw>؟Df$eP +.23+%~"QgG7&66dPcڏ'spc(V4&x\Gz ]qx!<:d ~ }в>C׿ 7!}hozZ~Ÿ_o~_ܶdYtN[IU~ȤZO GjhڜWL,=d{[zFA]_Y.%.[q+")Y̾>X+2D;KnҖ˖yEe &h#6URw=7dB\F򤥤ůEfv{X )nG2$mbfۗ6M{^~/gucq}6r^\EȢKE#$n0^3V Q+;Lů=YJ}-mBcU kY^v|5i+%V*Xu^lz5ۉ6ɢ.L EȢPnYUVO)'%Wfi޽e(뫦my7r}^6ێ1" deHYɾ2v%]YA+nc5dBmWS 20D%e}A?9ڎQQU.{>|EȢK,n-I,i}\ 7EEsdbF)׳iŵ9577%[^0M׊A]&,͐ y=VY4"(Ena\\^YtEؒL,ŵ۷oY-䫋A]E-%J#lmEd:Y, E7&܆ m@]E茾ʢᢷlEd:cZe~mF駒_m@]Eiō6Ȣ ,@@]E d,Bg 7Ed:Y, " dY@o ."t}YtA3Y/,xKf`F| ."tƴ5[[Lb2?e.=  Ȣ 1XBŽGgf&KWo~-j6.[VK$N^{mhΖ2v QmO78+DE[wvV¹4zg` ."tvEZsrY3r]HeeSVn]iG }N_oB'mxCfeQtj.(fHYwD`B ."tN!E_G^DeLd1?Bs,V4Ȣ ,jZb±,VudEd:YԴR_u`nŢ k [EI}FEST`z@]EiEA[Oa^gIOp\ĝD@lS,Bgl Y\S v"j%eT.-RZbֱr"4?鲩3uњZ4ʢ?e1 0"^ʯMt`z@]E>ˢƹ/_Fd1v%2jUQ#"_$ذ+iu~05 ."tF_e<)ջOXNdu y */dhC1Y @u>p)!4E/ȶ>w|JWSet, *k3X9o==&f)-!_7,k >n2~;y|9IȋmBD>Ŵ)—oǂX<%[K弮ԳfeKoV?KRM%^k{5i}~0 ."tƴF"+Ri&VBYtA3fU-z=)4Ot~]m dY@o ."t}YtA3EȢ ,@@]E d4;J#E& Y/,K߷ྔ~almQqE *F}kIY4" `k3m b(JE *E*d1}ܴE ߲ `1(iE ߲"=VYldzVEʘ.YtA3* sjqmqk ."tƴʢڌ % ,BgL,n$,ZRa,Bo,Y$B!dI͙ 0q~XSi&B! d&H!BY,B!2}Aa B!L_E8"!B!d&H!BY,B!2}Aa B!L_E8EB!BH7I EB!Bd,Bg B!7"tH!BH,Bg B!7"tH!BH,Bg B!7"tH!BH,Bg B!7"tFeqΝdIB!}Ij~C¤[*"t8d+_ B!i s 0 H!9d}E Y$BH3W?pZg]f_Y`}>"!BqgݭPxӟ)3ktUjYo{:54eRu5"L;Փ3~J'A !KOUeUSv)utRTuΫmLcE &!"O|!KN}:M'mJyz߮I B{!M_?DGԫP^oKЂRy>Mk~7tmM[E 6[e>ͪHQ3E~w|#J6WA !C?rյ?]Ox'?ԙ)7z}K]f!^$ے\G{s_,W͜r 0t!{I-|Qi\Wcԥw\yO!_գT7aNgVeFM?cMؐEB!=^s!?.V]3~}U;N.127Q 'g>U~`u:0ڌyN,T0nYii?T=}uOU=ǩKv_'1 ;R-hä'?,vۏv:t[G{1::^o~g~?6}VG{cH!gx'ߨ͌qn8duuQ5{9?ZGstÌߝ']ljyMͼm 0SoF?W]ug'Xu}_COT|oڡzR?w`(O?ډXl![Q?~:AnE?{q8H!gxF}ͨZ3WnGoרa] ۫sqgW1ZE )/+/~:~ϱ̻NQA'z»QC긽RG~IWm_bEF~ M }m~`SR۲+$IO؏-f+ՎH!g7zͨ/83YuC~LC!?@ {:.]-vF_f^Ŝ Ym8eQrU9`ԉߥU4ooOgԃ\~NW7?gsVxmIgj6jE Eao{:S';/Yn['L B{!p-'S_{o_"DpS3ԃ/}1TSjcg-ɶ0?wE Esgꮯޙ<,B!df(leU]}(Yޤ|׾+rUˇ?͔M]%wtdeQrWJ'Y$BH3.YpZ|ߩx.کeN-_Q]ǿI-{[u5"L!dA !t1n- Q9]kN{E Y$BH>"Lb,B!aYdq" B!$5!aR-d:cH!B\E dB!Y@ !BoE dB!Y@ !BoE dB!Y@ !BoE dB!Y@ !BoY$dI}!B!ӟB !B!/IENDB`PythonQwt-0.5.5/doc/images/tests/CurveDemo1.png0000666000000000000000000003320612605040216020034 0ustar rootrootPNG  IHDR.x4sRGBgAMA a pHYsod6IDATx^_ye 抛!PUQ(67pP $&mI9 QhP- " M: wIr ۯglc/~gs~3_*D%.BK!dwA\qBvE]!dwA\%Y\_\B!'Y\䛿7.? u/rއ2 w}2$%vs9,Go_鷋_+ƗZ?ogWˋY|;8)Gq1?gM~\듿{[_/~w++*E+D j i$W~?K_x_bq}xO_F\L̞j)i$i~?\cs>JTϟLW-o|x77Jެ oQ[*xx7vU>|x/ {W k)iW/ZW7|ӕöqqx˥Tߞ"s}-N Gʣ/n:;ٺ!OSRmݸWc+_G'[__+PG,H@&W |_-aBٻLZk9w&//A7w׷)NWW|հ(o^ɫKW\?~w~UlMͷʷDQUo"l?!gJ~Q?_{{;VxKzz7?7ސuTǚOU~PTl %iHL\y'볿JB}|R<}g;ǿw_˯vrK>i*AZ_rKInojG!l,.BE]!dwA\qBvE]!dwA\qBvEU򕯼W8q@\9G[~}G>^|>/>2֦qg޹dw/uݯ؜ËMB?|TC2Aw\c1CsMaq}SuSj5C$wyg"(KE8ST<:\<=?o_ǧKpB@8WKqqvo}F矟q>JZV:Kqy|6cV;_gwnzW5)W;â2[va?7oFm%|"ysx[FRv|蚇\m~(A:/Z]K%ٯh?v)Z{¢!rKUܦ`~20}n%"m+ysxMg.c\eNA~ "Cѹ!O}5jo&޿F5rZVS41k`M@ks1xNrC8hqh}+.ޛ/5rҡEd!lQq&i?(W6oc/ ۸!bR}?kyn}t֌)C'xWq?z}kΰ('^\P xnb.z+Bxc-W iBҸqe>ןmg%:߷n:FM=s65^_Mgx3cQNB\~Go=;zfnD(eqƢ~'$I)fXWM٤j)5z6,I$ly+djs$,I$A٨ @ƢJX j5$UrƢ_P('!P1 RhɲQשE9 qEMJQŨ6G¢"R}E9 qE . j5$`+XW₭F_cQNB\ }E9 qE . j5$`+XWₜB1Rk/('V\qAT#f#dYP-}E9i(;XWęr8SQPcQNB\g* XP1Z{펀E9i Pmԛ ܵw('V\CǢ"T|,I+LE@Ǣ"T|,I+LE@Ǣ"T|,I+LE@Ǣ"T|,I+LE@Ǣ"TG]-TI펀E9 qE(Nyʄ[BǢ"PZ6G \a Rǻnw,I+"]٤ja>Zxc.0)uKvG"ڕ}X/'{Yk:%g1ע"څQ6jƉkc2x?3LL]؇5ĸ$1nB[oKq//Wp} Tۜ,1E9 qE[0ep=3^#]nr/k-TScڟwkˉE9nqMlQKiZ![6e#jt,IkA<E RmTRW6i셔RĢJ\+C ) Mjx_nR[6i,E9ɝDz6K2@v}mfl2/cmn,IkA<E ~lVx_n[ެF-IkA<E ~lVx_n[ެF-IkA<E'F1BuKJԾr3f5jQNB\ nnL_P1}ms2upR.>{RƢZlѷxO&c2'.)Rڂ1Q+W\[`QNZE\{ؤx_xv}=;{XE9 q-N_8wIiLݓZaa>1]Rڥ6aɹg9z-Ik!bTwIiڄ{TNuE9iqon:%]j_[`i6%U6)R('!.qX` ,I FA\$ .rQlE9 q( `[`QNB\0J.q[6E9 q(cB2Q Z /¢`[`QNB\0 -('!.qXC\ ('!.eLHFj;,I FA\$ .rQlE9 q( `[`QNB\0 -('!.qX` ,I FA\$.j rbQNB\KUiCB\r^lE9 qA/ Ģ[bQNB\0HUjc .ȉE9 qA6Ģ  rbQNB\UPm`QNB\p7ATZ /r₻A\$w` ,I qXd!*FE9 qK,I \bQNB\rXĢ%$.('!%^Ƽqڹkqy|*cDZ67udڇӳ{.$ĵq)#ALi{].5TJr!.XR>]1񜟟_ϭ=]LVmq9i^,Iç ꭠ;O`՟7ITZYmIsWNW[Ev8&nݷmfZuwҹ.;&Z[$>[ϝFO[ըg,I׶ =z[.d tW_]?}vlDgck\UskI5F܇":޺:0g¾ק}âtxqŰua4\|8fm:(}z 7ֺ{qU/寍kxQOXN%.b޷ۯ$8M{kگ+q͵^9:==ItF8uWs57룳X~4kt·5=׎9o`uE9+p|-+D+TtzP*4mB je qi?7hأf| ߧ[ C5 r)e)΃-a֥ˢtXq͋~ZOI=&yk];,IWۑZwgOrA^&8CkMF^sjliQNB\ ւǢt8q{c,=5n  Mâ: .X&\7yR t,IK` rjԢtqQjt$%@\4,I7yANrB\9FcQNB\= .Rv,%o yX e:rZv-.CǢF@^pk>$5₹U;GQrnEQw('!̅rJyT('R\k(SFǢ@\0j~,I+Py('N\[ Py('!DB& ĕr1#ըE9iW(;h>,Ik ƠFaQNB\A^rn ._xh^,Ik"v/6GQr⚁s;Jz=QE9iv#i;j}[۞,IkCx۳\x]j>9ǵ('!l10f^ִ՚=s=E9ɽ<E`y!u-[>=Y99cy޻)L]kk,I֚q<]*sְ溽Z[brkqQ7u{5$u'K^c{ߥT$u'{(=c{{ۢ2"e]͖('-*Zm]^xۣ -I+6Eae͒5{ZE9iq 9CQl 2'ZNCr:KAenZ-I+p [o5A bQNB\'qW,I 6҂!,I ĢqK,I 6҂1,I * 1,I *xâq7,I ֒ ҂,I x¢q',I Z,-X@\r `QNB\䂴` $@\rW,!S('!. ₩XHlE9 q$h@`k,I z%s('!.%pŢ[bQNB\0=AZp$ . rAlE9 q(s^,I FA\$IL{('!.H&(EHH r`QNB\011!.ȁE9 q,䄴 $QB\ r₻e $Y ^,I \bQNB\rXĢ%$.('!.pE9 qK,I \bQNB\rX#xxxh\ܵ<>1OXEusCg2ucَ=Os<OkzIY[8?u&+}uE9Kqm}_?)E5-5TJr,03ڧ!^qUQTn7z)ϿV_4[S87_/n%rh%o osy|6cV;9??=[3=zsҾ![ϝFIcֵXJNpK3ƈw?7v&kמ @X/?nnn~\Tai՘)N_}cqUyHh%:x/1CTd}v㛟6.j+*M#DMU`6^t?{9=5͵=tZc`u7X}U]ߣx$5₹U;GQrnEQw('!̅rJyT('R\k(SFǢ@\0j~,I+Py('N\[ Py('!DB& ĕr1#ըE9iW(;h>,Ik ƠFaQNB\A^rn ._xh^,Ik"v/6GQr⚁s;Jz=QE9iv#i;j}[۞,IkCx۳\x]j>9ǵ('!l10f^ִ՚=s=E9ɽ<E`y!u-[>=Y99cy޻)L]kk,I֚q<]*sְ溽Z[brkqQ7u{5$u'K^c{ߥT$u'{(=c{{ۢ2"e]͖('-*Zm]^xۣ -I+6Eae͒5{ZE9iq 9CQl 2'ZNCr:KAenZ-I+p [o5A bQNB\'qW,I 6҂!,I ĢqK,I 6҂1,I * 1,I *xâq7,I ֒ ҂,I x¢q',I Z,-X@\r `QNB\䂴` $@\rW,!S('!. ₩XHlE9 q$h@`k,I z%s('!.%pŢ[bQNB\0=AZp$ . rAlE9 q(s^,I FA\$IL{('!.H&(EHH r`QNB\011!.ȁE9 q,䄴 $QB\ r₻e $Y ^,I \bQNB\rXĢ%$.('!.pE9 qK,I \bQNB\rXR\/Cjxy~*qϸ9LXpxzcc=lE9 q-AP\kHyS(7CMaWs*z7,Ik zpo!/ͥyʷX F⸽V_7֜xeǧk3fվwnOax.&zss kq=|ɨ$$ĵ-HMڄkJGsmq9$}[B245}f}tlkz3!nzBk>$ĵ^Ŀ:4:EMW @O&qUոyD֬>-cG1F>c}#q}B\`QNB\y0'phq h cQNB\[b?7-,R@Om cQNB\ BǢE94JE]0Ԛ^cQN:Rx 7SĕXWęցE9 qE . 5:r@\jt>$ 7|,I+qAnXWP('!FcQNB\ rCǢ"E9 qE . 5:r@\jt>$ 7|,I+qA*VRQ%w('WKlU+gQr[qK jty,I+qA*XWT('!PcQNB\ RFǢ"B.E9 qE .H]r@\ 5<$ jty,I+qA*XWT('!PcQNB\ RFǢ"B.E9 qE .?{~i6XWę;lr('!E旊~Oam-('!0n*IwZ6X Z7u\T}`QNB\3=3}soZ K!u [,Ikދ}PS$5E}RH]VkG)r⚁)az#ԔE9 q{Qx޿0T]RfqS=-kQNB\3^(kǁ#E9 q{Qx޿$5EyV{8>[(Řz޿$5K5u\r#עf}oڥBm^rҪ 罿l5q=PSť Ey[긞n G)r⚁[-u\{7#ԔE9 q{Qx:罛jʢf({{7#עf}aTKŘq_n^rAXG*z/l1$u\,T('eW*z؆-V5zXrRq>!E E9 q('!3$ubxƢN X׉A\r:1 B;5 ݷLˑOCRrw'Ģthq}; R R\[Iy>lF5N!A{|z/ڒߊ [n S2}U_Ai-YjX-qyC_4Hx ܨ{WPՌ+|-鞋_7ץx7^G`b.FK^\BkUxѳv.IG{g9~\#qcQN:)ׇ:wܜf<_=#fMС5~!2}+KEmub?xlE-zg#8;kX$BEbGL뭣Kǵz$y=l?ܷs^}_> st.y/KƷq('^\q1*)|8fmEw|)mWw>1s[7:ִAk_s[\MHQ[{o|;_&rҡť`ra޷?֦*Z݇D;< l9EmDPTs\W )ڷ\qЇ^Z>>ю[;uât7TVd t9Ի*aiڄ>F>/Y<@9M~%m[tohPsFO={-;~I蚾E9 qe$\/Z\Üs rʅtRO)k>^,I$ +$U i7բ"B! -J\gSr@\3ըE9 qŀ+*QqM>-I `E9 qK,I \bQNB\rXĢ%$.('!.8*7]=$9rn߸( 6yXv+EޡFcQNB\ &u:rnEQgTMRӰ('R\(' E9 qK,I \bQNB\rXĢ%$.('!.pE9 q.ߞ8>$%:$u ևE9 qM`bs: <#$5=,ڨ}Xcoעfn`L-SLעfnʚH-vpl½ע&ߔ c(Թ>9E9 qM@ݐ5#gQqkdTbQNB\;ab}jczYb\r[i0E9 q;bQ!bQNB\ ,un,I \tTaQlp/q QOŢtE9Q5tO]ât(J\֩_r+qQj4C}.E9nq徉,A?xE9.qfRr5<夻߸\:('EQ5I.E9ɕ( _lLZܽq D { 5Ӣ6fB͵ӢEQLq+. 5E !5Ģ`*{~ϱ%('"-ֺ3[ߋ-q޸ouz^Mg5`QNZM\ϛLd l <ע"^5後,{-I&Dk>Ԩ5[V6 5rjo\S('!.pE9 qK,I \bQNB\rXĢ%$bQNB\aQNB\*AXH R('!.XXF,,cX!,I \bQNB\rXĢ%$.('!.pE9 qK,I \bQNB\r^t`K,I E`M,I F BZ6$ .rAba!/Xr^E9 qK,I \bQNB\p7J K`QNB\XTH r`QNB\ ,('!.ₜXd!X܍{('%BrҨ!dtHz݁`w .-qhTm5IENDB`PythonQwt-0.5.5/doc/images/tests/EventFilterDemo.png0000666000000000000000000003644512605040216021126 0ustar rootrootPNG  IHDRkޜsRGBgAMA a pHYsod;yw~vo|u!xPEQE.EQEQAQEQT"xPEQEQE+EQEQAQEQT</~}CwBQEQT5:x9xzwU\_>{챻{((<f裏 EQEYjJvY2G~OT]7U_7V_7T[U=-?xz}w(1'?? Ccskt]oGyW_Лg.p EQ|<#tH>!xů>>_Y}η<O<~9y(*RR͠+tH o>R}֫_Q}wzw<|CP__="x܅=X}} ՃW=^z}C}ջ>n`}((jB<4p R8|7WmTڗTS࡮uԨV/$h4]܇4_oQEQKLjOlܭ(|V_W_}Pmv?jezh97F#@hCh>([ ˮkr׿‡7}}|ve?w'GG~G#w?xc] (*P<+|x Ca{ s }!o;Oof膏pA fs>7xaq : .eO$w<:( sa!xe;M53x !p7xF<@0ַ+V<@0 C<t{s` m;Y6;6F3HX8#xK<~6;s ڮZ8Xo-#`IrJ#Vh$xg`IT/x<ȮF,i0xNXiM_>!?x_o˺~NnNԭ01<:;2x?.={q#xKox)}R<1'shD9Ww>JOZ<Gk{~<.~s|Ԃ<%M͓|߉oy] g>2N1Bȉ{ߴsZJO<% }  mj${wN}3@ Q;~NRW~8~cqwW0xt c+i? b` WN8G}OLw ]'ZCvYK] OHwYZ)DCc7aq Fz_KZy3kQvEK{vՇAà5}㰢v~14:k𐲚=篴~=KWi 0 CcGV&g8iMIki|V:N NZxC2:ht\t9+ \ZJkfhlka#_tLJ>feqq ? Ht146G:k𐲚=篴~=KWi]YXV7x|V&g8i<wZII5xHYM猞[x7RWָ+1Bi*,00Z=޻ILEFu;| ZQ ]?ieqn֙ll72 [h%ajq~VZ+aqV< e"x}pJ*x\z]b9UZqq+m?sԠ }ʤxfV>C[huV<!aD~h|/d/c#x V>PСK<}1GcO/V;_4=}<~+k𐲚=;ypo9%=VZJ<~/ıj*|Ѻ-x 'ntI YϤK@Iк-xx67xHYM猞LoF >CG,=RZJ+q^H[s~ʤxd5@†Ɓ)ZBqbpF+=u0)ѳM=//DgJWie0cXosFW7xoL:nxpˣ/O/vRгzo{~֍q[BH7xW&a5 3vs C}G@9n ՎK_W۽= (}[C8Nt&]nd> cWmyxEc>Waq RV9gdU)z^ eS!֯Қ<m7Ζ Vd`)t'PgǏZ?<I p  xYcYv~?!)ѳM=//DgJWi\<~鿮L:Ί$x Z.@3ZII5xHYM猞=loyy|^i*{!mq+tgNNWP$l4_!V>.N+_]tu&[E@ICXcX*VCj:g@dM=:=e.7saqVZ^uc0fCc~Ɛ7Yt\T.K]'ZCtpO xYkh.Y>!/k8\ZCtYo)k{ʗ)x1˄0q) 67x4fc__RiYc9jf8~0}c55WF4V2,:NSH =k|r^н>5tq!e53z Bn׽+Sfh8~D+ á0_D<_L:Ίb_˼)Ou 0qtCˤPҼ1?tu+t'x)@|e_<>o2)ѳFG-}reL!!V\)k{H1Cl\rƵLB*1|,sT32)ѳ& #0stK1=5-k>3ZQ ]q+tg7_L:Nz&]BGy km//|,:݇?$ZY)ѳCI7߈-scͽ=7uÇ111J*x|\t8#ƑY7O~"aCYIY]}"x}E}XBJ;ZIŕI5xHYM猞=,xx7BK\Dsskc{>%~f Vdx~q!cIYr}TcuȰ8N<"gXk|o(q 4V:Cj:gAd{ sz!{#D}-ZX(Kh%񑿰28+pbϴS1}[ni<>"șRK.c֌౯ETM~[O7}4ϯL:Cj:gAFRM9{*D}՜5;xnSj8s00+٫as!K!R>S?Znac]mW=ʤ8EȚQy_fv]OwB>Zip쪍kqV<+٪YsoK[SIYus8U+U>!tZ2g_'}z0\_Ol5x.re.!xg?̤V(={;=FuC]|+ >MY)7V)KZӃtQ b<"\bmgR 0E1T3tt5cq8!.|I pXNX)+t()v֌!e53z 깾s] <,e%e.F)KZi}IYSxd~KcHYCIYC<@Gb?O~I Iddܼ |؎/t(JkB5xHYM猞J,x|I5xHYM猞LoFZ=5>J7| Ji*,00LgܞX{q` [I p6Zr&,֞pE+!e53z {z\lˁe#}+_U٤x)EI΄W2ha8ń\C?\J+xħt\tك&\Fs}5#s Bz~VjwzQ3='1"x Zi&'x)&h:Tl]{ܷOZCx}J,^rs{<{>9]'z]R'g.k; +| Z@< uYǐ_ _eq RV9g" >zs{c9TKsTZJ+a5 3.:R'g+k; 8J+xӿoqLۄ:Y" J+!e53zvL&Ye/kGln+Şou[)$l(k)k{JWijq~֌ݦ7p=a5 L;= H@n֊~"n ! 58N18u[]!2!e53zvL&[e/keIPָ+1Bi*KS}IseOk7ޤxzm亹1 +1&tq u&[E@IC>p c tXrt{Ǥjq~VTCFWkBYSA+Τ8ń C[huǮڬv>5?7fg{ܶujqn3*7od/t(:FB'Xz;]oj:r sF!e53zv sxK{~ֈݦ7<֠x& V<1frr,z&]nd/|:p iC>v&!eOk~I5xHYM猞Lr/0oF >CG,=RZJkB8~tc}qR#tg$q65L52x q:htlBLhE<$ht?%&!e53z {Q{Z0BqVxպlH^hqV<౯U]mz[/@jǺz\|I5xHYM猞WZPZϥ<Fa-C>Zip쪍788i-!F:Cj:g\z._!xGaqV<Z!t@!ЏJܭvxI5xHYM猞WZPZϥ<!t⤕VXoqqJ*x\C NZ(⭬EQEQqWVč)"xt3v9bc:QʱRzME)r_Ǯt,cRzk]< >XD).J5cʱ~)"xd^D9^G ^s'zz4Oz#3'zi"C<@0 ooJMIƮڬvWMjn{YdT{co47lvW(;Ǟkjmw7Zw׫uީ >Gv3 '/iHnOb@/~xbD{3vonOc=ɶתr_~xzO͞/yG94֖g8v;7ohIObyMcO;(*yvb^yg9ᵛqh4}@s=xyuLG)N֊G= ywqI{KcO{wF_}G ݴ:/G)XZn"uX1ڕ?<K=WzL1$s܃ے|Nﴎ=ܕZW~wKlŁp47^{=6;m;b# C>ğ՘SpcOkN-תs <<ɩпcϡnpI p>u")ҫ=S=#]$S}^{s򢪪 QEQt((E QEQA(DQE)B(REQ /r?ǗSEQA$ߊ7}o~^0L>)( >,A\NQEW^t4oۗGk~ɏ~|]~s{+?X>EQ!(#O)0 y2H~w~?-7~λ;8t(RY 5@$|6H?ӗ翲O@?h+:HE `ZC _ɟ?[ܯ'/||~$|68Pe|^[^[m"|gyזaVm|7m-q;Ag^ۧ(3 p"}mė?뿺_X~+6H~q!;X>/w08Ipw ﵷg>|OQi#Ag4]$g_?Zw2_m]{(=0fO68!>(r>Hp4A󗖟.7/wn$ַv㏗}?.s;ol_7ំ Qop&A>Yo WA?\.?$} z7Bcs>sEwf$Qc}ˏ>zz m}|,w#pGG#y7_x] =~#1qY:HE9C>&7y| ~ /-s_~(y|۝Wa پ_+(#B$u KWQ(ʈt$( QEQA(DQE)B(REQ$(J:HEQ"A﫪Uj QUUUU-R$EDtۯ_7QUU1ɻo^?oN$!ort7S `MOa]Q>-on5^K}ݦ(u{}y=^ixѺ:4H7[:HjhK} '.㽷׷uo/0Јu$ciKH>(j:HPÃ&wY oQ7~zOt??5VuZkoQ=׷-{xyrjfȷj|sHWW:}0e8=>:K OpO Xkcr56z=-c PXzٍWHM's>X.5 rR )>s}~_݃&fo$uz>b}>{z1IpZ8N&4zS?KAw ?ؤ4KVہtAaM'P-^:;1>yF/IGHS!_·G 9L?zx>+P㇀{ћ''-ԛ52JYlaȾ7"w}fJHb}eӃ^?g۾3Tjm!L?c${нP !&vlҸ"y9S5?ߙ9$GRzs? #h[7qO{~1H܆uѯ&tO~C 4X`ho@P\7?&)5 G7ߵ_^SCxͣArG 7~?Z )W`5Jxy#pF6>s)ᓵ>UyZJ ^}*(ុ16{1~]/gA<38Ng0䦀7ɹ b[ćҪ7Vh}wC,9HJ)/1ǡ[R<77yK-ѶÁ9gv6 ` qv=bӁ [Hno;!%dwޗ"žvnүkѽlRq1\FɜP>lקkv gU\Ǹ7u$y&gUIPP{Đ6H.jd$hdž 6a]۪qy~oN7 Л՟sQi8nsLM+l;{gj6o3tAZ_Q$w@?ݪΧ:HTUQR_稪T;wZKLuUUUU"AZ:HTUUUUTj+_*K$Y )Q)$ϒV H{Ž+L92H)!9[HiM@=+XAs">Hܦq 7>2'v%!u[NAb#DgHK*ViFiJr{ 񹏕83Jt`z~fBW楤^8k=cNA to3% >_rhb:[AlP(:RIރ|ܥ ڌ񸱝-ޚ̔'ALMto;uLzU :H2x:z_=$(P(e4Q^I(q*HV.Խ珆/_\g63Hd`cPjuԐZȽū\$DbȅZ{\cc$*aA2BWQqjY;"/ZL; -^ƺSq֎Ut$6zeL|x# PD/x%Y]s\ϵ3 !Ck+fO ]?9b5C=#~s|٣ 0s[Eg\gW i=4#0+a31co+fMAD3cUp[oܗ'*󣃄Aiū\Ԯ3I3ƪ)$-(g>%{mf8+u^|2c-)7"$UpjwO}\Wd&W]JUt$@^M/+Eo3C]iK&o*mLynbUnUdl{i"Xgw G+o/yyz8ayz1{~ h+PxsF$=mg\g@Ϗm! ˡNg"k*bmLynPxyZu:Hj~*qҟ!M$ ܉@hz7=:|u}n58R~f]ۊ\ۊĮ1u=RzS#'g=05 r7_],qP]ۊ\cz5>Gk|L]S#-w$95XG6o8ލR7oi%hīABbGܑ8nz (uj"2/Y ߣ$lrG @/٬n*gS $Ϫ{d  𬑓V#^0%S!JOj:rJ$=b9B=ǫęnn6MʮY\z=IVP0z=)zGbU`lMA]GcҾFI{U UIrO*i$g{RQgJ~Q|O=)A ƙRSJTjt>XكDQ_ƪIbUhL7HZ6$mƙz< ըi3%Y7b=)zGbUUxԌU$)V59:>ᙣŪИnlޱJYHVMk C;{`\d\FUiO^X5-9wj <[ԃ R;g+uժf-s^wh*c$XŦ+oć: ?<;Dk{g" J{ nܵgg\d ZP·$R+ޚg+j5BBbXj@ے ޱ*cˊij-ɅzFXf@H:ghmof>H^J]JqA?8rr֟r1 YEعbU$V^}"IVqִaA>K BUXz +J |g׎haCgA԰2w&B:̭{hZlę̔!ɦgQ69BujQCzKm\[cUaA V SM/ҊUƪtj8VvF(wjv@Zckz2S$=,7heGჵG5IZ ZoC8<ȃ \C4*5!VFN-ju$TIMV I;- }@E*ViZڪ5Zh{a .M]t#@Bmޙ) ӞFm[)v.RqN|ކEh@kSboK-zǪb`cl;6Ԓ<,O}'jh:"P_<ڮk;VP٪`m DX5hi5:N| Ɨ;<o(㊶(/9>uf wql BٞE$mE7Y\ۊ赁z_k?o8K5E:sA{9{Lw$1z4&w=Tj$Y;+z%eWJ 6HDH#N% V'%{J&M㽜_=T6HRll.jH)TsFI { $]*?M=o:H@Rh$Mj)zĩqk$Y7N/Gi0RJ82  he3qHr8F%FRu>к_8hRFA BgfsqX@J,3NnKkg:) eZw]\{91͖q*|Ht*{%7&T @8l&űg"2RB8HJqh]rcb땓O!eu(A†1LO7Hj ec"FV}LeݵK)>ps -i#h&I9qq*صɭ >/x cT .9MFc#7vIB>ع˩9>p:ȉv3SD$ic"N%69EkǨQmH>jC jƨc׆[+)j?1*rlIl?#~>ع֊ ζ0Y`b|30SU45\ԌQ)Ǯ V^Rgjy>ԊQCIa+>E\jE9 64MʼnB-?ljfqj.?HGGgI j6Sճ9RDP‰Ɇ Pd$ 5@?ֹ%Em_yy|xZ^.nxyZ=_-sǩ|zR} y HZ1Lq' jSPj邹IŸ); 9<=$XsZZPsA R ULg:8+/>Xj 5A_(pq! |Qʹ$X4k!H3OT|^ß@. F_oyޮqk(jE|]lj@5YOz}mKdȽ\cz5"vGR?js?A#7ʋօR#J)|IK$J,*iFk8HEd,>)< % %y_=@]QZ %F.L"yifh}*M99@@BK9b!WE/*RpBgAH4r7% %TSU#sr宧_ @$s⣦ob>XPME;T/RPϋ密_30SAb/_$(!޾)~.ژ[7*gbgĥ_ qBB$(s7WSI#(X|0䷳_JAJK`2qEz<3X_M%;S7*Cp }t(mr9o-X6FzR7%< /ɤ|S}M {C~)B?y`Az6U^iL6 YXqTfwA4VΞڸG.nF}OΞژZGJr`AAF"rs =8[Sj\]#}T L))#=-ps\cPQpL]ϥl;ǵ  2'5-lk np׷19i 8k{`zt_&vFgmkL3 X=s]ב-JCT^ L> _#,ڈl,Lyyys>cEf`yU$eϣ0SߑϞćyL~2=^W d&B=ymTO=$a_sU`BFiyc.qc:vZޛ hU?ы z1TBVavw \~LcΙ%kjkJf 0r)f65rڿlg}wGs,/;{.D5 %S{]u0im: v㐷_~D*xS}ľ >>R?w˾w Iirg "Eܡ.{< |d>GB:oX Y}"<z7[u  EC}^-g{$C=тXx^:H "A>^ l>tz\dRz=wg$"f0 >tz\dRz=wg$B)fv_6(kFd:#"HBkb{zwArO`PFmu=|orwt(#C꺞vA20X,[Fm{L0>aFkbiTC]7j; =ْ J 5sgvבykbɈ~r[K .ޑ ]K @? Ye#cflJ}zJ K̾og:H.d4|%nQGzj K̾og =S7 nG{1aJ}ׇϠrIӊ+#:~yo.aI:.o6 SbF܂ E&_7\.}"gOm0 >#: h#akq <= rͭS79ӧ߰n HCI#_mgÎYesz~n.q49{jztɭ3H.}w/?d猯57ϰ4Wぜ >߆$/Oӷ=,E3%5sO~.]ྜ5|| Ѧ_\k7>)\H _^}MvGb>{ۄGd4ǜ:DJЮ_u_7Q#6$ͽ"}hly5v~b >ͩvSH\>l}kl x $_N_gvqB5Q JJz b6#Ҳ'[qHd}5XۚH9\Ry 5 Ě B2w1HE%3קZys㒪o-R~Q:K*o!dRM*~ R>q< $SK4}ɳ@P||A22op@'鳞Go=?y%?^%{Z[:KBy)R3 iQH56H ܡ? _׷ $S?%Nui'k_CqPkCk3ƒqb={ rk[*׶Dw$*˺jP=tGbnn' F!L}g܀d̈́ R 7I)E:^hXP^.E07 )bL7Hj&uTMKIй?d#+m%) Bf|uux6&+c$P T?G89R#f NpE8yd8 i.?8๔M`2B̟_J[`AjCs/ ZINCC"w?:BGNͼ\P3EL6w$i(li8~pu67臫Xp벮E=Rlf.b~ n3qCMPQ+f%6{c@n,p ).5ws@G͸Lj#Rp}g8!ޑLBN3Vk88?'>?jƭ)>p>A$46 9>plȍP "V)LG3ğaAR`#LvӔ.6ة׎[^1r"FL1v)dr)iBA !dvJ9}R گ6 f.):H&!q@JܘѾOȭͺ+?\ZKtEFyJsW h7_;R;oz)!>\ ;%$`!sɴrsQk~Юv?JB5b:& r(B= iU)i& KmrcClxm§EJޅCF[\{&*#PD4KnSIئc %fU ~q rBd"r -M%ǖ07bW  0(v$- 7Fr*QmhIE9"oq\{xBPD`A9ô 6plrb:~WbpH.=]$id )lr@JlB+)@R͖}gbwAҲp#i&wJlBIBfBa$ln2C^ |MD]# j3Qf!i &"mmUEFIɞh3ex/ M$A)*r$ Ŧ\0WxP HI{Rvo qN7Hzo$ۗj{%D<~<<\^py^/}XKWb6(QZ-V6 !mχݔ\qdP Kć=*hgxk Ѐ52C]D j:H&ZX| .5Q ><= wbgGjأ7j  ja}^)!__u8~ jg=iXrYOť=!'$8d2r0-hĦ-*ha%K|-"ҟTrգ'|?iq z15>Gb׶ u䵭s`q0]OQD #]ßږ'iok[.둜k[Ľ e4$v}cK׶l} _gy.%WXϏ#d}I*^9VyKկ;qE[6!} X$JFƦܤ G꺨Wه\"ҟTJYOdl+ڷB:H&Z\Lk5@DY@^ŦET=IկM*JQjsAҳ) g֢v@0p9G^S´֯=Ӆ !h8dB8ekvR 5I;JT %:h&8uL% b =Nˎ2&-4B?ccAR;y3D=hc;$1jpVZRRJM|JAH& \vdZ9NbJ9-{ŖV6[JAɤn$jj PlA)uWƵ: ɛFwχFk''N.}8+-Fc) SԎϧ;IiH63w>:Vj XDlvuLJFfF$vnXmjWd:֪U]"IFNq__٥,9r٪iJK)zw$R` ;V}EBKrID|hv[(ARԌN_˾" P^WH {-JbE$7XhN>kg+.Գk?;FgܳsI)V#Z{. k ۊ<={E.#Ūdbj5RsKhP9tdB[lZ-1 F'O5{߹{r!zGr"Jh6VQSRkD:HND&Exq|/g(.t43jk=#ěG#oܚs"ׇɉ6whp}/u)mdzīdpBz7wnKd8x$XS'xgS/F֝ gmmr}/ޑMj .7}Pk_#1Wɠ6uPc(u<;dZ; 9CkjК8RpU7J qH>BC*^#9!jhYru`cuL>xck 9!-hZ[WThwArB>63O(&xK+#-[SKxĚDFvg=}eY{-$'UBxK+cVN7HX`=wBI+q=2Zް\|F>\)zGrbZ4HHk=Cgq(^ItlWϊ/VP QY *SFUa(( }((J QUUUUU//}ނFeIENDB`PythonQwt-0.5.5/doc/images/tests/HistogramDemo.png0000666000000000000000000003666612605040216020641 0ustar rootrootPNG  IHDR0$BqWsRGBgAMA a pHYsod=KIDATx^]|[vύGO1_ Ћ!$&1ΫQib" EA4?Aūt:zNmGUO=kZk}J$IrnT*JQST*uhe#LRԡ0JRV6T*JZST*uhe#LRԡ5ȏd?FIRTj[o~/~ {yi_4J*JRFO84¯|+%JRK>$/裷טg7o}[{~7NO~?+{wog?7ϝ>ԇ|QRT9bPݟmcE5~[:_~7O?neFJRף f7¯|F~'?9}/\7q?lT*jٍOӟ|O_?}Fh>ӇS|xh_<}O>娳>S~x=/ޞn1ug^zRTPd Wo_~Ϟ>:}gFG7Bc6?^unZzu':>&V^1dާ>z}5 ƷyݼT*49lIslsM6B~ӻ]n-FMM6[T*J5~'+Z O'֨L*JR|n}4/4s+3JR)VnT*JlT*:RTFJRC+a*JlT*:RTFJRC/'I$F$IlI$ɡF$IlI$ɡF$/ON$IlIRo/{sq;0?Iz&IA6$9`[dy&IAK#| ]yz>Woy@>c(qm.K~7 ubpzގg,M6$)oi D&2N5!eS­Mӓ_7D$IG&I5Inύ- ~ߠqJ3ּnIsW CWIҁlIR@5Z\7{F]Wqx29~N#yhXC0YlIR@oYvx#oo=nN6‘5 5I0I >)kM|mF83$a}PamW! 67sJ_z&C6$)78ޱ㹇 ƺ+naiب&N6$)oϿr rW rpzd#LlIR@oES@4mv>M_8M4BM3|6l㐍0I߰d#L=s*j:7d#L=Vf$9d 6$%a$Irh&I$&a$Irh&I$&a$Irh~M$IlI$!1=d#,%]TDh"sDK[Gc&K{\r^Y%=FE#LRT*lT*:v| jDyGj{dzW!!_r|Q/E9ǨF(_@y8"LRT*Fxβ^yf/F8yKCudiK=V{#KCRW+>xX^#W{}FLlܰf50:ܦVkFR}#<'^1c]c/el&=ϚExKf9^so?k᜹Ǜ"5s7ExΙ{HQsHx_pzy51Ռg"5|65cjǛZƨUeMa*u<]?7_5.&;Y`# a*~0{ n.Gh޺[Fhx6ѹPh2yL9%-Y#1R=.9RQa˹Ut^Fx'& ^+^Cb"Ljk5qE.C E9(iܣڸw#l1jSry>tE(E4T1E或d=DPHMG#=G#aZuJ=a*:j5GT6T*ie#W6v.ygKȼ%!z:Fj%=Y>Z#l9׵+r|ZQ^?Z=Zz.Ňz)КxF(_"LÈM~D]k}Xeu@{!Kq v0ђ󖬇IM5?"5y=B:#(5s]RhڇўD{]S)è(qskSP_ n/e,klog"f9^so?k᜹Ǜ"5sa4cԣxM=ԶPFkbϚE8_sZx1*S)zYڕ0"(?R1^v师.#Zrޒ=B#5ɵQ&G(Q{ŜrkW*S)a'Qj:k}#< |!|yFe_[w y0UQ?Qj:k4Zzy~zmDC_{ixO/sCP7Yô.#sM5c+GsEjd.Qd\јHMg"RFmu/Dz\+=Sx}hpx:h+Dea*50:(qs OoDw"|sS& hiS0'Jmw\{k+wηH[.W*P.k#Zrޒ=B#5ɵQ&G(Q{ŜrkWϷB?)#N3~l)"LmU[0ړ(qs\mb+)j=C E9hqo>ԶPoqjǨ֌(uĀWi^q eL9%-Y#1R\?#JmgԱA1筹|/+^o7_ă)myèۣEQs]T;F⊰mo,ט"jq]' ӒEHM}!#%ϛ͡vF{sޚ{S˹Ut^]4Zͷdk }kjvu̽׮Fx[(.OX6y/@oR:"R1^W弗7F(E4vY~-y_Sꛨ⍳E#2jfs(Q{ŜrkWia{oS1Mu3hM=Q[؛&?[;vjlx5x{,Ɍ(axmQ7uD\S;cv5»3M-cTAʆoyEME6ZӣwIQj:kWpME6ZӣwIQj:kצJ>师.E7X1Eԃ&R"F=RS5{6RuGPyk=O-Vyp@KME6ZӣwIQj:kC6B-טx0SD^j\DsD+1>zSju48Ec.{ar)275쑚گ{>9Ψc=b[syj9׵+FCKME6ZӣwIQj:kצ4|=!_7hM%E{]""fME G"{kA-m3{{ZuJ=d#|cKhzE=G[cJm?[޻vaxmQ7hM%E{]jJF(E4ہ5ȼzPD^j\r(Gjjz{ԒPj;9oͽ\*:T^h7ڢ`ԸFkz.)Jmw\{F8o"X/5.њmKR1^尌F)2f"g"3uM=3c̳9Ψc=b[syj9׵+umP>E#f`w{8ǫ\6oc5+umw7v)3y5\Z7_8^#k1yc: zˬW\\Ws[U5y3]+?E챬-2׭?>{/mx#"+7#ecStGT*3Z[xfPm喦]!7c0Úi6B9."~i|<r(GG5;gǁ55AEJmgԱA1Q1fZuJ]5< ovpi?5I!ě 10|y@-[qCuvCaR׬4&4fF|x`sj1~VyP)ZlmLdž@1" mv%y_""* ?×GQdRkw*ykj̳9Ψc=bΣcF;YM5Bi|%5B1u#ɧĄc0|yhkQT%1kls 1xQǁ؋/b1W>oE9^syJ}<9s7Q"Nf-z)15VGˣW\Ds\<@[S"vCaR׬T6=~ /^q^x{T;o6H+jSj1ژR cT^k5MOϧ7cn=#ӏ+.9㔆4zwLs}Vy[S5oac\fQ>oj9R5Yu,mǿ>+L?×GL^/CU{E=\{>B\DǎkV*֥p[_7g#|`eZy׳5Q㚕 4{^^6;zw.aGˣW\Ds)v1Yq oVT6!<XJ>oj-=o>3j)X^^V^x5Byt>SbF c1x{T=o:V^B\DǎkVJs{rkԚ]~ /^qTsۥz4gǁ[Qڨ|d`=1-}KϛGnjym: \^e\(ՠˣW\DsGz*kECx>B\DǎkV*#+L?×GL^/CUʋ|R׬Ԧ*n/eLz)1a#1 _"#<ʽd=;{c1*y_õQ2>_?o"bg"S5cjǛZƨ",ѓO<2 _"#<2{ Uy\+/z&J}xdzV^D=#lMxfɧĄc0|yhp|8(zwLs}Vy[SS6!<XJ>oj9R5Yu,DO>%&lp<ˣW\DsGz*kECx>B\DǎkVjSP_ n 1u#ɧĄc0|yhp|(*v{-l\qzG4)ƔxhZmy+=#ӏ+.9㔆4zwLs}Vy[SS6!<XJ>oj9R5Yu,DO>%&lp<ˣW\DsGz*kECx>B\DǎkV*a|JLHxL?×GL^/CUʋ|R׬Ԧ4|=aˣW\Ds)v1Yq oVT6!<XJ>oj-=o>3j)X!q2n=~ /^qE^2Վ{ś<Q2E"ژRY-QKQ7|JL`eZy׳5Q㚕FX'6R8ӏ+.9㔆~{{c@ޚµQzPyk`F v_;A9O׳61xdzV^D=#lMxfno4oF2n=~ /^qE^2c1xQǁ؋/ڨ|Dc\7Sk1x)К1M-c\7sc2u#ɧXM~ /^q^<Qq=[>;YFw' ] ^ۚBKSF ӏ+.9#SPǵ"!<!gkc5+o!2x5K83F ӏ+.9㔆nx1Yq oMMLڨ|d`=(U"B\DǎkVƹ}$+L?×GL^/CUʋ|R׬T[#?ysg#lc0|yhp|8%]3G;><䭩)\ %7CĜGnjym:jkF ӏ+.9#SPǵ"!<!gkc5+ 1u#ɧĄc0|yhp|(*v{-l\^E86ehcJ}xdzykG4EǎkVjSP_ я+.9#SPG^Xm>(J}$7[Ryx̨ږc",ѓO )`l~ykG4EǎkV*a|JLHxL?×Gg{l3mUG^Xm>(J}c)ouz(xTo~am|Sd#|`l~CS# Mc6XcϛZͭ})o^:a=x̨HMu(pEX Ix&lwF c1c)ooj5yUP1f"5a1Fx5rP1l~ /^qg ۪*76|DcmYUP5c)4AaNˣW\Ds=# Mc6X[V:a=xhk^lH4d#|`l~C{{# Mc6XcϛZ1oUCx[}{ /M| [#M+F?×Gg{l3mUG^Xm>uz( 5F(_F`l~C[3XcϛZ֨wL+n.H6W~ /^qg ۪*76|DcmYUP9`k*a Ic1c)oLn}^?oE,Z'R-9.cT^0Ojc0|yhp|V?SVUyi#k˪Z'R-XSjJl~ /^qg ۪*76|DcmYUP9`k*K'Nr8ӏ+.9[L~4tTG^Xm>$7[R޼uz( YLj0Ojc0|yhp|V?SVUyi#k˪Z'R-XSSOIc1c)4~M5*76|Dcj&Zb\NX>[|!T;FmJ+я+.9[L~4.Uצ1Vh1M־7jJ}<4G`1",E=Q̓<2 _"#UU!oxmcڲ Cst/:T6ɇc0|yhp|V?SVUyi#k˪Z'R-XSK'Nr8ӏ+.9[L~FgƔF 4G[# 5B\'y5(`l~7Ռ˭u^ <_+F?×Gg{l3mUG^Xm>uz( 5umY)Oϧ{K#F ӏ+.9[L~4[n*76|DcI>ojw._멚W_spR3 P?XM~ /^qg ۪*76|Dc-9s4GcMU79W+ -M1+L?×Gg{l37BG/wcZBU~"j 5U6iEs;7ͩ+!^˘z'Nr8ӏ+.9[Ly-^^1"y2!"Ռ/W-9vSƨFx!.s]5aˣW\Ds=7qH|Dc-*ky5hn2G/VC#MPoSd#|`l~`x\+/:XK/Zd^ [|1p ^nxa:5d#|`l~`x\+/:XcϛZyڪEՠ91k#AD^4ܚiގ7AAbl~ /^qg ߈ǵ"Eՠ91⊰fl0 _"# kEZ#k U_̫Ast/,cLmPVh++5Byt>YD扁c0|yhp|V?SX}V^u>LgՌ_FU/,\+8_F1я+.9[LV^u>P_ȼ4G‚9Ɣyb1 _"# kEZ#k U_̫Ast/,cLK'N@8ӏ+.9[LV^u>4tlLW,cLK'N@8ӏ+.9[LV^u>P_ȼ4G‚9ƴF(_FX/sEd8xL?×Gg{l3okEZ#T~*KFUAst/,C W%' `l~`x\+/:XcϛZyU~"j S6扁c0|yhp|V?S0F<ih%T|-2-`1e#,a8xL?×Gg{l3oZyZBU~"j Ӧ4|=aˣW\Ds=7qH|DcI>ojFY0ǘ7_?2O<1p~ /^qg ߴ#ʋG4 11U~՗̫̍W_X0!a)j'N@8ӏ+.9[LV^u>P_ȼ4G‚9Ɣyb1 _"# kEZ#kLyk@st/,cLK'N@8ӏ+.9[LV^u>ӃZ/︒4G‚9of?v>˜{l~ /^qg ߈ǵ"u6/︒4G‚9Dn/}^4j?PS<1p~ /^qg ߴ#ʋG4{D:ͭsw+L?×Gg{l3oZyjZQ_y5hnsC# _a1c)~#ʋG4VzΫAst/,cLF(Bߟe#lc0|yhp|V?S0F<ihLW_X0ǘV"F7я+.9[LV^u>3EoWt^ [|acyFXb='N@8ӏ+.9[L"E;::ose"(n#$hI^61c)~#ʋG4VzΫAst/,cLF(ȿ ڭMՠя+.9[LV^u>3EoWt^ [|acя+.9[LV^u>3EoWt^ [|acW#2 Ƥx$1(zŽ1>#3o`ZyhP3E ~Wt^ C6Bj:߂Q@b̳xH+{#ʋkK? yeZA@0JRvvL9%-Y#1R=.9/[u,YђJa*JlT*:RT*C5T*JzF$I$l&I$fP7I$I WmSDe{2=Y랭Zl^Oz溱d.!q#sݳ\^QI\7%d=nd{Kk=JǍ=Y{@F➬=Y{Rl q#kqO㞬=GG6'qO㞣c0I$I撍0I$94$IC0I$94$IC0I$94i/ϧ4;f?]x:=k/'|»8oLh{:\y#y{C+^=YFx9A"o˛vYj/O6`l+g.5xz~?޿l?w 㠟%h8_AwȚsx8W@ca{e 9^Q# Zn[㌬{R[1t#o-Œt:A`[ʛ߳2}ᮐ{\oC9~CV| -7!qlWos !k+>S,G#{"4v]ٸ- lk>^:>Wc~ xGܝЗ-fƺGԚW.~csca{e,Q#L$Iz&I$$I|EP,IENDB`PythonQwt-0.5.5/doc/images/symbol_path_example.png0000666000000000000000000001546212605040216020760 0ustar rootrootPNG  IHDRY-;u sRGBgAMA a pHYsodIDATx^]:{&4t;Ig i|AfX  B%Wsus*Y~`:`ۿ|?hZ{U~wW???(K x}_\9h Y#ZDjm(! `DQB,xf;%dUjD Y"u!J`N?,CԬBV=(,*ݲNQBH hH/!*=4z 'J[U>|-)JE"@:yt*ak0 YLA= ?}2_QVEPUkE Y"uXeE imka՞^:Ú(!k"8SP%H/f2CVj)o鋚t*B+0XsyG75su+5`)i:T9OQ՚?LŔ&tE Y3w-L O]0QB Qs Z/JȚVظCV)ϒLKkZS6yɢ&j}S>|Z,f%Uz8ZE îۜn`n'o/nt'}~v۶![>_CJ]nUmu-ihR1gQ!G0f~N??j JD:"hc!븊vլqe+WC5"TաkPu~^~CVn}bO΀'^ iپK۔ECjT]:6i=aaZZNA`F\AJ}dK>1`j5wT7OV@R7 Y<hISF}Oj տ.??NlΎ)SDdOjcBVUumڧUF:oBY^u%d YQ@RvR&kV!+tX|N J\ QyC Tn SԤCVm!+u8̳?9'%d(& >-JQ5V_^z?jsE Y, wF5鐕CU>ǐlype%U01.QB70.g8E YIwF%dqW{SԤCVUr,Qe% `DQυîۜVݾѡm.WmBV<|:}=kD)(f~UCd_,>kx:bmݡ|>%dZ!oEԓ!Uu͚rJjǘO#MCkz[Xb,ih5_suEFV9$dtۥ1K5Uߠ)5ytPE YPHCmPE Y(n(!үl>uﳐ5aS Y, ߈tȊ[* YDY4Nkyߣ,hpשrX(! x]k4Y:پQB\-U(j!P',W_uy DZ67ۗ JȂ<E:^X =^.Jm"Gn[\__nwmn)cBmu;M@) f~0ݟvbu~^lz+D Z뵤{nT;dWkYN;dA˒&!%PU+Z+Yqd \'dSYQY!Ɔ1]y2 )eJT̡`e5V4$Wԕ5gL]!+\ӫCU CӜS!3ۭd1iX6XIL]X(0q%dHdȢ,E>dY}(45鐕CUn%ICzD`DQ YV~<ʻmEYɂpX(! `DQ YV~<'eEYɂqXIjv! UdÛ\'aE^`},E&dYQ(S1e% `=&rۅ,`jip?s)S4quutmos'+Y̙:D8df~w! ׆*֦ϯf=|ʔMq|F!d]a,[kCVxbjm%jR+Y9R Y1imÔLmF-;YuȪ)HòE6d}_Fw9BԋCVAUJBVPOilTMiF!dH,Cy |Bv-j5!+P [QYu C3!++_/+X4-6a0~VBy|Jv)j*SCV<\Jd8j!kH/LEv'j!n/SԢCV^n.MvOI20P*ZSeyZJ-BJQ Y\ʒ}b|G Y01nx%d(JX1+yԤCVUrRl`d(JX!;|F0?QB,zoY!e5&ͥVnXkiV`zCamN۷9wvvvr[o/_>|.dtqͤϡ?ܒNA@) f~0ݟvbu~Y,]-B8>'XŊլBd},RA+!Uu躢U ~ꛉub{hdix4a^yMDCyRQY!Ɔ1Zꛈŵ^uxW/ovƶ3EKŸOLD{Vv=dRH,.Tz X㠔MUFT;dׅ~yx5]WP,.Tz1|V@Oe0)Tu9g5V_|.dtku u%d YQT,Uq aH"vl$WߩY,ݵ05gE=ިn(BKWZೢ&jB\Eh`P#JQ0V69c];QB,+4D Y#tʡVn.igk(J,,gE Y# &7T QB&rۅ,*^[QV`܀+JQ0cV*ᵞƢ,X7bF%d̔JxGIjv! .f;㉲0BamNKn6+Qv|l\(MvY}p:Ïj_`nx:bmݡ|>%dd*BU.7K.Mv=ns G_ǃ+Wn;.7V/dK^UˁzwjB<4ܺY7uawl?{ Bύ`\QO_'U>mv~z2d xY5JS_k3jܐ?:,>F0qC3ۅ,lEM:dՄ,`mҴl>\*3J\HYKuȊ%de(DY45ہiU gu. QB,00OQBt)6y\J}y>$Mi+#j!#dss[z8Ze% xJ\WT ߢ-5nsȷݾO^L'}~v۶![>y1=ZQ!G0f~N??jD#dsf{!SCl%Xnw(_Y*Wb_! .]vx">:tD] YECנr~E*MWZ̍*-'wXKg4aB0w1:WrRu>FȂHg}M9\jL[h:RrPCUu 0mQUaׅ~lj??yc G>-MK{ա*90Q!+Sr_~ibQFȂIi3L u%d YQO6B i*j?V`~Y#w`~YoK.d4K>gKQY5! (! `+QB2B FQB@A0~+JQԤCVUr.fTL%J,`^%do#k%d!%d(JݫIVwtʡVn,N*%d!K%d S%d(J>7+PV)tʡVnJS5U`9'`),`0`N,E Yd+`,`2I\CɨCQLFp9(! T ZY8)Ɉ -`fRJ)XYJ)RSY,^`!,#ZՄV5ϞwYӧ3}SOztOw~Bր%tO=}SOOg?!kp}:ӧ>Ogӧ޳"ÙsszCy8s.zC dp\sΜ{<Y+<9=<3<=B֊8gEy9=̹9g*B Y/ dB Y/ܐuugo}kEwۯM;O/?tMݗɵ֖?W}ie }Vכ]wh9":}{9mDXh σepr6kA"<Mq>s5wWZ[sk3aFy`MsDS9/}!s11|_|gXf:/u5k q_s1.b9zYxkGx9xj-5{%]8d-od?fҟ>/{k#~ۖ~?bsX}>&Bb!JX홍vD|suG{Z|9"${vsp߽J=V7}{Z[J߿u ;/M cp{9"{/3{}/<[2DS'sd^c04б޾֖o7Ҵ>xs{圫i}"xs's2Cŧ7<.9i __3_:^krѿ=r'ǟ7&{4u/iѿ=s~osDw}_}.W,4de进7OMK 7鷗),h?u߿OsXyiZCMcsķ_]WD9b Y+v\Z5cf0&F&d-QJ˷5cfhoioqIENDB`PythonQwt-0.5.5/doc/images/QwtPlot_example.png0000666000000000000000000002641112605040216020045 0ustar rootrootPNG  IHDRZL<sRGBgAMA a pHYsod,IDATx^],y?e~APP– 5'p l) $Ip CF&FZ"Bg&- |2P}VwU[4:ho~_V?_~WV[7~Be* VBϫ+/W//AO?wOMZ/!ݷWoU̫:1}z[w{zu֫Wjܟw܏򺷟:BJ=h9YRگVz[_~❟~Z?r^US.X5ֻ-$wc׽aǷBRҔzRWA}$u7I7UG/ZN3P5 xG#V;B!U59hɇT=??}u~P?mO>>|M}G{~wׯ_ݾ~ E/rwr}j$0e`EB!ʅ)&WPrB=9h}R>_N_Z%o'x>B!-MZ}Y}gj..d#AоB!A+KB!sB!W-BH"h!BEA !B(Z!BDB!$B!P$B!"޼y!hD @$Z hL.T==Mp.̄0 ^s>eB hJƃط\Cu_s9>~lg) r{]wm]scϱxL}TjA q>ޟ zBN_k^CO L Zck6oL D+Zp ,vv8V =VgsSunWxjim/U3}+]_bo<=Y{o-dzz S̀gLu&n837hjZA{ 1!hJfI~w߷x k>:tڷZ6N"AA -H"AA -H"!yp"hƔM&@`L-Z1 :OOOt7*uBTfit y4wZca"hANN::!}ՓR/RP-)չ&gꉔ 78P/eߒ5mc ZZi1[ޥ\ rq,WGL-gNV˚n=|Z6*ǧݘZ'{V']Z 1;| q[3-gO1 N'Jr)ͧ9{DЂՄh4şGTu\야a-XL#U8S|@8R{b"hbb!JY+M|z%7Z-NR'{6!TޘZ-NI!fEx4M#xՔ|r in'hm?y/:!|[dljf`{qa(|MM^5`J'ֵpa qzKo/Ϩ{DЂYa%R'WGit&BP.Jǧ2=SA &rqa(|: /t>5%\} Z~kJ=?i(.-D fZrQi*OSA FQ:RU*O#xuTOa"h() 4WEext=& P.*Ot[TOc"h J'IP?>])NЊIV >]>0Tt]8Ǘ}05  SSuyدkKhV -TO2.C|Z.>ţ0 šz׊O':M-|rqa(||0ZpC?JU~>݆4%\#} Za2}AשÌi(VS-Q9a 0 >|ՊOc"hAM'J>< ՋOc"hAM.'|QPےr&dsappqȗfOEgC沎=1%\} Z{*Si-n>Rjŧ0qG :TP5y( DЂN&.GN'uZr"ͩ35>|:|)i"hP3~}IVSAK{ҧ9u*NFTjZq>(hn*kç|:>Mj5- Zt Kct8U޾+J,3絕>|:>Mj5- ZݬCu4"hJԩ4{|: >M|OueVFWǏׄ.e(fi*kħ0C|O3T}J-Qaֹ:6&ܟ ެ5>M >MTj5mpGDž_7̱%*}YSg k|x|jZ~Gˇ*}YSi74-#%xԁOcZF~Kr!xm~ Z)QY+>}٩)ŧ ޙr jk;.o Ak>*= QgIk |&x >Mj5Z=|-9bSډ^y("D7'TO˥Ĺ9:MVdԋP)ͧjǫWT׼A ^u;%Lix >Mk5JE'HVFlJuD*u+|Z.%Ke{i"hRBZ(uVjsǧQgk)NКC׫~uSiسVS1wJM'FWBev=,}S0 Ai1jU}^ŧe_Ӄ=4 AijÌ*Y'=D*uǪW%z\飲n|zeZM- 9A῾V6J/J\ØP1FHV6J/J\Ø ZsBikl4:|:UJ]+HnQJOJ򩃹ܡØZ-ji\QII>u06SAˡb(sZG0GTzO٣NSA5ǧ5:Si4=U'xԁOTs>=s>5xqNvN+sqm>&h -T} >U[VӴ`ݨe:MAn|3.ϷǗӡw"hƭkU >M | >M-k/V}7qg~M|: >M|:̖uo^?5\@#q0 7iMr(@sУaУQZ^GLAq Pv8[xq^GL#AI׊$V7~i?ִgi_4TeeAj|v#JkڻV^HNzJkڳViYAkC SPYO uҫӡWkŧmLgN)`(>Nnu(iZiAi }ktfOֲw*ڪNAi >=JkBx4ZRUgt{RmZR1%\3|C-\UT֑Rl{|(%Zw^P:Q=>|:<=t>[i"htR ɗA߶~Gi9~/1^P2*HNz9n;2fVAAP?:vлQZMJBwACﶃ^/Gi1k5%\| ZwTON y9nǧY;Z хJIT{ҿmP=:A9|0zTzOr0>xjUfC뤇롇QZM-%s4Q;:fOOÀOOVSA5uE뤏qQOoAr qyz:V}&7uE뤏PCͫ*ѰĨ״AкTSu<<>é<7 -SU4 u*_ͫ4?hw|ߌAˡdJ uxp(_ͫ4ԴQj+?xoAK^:fOOBqaQ)~r`#A+(*RgX|JaQ꧃s*,;Fs҃:COC\i-'\XJ>bAOCOVS%[98@Pkx3մAreGf9FRz3,SqP~Cu6 ZAޖJ:TŧaHV^yQjoMR7FqP^Ckŧ(է䃖[Os{+%RGVC4.4 >u_wHJ'R J/i\i}(&)CdS =J}Vs*QѧCi"h 1N6ZPգ}j"h 1N6z :T=PZM-`A_ XSJn>]OR4ɦTZՋO>mJ4FHJ'R]лQ靺GDК@JP:ٔj~/m ^J鶄DК@J(ͨ{CA߶GeI߶~ZI%4ցOW%:۶oSA-֧&*uNAi-t>xχm:BiDR1GI&MPR'ڞzn"hM$s(lJAACe=IZ335֓Bk?TփOOam& 6G)L5UP'>ݏR|j"h`osbQZӞ>7iЧ}QY:M-PvVz9?iRoZSux~FOOܵO͹:sŶ]ӡ|tk ZbJK`(?WqS@к[ :é< dthg% ZiR8Jիt|?%Y;S㡻Z \(K A T9 4\GQ;>ݗ5uFV#Xk;ZXcMMJ :Q=>M|:L>-[rZw{XVTs%=s)J.Ae}t|t}ZLh]8]:}urlmC*8KaЛt`(?wSHj+?x=0%d9Zs݌J(vOWs?h#@ ^!470 ZRZ#J/̣*k\Zi h9ʽI;]C-Xjq5- ZV4~}֢Rg#$=#J/ū9Դ,h툿XV ҆~3i4{|K49r62i6%4gZ J'R!PY/>SGHέDZAlPD C/҅ܡlL6G5+4]|.s4%| ZRghK)҇=է&h 9r5^NQ39Z+e) aF@fDPZZM2N(ԉY'=HODZI,sNAs§飴Q r)N:cQ{[JSO0N<+|>ޖTSj5*fS:)bQ{[JSLD @hsM餈RiR׭Jn2*fS:)bR⼔fO˝ʺ|j"h 9r3Z D;JU+>BcZn!>)-_RB'6*unA3çz("TT|jZ.&չkKu:xwc>ԹJROK[o.47 u Ot>VOSuya9 V*֋QXeAuN㾻ZdPGI>ţ(dmT\|jZ^-?:\EC ckF֭PI:KZkn(dmT\|j Zӡ>Е0>*l:Wޠziv/=JnEISY+>}ΜQP hPW=Fs(պ%*}֓ M+V#Xk5ɦVVeM%1wzV|O>5- Z#uxx%xv<^ڶA)-s`((c*kħtèѴ0h9w5N}/-JnMsħyOҟ:M+V5AmQur%>|?xtY-1bµ4Z[ԟ9溮Ry*kç>hE_LjA1F6J֙JEy&VCdSu/z4^|*}S'>>ZP6Tmtԡ&<:^Zb%qaNnsŧүu)P26W|/*Zgn+2%\}S ZVRg*+:-|>Z>8ԩiy(ժ6{>T^ t]8Ǘ}ţqPi h]-0u8Up4XMV ZpQi8հYti>դe spQROi04^4fkB`VxTTCF|Z64,4<*=5݂T뮔\׵>`eunSFF:px( ccwh^2 ŧS:(4^wF0?%7. qIiѸ(/{uxh#@Vo_7 A˯C >]>O=6 -5<ŴÑ7Oݬ1o,A+֕)S:!4z}8%h 6箚imHϦeA}80&>-ޱ$ŚrDexlrvkӲ#~S Za°)>]>ݎ{m"hE tRjeOG%~Z3@*°=)|:!JOV|=DЊ#SCi\R . 0F }W)]+>ݏ{oJ>h4+-bL RJ]+>ݏ{o֞M{#Esi~z963}4c/=U-Ӳc}i"hda ^ mWh:~i:SA+ kOV >il92%"|ۇ?&kk:[ MӲb.ca>h9RhTb׺E/`>Ju P=-o"h c|zi0{SA顏֘=0(>jڨ7%b|Z1rLC«S|2sG1O4Z!uU=ȠXN6:!ki(7KɦR'g )l׼>gk䂖ϚN6:!>>>,{j111ò8Z ayZ>>>>,A z`z`z`Ї= ha@ @ @ -X"AA -H"Qh:WǧCux_Ncunnˁemdz:;Fikv@sÍBz߂S  7jar۞#5N糾!tN=WsV[-k%^h zQ@oJ<>_ lx&\N hs~fy}vb;Shdݛ6ϿE[??lV}ḙoDG|>B K>ǚG[_,_0sCMf, mhs &ns&2w(euv {S߄Sz5޸ -{oЌ2닾%]h|] {SO򳡉[[@к=wfwpT>1_ FR?t[}?dC){)Wφ6rBqAk/t5u[` ˏ>ۋ6AsW(7%^$tN(pKyHP,l?x!-q; ? / 4_?\ozIENDB`PythonQwt-0.5.5/doc/index.rst0000666000000000000000000000136512613464704014622 0ustar rootroot.. automodule:: qwt .. only:: html and not htmlhelp .. note:: Windows users may download the :download:`CHM Manual <../PythonQwt.chm.zip>`. After downloading this file, you may see blank pages in the documentation. That's because Windows is blocking CHM files for security reasons. Fixing this problem is easy: * Right-click the CHM file, select properties, then click “Unblock”. * Or compress the CHM file into a zip archive and decompress it in another directory. * Do not open the CHM file on a network drive. Contents: .. toctree:: :maxdepth: 2 overview installation examples/index reference/index Indices and tables: * :ref:`genindex` * :ref:`search` PythonQwt-0.5.5/PKG-INFO0000666000000000000000000000374612646715536013326 0ustar rootrootMetadata-Version: 1.1 Name: PythonQwt Version: 0.5.5 Summary: Qt plotting widgets for Python Home-page: https://github.com/PierreRaybaut/PythonQwt Author: Pierre Raybaut Author-email: pierre.raybaut@gmail.com License: UNKNOWN Description: The ``PythonQwt`` package is a 2D-data plotting library using Qt graphical user interfaces for the Python programming language. It is compatible with both ``PyQt4`` and ``PyQt5`` (``PySide`` is currently not supported but it could be in the near future as it would "only" requires testing to support it as a stable alternative to PyQt). The ``PythonQwt`` project was initiated to solve -at least temporarily- the obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is no longer maintained. The idea was to translate the original Qwt C++ code to Python and then to optimize some parts of the code by writing new modules based on NumPy and other libraries. The ``PythonQwt`` package consists of a single Python package named `qwt` which is a pure Python implementation of Qwt C++ library with some limitations: efforts were concentrated on basic plotting features, leaving higher level features to the `guiqwt` library. Platform: Any Classifier: Development Status :: 3 - Alpha Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2) Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Scientific/Engineering :: Visualization Classifier: Topic :: Software Development :: Widget Sets Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 PythonQwt-0.5.5/MANIFEST.in0000666000000000000000000000016512627660526013754 0ustar rootrootrecursive-include doc *.py *.rst *.png include MANIFEST.in include LICENSE include README.md include CHANGELOG.mdPythonQwt-0.5.5/qwt/0000755000000000000000000000000012651077706013022 5ustar rootrootPythonQwt-0.5.5/qwt/text.py0000666000000000000000000012125212615225450014356 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ Text widgets ------------ QwtText ~~~~~~~ .. autoclass:: QwtText :members: QwtTextLabel ~~~~~~~~~~~~ .. autoclass:: QwtTextLabel :members: Text engines ------------ QwtTextEngine ~~~~~~~~~~~~~ .. autoclass:: QwtTextEngine :members: QwtPlainTextEngine ~~~~~~~~~~~~~~~~~~ .. autoclass:: QwtPlainTextEngine :members: QwtRichTextEngine ~~~~~~~~~~~~~~~~~ .. autoclass:: QwtRichTextEngine :members: """ import numpy as np import struct from qwt.qt.QtGui import (QPainter, QFrame, QSizePolicy, QPalette, QFont, QFontMetrics, QApplication, QColor, QWidget, QTextDocument, QTextOption, QFontMetricsF, QPixmap, QFontInfo, QTransform, QAbstractTextDocumentLayout) from qwt.qt.QtCore import Qt, QSizeF, QSize, QRectF from qwt.painter import QwtPainter QWIDGETSIZE_MAX = (1<<24)-1 def taggedRichText(text, flags): richText = text if flags & Qt.AlignJustify: richText = "
" + richText + "
" elif flags & Qt.AlignRight: richText = "
" + richText + "
" elif flags & Qt.AlignHCenter: richText = "
" + richText + "
" return richText class QwtRichTextDocument(QTextDocument): def __init__(self, text, flags, font): super(QwtRichTextDocument, self).__init__(None) self.setUndoRedoEnabled(False) self.setDefaultFont(font) self.setHtml(text) option = self.defaultTextOption() if flags & Qt.TextWordWrap: option.setWrapMode(QTextOption.WordWrap) else: option.setWrapMode(QTextOption.NoWrap) option.setAlignment(flags) self.setDefaultTextOption(option) root = self.rootFrame() fm = root.frameFormat() fm.setBorder(0) fm.setMargin(0) fm.setPadding(0) fm.setBottomMargin(0) fm.setLeftMargin(0) root.setFrameFormat(fm) self.adjustSize(); class QwtTextEngine(object): """ Abstract base class for rendering text strings A text engine is responsible for rendering texts for a specific text format. They are used by `QwtText` to render a text. `QwtPlainTextEngine` and `QwtRichTextEngine` are part of the `PythonQwt` library. The implementation of `QwtMathMLTextEngine` uses code from the `Qt` solution package. Because of license implications it is built into a separate library. .. seealso:: :py:meth:`qwt.text.QwtText.setTextEngine()` """ def __init__(self): pass def heightForWidth(self, font, flags, text, width): """ Find the height for a given width :param QFont font: Font of the text :param int flags: Bitwise OR of the flags used like in QPainter::drawText :param str text: Text to be rendered :param float width: Width :return: Calculated height """ pass def textSize(self, font, flags, text): """ Returns the size, that is needed to render text :param QFont font: Font of the text :param int flags: Bitwise OR of the flags like in for QPainter::drawText :param str text: Text to be rendered :return: Calculated size """ pass def mightRender(self, text): """ Test if a string can be rendered by this text engine :param str text: Text to be tested :return: True, if it can be rendered """ pass def textMargins(self, font): """ Return margins around the texts The textSize might include margins around the text, like QFontMetrics::descent(). In situations where texts need to be aligned in detail, knowing these margins might improve the layout calculations. :param QFont font: Font of the text :return: tuple (left, right, top, bottom) representing margins """ pass def draw(self, painter, rect, flags, text): """ Draw the text in a clipping rectangle :param QPainter painter: Painter :param QRectF rect: Clipping rectangle :param int flags: Bitwise OR of the flags like in for QPainter::drawText() :param str text: Text to be rendered """ pass ASCENTCACHE = {} def qwtScreenResolution(): screenResolution = QSize() if not screenResolution.isValid(): desktop = QApplication.desktop() if desktop is not None: screenResolution.setWidth(desktop.logicalDpiX()) screenResolution.setHeight(desktop.logicalDpiY()) return screenResolution def qwtUnscaleFont(painter): if painter.font().pixelSize() >= 0: return screenResolution = qwtScreenResolution() pd = painter.device() if pd.logicalDpiX() != screenResolution.width() or\ pd.logicalDpiY() != screenResolution.height(): pixelFont = QFont(painter.font(), QApplication.desktop()) pixelFont.setPixelSize(QFontInfo(pixelFont).pixelSize()) painter.setFont(pixelFont) class QwtPlainTextEngine(QwtTextEngine): """ A text engine for plain texts `QwtPlainTextEngine` renders texts using the basic `Qt` classes `QPainter` and `QFontMetrics`. """ def __init__(self): self.qrectf_max = QRectF(0, 0, QWIDGETSIZE_MAX, QWIDGETSIZE_MAX) self._fm_cache = {} self._fm_cache_f = {} def fontmetrics(self, font): fid = font.toString() fm = self._fm_cache.get(fid) if fm is None: return self._fm_cache.setdefault(fid, QFontMetrics(font)) else: return fm def fontmetrics_f(self, font): fid = font.toString() fm = self._fm_cache_f.get(fid) if fm is None: return self._fm_cache_f.setdefault(fid, QFontMetricsF(font)) else: return fm def heightForWidth(self, font, flags, text, width): """ Find the height for a given width :param QFont font: Font of the text :param int flags: Bitwise OR of the flags used like in QPainter::drawText :param str text: Text to be rendered :param float width: Width :return: Calculated height """ fm = self.fontmetrics_f(font) rect = fm.boundingRect(QRectF(0, 0, width, QWIDGETSIZE_MAX), flags, text) return rect.height() def textSize(self, font, flags, text): """ Returns the size, that is needed to render text :param QFont font: Font of the text :param int flags: Bitwise OR of the flags like in for QPainter::drawText :param str text: Text to be rendered :return: Calculated size """ fm = self.fontmetrics_f(font) rect = fm.boundingRect(self.qrectf_max, flags, text) return rect.size() def effectiveAscent(self, font): global ASCENTCACHE fontKey = font.key() ascent = ASCENTCACHE.get(fontKey) if ascent is not None: return ascent return ASCENTCACHE.setdefault(fontKey, self.findAscent(font)) def findAscent(self, font): dummy = "E" white = QColor(Qt.white) fm = self.fontmetrics(font) pm = QPixmap(fm.width(dummy), fm.height()) pm.fill(white) p = QPainter(pm) p.setFont(font) p.drawText(0, 0, pm.width(), pm.height(), 0, dummy) p.end() img = pm.toImage() w = pm.width() linebytes = w*4 for row in range(img.height()): line = img.scanLine(row).asstring(linebytes) for col in range(w): color = struct.unpack('I', line[col*4:(col+1)*4])[0] if color != white.rgb(): return fm.ascent()-row+1 return fm.ascent() def textMargins(self, font): """ Return margins around the texts The textSize might include margins around the text, like QFontMetrics::descent(). In situations where texts need to be aligned in detail, knowing these margins might improve the layout calculations. :param QFont font: Font of the text :return: tuple (left, right, top, bottom) representing margins """ left = right = top = 0 fm = self.fontmetrics_f(font) top = fm.ascent() - self.effectiveAscent(font) bottom = fm.descent() return left, right, top, bottom def draw(self, painter, rect, flags, text): """ Draw the text in a clipping rectangle :param QPainter painter: Painter :param QRectF rect: Clipping rectangle :param int flags: Bitwise OR of the flags like in for QPainter::drawText() :param str text: Text to be rendered """ painter.save() qwtUnscaleFont(painter) painter.drawText(rect, flags, text) painter.restore() def mightRender(self, text): """ Test if a string can be rendered by this text engine :param str text: Text to be tested :return: True, if it can be rendered """ return True class QwtRichTextEngine(QwtTextEngine): """ A text engine for `Qt` rich texts `QwtRichTextEngine` renders `Qt` rich texts using the classes of the Scribe framework of `Qt`. """ def __init__(self): pass def heightForWidth(self, font, flags, text, width): """ Find the height for a given width :param QFont font: Font of the text :param int flags: Bitwise OR of the flags used like in QPainter::drawText :param str text: Text to be rendered :param float width: Width :return: Calculated height """ doc = QwtRichTextDocument(text, flags, font) doc.setPageSize(QSizeF(width, QWIDGETSIZE_MAX)) return doc.documentLayout().documentSize().height() def textSize(self, font, flags, text): """ Returns the size, that is needed to render text :param QFont font: Font of the text :param int flags: Bitwise OR of the flags like in for QPainter::drawText :param str text: Text to be rendered :return: Calculated size """ doc = QwtRichTextDocument(text, flags, font) option = doc.defaultTextOption() if option.wrapMode() != QTextOption.NoWrap: option.setWrapMode(QTextOption.NoWrap) doc.setDefaultTextOption(option) doc.adjustSize() return doc.size() def draw(self, painter, rect, flags, text): """ Draw the text in a clipping rectangle :param QPainter painter: Painter :param QRectF rect: Clipping rectangle :param int flags: Bitwise OR of the flags like in for QPainter::drawText() :param str text: Text to be rendered """ txt = QwtRichTextDocument(text, flags, painter.font()) painter.save() unscaledRect = QRectF(rect) if painter.font().pixelSize() < 0: res = qwtScreenResolution() pd = painter.device() if pd.logicalDpiX() != res.width()\ or pd.logicalDpiY() != res.height(): transform = QTransform() transform.scale(res.width()/float(pd.logicalDpiX()), res.height()/float(pd.logicalDpiY())) painter.setWorldTransform(transform, True) invtrans, _ok = transform.inverted() unscaledRect = invtrans.mapRect(rect) txt.setDefaultFont(painter.font()) txt.setPageSize(QSizeF(unscaledRect.width(), QWIDGETSIZE_MAX)) layout = txt.documentLayout() height = layout.documentSize().height() y = unscaledRect.y() if flags & Qt.AlignBottom: y += unscaledRect.height()-height elif flags & Qt.AlignVCenter: y += (unscaledRect.height()-height)/2 context = QAbstractTextDocumentLayout.PaintContext() context.palette.setColor(QPalette.Text, painter.pen().color()) painter.translate(unscaledRect.x(), y) layout.draw(painter, context) painter.restore() def taggedText(self, text, flags): return self.taggedRichText(text,flags) def mightRender(self, text): """ Test if a string can be rendered by this text engine :param str text: Text to be tested :return: True, if it can be rendered """ return Qt.mightBeRichText(text) def textMargins(self, font): """ Return margins around the texts The textSize might include margins around the text, like QFontMetrics::descent(). In situations where texts need to be aligned in detail, knowing these margins might improve the layout calculations. :param QFont font: Font of the text :return: tuple (left, right, top, bottom) representing margins """ return 0, 0, 0, 0 class QwtText_PrivateData(object): def __init__(self): self.renderFlags = Qt.AlignCenter self.borderRadius = 0 self.borderPen = Qt.NoPen self.backgroundBrush = Qt.NoBrush self.paintAttributes = 0 self.layoutAttributes = 0 self.textEngine = None self.text = None self.font = None self.color = None class QwtText_LayoutCache(object): def __init__(self): self.textSize = QSizeF() self.font = None def invalidate(self): self.textSize = QSizeF() class QwtText(object): """ A class representing a text A `QwtText` is a text including a set of attributes how to render it. - Format: A text might include control sequences (f.e tags) describing how to render it. Each format (f.e MathML, TeX, Qt Rich Text) has its own set of control sequences, that can be handles by a special `QwtTextEngine` for this format. - Background: A text might have a background, defined by a `QPen` and `QBrush` to improve its visibility. The corners of the background might be rounded. - Font: A text might have an individual font. - Color A text might have an individual color. - Render Flags Flags from `Qt.AlignmentFlag` and `Qt.TextFlag` used like in `QPainter.drawText()`. ..seealso:: :py:meth:`qwt.text.QwtTextEngine`, :py:meth:`qwt.text.QwtTextLabel` Text formats: * `QwtText.AutoText`: The text format is determined using `QwtTextEngine.mightRender()` for all available text engines in increasing order > PlainText. If none of the text engines can render the text is rendered like `QwtText.PlainText`. * `QwtText.PlainText`: Draw the text as it is, using a QwtPlainTextEngine. * `QwtText.RichText`: Use the Scribe framework (Qt Rich Text) to render the text. * `QwtText.MathMLText`: Use a MathML (http://en.wikipedia.org/wiki/MathML) render engine to display the text. The Qwt MathML extension offers such an engine based on the MathML renderer of the Qt solutions package. To enable MathML support the following code needs to be added to the application:: QwtText.setTextEngine(QwtText.MathMLText, QwtMathMLTextEngine()) * `QwtText.TeXText`: Use a TeX (http://en.wikipedia.org/wiki/TeX) render engine to display the text ( not implemented yet ). * `QwtText.OtherFormat`: The number of text formats can be extended using `setTextEngine`. Formats >= `QwtText.OtherFormat` are not used by Qwt. Paint attributes: * `QwtText.PaintUsingTextFont`: The text has an individual font. * `QwtText.PaintUsingTextColor`: The text has an individual color. * `QwtText.PaintBackground`: The text has an individual background. Layout attributes: * `QwtText.MinimumLayout`: Layout the text without its margins. This mode is useful if a text needs to be aligned accurately, like the tick labels of a scale. If `QwtTextEngine.textMargins` is not implemented for the format of the text, `MinimumLayout` has no effect. .. py:class:: QwtText([text=None], [textFormat=None], [other=None]) :param str text: Text content :param int textFormat: Text format :param qwt.text.QwtText other: Object to copy (text and textFormat arguments are ignored) """ # enum TextFormat AutoText, PlainText, RichText, MathMLText, TeXText = list(range(5)) OtherFormat = 100 # enum PaintAttribute PaintUsingTextFont = 0x01 PaintUsingTextColor = 0x02 PaintBackground = 0x04 # enum LayoutAttribute MinimumLayout = 0x01 # Optimization: a single text engine for all QwtText objects # (this is not how it's implemented in Qwt6 C++ library) __map = {PlainText: QwtPlainTextEngine(), RichText: QwtRichTextEngine()} def __init__(self, text=None, textFormat=None, other=None): self.__desktopwidget = None if text is None: text = '' if textFormat is None: textFormat = self.AutoText if other is not None: text = other if isinstance(text, QwtText): self.__data = text.__data self.__layoutCache = text.__layoutCache else: self.__data = QwtText_PrivateData() self.__data.text = text self.__data.textEngine = self.textEngine(text, textFormat) self.__layoutCache = QwtText_LayoutCache() @property def _desktopwidget(self): """ Property used to store the Application Desktop Widget to avoid calling the `QApplication.desktop()" function more than necessary as its calling time is not negligible. """ if self.__desktopwidget is None: self.__desktopwidget = QApplication.desktop() return self.__desktopwidget def __eq__(self, other): return self.__data.renderFlags == other.__data.renderFlags and\ self.__data.text == other.__data.text and\ self.__data.font == other.__data.font and\ self.__data.color == other.__data.color and\ self.__data.borderRadius == other.__data.borderRadius and\ self.__data.borderPen == other.__data.borderPen and\ self.__data.backgroundBrush == other.__data.backgroundBrush and\ self.__data.paintAttributes == other.__data.paintAttributes and\ self.__data.textEngine == other.__data.textEngine def __ne__(self, other): return not self.__eq__(other) def isEmpty(self): """ :return: True if text is empty """ return len(self.text()) == 0 def setText(self, text, textFormat=None): """ Assign a new text content :param str text: Text content :param int textFormat: Text format .. seealso:: :py:meth:`text()` """ if textFormat is None: textFormat = self.AutoText self.__data.text = text self.__data.textEngine = self.textEngine(text, textFormat) self.__layoutCache.invalidate() def text(self): """ :return: Text content .. seealso:: :py:meth:`setText()` """ return self.__data.text def setRenderFlags(self, renderFlags): """ Change the render flags The default setting is `Qt.AlignCenter` :param int renderFlags: Bitwise OR of the flags used like in `QPainter.drawText()` .. seealso:: :py:meth:`renderFlags()`, :py:meth:`qwt.text.QwtTextEngine.draw()` """ renderFlags = Qt.AlignmentFlag(renderFlags) if renderFlags != self.__data.renderFlags: self.__data.renderFlags = renderFlags self.__layoutCache.invalidate() def renderFlags(self): """ :return: Render flags .. seealso:: :py:meth:`setRenderFlags()` """ return self.__data.renderFlags def setFont(self, font): """ Set the font. :param QFont font: Font .. note:: Setting the font might have no effect, when the text contains control sequences for setting fonts. .. seealso:: :py:meth:`font()`, :py:meth:`usedFont()` """ self.__data.font = font self.setPaintAttribute(self.PaintUsingTextFont) def font(self): """ :return: Return the font .. seealso:: :py:meth:`setFont()`, :py:meth:`usedFont()` """ return self.__data.font def usedFont(self, defaultFont): """ Return the font of the text, if it has one. Otherwise return defaultFont. :param QFont defaultFont: Default font :return: Font used for drawing the text .. seealso:: :py:meth:`setFont()`, :py:meth:`font()` """ if self.__data.paintAttributes & self.PaintUsingTextFont: return self.__data.font return defaultFont def setColor(self, color): """ Set the pen color used for drawing the text. :param QColor color: Color .. note:: Setting the color might have no effect, when the text contains control sequences for setting colors. .. seealso:: :py:meth:`color()`, :py:meth:`usedColor()` """ self.__data.color = QColor(color) self.setPaintAttribute(self.PaintUsingTextColor) def color(self): """ :return: Return the pen color, used for painting the text .. seealso:: :py:meth:`setColor()`, :py:meth:`usedColor()` """ return self.__data.color def usedColor(self, defaultColor): """ Return the color of the text, if it has one. Otherwise return defaultColor. :param QColor defaultColor: Default color :return: Color used for drawing the text .. seealso:: :py:meth:`setColor()`, :py:meth:`color()` """ if self.__data.paintAttributes & self.PaintUsingTextColor: return self.__data.color return defaultColor def setBorderRadius(self, radius): """ Set the radius for the corners of the border frame :param float radius: Radius of a rounded corner .. seealso:: :py:meth:`borderRadius()`, :py:meth:`setBorderPen()`, :py:meth:`setBackgroundBrush()` """ self.__data.borderRadius = max([0., radius]) def borderRadius(self): """ :return: Radius for the corners of the border frame .. seealso:: :py:meth:`setBorderRadius()`, :py:meth:`borderPen()`, :py:meth:`backgroundBrush()` """ return self.__data.borderRadius def setBorderPen(self, pen): """ Set the background pen :param QPen pen: Background pen .. seealso:: :py:meth:`borderPen()`, :py:meth:`setBackgroundBrush()` """ self.__data.borderPen = pen self.setPaintAttribute(self.PaintBackground) def borderPen(self): """ :return: Background pen .. seealso:: :py:meth:`setBorderPen()`, :py:meth:`backgroundBrush()` """ return self.__data.borderPen def setBackgroundBrush(self, brush): """ Set the background brush :param QBrush brush: Background brush .. seealso:: :py:meth:`backgroundBrush()`, :py:meth:`setBorderPen()` """ self.__data.backgroundBrush = brush self.setPaintAttribute(self.PaintBackground) def backgroundBrush(self): """ :return: Background brush .. seealso:: :py:meth:`setBackgroundBrush()`, :py:meth:`borderPen()` """ return self.__data.backgroundBrush def setPaintAttribute(self, attribute, on=True): """ Change a paint attribute :param int attribute: Paint attribute :param bool on: On/Off .. note:: Used by `setFont()`, `setColor()`, `setBorderPen()` and `setBackgroundBrush()` .. seealso:: :py:meth:`testPaintAttribute()` """ if on: self.__data.paintAttributes |= attribute else: self.__data.paintAttributes &= ~attribute def testPaintAttribute(self, attribute): """ Test a paint attribute :param int attribute: Paint attribute :return: True, if attribute is enabled .. seealso:: :py:meth:`setPaintAttribute()` """ return self.__data.paintAttributes & attribute def setLayoutAttribute(self, attribute, on=True): """ Change a layout attribute :param int attribute: Layout attribute :param bool on: On/Off .. seealso:: :py:meth:`testLayoutAttribute()` """ if on: self.__data.layoutAttributes |= attribute else: self.__data.layoutAttributes &= ~attribute def testLayoutAttribute(self, attribute): """ Test a layout attribute :param int attribute: Layout attribute :return: True, if attribute is enabled .. seealso:: :py:meth:`setLayoutAttribute()` """ return self.__data.layoutAttributes & attribute def heightForWidth(self, width, defaultFont=None): """ Find the height for a given width :param float width: Width :param QFont defaultFont: Font, used for the calculation if the text has no font :return: Calculated height """ if defaultFont is None: defaultFont = QFont() font = QFont(self.usedFont(defaultFont), self._desktopwidget) h = 0 if self.__data.layoutAttributes & self.MinimumLayout: (left, right, top, bottom ) = self.__data.textEngine.textMargins(font) h = self.__data.textEngine.heightForWidth(font, self.__data.renderFlags, self.__data.text, width + left + right) h -= top + bottom else: h = self.__data.textEngine.heightForWidth(font, self.__data.renderFlags, self.__data.text, width) return h def textSize(self, defaultFont): """ Returns the size, that is needed to render text :param QFont defaultFont Font, used for the calculation if the text has no font :return: Caluclated size """ font = QFont(self.usedFont(defaultFont), self._desktopwidget) if not self.__layoutCache.textSize.isValid() or\ self.__layoutCache.font is not font: self.__layoutCache.textSize =\ self.__data.textEngine.textSize(font, self.__data.renderFlags, self.__data.text) self.__layoutCache.font = font sz = self.__layoutCache.textSize if self.__data.layoutAttributes & self.MinimumLayout: (left, right, top, bottom ) = self.__data.textEngine.textMargins(font) sz -= QSizeF(left + right, top + bottom) return sz def draw(self, painter, rect): """ Draw a text into a rectangle :param QPainter painter: Painter :param QRectF rect: Rectangle """ if self.__data.paintAttributes & self.PaintBackground: if self.__data.borderPen != Qt.NoPen or\ self.__data.backgroundBrush != Qt.NoBrush: painter.save() painter.setPen(self.__data.borderPen) painter.setBrush(self.__data.backgroundBrush) if self.__data.borderRadius == 0: painter.drawRect(rect) else: painter.setRenderHint(QPainter.Antialiasing, True) painter.drawRoundedRect(rect, self.__data.borderRadius, self.__data.borderRadius) painter.restore() painter.save() if self.__data.paintAttributes & self.PaintUsingTextFont: painter.setFont(self.__data.font) if self.__data.paintAttributes & self.PaintUsingTextColor: if self.__data.color.isValid(): painter.setPen(self.__data.color) expandedRect = rect if self.__data.layoutAttributes & self.MinimumLayout: font = QFont(painter.font(), self._desktopwidget) (left, right, top, bottom ) = self.__data.textEngine.textMargins(font) expandedRect.setTop(rect.top()-top) expandedRect.setBottom(rect.bottom()+bottom) expandedRect.setLeft(rect.left()-left) expandedRect.setRight(rect.right()+right) self.__data.textEngine.draw(painter, expandedRect, self.__data.renderFlags, self.__data.text) painter.restore() def textEngine(self, text=None, format_=None): """ Find the text engine for a text format In case of `QwtText.AutoText` the first text engine (beside `QwtPlainTextEngine`) is returned, where `QwtTextEngine.mightRender` returns true. If there is none `QwtPlainTextEngine` is returned. If no text engine is registered for the format `QwtPlainTextEngine` is returned. :param str text: Text, needed in case of AutoText :param int format: Text format :return: Corresponding text engine """ if text is None: return self.__map.get(format_) elif format_ is not None: if format_ == QwtText.AutoText: for key, engine in list(self.__map.items()): if key != QwtText.PlainText: if engine and engine.mightRender(text): return engine engine = self.__map.get(format_) if engine is not None: return engine return self.__map[QwtText.PlainText] else: raise TypeError("%s().textEngine() takes 1 or 2 argument(s) (none"\ " given)" % self.__class__.__name__) def setTextEngine(self, format_, engine): """ Assign/Replace a text engine for a text format With setTextEngine it is possible to extend `PythonQwt` with other types of text formats. For `QwtText.PlainText` it is not allowed to assign a engine to None. :param int format_: Text format :param qwt.text.QwtTextEngine engine: Text engine .. seealso:: :py:meth:`setPaintAttribute()` .. warning:: Using `QwtText.AutoText` does nothing. """ if format_ == QwtText.AutoText: return if format_ == QwtText.PlainText and engine is None: return self.__map.setdefault(format_, engine) class QwtTextLabel_PrivateData(object): def __init__(self): self.indent = 4 self.margin = 0 self.text = QwtText() class QwtTextLabel(QFrame): """ A Widget which displays a QwtText .. py:class:: QwtTextLabel(parent) :param QWidget parent: Parent widget .. py:class:: QwtTextLabel([text=None], [parent=None]) :param str text: Text :param QWidget parent: Parent widget """ def __init__(self, *args): if len(args) == 0: text, parent = None, None elif len(args) == 1: if isinstance(args[0], QWidget): text = None parent, = args else: parent = None text, = args elif len(args) == 2: text, parent = args else: raise TypeError("%s() takes 0, 1 or 2 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) super(QwtTextLabel, self).__init__(parent) self.init() if text is not None: self.__data.text = text def init(self): self.__data = QwtTextLabel_PrivateData() self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) def setPlainText(self, text): """ Interface for the designer plugin - does the same as setText() :param str text: Text .. seealso:: :py:meth:`plainText()` """ self.setText(QwtText(text)) def plainText(self): """ Interface for the designer plugin :return: Text as plain text .. seealso:: :py:meth:`setPlainText()` """ return self.__data.text.text() def setText(self, text, textFormat=QwtText.AutoText): """ Change the label's text, keeping all other QwtText attributes :param text: New text :type text: qwt.text.QwtText or str :param int textFormat: Format of text .. seealso:: :py:meth:`text()` """ if isinstance(text, QwtText): self.__data.text = text else: self.__data.text.setText(text, textFormat) self.update() self.updateGeometry() def text(self): """ :return: Return the text .. seealso:: :py:meth:`setText()` """ return self.__data.text def clear(self): """ Clear the text and all `QwtText` attributes """ self.__data.text = QwtText() self.update() self.updateGeometry() def indent(self): """ :return: Label's text indent in pixels .. seealso:: :py:meth:`setIndent()` """ return self.__data.indent def setIndent(self, indent): """ Set label's text indent in pixels :param int indent: Indentation in pixels .. seealso:: :py:meth:`indent()` """ if indent < 0: indent = 0 self.__data.indent = indent self.update() self.updateGeometry() def margin(self): """ :return: Label's text indent in pixels .. seealso:: :py:meth:`setMargin()` """ return self.__data.margin def setMargin(self, margin): """ Set label's margin in pixels :param int margin: Margin in pixels .. seealso:: :py:meth:`margin()` """ self.__data.margin = margin self.update() self.updateGeometry() def sizeHint(self): """ Return a size hint """ return self.minimumSizeHint() def minimumSizeHint(self): """ Return a minimum size hint """ sz = self.__data.text.textSize(self.font()) mw = 2*(self.frameWidth()+self.__data.margin) mh = mw indent = self.__data.indent if indent <= 0: indent = self.defaultIndent() if indent > 0: align = self.__data.text.renderFlags() if align & Qt.AlignLeft or align & Qt.AlignRight: mw += self.__data.indent elif align & Qt.AlignTop or align & Qt.AlignBottom: mh += self.__data.indent sz += QSizeF(mw, mh) return QSize(np.ceil(sz.width()), np.ceil(sz.height())) def heightForWidth(self, width): """ :param int width: Width :return: Preferred height for this widget, given the width. """ renderFlags = self.__data.text.renderFlags() indent = self.__data.indent if indent <= 0: indent = self.defaultIndent() width -= 2*self.frameWidth() if renderFlags & Qt.AlignLeft or renderFlags & Qt.AlignRight: width -= indent height = np.ceil(self.__data.text.heightForWidth(width, self.font())) if renderFlags & Qt.AlignTop or renderFlags & Qt.AlignBottom: height += indent height += 2*self.frameWidth() return height def paintEvent(self, event): painter = QPainter(self) if not self.contentsRect().contains(event.rect()): painter.save() painter.setClipRegion(event.region() & self.frameRect()) self.drawFrame(painter) painter.restore() painter.setClipRegion(event.region() & self.contentsRect()) self.drawContents(painter) def drawContents(self, painter): """ Redraw the text and focus indicator :param QPainter painter: Painter """ r = self.textRect() if r.isEmpty(): return painter.setFont(self.font()) painter.setPen(self.palette().color(QPalette.Active, QPalette.Text)) self.drawText(painter, QRectF(r)) if self.hasFocus(): m = 2 focusRect = self.contentsRect().adjusted(m, m, -m+1, -m+1) QwtPainter.drawFocusRect(painter, self, focusRect) def drawText(self, painter, textRect): """ Redraw the text :param QPainter painter: Painter :param QRectF textRect: Text rectangle """ self.__data.text.draw(painter, textRect) def textRect(self): """ Calculate geometry for the text in widget coordinates :return: Geometry for the text """ r = self.contentsRect() if not r.isEmpty() and self.__data.margin > 0: r.setRect(r.x()+self.__data.margin, r.y()+self.__data.margin, r.width()-2*self.__data.margin, r.height()-2*self.__data.margin) if not r.isEmpty(): indent = self.__data.indent if indent <= 0: indent = self.defaultIndent() if indent > 0: renderFlags = self.__data.text.renderFlags() if renderFlags & Qt.AlignLeft: r.setX(r.x()+indent) elif renderFlags & Qt.AlignRight: r.setWidth(r.width()-indent) elif renderFlags & Qt.AlignTop: r.setY(r.y()+indent) elif renderFlags & Qt.AlignBottom: r.setHeight(r.height()-indent) return r def defaultIndent(self): if self.frameWidth() <= 0: return 0 if self.__data.text.testPaintAttribute(QwtText.PaintUsingTextFont): fnt = self.__data.text.font() else: fnt = self.font() return QFontMetrics(fnt).width('x')/2 PythonQwt-0.5.5/qwt/painter_command.py0000666000000000000000000001546712605040216016536 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPainterCommand ----------------- .. autoclass:: QwtPainterCommand :members: """ from qwt.qt.QtGui import QPainterPath, QPaintEngine class PixmapData(object): def __init__(self): self.rect = None self.pixmap = None self.subRect = None class ImageData(object): def __init__(self): self.rect = None self.image = None self.subRect = None self.flags = None class StateData(object): def __init__(self): self.flags = None self.pen = None self.brush = None self.brushOrigin = None self.backgroundBrush = None self.backgroundMode = None self.font = None self.matrix = None self.transform = None self.clipOperation = None self.clipRegion = None self.clipPath = None self.isClipEnabled = None self.renderHints = None self.compositionMode = None self.opacity = None class QwtPainterCommand(object): """ `QwtPainterCommand` represents the attributes of a paint operation how it is used between `QPainter` and `QPaintDevice` It is used by :py:class:`qwt.graphic.QwtGraphic` to record and replay paint operations .. seealso:: :py:meth:`qwt.graphic.QwtGraphic.commands()` .. py:class:: QwtPainterCommand() Construct an invalid command .. py:class:: QwtPainterCommand(path) Copy constructor :param QPainterPath path: Source .. py:class:: QwtPainterCommand(rect, pixmap, subRect) Constructor for Pixmap paint operation :param QRectF rect: Target rectangle :param QPixmap pixmap: Pixmap :param QRectF subRect: Rectangle inside the pixmap .. py:class:: QwtPainterCommand(rect, image, subRect, flags) Constructor for Image paint operation :param QRectF rect: Target rectangle :param QImage image: Image :param QRectF subRect: Rectangle inside the image :param Qt.ImageConversionFlags flags: Conversion flags .. py:class:: QwtPainterCommand(state) Constructor for State paint operation :param QPaintEngineState state: Paint engine state """ # enum Type Invalid = -1 Path, Pixmap, Image, State = list(range(4)) def __init__(self, *args): if len(args) == 0: self.__type = self.Invalid elif len(args) == 1: arg, = args if isinstance(arg, QPainterPath): path = arg self.__type = self.Path self.__path = QPainterPath(path) elif isinstance(arg, QwtPainterCommand): other = arg self.copy(other) else: state = arg self.__type = self.State self.__stateData = StateData() self.__stateData.flags = state.state() if self.__stateData.flags & QPaintEngine.DirtyPen: self.__stateData.pen = state.pen() if self.__stateData.flags & QPaintEngine.DirtyBrush: self.__stateData.brush = state.brush() if self.__stateData.flags & QPaintEngine.DirtyBrushOrigin: self.__stateData.brushOrigin = state.brushOrigin() if self.__stateData.flags & QPaintEngine.DirtyFont: self.__stateData.font = state.font() if self.__stateData.flags & QPaintEngine.DirtyBackground: self.__stateData.backgroundMode = state.backgroundMode() self.__stateData.backgroundBrush = state.backgroundBrush() if self.__stateData.flags & QPaintEngine.DirtyTransform: self.__stateData.transform = state.transform() if self.__stateData.flags & QPaintEngine.DirtyClipEnabled: self.__stateData.isClipEnabled = state.isClipEnabled() if self.__stateData.flags & QPaintEngine.DirtyClipRegion: self.__stateData.clipRegion = state.clipRegion() self.__stateData.clipOperation = state.clipOperation() if self.__stateData.flags & QPaintEngine.DirtyClipPath: self.__stateData.clipPath = state.clipPath() self.__stateData.clipOperation = state.clipOperation() if self.__stateData.flags & QPaintEngine.DirtyHints: self.__stateData.renderHints = state.renderHints() if self.__stateData.flags & QPaintEngine.DirtyCompositionMode: self.__stateData.compositionMode = state.compositionMode() if self.__stateData.flags & QPaintEngine.DirtyOpacity: self.__stateData.opacity = state.opacity() elif len(args) == 3: rect, pixmap, subRect = args self.__type = self.Pixmap self.__pixmapData = PixmapData() self.__pixmapData.rect = rect self.__pixmapData.pixmap = pixmap self.__pixmapData.subRect = subRect elif len(args) == 4: rect, image, subRect, flags = args self.__type = self.Image self.__imageData = ImageData() self.__imageData.rect = rect self.__imageData.image = image self.__imageData.subRect = subRect self.__imageData.flags = flags else: raise TypeError("%s() takes 0, 1, 3 or 4 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def copy(self, other): self.__type = other.__type if other.__type == self.Path: self.__path = QPainterPath(other.__path) elif other.__type == self.Pixmap: self.__pixmapData = PixmapData(other.__pixmapData) elif other.__type == self.Image: self.__imageData = ImageData(other.__imageData) elif other.__type == self.State: self.__stateData == StateData(other.__stateData) def reset(self): self.__type = self.Invalid def type(self): return self.__type def path(self): return self.__path def pixmapData(self): return self.__pixmapData def imageData(self): return self.__imageData def stateData(self): return self.__stateData PythonQwt-0.5.5/qwt/scale_widget.py0000666000000000000000000006436112615225450016033 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtScaleWidget -------------- .. autoclass:: QwtScaleWidget :members: """ from qwt.scale_draw import QwtScaleDraw from qwt.scale_engine import QwtLinearScaleEngine from qwt.color_map import QwtLinearColorMap from qwt.text import QwtText from qwt.painter import QwtPainter from qwt.interval import QwtInterval from qwt.color_map import QwtColorMap from qwt.qt.QtGui import (QWidget, QSizePolicy, QPainter, QStyleOption, QStyle, QPalette) from qwt.qt.QtCore import Qt, QRectF, QSize, Signal import numpy as np class ColorBar(object): def __init__(self): self.isEnabled = None self.width = None self.interval = QwtInterval() self.colorMap = QwtColorMap() class QwtScaleWidget_PrivateData(object): def __init__(self): self.scaleDraw = None self.borderDist = [None] * 2 self.minBorderDist = [None] * 2 self.scaleLength = None self.margin = None self.titleOffset = None self.spacing = None self.title = QwtText() self.layoutFlags = None self.colorBar = ColorBar() class QwtScaleWidget(QWidget): """ A Widget which contains a scale This Widget can be used to decorate composite widgets with a scale. Layout flags: * `QwtScaleWidget.TitleInverted`: The title of vertical scales is painted from top to bottom. Otherwise it is painted from bottom to top. .. py:class:: QwtScaleWidget([parent=None]) Alignment default is `QwtScaleDraw.LeftScale`. :param parent: Parent widget :type parent: QWidget or None .. py:class:: QwtScaleWidget(align, parent) :param int align: Alignment :param QWidget parent: Parent widget """ scaleDivChanged = Signal() # enum LayoutFlag TitleInverted = 1 def __init__(self, *args): self.__data = None align = QwtScaleDraw.LeftScale if len(args) == 0: parent = None elif len(args) == 1: parent, = args elif len(args) == 2: align, parent = args else: raise TypeError("%s() takes 0, 1 or 2 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) super(QwtScaleWidget, self).__init__(parent) self.initScale(align) def initScale(self, align): """ Initialize the scale :param int align: Alignment """ self.__data = QwtScaleWidget_PrivateData() self.__data.layoutFlags = 0 if align == QwtScaleDraw.RightScale: self.__data.layoutFlags |= self.TitleInverted self.__data.borderDist = [0, 0] self.__data.minBorderDist = [0, 0] self.__data.margin = 4 self.__data.titleOffset = 0 self.__data.spacing = 2 self.__data.scaleDraw = QwtScaleDraw() self.__data.scaleDraw.setAlignment(align) self.__data.scaleDraw.setLength(10) self.__data.scaleDraw.setScaleDiv( QwtLinearScaleEngine().divideScale(0.0, 100.0, 10, 5)) self.__data.colorBar.colorMap = QwtLinearColorMap() self.__data.colorBar.isEnabled = False self.__data.colorBar.width = 10 flags = Qt.AlignmentFlag(Qt.AlignHCenter|Qt.TextExpandTabs|Qt.TextWordWrap) self.__data.title.setRenderFlags(flags) self.__data.title.setFont(self.font()) policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) if self.__data.scaleDraw.orientation() == Qt.Vertical: policy.transpose() self.setSizePolicy(policy) self.setAttribute(Qt.WA_WState_OwnSizePolicy, False) def setLayoutFlag(self, flag, on=True): """ Toggle an layout flag :param int flag: Layout flag :param bool on: True/False .. seealso:: :py:meth:`testLayoutFlag()` """ if (self.__data.layoutFlags & flag != 0) != on: if on: self.__data.layoutFlags |= flag else: self.__data.layoutFlags &= ~flag def testLayoutFlag(self, flag): """ Test a layout flag :param int flag: Layout flag :return: True/False .. seealso:: :py:meth:`setLayoutFlag()` """ return self.__data.layoutFlags & flag def setTitle(self, title): """ Give title new text contents :param title: New title :type title: qwt.text.QwtText or str .. seealso:: :py:meth:`title()` """ if isinstance(title, QwtText): flags = title.renderFlags() & (~ int(Qt.AlignTop|Qt.AlignBottom)) title.setRenderFlags(flags) if title != self.__data.title: self.__data.title = title self.layoutScale() else: if self.__data.title.text() != title: self.__data.title.setText(title) self.layoutScale() def setAlignment(self, alignment): """ Change the alignment :param int alignment: New alignment Valid alignment values: see :py:class:`qwt.scale_draw.QwtScaleDraw` .. seealso:: :py:meth:`alignment()` """ if self.__data.scaleDraw: self.__data.scaleDraw.setAlignment(alignment) if not self.testAttribute(Qt.WA_WState_OwnSizePolicy): policy = QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) if self.__data.scaleDraw.orientation() == Qt.Vertical: policy.transpose() self.setSizePolicy(policy) self.setAttribute(Qt.WA_WState_OwnSizePolicy, False) self.layoutScale() def alignment(self): """ :return: position .. seealso:: :py:meth:`setAlignment()` """ if not self.scaleDraw(): return QwtScaleDraw.LeftScale return self.scaleDraw().alignment() def setBorderDist(self, dist1, dist2): """ Specify distances of the scale's endpoints from the widget's borders. The actual borders will never be less than minimum border distance. :param int dist1: Left or top Distance :param int dist2: Right or bottom distance .. seealso:: :py:meth:`borderDist()` """ if dist1 != self.__data.borderDist[0] or\ dist2 != self.__data.borderDist[1]: self.__data.borderDist = [dist1, dist2] self.layoutScale() def setMargin(self, margin): """ Specify the margin to the colorBar/base line. :param int margin: Margin .. seealso:: :py:meth:`margin()` """ margin = max([0, margin]) if margin != self.__data.margin: self.__data.margin = margin self.layoutScale() def setSpacing(self, spacing): """ Specify the distance between color bar, scale and title :param int spacing: Spacing .. seealso:: :py:meth:`spacing()` """ spacing = max([0, spacing]) if spacing != self.__data.spacing: self.__data.spacing = spacing self.layoutScale() def setLabelAlignment(self, alignment): """ Change the alignment for the labels. :param int spacing: Spacing .. seealso:: :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAlignment()`, :py:meth:`setLabelRotation()` """ self.__data.scaleDraw.setLabelAlignment(alignment) self.layoutScale() def setLabelRotation(self, rotation): """ Change the rotation for the labels. :param float rotation: Rotation .. seealso:: :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelRotation()`, :py:meth:`setLabelFlags()` """ self.__data.scaleDraw.setLabelRotation(rotation) self.layoutScale() def setLabelAutoSize(self, state): """ Set the automatic size option for labels (default: on). :param bool state: On/off .. seealso:: :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAutoSize()` """ self.__data.scaleDraw.setLabelAutoSize(state) self.layoutScale() def setScaleDraw(self, scaleDraw): """ Set a scale draw scaleDraw has to be created with new and will be deleted in class destructor or the next call of `setScaleDraw()`. scaleDraw will be initialized with the attributes of the previous scaleDraw object. :param qwt.scale_draw.QwtScaleDraw scaleDraw: ScaleDraw object .. seealso:: :py:meth:`scaleDraw()` """ if scaleDraw is None or scaleDraw == self.__data.scaleDraw: return sd = self.__data.scaleDraw if sd is not None: scaleDraw.setAlignment(sd.alignment()) scaleDraw.setScaleDiv(sd.scaleDiv()) transform = None if sd.scaleMap().transformation(): transform = sd.scaleMap().transformation().copy() scaleDraw.setTransformation(transform) self.__data.scaleDraw = scaleDraw self.layoutScale() def scaleDraw(self): """ :return: scaleDraw of this scale .. seealso:: :py:meth:`qwt.scale_draw.QwtScaleDraw.setScaleDraw()` """ return self.__data.scaleDraw def title(self): """ :return: title .. seealso:: :py:meth:`setTitle` """ return self.__data.title def startBorderDist(self): """ :return: start border distance .. seealso:: :py:meth:`setBorderDist` """ return self.__data.borderDist[0] def endBorderDist(self): """ :return: end border distance .. seealso:: :py:meth:`setBorderDist` """ return self.__data.borderDist[1] def margin(self): """ :return: margin .. seealso:: :py:meth:`setMargin` """ return self.__data.margin def spacing(self): """ :return: distance between scale and title .. seealso:: :py:meth:`setSpacing` """ return self.__data.spacing def paintEvent(self, event): painter = QPainter(self) painter.setClipRegion(event.region()) opt = QStyleOption() opt.initFrom(self) self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) self.draw(painter) def draw(self, painter): """ Draw the scale :param QPainter painter: Painter """ self.__data.scaleDraw.draw(painter, self.palette()) if self.__data.colorBar.isEnabled and\ self.__data.colorBar.width > 0 and\ self.__data.colorBar.interval.isValid(): self.drawColorBar(painter, self.colorBarRect(self.contentsRect())) r = self.contentsRect() if self.__data.scaleDraw.orientation() == Qt.Horizontal: r.setLeft(r.left() + self.__data.borderDist[0]) r.setWidth(r.width() - self.__data.borderDist[1]) else: r.setTop(r.top() + self.__data.borderDist[0]) r.setHeight(r.height() - self.__data.borderDist[1]) if not self.__data.title.isEmpty(): self.drawTitle(painter, self.__data.scaleDraw.alignment(), r) def colorBarRect(self, rect): """ Calculate the the rectangle for the color bar :param QRectF rect: Bounding rectangle for all components of the scale :return: Rectangle for the color bar """ cr = QRectF(rect) if self.__data.scaleDraw.orientation() == Qt.Horizontal: cr.setLeft(cr.left() + self.__data.borderDist[0]) cr.setWidth(cr.width() - self.__data.borderDist[1] + 1) else: cr.setTop(cr.top() + self.__data.borderDist[0]) cr.setHeight(cr.height() - self.__data.borderDist[1] + 1) sda = self.__data.scaleDraw.alignment() if sda == QwtScaleDraw.LeftScale: cr.setLeft(cr.right()-self.__data.margin-self.__data.colorBar.width) cr.setWidth(self.__data.colorBar.width) elif sda == QwtScaleDraw.RightScale: cr.setLeft(cr.left()+self.__data.margin) cr.setWidth(self.__data.colorBar.width) elif sda == QwtScaleDraw.BottomScale: cr.setTop(cr.top()+self.__data.margin) cr.setHeight(self.__data.colorBar.width) elif sda == QwtScaleDraw.TopScale: cr.setTop(cr.bottom()-self.__data.margin-self.__data.colorBar.width) cr.setHeight(self.__data.colorBar.width) return cr def resizeEvent(self, event): self.layoutScale(False) def layoutScale(self, update_geometry=True): """ Recalculate the scale's geometry and layout based on the current geometry and fonts. :param bool update_geometry: Notify the layout system and call update to redraw the scale """ bd0, bd1 = self.getBorderDistHint() if self.__data.borderDist[0] > bd0: bd0 = self.__data.borderDist[0] if self.__data.borderDist[1] > bd1: bd1 = self.__data.borderDist[1] colorBarWidth = 0 if self.__data.colorBar.isEnabled and\ self.__data.colorBar.interval.isValid(): colorBarWidth = self.__data.colorBar.width + self.__data.spacing r = self.contentsRect() if self.__data.scaleDraw.orientation() == Qt.Vertical: y = r.top() + bd0 length = r.height() - (bd0 +bd1) if self.__data.scaleDraw.alignment() == QwtScaleDraw.LeftScale: x = r.right() - 1. - self.__data.margin - colorBarWidth else: x = r.left() + self.__data.margin + colorBarWidth else: x = r.left() + bd0 length = r.width() - (bd0 + bd1) if self.__data.scaleDraw.alignment() == QwtScaleDraw.BottomScale: y = r.top() + self.__data.margin + colorBarWidth else: y = r.bottom() - 1. - self.__data.margin - colorBarWidth self.__data.scaleDraw.move(x, y) self.__data.scaleDraw.setLength(length) extent = np.ceil(self.__data.scaleDraw.extent(self.font())) self.__data.titleOffset = self.__data.margin + self.__data.spacing +\ colorBarWidth + extent if update_geometry: self.updateGeometry() self.update() def drawColorBar(self, painter, rect): """ Draw the color bar of the scale widget :param QPainter painter: Painter :param QRectF rect: Bounding rectangle for the color bar .. seealso:: :py:meth:`setColorBarEnabled()` """ if not self.__data.colorBar.interval.isValid(): return sd = self.__data.scaleDraw QwtPainter.drawColorBar(painter, self.__data.colorBar.colorMap, self.__data.colorBar.interval.normalized(), sd.scaleMap(), sd.orientation(), rect) def drawTitle(self, painter, align, rect): """ Rotate and paint a title according to its position into a given rectangle. :param QPainter painter: Painter :param int align: Alignment :param QRectF rect: Bounding rectangle """ r = rect flags = self.__data.title.renderFlags()\ &(~ int(Qt.AlignTop|Qt.AlignBottom|Qt.AlignVCenter)) if align == QwtScaleDraw.LeftScale: angle = -90. flags |= Qt.AlignTop r.setRect(r.left(), r.bottom(), r.height(), r.width()-self.__data.titleOffset) elif align == QwtScaleDraw.RightScale: angle = -90. flags |= Qt.AlignTop r.setRect(r.left()+self.__data.titleOffset, r.bottom(), r.height(), r.width()-self.__data.titleOffset) elif align == QwtScaleDraw.BottomScale: angle = 0. flags |= Qt.AlignBottom r.setTop(r.top()+self.__data.titleOffset) else: angle = 0. flags |= Qt.AlignTop r.setBottom(r.bottom()-self.__data.titleOffset) if self.__data.layoutFlags & self.TitleInverted: if align in (QwtScaleDraw.LeftScale, QwtScaleDraw.RightScale): angle = -angle r.setRect(r.x()+r.height(), r.y()-r.width(), r.width(), r.height()) painter.save() painter.setFont(self.font()) painter.setPen(self.palette().color(QPalette.Text)) painter.translate(r.x(), r.y()) if angle != 0.: painter.rotate(angle) title = self.__data.title title.setRenderFlags(flags) title.draw(painter, QRectF(0., 0., r.width(), r.height())) painter.restore() def scaleChange(self): """ Notify a change of the scale This method can be overloaded by derived classes. The default implementation updates the geometry and repaints the widget. """ self.layoutScale() def sizeHint(self): return self.minimumSizeHint() def minimumSizeHint(self): o = self.__data.scaleDraw.orientation() length = 0 mbd1, mbd2 = self.getBorderDistHint() length += max([0, self.__data.borderDist[0]-mbd1]) length += max([0, self.__data.borderDist[1]-mbd2]) length += self.__data.scaleDraw.minLength(self.font()) dim = self.dimForLength(length, self.font()) if length < dim: length = dim dim = self.dimForLength(length, self.font()) size = QSize(length+2, dim) if o == Qt.Vertical: size.transpose() left, right, top, bottom = self.getContentsMargins() return size + QSize(left + right, top + bottom) def titleHeightForWidth(self, width): """ Find the height of the title for a given width. :param int width: Width :return: Height """ return np.ceil(self.__data.title.heightForWidth(width, self.font())) def dimForLength(self, length, scaleFont): """ Find the minimum dimension for a given length. dim is the height, length the width seen in direction of the title. :param int length: width for horizontal, height for vertical scales :param QFont scaleFont: Font of the scale :return: height for horizontal, width for vertical scales """ extent = np.ceil(self.__data.scaleDraw.extent(scaleFont)) dim = self.__data.margin + extent + 1 if not self.__data.title.isEmpty(): dim += self.titleHeightForWidth(length)+self.__data.spacing if self.__data.colorBar.isEnabled and self.__data.colorBar.interval.isValid(): dim += self.__data.colorBar.width+self.__data.spacing return dim def getBorderDistHint(self): """ Calculate a hint for the border distances. This member function calculates the distance of the scale's endpoints from the widget borders which is required for the mark labels to fit into the widget. The maximum of this distance an the minimum border distance is returned. :param int start: Return parameter for the border width at the beginning of the scale :param int end: Return parameter for the border width at the end of the scale .. warning:: The minimum border distance depends on the font. .. seealso:: :py:meth:`setMinBorderDist()`, :py:meth:`getMinBorderDist()`, :py:meth:`setBorderDist()` """ start, end = self.__data.scaleDraw.getBorderDistHint(self.font()) if start < self.__data.minBorderDist[0]: start = self.__data.minBorderDist[0] if end < self.__data.minBorderDist[1]: end = self.__data.minBorderDist[1] return start, end def setMinBorderDist(self, start, end): """ Set a minimum value for the distances of the scale's endpoints from the widget borders. This is useful to avoid that the scales are "jumping", when the tick labels or their positions change often. :param int start: Minimum for the start border :param int end: Minimum for the end border .. seealso:: :py:meth:`getMinBorderDist()`, :py:meth:`getBorderDistHint()` """ self.__data.minBorderDist = [start, end] def getMinBorderDist(self): """ Get the minimum value for the distances of the scale's endpoints from the widget borders. :param int start: Return parameter for the border width at the beginning of the scale :param int end: Return parameter for the border width at the end of the scale .. seealso:: :py:meth:`setMinBorderDist()`, :py:meth:`getBorderDistHint()` """ return self.__data.minBorderDist def setScaleDiv(self, scaleDiv): """ Assign a scale division The scale division determines where to set the tick marks. :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale Division .. seealso:: For more information about scale divisions, see :py:class:`qwt.scale_div.QwtScaleDiv`. """ sd = self.__data.scaleDraw if sd.scaleDiv() != scaleDiv: sd.setScaleDiv(scaleDiv) self.layoutScale() self.scaleDivChanged.emit() def setTransformation(self, transformation): """ Set the transformation :param qwt.transform.Transform transformation: Transformation .. seealso:: :py:meth:`qwt.scale_draw.QwtAbstractScaleDraw.scaleDraw()`, :py:class:`qwt.scale_map.QwtScaleMap` """ self.__data.scaleDraw.setTransformation(transformation) self.layoutScale() def setColorBarEnabled(self, on): """ En/disable a color bar associated to the scale :param bool on: On/Off .. seealso:: :py:meth:`isColorBarEnabled()`, :py:meth:`setColorBarWidth()` """ if on != self.__data.colorBar.isEnabled: self.__data.colorBar.isEnabled = on self.layoutScale() def isColorBarEnabled(self): """ :return: True, when the color bar is enabled .. seealso:: :py:meth:`setColorBarEnabled()`, :py:meth:`setColorBarWidth()` """ return self.__data.colorBar.isEnabled def setColorBarWidth(self, width): """ Set the width of the color bar :param int width: Width .. seealso:: :py:meth:`colorBarWidth()`, :py:meth:`setColorBarEnabled()` """ if width != self.__data.colorBar.width: self.__data.colorBar.width = width if self.isColorBarEnabled(): self.layoutScale() def colorBarWidth(self): """ :return: Width of the color bar .. seealso:: :py:meth:`setColorBarWidth()`, :py:meth:`setColorBarEnabled()` """ return self.__data.colorBar.width def colorBarInterval(self): """ :return: Value interval for the color bar .. seealso:: :py:meth:`setColorMap()`, :py:meth:`colorMap()` """ return self.__data.colorBar.interval def setColorMap(self, interval, colorMap): """ Set the color map and value interval, that are used for displaying the color bar. :param qwt.interval.QwtInterval interval: Value interval :param qwt.color_map.QwtColorMap colorMap: Color map .. seealso:: :py:meth:`colorMap()`, :py:meth:`colorBarInterval()` """ self.__data.colorBar.interval = interval if colorMap != self.__data.colorBar.colorMap: self.__data.colorBar.colorMap = colorMap if self.isColorBarEnabled(): self.layoutScale() def colorMap(self): """ :return: Color map .. seealso:: :py:meth:`setColorMap()`, :py:meth:`colorBarInterval()` """ return self.__data.colorBar.colorMap PythonQwt-0.5.5/qwt/plot_curve.py0000666000000000000000000010110712632324736015557 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPlotCurve ------------ .. autoclass:: QwtPlotCurve :members: """ from qwt.text import QwtText from qwt.plot import QwtPlotItem, QwtPlotItem_PrivateData from qwt.painter import QwtPainter from qwt.math import qwtSqr from qwt.graphic import QwtGraphic from qwt.plot_series import (QwtPlotSeriesItem, QwtSeriesStore, QwtSeriesData, QwtPointArrayData) from qwt.symbol import QwtSymbol from qwt.plot_directpainter import QwtPlotDirectPainter from qwt.qt.QtGui import QPen, QBrush, QPainter, QPolygonF, QColor from qwt.qt.QtCore import QSize, Qt, QRectF, QPointF import numpy as np def qwtUpdateLegendIconSize(curve): if curve.symbol() and\ curve.testLegendAttribute(QwtPlotCurve.LegendShowSymbol): sz = curve.symbol().boundingRect().size() sz += QSize(2, 2) if curve.testLegendAttribute(QwtPlotCurve.LegendShowLine): w = np.ceil(1.5*sz.width()) if w % 2: w += 1 sz.setWidth(max([8, w])) curve.setLegendIconSize(sz) def qwtVerifyRange(size, i1, i2): if size < 1: return 0 i1 = max([0, min([i1, size-1])]) i2 = max([0, min([i2, size-1])]) if i1 > i2: i1, i2 = i2, i1 return i2-i1+1 def series_to_polyline(xMap, yMap, series, from_, to): """ Convert series data to QPolygon(F) polyline """ polyline = QPolygonF(to-from_+1) pointer = polyline.data() dtype, tinfo = np.float, np.finfo # integers: = np.int, np.iinfo pointer.setsize(2*polyline.size()*tinfo(dtype).dtype.itemsize) memory = np.frombuffer(pointer, dtype) memory[:(to-from_)*2+1:2] = xMap.transform(series.xData()[from_:to+1]) memory[1:(to-from_)*2+2:2] = yMap.transform(series.yData()[from_:to+1]) return polyline class QwtPlotCurve_PrivateData(QwtPlotItem_PrivateData): def __init__(self): QwtPlotItem_PrivateData.__init__(self) self.style = QwtPlotCurve.Lines self.baseline = 0. self.symbol = None self.attributes = 0 self.legendAttributes = QwtPlotCurve.LegendShowLine self.pen = QPen(Qt.black) self.brush = QBrush() class QwtPlotCurve(QwtPlotSeriesItem, QwtSeriesStore): """ A plot item, that represents a series of points A curve is the representation of a series of points in the x-y plane. It supports different display styles and symbols. .. seealso:: :py:class:`qwt.symbol.QwtSymbol()`, :py:class:`qwt.scale_map.QwtScaleMap()` Curve styles: * `QwtPlotCurve.NoCurve`: Don't draw a curve. Note: This doesn't affect the symbols. * `QwtPlotCurve.Lines`: Connect the points with straight lines. * `QwtPlotCurve.Sticks`: Draw vertical or horizontal sticks ( depending on the orientation() ) from a baseline which is defined by setBaseline(). * `QwtPlotCurve.Steps`: Connect the points with a step function. The step function is drawn from the left to the right or vice versa, depending on the QwtPlotCurve::Inverted attribute. * `QwtPlotCurve.Dots`: Draw dots at the locations of the data points. Note: This is different from a dotted line (see setPen()), and faster as a curve in QwtPlotCurve::NoStyle style and a symbol painting a point. * `QwtPlotCurve.UserCurve`: Styles >= QwtPlotCurve.UserCurve are reserved for derived classes of QwtPlotCurve that overload drawCurve() with additional application specific curve types. Curve attributes: * `QwtPlotCurve.Inverted`: For `QwtPlotCurve.Steps` only. Draws a step function from the right to the left. Legend attributes: * `QwtPlotCurve.LegendNoAttribute`: `QwtPlotCurve` tries to find a color representing the curve and paints a rectangle with it. * `QwtPlotCurve.LegendShowLine`: If the style() is not `QwtPlotCurve.NoCurve` a line is painted with the curve pen(). * `QwtPlotCurve.LegendShowSymbol`: If the curve has a valid symbol it is painted. * `QwtPlotCurve.LegendShowBrush`: If the curve has a brush a rectangle filled with the curve brush() is painted. .. py:class:: QwtPlotCurve([title=None]) Constructor :param title: Curve title :type title: qwt.text.QwtText or str or None """ # enum CurveStyle NoCurve = -1 Lines, Sticks, Steps, Dots = list(range(4)) UserCurve = 100 # enum CurveAttribute Inverted = 0x01 # enum LegendAttribute LegendNoAttribute = 0x00 LegendShowLine = 0x01 LegendShowSymbol = 0x02 LegendShowBrush = 0x04 def __init__(self, title=None): if title is None: title = QwtText("") if not isinstance(title, QwtText): title = QwtText(title) self.__data = None QwtPlotSeriesItem.__init__(self, title) QwtSeriesStore.__init__(self) self.init() def init(self): """Initialize internal members""" self.__data = QwtPlotCurve_PrivateData() self.setItemAttribute(QwtPlotItem.Legend) self.setItemAttribute(QwtPlotItem.AutoScale) self.setData(QwtPointArrayData()) self.setZ(20.) def rtti(self): """:return: `QwtPlotItem.Rtti_PlotCurve`""" return QwtPlotItem.Rtti_PlotCurve def setLegendAttribute(self, attribute, on=True): """ Specify an attribute how to draw the legend icon Legend attributes: * `QwtPlotCurve.LegendNoAttribute` * `QwtPlotCurve.LegendShowLine` * `QwtPlotCurve.LegendShowSymbol` * `QwtPlotCurve.LegendShowBrush` :param int attribute: Legend attribute :param bool on: On/Off .. seealso:: :py:meth:`testLegendAttribute()`, :py:meth:`legendIcon()` """ if on != self.testLegendAttribute(attribute): if on: self.__data.legendAttributes |= attribute else: self.__data.legendAttributes &= ~attribute qwtUpdateLegendIconSize(self) self.legendChanged() def testLegendAttribute(self, attribute): """ :param int attribute: Legend attribute :return: True, when attribute is enabled .. seealso:: :py:meth:`setLegendAttribute()` """ return self.__data.legendAttributes & attribute def setStyle(self, style): """ Set the curve's drawing style Valid curve styles: * `QwtPlotCurve.NoCurve` * `QwtPlotCurve.Lines` * `QwtPlotCurve.Sticks` * `QwtPlotCurve.Steps` * `QwtPlotCurve.Dots` * `QwtPlotCurve.UserCurve` :param int style: Curve style .. seealso:: :py:meth:`style()` """ if style != self.__data.style: self.__data.style = style self.legendChanged() self.itemChanged() def style(self): """ :return: Style of the curve .. seealso:: :py:meth:`setStyle()` """ return self.__data.style def setSymbol(self, symbol): """ Assign a symbol The curve will take the ownership of the symbol, hence the previously set symbol will be delete by setting a new one. If symbol is None no symbol will be drawn. :param qwt.symbol.QwtSymbol symbol: Symbol .. seealso:: :py:meth:`symbol()` """ if symbol != self.__data.symbol: self.__data.symbol = symbol qwtUpdateLegendIconSize(self) self.legendChanged() self.itemChanged() def symbol(self): """ :return: Current symbol or None, when no symbol has been assigned .. seealso:: :py:meth:`setSymbol()` """ return self.__data.symbol def setPen(self, *args): """ Build and/or assign a pen, depending on the arguments. .. py:method:: setPen(color, width, style) Build and assign a pen In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it non cosmetic (see `QPen.isCosmetic()`). This method signature has been introduced to hide this incompatibility. :param QColor color: Pen color :param float width: Pen width :param Qt.PenStyle style: Pen style .. py:method:: setPen(pen) Assign a pen :param QPen pen: New pen .. seealso:: :py:meth:`pen()`, :py:meth:`brush()` """ if len(args) == 3: color, width, style = args elif len(args) == 1: pen, = args else: raise TypeError("%s().setPen() takes 1 or 3 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) if pen != self.__data.pen: if isinstance(pen, QColor): pen = QPen(pen) else: assert isinstance(pen, QPen) self.__data.pen = pen self.legendChanged() self.itemChanged() def pen(self): """ :return: Pen used to draw the lines .. seealso:: :py:meth:`setPen()`, :py:meth:`brush()` """ return self.__data.pen def setBrush(self, brush): """ Assign a brush. In case of `brush.style() != QBrush.NoBrush` and `style() != QwtPlotCurve.Sticks` the area between the curve and the baseline will be filled. In case `not brush.color().isValid()` the area will be filled by `pen.color()`. The fill algorithm simply connects the first and the last curve point to the baseline. So the curve data has to be sorted (ascending or descending). :param brush: New brush :type brush: QBrush or QColor .. seealso:: :py:meth:`brush()`, :py:meth:`setBaseline()`, :py:meth:`baseline()` """ if isinstance(brush, QColor): brush = QBrush(brush) else: assert isinstance(brush, QBrush) if brush != self.__data.brush: self.__data.brush = brush self.legendChanged() self.itemChanged() def brush(self): """ :return: Brush used to fill the area between lines and the baseline .. seealso:: :py:meth:`setBrush()`, :py:meth:`setBaseline()`, :py:meth:`baseline()` """ return self.__data.brush def directPaint(self, from_, to): """ When observing a measurement while it is running, new points have to be added to an existing seriesItem. This method can be used to display them avoiding a complete redraw of the canvas. Setting `plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)` will result in faster painting, if the paint engine of the canvas widget supports this feature. :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted .. seealso:: :py:meth:`drawSeries()` """ directPainter = QwtPlotDirectPainter(self.plot()) directPainter.drawSeries(self, from_, to) def drawSeries(self, painter, xMap, yMap, canvasRect, from_, to): """ Draw an interval of the curve :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. .. seealso:: :py:meth:`drawCurve()`, :py:meth:`drawSymbols()` """ numSamples = self.dataSize() if not painter or numSamples <= 0: return if to < 0: to = numSamples-1 if qwtVerifyRange(numSamples, from_, to) > 0: painter.save() painter.setPen(self.__data.pen) self.drawCurve(painter, self.__data.style, xMap, yMap, canvasRect, from_, to) painter.restore() if self.__data.symbol and\ self.__data.symbol.style() != QwtSymbol.NoSymbol: painter.save() self.drawSymbols(painter, self.__data.symbol, xMap, yMap, canvasRect, from_, to) painter.restore() def drawCurve(self, painter, style, xMap, yMap, canvasRect, from_, to): """ Draw the line part (without symbols) of a curve interval. :param QPainter painter: Painter :param int style: curve style, see `QwtPlotCurve.CurveStyle` :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. .. seealso:: :py:meth:`draw()`, :py:meth:`drawDots()`, :py:meth:`drawLines()`, :py:meth:`drawSteps()`, :py:meth:`drawSticks()` """ if style == self.Lines: self.drawLines(painter, xMap, yMap, canvasRect, from_, to) elif style == self.Sticks: self.drawSticks(painter, xMap, yMap, canvasRect, from_, to) elif style == self.Steps: self.drawSteps(painter, xMap, yMap, canvasRect, from_, to) elif style == self.Dots: self.drawDots(painter, xMap, yMap, canvasRect, from_, to) def drawLines(self, painter, xMap, yMap, canvasRect, from_, to): """ Draw lines :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. .. seealso:: :py:meth:`draw()`, :py:meth:`drawDots()`, :py:meth:`drawSteps()`, :py:meth:`drawSticks()` """ if from_ > to: return doFill = self.__data.brush.style() != Qt.NoBrush\ and self.__data.brush.color().alpha() > 0 polyline = series_to_polyline(xMap, yMap, self.data(), from_, to) painter.drawPolyline(polyline) if doFill: self.fillCurve(painter, xMap, yMap, canvasRect, polyline) def drawSticks(self, painter, xMap, yMap, canvasRect, from_, to): """ Draw sticks :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. .. seealso:: :py:meth:`draw()`, :py:meth:`drawDots()`, :py:meth:`drawSteps()`, :py:meth:`drawLines()` """ painter.save() painter.setRenderHint(QPainter.Antialiasing, False) x0 = xMap.transform(self.__data.baseline) y0 = yMap.transform(self.__data.baseline) o = self.orientation() series = self.data() for i in range(from_, to+1): sample = series.sample(i) xi = xMap.transform(sample.x()) yi = yMap.transform(sample.y()) if o == Qt.Horizontal: painter.drawLine(xi, y0, xi, yi) else: painter.drawLine(x0, yi, xi, yi) painter.restore() def drawDots(self, painter, xMap, yMap, canvasRect, from_, to): """ Draw dots :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. .. seealso:: :py:meth:`draw()`, :py:meth:`drawSticks()`, :py:meth:`drawSteps()`, :py:meth:`drawLines()` """ doFill = self.__data.brush.style() != Qt.NoBrush\ and self.__data.brush.color().alpha() > 0 polyline = series_to_polyline(xMap, yMap, self.data(), from_, to) painter.drawPoints(polyline) if doFill: self.fillCurve(painter, xMap, yMap, canvasRect, polyline) def drawSteps(self, painter, xMap, yMap, canvasRect, from_, to): """ Draw steps :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. .. seealso:: :py:meth:`draw()`, :py:meth:`drawSticks()`, :py:meth:`drawDots()`, :py:meth:`drawLines()` """ polygon = QPolygonF(2*(to-from_)+1) inverted = self.orientation() == Qt.Vertical if self.__data.attributes & self.Inverted: inverted = not inverted series = self.data() ip = 0 for i in range(from_, to+1): sample = series.sample(i) xi = xMap.transform(sample.x()) yi = yMap.transform(sample.y()) if ip > 0: p0 = polygon[ip-2] if inverted: polygon[ip-1] = QPointF(p0.x(), yi) else: polygon[ip-1] = QPointF(xi, p0.y()) polygon[ip] = QPointF(xi, yi) ip += 2 painter.drawPolyline(polygon) if self.__data.brush.style() != Qt.NoBrush: self.fillCurve(painter, xMap, yMap, canvasRect, polygon) def setCurveAttribute(self, attribute, on=True): """ Specify an attribute for drawing the curve Supported curve attributes: * `QwtPlotCurve.Inverted` :param int attribute: Curve attribute :param bool on: On/Off .. seealso:: :py:meth:`testCurveAttribute()` """ if (self.__data.attributes & attribute) == on: return if on: self.__data.attributes |= attribute else: self.__data.attributes &= ~attribute self.itemChanged() def testCurveAttribute(self, attribute): """ :return: True, if attribute is enabled .. seealso:: :py:meth:`setCurveAttribute()` """ return self.__data.attributes & attribute def fillCurve(self, painter, xMap, yMap, canvasRect, polygon): """ Fill the area between the curve and the baseline with the curve brush :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas :param QPolygonF polygon: Polygon - will be modified ! .. seealso:: :py:meth:`setBrush()`, :py:meth:`setBaseline()`, :py:meth:`setStyle()` """ if self.__data.brush.style() == Qt.NoBrush: return self.closePolyline(painter, xMap, yMap, polygon) if polygon.count() <= 2: return brush = self.__data.brush if not brush.color().isValid(): brush.setColor(self.__data.pen.color()) painter.save() painter.setPen(Qt.NoPen) painter.setBrush(brush) painter.drawPolygon(polygon) painter.restore() def closePolyline(self, painter, xMap, yMap, polygon): """ Complete a polygon to be a closed polygon including the area between the original polygon and the baseline. :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QPolygonF polygon: Polygon to be completed """ if polygon.size() < 2: return baseline = self.__data.baseline if self.orientation() == Qt.Horizontal: if yMap.transformation(): baseline = yMap.transformation().bounded(baseline) refY = yMap.transform(baseline) polygon += QPointF(polygon.last().x(), refY) polygon += QPointF(polygon.first().x(), refY) else: if xMap.transformation(): baseline = xMap.transformation().bounded(baseline) refX = xMap.transform(baseline) polygon += QPointF(refX, polygon.last().y()) polygon += QPointF(refX, polygon.first().y()) def drawSymbols(self, painter, symbol, xMap, yMap, canvasRect, from_, to): """ Draw symbols :param QPainter painter: Painter :param qwt.symbol.QwtSymbol symbol: Curve symbol :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted. If to < 0 the curve will be painted to its last point. .. seealso:: :py:meth:`setSymbol()`, :py:meth:`drawSeries()`, :py:meth:`drawCurve()` """ chunkSize = 500 for i in range(from_, to+1, chunkSize): n = min([chunkSize, to-i+1]) points = series_to_polyline(xMap, yMap, self.data(), i, i+n-1) if points.size() > 0: symbol.drawSymbols(painter, points) def setBaseline(self, value): """ Set the value of the baseline The baseline is needed for filling the curve with a brush or the Sticks drawing style. The interpretation of the baseline depends on the `orientation()`. With `Qt.Horizontal`, the baseline is interpreted as a horizontal line at y = baseline(), with `Qt.Vertical`, it is interpreted as a vertical line at x = baseline(). The default value is 0.0. :param float value: Value of the baseline .. seealso:: :py:meth:`baseline()`, :py:meth:`setBrush()`, :py:meth:`setStyle()` """ if self.__data.baseline != value: self.__data.baseline = value self.itemChanged() def baseline(self): """ :return: Value of the baseline .. seealso:: :py:meth:`setBaseline()` """ return self.__data.baseline def closestPoint(self, pos): """ Find the closest curve point for a specific position :param QPoint pos: Position, where to look for the closest curve point :return: tuple `(index, dist)` `dist` is the distance between the position and the closest curve point. `index` is the index of the closest curve point, or -1 if none can be found ( f.e when the curve has no points ). .. note:: `closestPoint()` implements a dumb algorithm, that iterates over all points """ numSamples = self.dataSize() if self.plot() is None or numSamples <= 0: return -1 series = self.data() xMap = self.plot().canvasMap(self.xAxis()) yMap = self.plot().canvasMap(self.yAxis()) index = -1 dmin = 1.0e10 for i in range(numSamples): sample = series.sample(i) cx = xMap.transform(sample.x())-pos.x() cy = yMap.transform(sample.y())-pos.y() f = qwtSqr(cx)+qwtSqr(cy) if f < dmin: index = i dmin = f dist = np.sqrt(dmin) return index, dist def legendIcon(self, index, size): """ :param int index: Index of the legend entry (ignored as there is only one) :param QSizeF size: Icon size :return: Icon representing the curve on the legend .. seealso:: :py:meth:`qwt.plot.QwtPlotItem.setLegendIconSize()`, :py:meth:`qwt.plot.QwtPlotItem.legendData()` """ if size.isEmpty(): return QwtGraphic() graphic = QwtGraphic() graphic.setDefaultSize(size) graphic.setRenderHint(QwtGraphic.RenderPensUnscaled, True) painter = QPainter(graphic) painter.setRenderHint(QPainter.Antialiasing, self.testRenderHint(QwtPlotItem.RenderAntialiased)) if self.__data.legendAttributes == 0 or\ (self.__data.legendAttributes & QwtPlotCurve.LegendShowBrush): brush = self.__data.brush if brush.style() == Qt.NoBrush and self.__data.legendAttributes == 0: if self.style() != QwtPlotCurve.NoCurve: brush = QBrush(self.pen().color()) elif self.__data.symbol and\ self.__data.symbol.style() != QwtSymbol.NoSymbol: brush = QBrush(self.__data.symbol.pen().color()) if brush.style() != Qt.NoBrush: r = QRectF(0, 0, size.width(), size.height()) painter.fillRect(r, brush) if self.__data.legendAttributes & QwtPlotCurve.LegendShowLine: if self.pen() != Qt.NoPen: pn = self.pen() # pn.setCapStyle(Qt.FlatCap) painter.setPen(pn) y = .5*size.height() painter.drawLine(0., y, size.width(), y) if self.__data.legendAttributes & QwtPlotCurve.LegendShowSymbol: if self.__data.symbol: r = QRectF(0, 0, size.width(), size.height()) self.__data.symbol.drawSymbol(painter, r) return graphic def setData(self, *args, **kwargs): """ Initialize data with a series data object or an array of points. .. py:method:: setData(data): :param data: Series data (e.g. `QwtPointArrayData` instance) :type data: qwt.plot_series.QwtSeriesData .. py:method:: setData(xData, yData, [size=None], [finite=True]): Initialize data with `x` and `y` arrays. This signature was removed in Qwt6 and is temporarily maintained here to ensure compatibility with Qwt5. Same as `setSamples(x, y, [size=None], [finite=True])` :param x: List/array of x values :param y: List/array of y values :param size: size of xData and yData :type size: int or None :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements .. seealso:: :py:meth:`setSamples()` """ if len(args) == 1 and not kwargs: super(QwtPlotCurve, self).setData(*args) elif len(args) in (2, 3, 4): self.setSamples(*args, **kwargs) else: raise TypeError("%s().setData() takes 1, 2, 3 or 4 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def setSamples(self, *args, **kwargs): """ Initialize data with an array of points. .. py:method:: setSamples(data): :param data: Series data (e.g. `QwtPointArrayData` instance) :type data: qwt.plot_series.QwtSeriesData .. py:method:: setSamples(samples): Same as `setData(QwtPointArrayData(samples))` :param samples: List/array of points .. py:method:: setSamples(xData, yData, [size=None], [finite=True]): Same as `setData(QwtPointArrayData(xData, yData, [size=None]))` :param xData: List/array of x values :param yData: List/array of y values :param size: size of xData and yData :type size: int or None :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements .. seealso:: :py:class:`qwt.plot_series.QwtPointArrayData` """ if len(args) == 1 and not kwargs: samples, = args if isinstance(samples, QwtSeriesData): self.setData(samples) else: self.setData(QwtPointArrayData(samples)) elif len(args) >= 2: xData, yData = args[:2] try: size = kwargs.pop('size') except KeyError: size = None try: finite = kwargs.pop('finite') except KeyError: finite = None if kwargs: raise TypeError("%s().setSamples(): unknown %s keyword "\ "argument(s)"\ % (self.__class__.__name__, ", ".join(list(kwargs.keys())))) for arg in args[2:]: if isinstance(arg, bool): finite = arg elif isinstance(arg, int): size = arg self.setData(QwtPointArrayData(xData, yData, size=size, finite=finite)) else: raise TypeError("%s().setSamples() takes 1, 2 or 3 argument(s) "\ "(%s given)" % (self.__class__.__name__, len(args))) PythonQwt-0.5.5/qwt/plot_grid.py0000666000000000000000000003203312615225450015353 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPlotGrid ----------- .. autoclass:: QwtPlotGrid :members: """ from qwt.scale_div import QwtScaleDiv from qwt.plot import QwtPlotItem from qwt.text import QwtText from qwt.painter import QwtPainter from qwt.math import qwtFuzzyGreaterOrEqual, qwtFuzzyLessOrEqual from qwt.qt.QtGui import QPen from qwt.qt.QtCore import Qt class QwtPlotGrid_PrivateData(object): def __init__(self): self.xEnabled = True self.yEnabled = True self.xMinEnabled = False self.yMinEnabled = False self.xScaleDiv = QwtScaleDiv() self.yScaleDiv = QwtScaleDiv() self.majorPen = QPen() self.minorPen = QPen() class QwtPlotGrid(QwtPlotItem): """ A class which draws a coordinate grid The `QwtPlotGrid` class can be used to draw a coordinate grid. A coordinate grid consists of major and minor vertical and horizontal grid lines. The locations of the grid lines are determined by the X and Y scale divisions which can be assigned with `setXDiv()` and `setYDiv()`. The `draw()` member draws the grid within a bounding rectangle. """ def __init__(self): QwtPlotItem.__init__(self, QwtText("Grid")) self.__data = QwtPlotGrid_PrivateData() self.setItemInterest(QwtPlotItem.ScaleInterest, True) self.setZ(10.) def rtti(self): """ :return: Return `QwtPlotItem.Rtti_PlotGrid` """ return QwtPlotItem.Rtti_PlotGrid def enableX(self, on): """ Enable or disable vertical grid lines :param bool on: Enable (true) or disable .. seealso:: :py:meth:`enableXMin()` """ if self.__data.xEnabled != on: self.__data.xEnabled = on self.legendChanged() self.itemChanged() def enableY(self, on): """ Enable or disable horizontal grid lines :param bool on: Enable (true) or disable .. seealso:: :py:meth:`enableYMin()` """ if self.__data.yEnabled != on: self.__data.yEnabled = on self.legendChanged() self.itemChanged() def enableXMin(self, on): """ Enable or disable minor vertical grid lines. :param bool on: Enable (true) or disable .. seealso:: :py:meth:`enableX()` """ if self.__data.xMinEnabled != on: self.__data.xMinEnabled = on self.legendChanged() self.itemChanged() def enableYMin(self, on): """ Enable or disable minor horizontal grid lines. :param bool on: Enable (true) or disable .. seealso:: :py:meth:`enableY()` """ if self.__data.yMinEnabled != on: self.__data.yMinEnabled = on self.legendChanged() self.itemChanged() def setXDiv(self, scaleDiv): """ Assign an x axis scale division :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division """ if self.__data.xScaleDiv != scaleDiv: self.__data.xScaleDiv = scaleDiv self.itemChanged() def setYDiv(self, scaleDiv): """ Assign an y axis scale division :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division """ if self.__data.yScaleDiv != scaleDiv: self.__data.yScaleDiv = scaleDiv self.itemChanged() def setPen(self, *args): """ Build and/or assign a pen for both major and minor grid lines .. py:method:: setPen(color, width, style) Build and assign a pen for both major and minor grid lines In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it non cosmetic (see `QPen.isCosmetic()`). This method signature has been introduced to hide this incompatibility. :param QColor color: Pen color :param float width: Pen width :param Qt.PenStyle style: Pen style .. py:method:: setPen(pen) Assign a pen for both major and minor grid lines :param QPen pen: New pen .. seealso:: :py:meth:`pen()`, :py:meth:`brush()` """ if len(args) == 3: color, width, style = args self.setPen(QPen(color, width, style)) elif len(args) == 1: pen, = args if self.__data.majorPen != pen or self.__data.minorPen != pen: self.__data.majorPen = pen self.__data.minorPen = pen self.legendChanged() self.itemChanged() else: raise TypeError("%s().setPen() takes 1 or 3 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def setMajorPen(self, *args): """ Build and/or assign a pen for both major grid lines .. py:method:: setMajorPen(color, width, style) Build and assign a pen for both major grid lines In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it non cosmetic (see `QPen.isCosmetic()`). This method signature has been introduced to hide this incompatibility. :param QColor color: Pen color :param float width: Pen width :param Qt.PenStyle style: Pen style .. py:method:: setMajorPen(pen) Assign a pen for the major grid lines :param QPen pen: New pen .. seealso:: :py:meth:`majorPen()`, :py:meth:`setMinorPen()`, :py:meth:`setPen()`, :py:meth:`pen()`, :py:meth:`brush()` """ if len(args) == 3: color, width, style = args self.setMajorPen(QPen(color, width, style)) elif len(args) == 1: pen, = args if self.__data.majorPen != pen: self.__data.majorPen = pen self.legendChanged() self.itemChanged() else: raise TypeError("%s().setMajorPen() takes 1 or 3 argument(s) (%s "\ "given)" % (self.__class__.__name__, len(args))) def setMinorPen(self, *args): """ Build and/or assign a pen for both minor grid lines .. py:method:: setMinorPen(color, width, style) Build and assign a pen for both minor grid lines In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it non cosmetic (see `QPen.isCosmetic()`). This method signature has been introduced to hide this incompatibility. :param QColor color: Pen color :param float width: Pen width :param Qt.PenStyle style: Pen style .. py:method:: setMinorPen(pen) Assign a pen for the minor grid lines :param QPen pen: New pen .. seealso:: :py:meth:`minorPen()`, :py:meth:`setMajorPen()`, :py:meth:`setPen()`, :py:meth:`pen()`, :py:meth:`brush()` """ if len(args) == 3: color, width, style = args self.setMinorPen(QPen(color, width, style)) elif len(args) == 1: pen, = args if self.__data.minorPen != pen: self.__data.minorPen = pen self.legendChanged() self.itemChanged() else: raise TypeError("%s().setMinorPen() takes 1 or 3 argument(s) (%s "\ "given)" % (self.__class__.__name__, len(args))) def draw(self, painter, xMap, yMap, canvasRect): """ Draw the grid The grid is drawn into the bounding rectangle such that grid lines begin and end at the rectangle's borders. The X and Y maps are used to map the scale divisions into the drawing region screen. :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: X axis map :param qwt.scale_map.QwtScaleMap yMap: Y axis :param QRectF canvasRect: Contents rectangle of the plot canvas """ minorPen = QPen(self.__data.minorPen) minorPen.setCapStyle(Qt.FlatCap) painter.setPen(minorPen) if self.__data.xEnabled and self.__data.xMinEnabled: self.drawLines(painter, canvasRect, Qt.Vertical, xMap, self.__data.xScaleDiv.ticks(QwtScaleDiv.MinorTick)) self.drawLines(painter, canvasRect, Qt.Vertical, xMap, self.__data.xScaleDiv.ticks(QwtScaleDiv.MediumTick)) if self.__data.yEnabled and self.__data.yMinEnabled: self.drawLines(painter, canvasRect, Qt.Horizontal, yMap, self.__data.yScaleDiv.ticks(QwtScaleDiv.MinorTick)) self.drawLines(painter, canvasRect, Qt.Horizontal, yMap, self.__data.yScaleDiv.ticks(QwtScaleDiv.MediumTick)) majorPen = QPen(self.__data.majorPen) majorPen.setCapStyle(Qt.FlatCap) painter.setPen(majorPen) if self.__data.xEnabled: self.drawLines(painter, canvasRect, Qt.Vertical, xMap, self.__data.xScaleDiv.ticks(QwtScaleDiv.MajorTick)) if self.__data.yEnabled: self.drawLines(painter, canvasRect, Qt.Horizontal, yMap, self.__data.yScaleDiv.ticks(QwtScaleDiv.MajorTick)) def drawLines(self, painter, canvasRect, orientation, scaleMap, values): x1 = canvasRect.left() x2 = canvasRect.right()-1. y1 = canvasRect.top() y2 = canvasRect.bottom()-1. for val in values: value = scaleMap.transform(val) if orientation == Qt.Horizontal: if qwtFuzzyGreaterOrEqual(value, y1) and\ qwtFuzzyLessOrEqual(value, y2): painter.drawLine(x1, value, x2, value) else: if qwtFuzzyGreaterOrEqual(value, x1) and\ qwtFuzzyLessOrEqual(value, x2): painter.drawLine(value, y1, value, y2) def majorPen(self): """ :return: the pen for the major grid lines .. seealso:: :py:meth:`setMajorPen()`, :py:meth:`setMinorPen()`, :py:meth:`setPen()` """ return self.__data.majorPen def minorPen(self): """ :return: the pen for the minor grid lines .. seealso:: :py:meth:`setMinorPen()`, :py:meth:`setMajorPen()`, :py:meth:`setPen()` """ return self.__data.minorPen def xEnabled(self): """ :return: True if vertical grid lines are enabled .. seealso:: :py:meth:`enableX()` """ return self.__data.xEnabled def yEnabled(self): """ :return: True if horizontal grid lines are enabled .. seealso:: :py:meth:`enableY()` """ return self.__data.yEnabled def xMinEnabled(self): """ :return: True if minor vertical grid lines are enabled .. seealso:: :py:meth:`enableXMin()` """ return self.__data.xMinEnabled def yMinEnabled(self): """ :return: True if minor horizontal grid lines are enabled .. seealso:: :py:meth:`enableYMin()` """ return self.__data.yMinEnabled def xScaleDiv(self): """ :return: the scale division of the x axis """ return self.__data.xScaleDiv def yScaleDiv(self): """ :return: the scale division of the y axis """ return self.__data.yScaleDiv def updateScaleDiv(self, xScaleDiv, yScaleDiv): """ Update the grid to changes of the axes scale division :param qwt.scale_map.QwtScaleMap xMap: Scale division of the x-axis :param qwt.scale_map.QwtScaleMap yMap: Scale division of the y-axis .. seealso:: :py:meth:`updateAxes()` """ self.setXDiv(xScaleDiv) self.setYDiv(yScaleDiv) PythonQwt-0.5.5/qwt/math.py0000666000000000000000000000274712605040216014324 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) from qwt.qt.QtCore import qFuzzyCompare import numpy as np def qwtFuzzyCompare(value1, value2, intervalSize): eps = abs(1.e-6*intervalSize) if value2 - value1 > eps: return -1 elif value1 - value2 > eps: return 1 else: return 0 def qwtFuzzyGreaterOrEqual(d1, d2): return (d1 >= d2) or qFuzzyCompare(d1, d2) def qwtFuzzyLessOrEqual(d1, d2): return (d1 <= d2) or qFuzzyCompare(d1, d2) def qwtSign(x): if x > 0.: return 1 elif x < 0.: return -1 else: return 0 def qwtSqr(x): return x**2 def qwtFastAtan(x): if x < -1.: return -.5*np.pi - x/(x**2 + .28) elif x > 1.: return .5*np.pi - x/(x**2 + .28) else: return x/(1. + x**2*.28) def qwtFastAtan2(y, x): if x > 0: return qwtFastAtan(y/x) elif x < 0: d = qwtFastAtan(y/x) if y >= 0: return d + np.pi else: return d - np.pi elif y < 0.: return -.5*np.pi elif y > 0.: return .5*np.pi else: return 0. def qwtRadians(degrees): return degrees * np.pi/180. def qwtDegrees(radians): return radians * 180./np.pi PythonQwt-0.5.5/qwt/dyngrid_layout.py0000666000000000000000000003165212605040216016425 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ qwt.dyngrid_layout ------------------ The `dyngrid_layout` module provides the `QwtDynGridLayout` class. .. autoclass:: QwtDynGridLayout :members: """ from qwt.qt.QtGui import QLayout from qwt.qt.QtCore import Qt, QRect, QSize class QwtDynGridLayout_PrivateData(object): def __init__(self): self.isDirty = True self.maxColumns = 0 self.numRows = 0 self.numColumns = 0 self.expanding = Qt.Orientations() self.itemSizeHints = [] self.itemList = [] def updateLayoutCache(self): self.itemSizeHints = [it.sizeHint() for it in self.itemList] self.isDirty = False class QwtDynGridLayout(QLayout): """ The `QwtDynGridLayout` class lays out widgets in a grid, adjusting the number of columns and rows to the current size. `QwtDynGridLayout` takes the space it gets, divides it up into rows and columns, and puts each of the widgets it manages into the correct cell(s). It lays out as many number of columns as possible (limited by :py:meth:`maxColumns()`). .. py:class:: QwtDynGridLayout(parent, margin, [spacing=-1]) :param QWidget parent: parent widget :param int margin: margin :param int spacing: spacing .. py:class:: QwtDynGridLayout(spacing) :param int spacing: spacing .. py:class:: QwtDynGridLayout() Initialize the layout with default values. :param int spacing: spacing """ def __init__(self, *args): self.__data = None parent = None margin = 0 spacing = -1 if len(args) in (2, 3): parent, margin = args[:2] if len(args) == 3: spacing = args[-1] elif len(args) == 1: if isinstance(args[0], int): spacing, = args else: parent, = args elif len(args) != 0: raise TypeError("%s() takes 0, 1, 2 or 3 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) QLayout.__init__(self, parent) self.__data = QwtDynGridLayout_PrivateData() self.setSpacing(spacing) self.setContentsMargins(margin, margin, margin, margin) def invalidate(self): """Invalidate all internal caches""" self.__data.isDirty = True QLayout.invalidate(self) def setMaxColumns(self, maxColumns): """Limit the number of columns""" self.__data.maxColumns = maxColumns def maxColumns(self): """Return the upper limit for the number of columns""" return self.__data.maxColumns def addItem(self, item): """Add an item to the next free position""" self.__data.itemList.append(item) self.invalidate() def isEmpty(self): """Return true if this layout is empty""" return self.count() == 0 def itemCount(self): """Return number of layout items""" return self.count() def itemAt(self, index): """Find the item at a specific index""" if index < 0 or index >= len(self.__data.itemList): return return self.__data.itemList[index] def takeAt(self, index): """Find the item at a specific index and remove it from the layout""" if index < 0 or index >= len(self.__data.itemList): return self.__data.isDirty = True return self.__data.itemList.pop(index) def count(self): """Return Number of items in the layout""" return len(self.__data.itemList) def setExpandingDirections(self, expanding): """ Set whether this layout can make use of more space than sizeHint(). A value of Qt.Vertical or Qt.Horizontal means that it wants to grow in only one dimension, while Qt.Vertical | Qt.Horizontal means that it wants to grow in both dimensions. The default value is 0. """ self.__data.expanding = expanding def expandingDirections(self): """ Returns whether this layout can make use of more space than sizeHint(). A value of Qt.Vertical or Qt.Horizontal means that it wants to grow in only one dimension, while Qt.Vertical | Qt.Horizontal means that it wants to grow in both dimensions. """ return self.__data.expanding def setGeometry(self, rect): """ Reorganizes columns and rows and resizes managed items within a rectangle. """ QLayout.setGeometry(self, rect) if self.isEmpty(): return self.__data.numColumns = self.columnsForWidth(rect.width()) self.__data.numRows = self.itemCount()/self.__data.numColumns if self.itemCount() % self.__data.numColumns: self.__data.numRows += 1 itemGeometries = self.layoutItems(rect, self.__data.numColumns) for it, geo in zip(self.__data.itemList, itemGeometries): it.setGeometry(geo) def columnsForWidth(self, width): """ Calculate the number of columns for a given width. The calculation tries to use as many columns as possible ( limited by maxColumns() ) """ if self.isEmpty(): return 0 maxColumns = self.itemCount() if self.__data.maxColumns > 0: maxColumns = min([self.__data.maxColumns, maxColumns]) if self.maxRowWidth(maxColumns) <= width: return maxColumns for numColumns in range(2, maxColumns+1): rowWidth = self.maxRowWidth(numColumns) if rowWidth > width: return numColumns-1 return 1 def maxRowWidth(self, numColumns): """Calculate the width of a layout for a given number of columns.""" colWidth = [0] * numColumns if self.__data.isDirty: self.__data.updateLayoutCache() for index, hint in enumerate(self.__data.itemSizeHints): col = index % numColumns colWidth[col] = max([colWidth[col], hint.width()]) margins = self.contentsMargins() margin_w = margins.left() + margins.right() return margin_w+(numColumns-1)*self.spacing()+sum(colWidth) def maxItemWidth(self): """Return the maximum width of all layout items""" if self.isEmpty(): return 0 if self.__data.isDirty: self.__data.updateLayoutCache() return max([hint.width() for hint in self.__data.itemSizeHints]) def layoutItems(self, rect, numColumns): """ Calculate the geometries of the layout items for a layout with numColumns columns and a given rectangle. """ itemGeometries = [] if numColumns == 0 or self.isEmpty(): return itemGeometries numRows = int(self.itemCount()/numColumns) if numColumns % self.itemCount(): numRows += 1 if numRows == 0: return itemGeometries rowHeight = [0]*numRows colWidth = [0]*numColumns self.layoutGrid(numColumns, rowHeight, colWidth) expandH = self.expandingDirections() & Qt.Horizontal expandV = self.expandingDirections() & Qt.Vertical if expandH or expandV: self.stretchGrid(rect, numColumns, rowHeight, colWidth) maxColumns = self.__data.maxColumns self.__data.maxColumns = numColumns alignedRect = self.alignmentRect(rect) self.__data.maxColumns = maxColumns xOffset = 0 if expandH else alignedRect.x() yOffset = 0 if expandV else alignedRect.y() colX = [0] * numColumns rowY = [0] * numRows xySpace = self.spacing() margins = self.contentsMargins() rowY[0] = yOffset + margins.bottom() for r in range(1, numRows): rowY[r] = rowY[r-1] + rowHeight[r-1] + xySpace colX[0] = xOffset + margins.left() for c in range(1, numColumns): colX[c] = colX[c-1] + colWidth[c-1] + xySpace itemCount = len(self.__data.itemList) for i in range(itemCount): row = int(i/numColumns) col = i % numColumns itemGeometry = QRect(colX[col], rowY[row], colWidth[col], rowHeight[row]) itemGeometries.append(itemGeometry) return itemGeometries def layoutGrid(self, numColumns, rowHeight, colWidth): """ Calculate the dimensions for the columns and rows for a grid of numColumns columns. """ if numColumns <= 0: return if self.__data.isDirty: self.__data.updateLayoutCache() for index in range(len(self.__data.itemSizeHints)): row = int(index/numColumns) col = index % numColumns size = self.__data.itemSizeHints[index] if col == 0: rowHeight[row] = size.height() else: rowHeight[row] = max([rowHeight[row], size.height()]) if row == 0: colWidth[col] = size.width() else: colWidth[col] = max([colWidth[col], size.width()]) def hasHeightForWidth(self): """Return true: QwtDynGridLayout implements heightForWidth().""" return True def heightForWidth(self, width): """Return The preferred height for this layout, given a width.""" if self.isEmpty(): return 0 numColumns = self.columnsForWidth(width) numRows = int(self.itemCount()/numColumns) if self.itemCount() % numColumns: numRows += 1 rowHeight = [0] * numRows colWidth = [0] * numColumns self.layoutGrid(numColumns, rowHeight, colWidth) margins = self.contentsMargins() margin_h = margins.top() + margins.bottom() return margin_h+(numRows-1)*self.spacing()+sum(rowHeight) def stretchGrid(self, rect, numColumns, rowHeight, colWidth): """ Stretch columns in case of expanding() & QSizePolicy::Horizontal and rows in case of expanding() & QSizePolicy::Vertical to fill the entire rect. Rows and columns are stretched with the same factor. """ if numColumns == 0 or self.isEmpty(): return expandH = self.expandingDirections() & Qt.Horizontal expandV = self.expandingDirections() & Qt.Vertical if expandH: xDelta = rect.width()-2*self.margin()-(numColumns-1)*self.spacing() for col in range(numColumns): xDelta -= colWidth[col] if xDelta > 0: for col in range(numColumns): space = xDelta/(numColumns-col) colWidth[col] += space xDelta -= space if expandV: numRows = self.itemCount()/numColumns if self.itemCount() % numColumns: numRows += 1 yDelta = rect.height()-2*self.margin()-(numRows-1)*self.spacing() for row in range(numRows): yDelta -= rowHeight[row] if yDelta > 0: for row in range(numRows): space = yDelta/(numRows-row) rowHeight[row] += space yDelta -= space def sizeHint(self): """ Return the size hint. If maxColumns() > 0 it is the size for a grid with maxColumns() columns, otherwise it is the size for a grid with only one row. """ if self.isEmpty(): return QSize() numColumns = self.itemCount() if self.__data.maxColumns > 0: numColumns = min([self.__data.maxColumns, numColumns]) numRows = int(self.itemCount()/numColumns) if self.itemCount() % numColumns: numRows += 1 rowHeight = [0] * numRows colWidth = [0] * numColumns self.layoutGrid(numColumns, rowHeight, colWidth) margins = self.contentsMargins() margin_h = margins.top() + margins.bottom() margin_w = margins.left() + margins.right() h = margin_h+(numRows-1)*self.spacing()+sum(rowHeight) w = margin_w+(numColumns-1)*self.spacing()+sum(colWidth) return QSize(w, h) def numRows(self): """Return Number of rows of the current layout.""" return self.__data.numRows def numColumns(self): """Return Number of columns of the current layout.""" return self.__data.numColumns PythonQwt-0.5.5/qwt/plot_marker.py0000666000000000000000000004331212615225450015711 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPlotMarker ------------- .. autoclass:: QwtPlotMarker :members: """ from qwt.plot import QwtPlotItem from qwt.text import QwtText from qwt.painter import QwtPainter from qwt.graphic import QwtGraphic from qwt.symbol import QwtSymbol from qwt.qt.QtGui import QPen, QPainter from qwt.qt.QtCore import Qt, QPointF, QRectF, QSizeF, QRect class QwtPlotMarker_PrivateData(object): def __init__(self): self.labelAlignment = Qt.AlignCenter self.labelOrientation = Qt.Horizontal self.spacing = 2 self.symbol = None self.style = QwtPlotMarker.NoLine self.xValue = 0. self.yValue = 0. self.label = QwtText() self.pen = QPen() class QwtPlotMarker(QwtPlotItem): """ A class for drawing markers A marker can be a horizontal line, a vertical line, a symbol, a label or any combination of them, which can be drawn around a center point inside a bounding rectangle. The `setSymbol()` member assigns a symbol to the marker. The symbol is drawn at the specified point. With `setLabel()`, a label can be assigned to the marker. The `setLabelAlignment()` member specifies where the label is drawn. All the Align*-constants in `Qt.AlignmentFlags` (see Qt documentation) are valid. The interpretation of the alignment depends on the marker's line style. The alignment refers to the center point of the marker, which means, for example, that the label would be printed left above the center point if the alignment was set to `Qt.AlignLeft | Qt.AlignTop`. Line styles: * `QwtPlotMarker.NoLine`: No line * `QwtPlotMarker.HLine`: A horizontal line * `QwtPlotMarker.VLine`: A vertical line * `QwtPlotMarker.Cross`: A crosshair """ # enum LineStyle NoLine, HLine, VLine, Cross = list(range(4)) def __init__(self, title=None): if title is None: title = "" if not isinstance(title, QwtText): title = QwtText(title) QwtPlotItem.__init__(self, title) self.__data = QwtPlotMarker_PrivateData() self.setZ(30.) def rtti(self): """:return: `QwtPlotItem.Rtti_PlotMarker`""" return QwtPlotItem.Rtti_PlotMarker def value(self): """:return: Value""" return QPointF(self.__data.xValue, self.__data.yValue) def xValue(self): """:return: x Value""" return self.__data.xValue def yValue(self): """:return: y Value""" return self.__data.yValue def setValue(self, *args): """ Set Value .. py:method:: setValue(pos): :param QPointF pos: Position .. py:method:: setValue(x, y): :param float x: x position :param float y: y position """ if len(args) == 1: pos, = args self.setValue(pos.x(), pos.y()) elif len(args) == 2: x, y = args if x != self.__data.xValue or y != self.__data.yValue: self.__data.xValue = x self.__data.yValue = y self.itemChanged() else: raise TypeError("%s() takes 1 or 2 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def setXValue(self, x): """ Set X Value :param float x: x position """ self.setValue(x, self.__data.yValue) def setYValue(self, y): """ Set Y Value :param float y: y position """ self.setValue(self.__data.xValue, y) def draw(self, painter, xMap, yMap, canvasRect): """ Draw the marker :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: x Scale Map :param qwt.scale_map.QwtScaleMap yMap: y Scale Map :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates """ pos = QPointF(xMap.transform(self.__data.xValue), yMap.transform(self.__data.yValue)) self.drawLines(painter, canvasRect, pos) if self.__data.symbol and\ self.__data.symbol.style() != QwtSymbol.NoSymbol: sz = self.__data.symbol.size() clipRect = QRectF(canvasRect.adjusted(-sz.width(), -sz.height(), sz.width(), sz.height())) if clipRect.contains(pos): self.__data.symbol.drawSymbols(painter, [pos]) self.drawLabel(painter, canvasRect, pos) def drawLines(self, painter, canvasRect, pos): """ Draw the lines marker :param QPainter painter: Painter :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates :param QPointF pos: Position of the marker, translated into widget coordinates .. seealso:: :py:meth:`drawLabel()`, :py:meth:`qwt.symbol.QwtSymbol.drawSymbol()` """ if self.__data.style == self.NoLine: return painter.setPen(self.__data.pen) if self.__data.style in (QwtPlotMarker.HLine, QwtPlotMarker.Cross): y = pos.y() painter.drawLine(canvasRect.left(), y, canvasRect.right()-1., y) if self.__data.style in (QwtPlotMarker.VLine, QwtPlotMarker.Cross): x = pos.x() painter.drawLine(x, canvasRect.top(), x, canvasRect.bottom()-1.) def drawLabel(self, painter, canvasRect, pos): """ Align and draw the text label of the marker :param QPainter painter: Painter :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates :param QPointF pos: Position of the marker, translated into widget coordinates .. seealso:: :py:meth:`drawLabel()`, :py:meth:`qwt.symbol.QwtSymbol.drawSymbol()` """ if self.__data.label.isEmpty(): return align = Qt.Alignment(self.__data.labelAlignment) alignPos = QPointF(pos) symbolOff = QSizeF(0, 0) if self.__data.style == QwtPlotMarker.VLine: # In VLine-style the y-position is pointless and # the alignment flags are relative to the canvas if bool(self.__data.labelAlignment & Qt.AlignTop): alignPos.setY(canvasRect.top()) align &= ~Qt.AlignTop align |= Qt.AlignBottom elif bool(self.__data.labelAlignment & Qt.AlignBottom): # In HLine-style the x-position is pointless and # the alignment flags are relative to the canvas alignPos.setY(canvasRect.bottom()-1) align &= ~Qt.AlignBottom align |= Qt.AlignTop else: alignPos.setY(canvasRect.center().y()) elif self.__data.style == QwtPlotMarker.HLine: if bool(self.__data.labelAlignment & Qt.AlignLeft): alignPos.setX(canvasRect.left()) align &= ~Qt.AlignLeft align |= Qt.AlignRight elif bool(self.__data.labelAlignment & Qt.AlignRight): alignPos.setX(canvasRect.right()-1) align &= ~Qt.AlignRight align |= Qt.AlignLeft else: alignPos.setX(canvasRect.center().x()) else: if self.__data.symbol and\ self.__data.symbol.style() != QwtSymbol.NoSymbol: symbolOff = self.__data.symbol.size()+QSizeF(1, 1) symbolOff /= 2 pw2 = self.__data.pen.widthF()/2. if pw2 == 0.: pw2 = .5 spacing = self.__data.spacing xOff = max([pw2, symbolOff.width()]) yOff = max([pw2, symbolOff.height()]) textSize = self.__data.label.textSize(painter.font()) if align & Qt.AlignLeft: alignPos.setX(alignPos.x()-(xOff+spacing)) if self.__data.labelOrientation == Qt.Vertical: alignPos.setX(alignPos.x()-textSize.height()) else: alignPos.setX(alignPos.x()-textSize.width()) elif align & Qt.AlignRight: alignPos.setX(alignPos.x()+xOff+spacing) else: if self.__data.labelOrientation == Qt.Vertical: alignPos.setX(alignPos.x()-textSize.height()/2) else: alignPos.setX(alignPos.x()-textSize.width()/2) if align & Qt.AlignTop: alignPos.setY(alignPos.y()-(yOff+spacing)) if self.__data.labelOrientation != Qt.Vertical: alignPos.setY(alignPos.y()-textSize.height()) elif align & Qt.AlignBottom: alignPos.setY(alignPos.y()+yOff+spacing) if self.__data.labelOrientation == Qt.Vertical: alignPos.setY(alignPos.y()+textSize.width()) else: if self.__data.labelOrientation == Qt.Vertical: alignPos.setY(alignPos.y()+textSize.width()/2) else: alignPos.setY(alignPos.y()-textSize.height()/2) painter.translate(alignPos.x(), alignPos.y()) if self.__data.labelOrientation == Qt.Vertical: painter.rotate(-90.) textRect = QRectF(0, 0, textSize.width(), textSize.height()) self.__data.label.draw(painter, textRect) def setLineStyle(self, style): """ Set the line style :param int style: Line style Line styles: * `QwtPlotMarker.NoLine`: No line * `QwtPlotMarker.HLine`: A horizontal line * `QwtPlotMarker.VLine`: A vertical line * `QwtPlotMarker.Cross`: A crosshair .. seealso:: :py:meth:`lineStyle()` """ if style != self.__data.style: self.__data.style = style self.legendChanged() self.itemChanged() def lineStyle(self): """ :return: the line style .. seealso:: :py:meth:`setLineStyle()` """ return self.__data.style def setSymbol(self, symbol): """ Assign a symbol :param qwt.symbol.QwtSymbol symbol: New symbol .. seealso:: :py:meth:`symbol()` """ if symbol != self.__data.symbol: self.__data.symbol = symbol if symbol is not None: self.setLegendIconSize(symbol.boundingRect().size()) self.legendChanged() self.itemChanged() def symbol(self): """ :return: the symbol .. seealso:: :py:meth:`setSymbol()` """ return self.__data.symbol def setLabel(self, label): """ Set the label :param label: Label text :type label: qwt.text.QwtText or str .. seealso:: :py:meth:`label()` """ if label != self.__data.label: self.__data.label = label self.itemChanged() def label(self): """ :return: the label .. seealso:: :py:meth:`setLabel()` """ return self.__data.label def setLabelAlignment(self, align): """ Set the alignment of the label In case of `QwtPlotMarker.HLine` the alignment is relative to the y position of the marker, but the horizontal flags correspond to the canvas rectangle. In case of `QwtPlotMarker.VLine` the alignment is relative to the x position of the marker, but the vertical flags correspond to the canvas rectangle. In all other styles the alignment is relative to the marker's position. :param Qt.Alignment align: Alignment .. seealso:: :py:meth:`labelAlignment()`, :py:meth:`labelOrientation()` """ if align != self.__data.labelAlignment: self.__data.labelAlignment = align self.itemChanged() def labelAlignment(self): """ :return: the label alignment .. seealso:: :py:meth:`setLabelAlignment()`, :py:meth:`setLabelOrientation()` """ return self.__data.labelAlignment def setLabelOrientation(self, orientation): """ Set the orientation of the label When orientation is `Qt.Vertical` the label is rotated by 90.0 degrees (from bottom to top). :param Qt.Orientation orientation: Orientation of the label .. seealso:: :py:meth:`labelOrientation()`, :py:meth:`setLabelAlignment()` """ if orientation != self.__data.labelOrientation: self.__data.labelOrientation = orientation self.itemChanged() def labelOrientation(self): """ :return: the label orientation .. seealso:: :py:meth:`setLabelOrientation()`, :py:meth:`labelAlignment()` """ return self.__data.labelOrientation def setSpacing(self, spacing): """ Set the spacing When the label is not centered on the marker position, the spacing is the distance between the position and the label. :param int spacing: Spacing .. seealso:: :py:meth:`spacing()`, :py:meth:`setLabelAlignment()` """ if spacing < 0: spacing = 0 if spacing != self.__data.spacing: self.__data.spacing = spacing self.itemChanged() def spacing(self): """ :return: the spacing .. seealso:: :py:meth:`setSpacing()` """ return self.__data.spacing def setLinePen(self, *args): """ Build and/or assigna a line pen, depending on the arguments. .. py:method:: setPen(color, width, style) Build and assign a line pen In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it non cosmetic (see `QPen.isCosmetic()`). This method signature has been introduced to hide this incompatibility. :param QColor color: Pen color :param float width: Pen width :param Qt.PenStyle style: Pen style .. py:method:: setPen(pen) Specify a pen for the line. :param QPen pen: New pen .. seealso:: :py:meth:`pen()`, :py:meth:`brush()` """ if len(args) == 1 and isinstance(args[0], QPen): pen, = args elif len(args) in (1, 2, 3): color = args[0] width = 0. style = Qt.SolidLine if len(args) > 1: width = args[1] if len(args) > 2: style = args[2] self.setLinePen(QPen(color, width, style)) else: raise TypeError("%s().setLinePen() takes 1, 2 or 3 argument(s) "\ "(%s given)" % (self.__class__.__name__, len(args))) if pen != self.__data.pen: self.__data.pen = pen self.legendChanged() self.itemChanged() def linePen(self): """ :return: the line pen .. seealso:: :py:meth:`setLinePen()` """ return self.__data.pen def boundingRect(self): return QRectF(self.__data.xValue, self.__data.yValue, 0., 0.) def legendIcon(self, index, size): """ :param int index: Index of the legend entry (ignored as there is only one) :param QSizeF size: Icon size :return: Icon representing the marker on the legend .. seealso:: :py:meth:`qwt.plot.QwtPlotItem.setLegendIconSize()`, :py:meth:`qwt.plot.QwtPlotItem.legendData()` """ if size.isEmpty(): return QwtGraphic() icon = QwtGraphic() icon.setDefaultSize(size) icon.setRenderHint(QwtGraphic.RenderPensUnscaled, True) painter = QPainter(icon) painter.setRenderHint(QPainter.Antialiasing, self.testRenderHint(QwtPlotItem.RenderAntialiased)) if self.__data.style != QwtPlotMarker.NoLine: painter.setPen(self.__data.pen) if self.__data.style in (QwtPlotMarker.HLine, QwtPlotMarker.Cross): y = .5*size.height() painter.drawLine(0., y, size.width(), y) if self.__data.style in (QwtPlotMarker.VLine, QwtPlotMarker.Cross): x = .5*size.width() painter.drawLine(x, 0., x, size.height()) if self.__data.symbol: r = QRect(0, 0, size.width(), size.height()) self.__data.symbol.drawSymbol(painter, r) return icon PythonQwt-0.5.5/qwt/plot_renderer.py0000666000000000000000000006441212627660526016254 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPlotRenderer --------------- .. autoclass:: QwtPlotRenderer :members: """ from __future__ import division from qwt.painter import QwtPainter from qwt.plot import QwtPlot from qwt.plot_layout import QwtPlotLayout from qwt.scale_draw import QwtScaleDraw from qwt.scale_map import QwtScaleMap from qwt.qt.QtGui import (QPrinter, QPainter, QImageWriter, QImage, QColor, QPaintDevice, QTransform, QPalette, QFileDialog, QPainterPath, QPen) from qwt.qt.QtCore import Qt, QRect, QRectF, QObject, QSizeF from qwt.qt.QtSvg import QSvgGenerator from qwt.qt.compat import getsavefilename import numpy as np import os.path as osp def qwtCanvasClip(canvas, canvasRect): """ The clip region is calculated in integers To avoid too much rounding errors better calculate it in target device resolution """ x1 = np.ceil(canvasRect.left()) x2 = np.floor(canvasRect.right()) y1 = np.ceil(canvasRect.top()) y2 = np.floor(canvasRect.bottom()) r = QRect(x1, y1, x2-x1-1, y2-y1-1) return canvas.borderPath(r) class QwtPlotRenderer_PrivateData(object): def __init__(self): self.discardFlags = QwtPlotRenderer.DiscardNone self.layoutFlags = QwtPlotRenderer.DefaultLayout class QwtPlotRenderer(QObject): """ Renderer for exporting a plot to a document, a printer or anything else, that is supported by QPainter/QPaintDevice Discard flags: * `QwtPlotRenderer.DiscardNone`: Render all components of the plot * `QwtPlotRenderer.DiscardBackground`: Don't render the background of the plot * `QwtPlotRenderer.DiscardTitle`: Don't render the title of the plot * `QwtPlotRenderer.DiscardLegend`: Don't render the legend of the plot * `QwtPlotRenderer.DiscardCanvasBackground`: Don't render the background of the canvas * `QwtPlotRenderer.DiscardFooter`: Don't render the footer of the plot * `QwtPlotRenderer.DiscardCanvasFrame`: Don't render the frame of the canvas .. note:: The `QwtPlotRenderer.DiscardCanvasFrame` flag has no effect when using style sheets, where the frame is part of the background Layout flags: * `QwtPlotRenderer.DefaultLayout`: Use the default layout as on screen * `QwtPlotRenderer.FrameWithScales`: Instead of the scales a box is painted around the plot canvas, where the scale ticks are aligned to. """ # enum DiscardFlag DiscardNone = 0x00 DiscardBackground = 0x01 DiscardTitle = 0x02 DiscardLegend = 0x04 DiscardCanvasBackground = 0x08 DiscardFooter = 0x10 DiscardCanvasFrame = 0x20 # enum LayoutFlag DefaultLayout = 0x00 FrameWithScales = 0x01 def __init__(self, parent=None): QObject.__init__(self, parent) self.__data = QwtPlotRenderer_PrivateData() def setDiscardFlag(self, flag, on=True): """ Change a flag, indicating what to discard from rendering :param int flag: Flag to change :param bool on: On/Off .. seealso:: :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlags()`, :py:meth:`discardFlags()` """ if on: self.__data.discardFlags |= flag else: self.__data.discardFlags &= ~flag def testDiscardFlag(self, flag): """ :param int flag: Flag to be tested :return: True, if flag is enabled. .. seealso:: :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`, :py:meth:`discardFlags()` """ return self.__data.discardFlags & flag def setDiscardFlags(self, flags): """ Set the flags, indicating what to discard from rendering :param int flags: Flags .. seealso:: :py:meth:`testDiscardFlag()`, :py:meth:`setDiscardFlag()`, :py:meth:`discardFlags()` """ self.__data.discardFlags = flags def discardFlags(self): """ :return: Flags, indicating what to discard from rendering .. seealso:: :py:meth:`setDiscardFlag()`, :py:meth:`setDiscardFlags()`, :py:meth:`testDiscardFlag()` """ return self.__data.discardFlags def setLayoutFlag(self, flag, on=True): """ Change a layout flag :param int flag: Flag to change .. seealso:: :py:meth:`testLayoutFlag()`, :py:meth:`setLayoutFlags()`, :py:meth:`layoutFlags()` """ if on: self.__data.layoutFlags |= flag else: self.__data.layoutFlags &= ~flag def testLayoutFlag(self, flag): """ :param int flag: Flag to be tested :return: True, if flag is enabled. .. seealso:: :py:meth:`setLayoutFlag()`, :py:meth:`setLayoutFlags()`, :py:meth:`layoutFlags()` """ return self.__data.layoutFlags & flag def setLayoutFlags(self, flags): """ Set the layout flags :param int flags: Flags .. seealso:: :py:meth:`setLayoutFlag()`, :py:meth:`testLayoutFlag()`, :py:meth:`layoutFlags()` """ self.__data.layoutFlags = flags def layoutFlags(self): """ :return: Layout flags .. seealso:: :py:meth:`setLayoutFlags()`, :py:meth:`setLayoutFlag()`, :py:meth:`testLayoutFlag()` """ return self.__data.layoutFlags def renderDocument(self, plot, filename, sizeMM=(300, 200), resolution=85, format_=None): """ Render a plot to a file The format of the document will be auto-detected from the suffix of the file name. :param qwt.plot.QwtPlot plot: Plot widget :param str fileName: Path of the file, where the document will be stored :param QSizeF sizeMM: Size for the document in millimeters :param int resolution: Resolution in dots per Inch (dpi) """ if isinstance(sizeMM, tuple): sizeMM = QSizeF(*sizeMM) if format_ is None: ext = osp.splitext(filename)[1] if not ext: raise TypeError("Unable to determine target format from filename") format_ = ext[1:] if plot is None or sizeMM.isEmpty() or resolution <= 0: return title = plot.title().text() if not title: title = "Plot Document" mmToInch = 1./25.4 size = sizeMM * mmToInch * resolution documentRect = QRectF(0.0, 0.0, size.width(), size.height()) fmt = format_.lower() if fmt in ("pdf", "ps"): printer = QPrinter() if fmt == "pdf": printer.setOutputFormat(QPrinter.PdfFormat) else: printer.setOutputFormat(QPrinter.PostScriptFormat) printer.setColorMode(QPrinter.Color) printer.setFullPage(True) printer.setPaperSize(sizeMM, QPrinter.Millimeter) printer.setDocName(title) printer.setOutputFileName(filename) printer.setResolution(resolution) painter = QPainter(printer) self.render(plot, painter, documentRect) painter.end() elif fmt == "svg": generator = QSvgGenerator() generator.setTitle(title) generator.setFileName(filename) generator.setResolution(resolution) generator.setViewBox(documentRect) painter = QPainter(generator) self.render(plot, painter, documentRect) painter.end() elif fmt in QImageWriter.supportedImageFormats(): imageRect = documentRect.toRect() dotsPerMeter = int(round(resolution*mmToInch*1000.)) image = QImage(imageRect.size(), QImage.Format_ARGB32) image.setDotsPerMeterX(dotsPerMeter) image.setDotsPerMeterY(dotsPerMeter) image.fill(QColor(Qt.white).rgb()) painter = QPainter(image) self.render(plot, painter, imageRect) painter.end() image.save(filename, fmt) else: raise TypeError("Unsupported file format '%s'" % fmt) def renderTo(self, plot, dest): """ Render a plot to a file Supported formats are: - pdf: Portable Document Format PDF - ps: Postcript - svg: Scalable Vector Graphics SVG - all image formats supported by Qt, see QImageWriter.supportedImageFormats() Scalable vector graphic formats like PDF or SVG are superior to raster graphics formats. :param qwt.plot.QwtPlot plot: Plot widget :param str fileName: Path of the file, where the document will be stored :param str format: Format for the document :param QSizeF sizeMM: Size for the document in millimeters. :param int resolution: Resolution in dots per Inch (dpi) .. seealso:: :py:meth:`renderTo()`, :py:meth:`render()`, :py:meth:`qwt.painter.QwtPainter.setRoundingAlignment()` """ if isinstance(dest, QPaintDevice): w = dest.width() h = dest.height() rect = QRectF(0, 0, w, h) elif isinstance(dest, QPrinter): w = dest.width() h = dest.height() rect = QRectF(0, 0, w, h) aspect = rect.width()/rect.height() if aspect < 1.: rect.setHeight(aspect*rect.width()) elif isinstance(dest, QSvgGenerator): rect = dest.viewBoxF() if rect.isEmpty(): rect.setRect(0, 0, dest.width(), dest.height()) if rect.isEmpty(): rect.setRect(0, 0, 800, 600) p = QPainter(dest) self.render(plot, p, rect) def render(self, plot, painter, plotRect): """ Paint the contents of a QwtPlot instance into a given rectangle. :param qwt.plot.QwtPlot plot: Plot to be rendered :param QPainter painter: Painter :param str format: Format for the document :param QRectF plotRect: Bounding rectangle .. seealso:: :py:meth:`renderDocument()`, :py:meth:`renderTo()`, :py:meth:`qwt.painter.QwtPainter.setRoundingAlignment()` """ if painter == 0 or not painter.isActive() or not plotRect.isValid()\ or plot.size().isNull(): return if not self.__data.discardFlags & self.DiscardBackground: QwtPainter.drawBackground(painter, plotRect, plot) # The layout engine uses the same methods as they are used # by the Qt layout system. Therefore we need to calculate the # layout in screen coordinates and paint with a scaled painter. transform = QTransform() transform.scale(float(painter.device().logicalDpiX())/plot.logicalDpiX(), float(painter.device().logicalDpiY())/plot.logicalDpiY()) invtrans, _ok = transform.inverted() layoutRect = invtrans.mapRect(plotRect) if not (self.__data.discardFlags & self.DiscardBackground): left, top, right, bottom = plot.getContentsMargins() layoutRect.adjust(left, top, -right, -bottom) layout = plot.plotLayout() baseLineDists = [None]*QwtPlot.axisCnt canvasMargins = [None]*QwtPlot.axisCnt for axisId in QwtPlot.validAxes: canvasMargins[axisId] = layout.canvasMargin(axisId) if self.__data.layoutFlags & self.FrameWithScales: scaleWidget = plot.axisWidget(axisId) if scaleWidget: baseLineDists[axisId] = scaleWidget.margin() scaleWidget.setMargin(0) if not plot.axisEnabled(axisId): left, right, top, bottom = 0, 0, 0, 0 # When we have a scale the frame is painted on # the position of the backbone - otherwise we # need to introduce a margin around the canvas if axisId == QwtPlot.yLeft: layoutRect.adjust(1, 0, 0, 0) elif axisId == QwtPlot.yRight: layoutRect.adjust(0, 0, -1, 0) elif axisId == QwtPlot.xTop: layoutRect.adjust(0, 1, 0, 0) elif axisId == QwtPlot.xBottom: layoutRect.adjust(0, 0, 0, -1) layoutRect.adjust(left, top, right, bottom) # Calculate the layout for the document. layoutOptions = QwtPlotLayout.IgnoreScrollbars if self.__data.layoutFlags & self.FrameWithScales or\ self.__data.discardFlags & self.DiscardCanvasFrame: layoutOptions |= QwtPlotLayout.IgnoreFrames if self.__data.discardFlags & self.DiscardLegend: layoutOptions |= QwtPlotLayout.IgnoreLegend if self.__data.discardFlags & self.DiscardTitle: layoutOptions |= QwtPlotLayout.IgnoreTitle if self.__data.discardFlags & self.DiscardFooter: layoutOptions |= QwtPlotLayout.IgnoreFooter layout.activate(plot, layoutRect, layoutOptions) maps = self.buildCanvasMaps(plot, layout.canvasRect()) if self.updateCanvasMargins(plot, layout.canvasRect(), maps): # recalculate maps and layout, when the margins # have been changed layout.activate(plot, layoutRect, layoutOptions) maps = self.buildCanvasMaps(plot, layout.canvasRect()) painter.save() painter.setWorldTransform(transform, True) self.renderCanvas(plot, painter, layout.canvasRect(), maps) if (not self.__data.discardFlags & self.DiscardTitle) and\ plot.titleLabel().text(): self.renderTitle(plot, painter, layout.titleRect()) if (not self.__data.discardFlags & self.DiscardFooter) and\ plot.titleLabel().text(): self.renderFooter(plot, painter, layout.footerRect()) if (not self.__data.discardFlags & self.DiscardLegend) and\ plot.titleLabel().text(): self.renderLegend(plot, painter, layout.legendRect()) for axisId in QwtPlot.validAxes: scaleWidget = plot.axisWidget(axisId) if scaleWidget: baseDist = scaleWidget.margin() startDist, endDist = scaleWidget.getBorderDistHint() self.renderScale(plot, painter, axisId, startDist, endDist, baseDist, layout.scaleRect(axisId)) painter.restore() for axisId in QwtPlot.validAxes: if self.__data.layoutFlags & self.FrameWithScales: scaleWidget = plot.axisWidget(axisId) if scaleWidget: scaleWidget.setMargin(baseLineDists[axisId]) layout.setCanvasMargin(canvasMargins[axisId]) layout.invalidate() def renderTitle(self, plot, painter, rect): """ Render the title into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param QRectF rect: Bounding rectangle """ painter.setFont(plot.titleLabel().font()) color = plot.titleLabel().palette().color(QPalette.Active, QPalette.Text) painter.setPen(color) plot.titleLabel().text().draw(painter, rect) def renderFooter(self, plot, painter, rect): """ Render the footer into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param QRectF rect: Bounding rectangle """ painter.setFont(plot.footerLabel().font()) color = plot.footerLabel().palette().color(QPalette.Active, QPalette.Text) painter.setPen(color) plot.footerLabel().text().draw(painter, rect) def renderLegend(self, plot, painter, rect): """ Render the legend into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param QRectF rect: Bounding rectangle """ if plot.legend(): fillBackground = not self.__data.discardFlags & self.DiscardBackground plot.legend().renderLegend(painter, rect, fillBackground) def renderScale(self, plot, painter, axisId, startDist, endDist, baseDist, rect): """ Paint a scale into a given rectangle. Paint the scale into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param int axisId: Axis :param int startDist: Start border distance :param int endDist: End border distance :param int baseDist: Base distance :param QRectF rect: Bounding rectangle """ if not plot.axisEnabled(axisId): return scaleWidget = plot.axisWidget(axisId) if scaleWidget.isColorBarEnabled() and scaleWidget.colorBarWidth() > 0: scaleWidget.drawColorBar(painter, scaleWidget.colorBarRect(rect)) baseDist += scaleWidget.colorBarWidth() + scaleWidget.spacing() painter.save() if axisId == QwtPlot.yLeft: x = rect.right() - 1.0 - baseDist y = rect.y() + startDist w = rect.height() - startDist - endDist align = QwtScaleDraw.LeftScale elif axisId == QwtPlot.yRight: x = rect.left() + baseDist y = rect.y() + startDist w = rect.height() - startDist - endDist align = QwtScaleDraw.RightScale elif axisId == QwtPlot.xTop: x = rect.left() + startDist y = rect.bottom() - 1.0 - baseDist w = rect.width() - startDist - endDist align = QwtScaleDraw.TopScale elif axisId == QwtPlot.xBottom: x = rect.left() + startDist y = rect.top() + baseDist w = rect.width() - startDist - endDist align = QwtScaleDraw.BottomScale scaleWidget.drawTitle(painter, align, rect) painter.setFont(scaleWidget.font()) sd = scaleWidget.scaleDraw() sdPos = sd.pos() sdLength = sd.length() sd.move(x, y) sd.setLength(w) palette = scaleWidget.palette() palette.setCurrentColorGroup(QPalette.Active) sd.draw(painter, palette) sd.move(sdPos) sd.setLength(sdLength) painter.restore() def renderCanvas(self, plot, painter, canvasRect, maps): """ Render the canvas into a given rectangle. :param qwt.plot.QwtPlot plot: Plot widget :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap maps: mapping between plot and paint device coordinates :param QRectF rect: Bounding rectangle """ canvas = plot.canvas() r = canvasRect.adjusted(0., 0., -1., 1.) if self.__data.layoutFlags & self.FrameWithScales: painter.save() r.adjust(-1., -1., 1., 1.) painter.setPen(QPen(Qt.black)) if not (self.__data.discardFlags & self.DiscardCanvasBackground): bgBrush = canvas.palette().brush(plot.backgroundRole()) painter.setBrush(bgBrush) painter.drawRect(r) painter.restore() painter.save() painter.setClipRect(canvasRect) plot.drawItems(painter, canvasRect, maps) painter.restore() elif canvas.testAttribute(Qt.WA_StyledBackground): clipPath = QPainterPath() painter.save() if not self.__data.discardFlags & self.DiscardCanvasBackground: QwtPainter.drawBackground(painter, r, canvas) clipPath = qwtCanvasClip(canvas, canvasRect) painter.restore() painter.save() if clipPath.isEmpty(): painter.setClipRect(canvasRect) else: painter.setClipPath(clipPath) plot.drawItems(painter, canvasRect, maps) painter.restore() else: clipPath = QPainterPath() frameWidth = 0 if not self.__data.discardFlags & self.DiscardCanvasFrame: frameWidth = canvas.frameWidth() clipPath = qwtCanvasClip(canvas, canvasRect) innerRect = canvasRect.adjusted(frameWidth, frameWidth, -frameWidth, -frameWidth) painter.save() if clipPath.isEmpty(): painter.setClipRect(innerRect) else: painter.setClipPath(clipPath) if not self.__data.discardFlags & self.DiscardCanvasBackground: QwtPainter.drawBackground(painter, innerRect, canvas) plot.drawItems(painter, innerRect, maps) painter.restore() if frameWidth > 0: painter.save() frameStyle = canvas.frameShadow() | canvas.frameShape() frameWidth = canvas.frameWidth() borderRadius = canvas.borderRadius() if borderRadius > 0.: QwtPainter.drawRoundedFrame(painter, canvasRect, r, r, canvas.palette(), frameWidth, frameStyle) else: midLineWidth = canvas.midLineWidth() QwtPainter.drawFrame(painter, canvasRect, canvas.palette(), canvas.foregroundRole(), frameWidth, midLineWidth, frameStyle) painter.restore() def buildCanvasMaps(self, plot, canvasRect): """ Calculated the scale maps for rendering the canvas :param qwt.plot.QwtPlot plot: Plot widget :param QRectF canvasRect: Target rectangle :return: Calculated scale maps """ maps = [] for axisId in QwtPlot.validAxes: map_ = QwtScaleMap() map_.setTransformation( plot.axisScaleEngine(axisId).transformation()) sd = plot.axisScaleDiv(axisId) map_.setScaleInterval(sd.lowerBound(), sd.upperBound()) if plot.axisEnabled(axisId): s = plot.axisWidget(axisId) scaleRect = plot.plotLayout().scaleRect(axisId) if axisId in (QwtPlot.xTop, QwtPlot.xBottom): from_ = scaleRect.left() + s.startBorderDist() to = scaleRect.right() - s.endBorderDist() else: from_ = scaleRect.bottom() - s.endBorderDist() to = scaleRect.top() + s.startBorderDist() else: margin = 0 if not plot.plotLayout().alignCanvasToScale(axisId): margin = plot.plotLayout().canvasMargin(axisId) if axisId in (QwtPlot.yLeft, QwtPlot.yRight): from_ = canvasRect.bottom() - margin to = canvasRect.top() + margin else: from_ = canvasRect.left() + margin to = canvasRect.right() - margin map_.setPaintInterval(from_, to) maps.append(map_) return maps def updateCanvasMargins(self, plot, canvasRect, maps): margins = plot.getCanvasMarginsHint(maps, canvasRect) marginsChanged = False for axisId in QwtPlot.validAxes: if margins[axisId] >= 0.: m = np.ceil(margins[axisId]) plot.plotLayout().setCanvasMargin(m, axisId) marginsChanged = True return marginsChanged def exportTo(self, plot, documentname, sizeMM=None, resolution=85): """ Execute a file dialog and render the plot to the selected file :param qwt.plot.QwtPlot plot: Plot widget :param str documentName: Default document name :param QSizeF sizeMM: Size for the document in millimeters :param int resolution: Resolution in dots per Inch (dpi) :return: True, when exporting was successful .. seealso:: :py:meth:`renderDocument()` """ if plot is None: return if sizeMM is None: sizeMM = QSizeF(300, 200) filename = documentname imageFormats = QImageWriter.supportedImageFormats() filter_ = ["PDF documents (*.pdf)", "SVG documents (*.svg)", "Postscript documents (*.ps)"] if imageFormats: imageFilter = "Images" imageFilter += " (" for idx, fmt in enumerate(imageFormats): if idx > 0: imageFilter += " " imageFilter += "*."+str(fmt) imageFilter += ")" filter_ += [imageFilter] filename, _s = getsavefilename(plot, "Export File Name", filename, ";;".join(filter_), options=QFileDialog.DontConfirmOverwrite) if not filename: return False self.renderDocument(plot, filename, sizeMM, resolution) return True PythonQwt-0.5.5/qwt/toqimage.py0000666000000000000000000000326712605040216015177 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the MIT License # (see LICENSE file for more details) """ NumPy array to QImage --------------------- .. autofunction:: array_to_qimage """ from qwt.qt.QtGui import QImage import numpy as np def array_to_qimage(arr, copy=False): """ Convert NumPy array to QImage object :param numpy.array arr: NumPy array :param bool copy: if True, make a copy of the array :return: QImage object """ # https://gist.githubusercontent.com/smex/5287589/raw/toQImage.py if arr is None: return QImage() if len(arr.shape) not in (2, 3): raise NotImplementedError("Unsupported array shape %r" % arr.shape) data = arr.data ny, nx = arr.shape[:2] stride = arr.strides[0] # bytes per line color_dim = None if len(arr.shape) == 3: color_dim = arr.shape[2] if arr.dtype == np.uint8: if color_dim is None: qimage = QImage(data, nx, ny, stride, QImage.Format_Indexed8) # qimage.setColorTable([qRgb(i, i, i) for i in range(256)]) qimage.setColorCount(256) elif color_dim == 3: qimage = QImage(data, nx, ny, stride, QImage.Format_RGB888) elif color_dim == 4: qimage = QImage(data, nx, ny, stride, QImage.Format_ARGB32) else: raise TypeError("Invalid third axis dimension (%r)" % color_dim) elif arr.dtype == np.uint32: qimage = QImage(data, nx, ny, stride, QImage.Format_ARGB32) else: raise NotImplementedError("Unsupported array data type %r" % arr.dtype) if copy: return qimage.copy() return qimage PythonQwt-0.5.5/qwt/plot.py0000666000000000000000000022242612642012056014351 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPlot ------- .. autoclass:: QwtPlot :members: QwtPlotItem ----------- .. autoclass:: QwtPlotItem :members: """ from qwt.qt.QtGui import (QWidget, QFont, QSizePolicy, QFrame, QApplication, QRegion, QPainter, QPalette) from qwt.qt.QtCore import Qt, Signal, QEvent, QSize, QRectF from qwt.text import QwtText, QwtTextLabel from qwt.scale_widget import QwtScaleWidget from qwt.scale_draw import QwtScaleDraw from qwt.scale_engine import QwtLinearScaleEngine from qwt.plot_canvas import QwtPlotCanvas from qwt.scale_div import QwtScaleDiv from qwt.scale_map import QwtScaleMap from qwt.graphic import QwtGraphic from qwt.legend import QwtLegendData from qwt.interval import QwtInterval import numpy as np def qwtEnableLegendItems(plot, on): if on: plot.legendDataChanged.connect(plot.updateLegendItems) else: plot.legendDataChanged.disconnect(plot.updateLegendItems) def qwtSetTabOrder(first, second, with_children): tab_chain = [first, second] if with_children: children = second.findChildren(QWidget) w = second.nextInFocusChain() while w in children: while w in children: children.remove(w) tab_chain += [w] w = w.nextInFocusChain() for idx in range(len(tab_chain)-1): w_from = tab_chain[idx] w_to = tab_chain[idx+1] policy1, policy2 = w_from.focusPolicy(), w_to.focusPolicy() proxy1, proxy2 = w_from.focusProxy(), w_to.focusProxy() for w in (w_from, w_to): w.setFocusPolicy(Qt.TabFocus) w.setFocusProxy(None) QWidget.setTabOrder(w_from, w_to) for w, pl, px in ((w_from, policy1, proxy1), (w_to, policy2, proxy2)): w.setFocusPolicy(pl) w.setFocusProxy(px) class ItemList(list): def sortItems(self): self.sort(key=lambda item: item.z()) def insertItem(self, obj): self.append(obj) self.sortItems() def removeItem(self, obj): self.remove(obj) self.sortItems() class QwtPlotDict_PrivateData(object): def __init__(self): self.itemList = ItemList() self.autoDelete = True class QwtPlotDict(object): """ A dictionary for plot items `QwtPlotDict` organizes plot items in increasing z-order. If `autoDelete()` is enabled, all attached items will be deleted in the destructor of the dictionary. `QwtPlotDict` can be used to get access to all `QwtPlotItem` items - or all items of a specific type - that are currently on the plot. .. seealso:: :py:meth:`QwtPlotItem.attach()`, :py:meth:`QwtPlotItem.detach()`, :py:meth:`QwtPlotItem.z()` """ def __init__(self): self.__data = QwtPlotDict_PrivateData() def setAutoDelete(self, autoDelete): """ En/Disable Auto deletion If Auto deletion is on all attached plot items will be deleted in the destructor of `QwtPlotDict`. The default value is on. :param bool autoDelete: enable/disable .. seealso:: :py:meth:`autoDelete()`, :py:meth:`insertItem()` """ self.__data.autoDelete = autoDelete def autoDelete(self): """ :return: true if auto deletion is enabled .. seealso:: :py:meth:`setAutoDelete()`, :py:meth:`insertItem()` """ return self.__data.autoDelete def insertItem(self, item): """ Insert a plot item :param qwt.plot.QwtPlotItem item: PlotItem .. seealso:: :py:meth:`removeItem()` """ self.__data.itemList.insertItem(item) def removeItem(self, item): """ Remove a plot item :param qwt.plot.QwtPlotItem item: PlotItem .. seealso:: :py:meth:`insertItem()` """ self.__data.itemList.removeItem(item) def detachItems(self, rtti, autoDelete): """ Detach items from the dictionary :param int rtti: In case of `QwtPlotItem.Rtti_PlotItem` detach all items otherwise only those items of the type rtti. :param bool autoDelete: If true, delete all detached items """ for item in self.__data.itemList[:]: if rtti == QwtPlotItem.Rtti_PlotItem and item.rtti() == rtti: item.attach(None) if self.autoDelete: self.__data.itemList.remove(item) def itemList(self, rtti=None): """ A list of attached plot items. Use caution when iterating these lists, as removing/detaching an item will invalidate the iterator. Instead you can place pointers to objects to be removed in a removal list, and traverse that list later. :param int rtti: In case of `QwtPlotItem.Rtti_PlotItem` detach all items otherwise only those items of the type rtti. :return: List of all attached plot items of a specific type. If rtti is None, return a list of all attached plot items. """ if rtti is None or rtti == QwtPlotItem.Rtti_PlotItem: return self.__data.itemList return [item for item in self.__data.itemList if item.rtti() == rtti] class QwtPlot_PrivateData(QwtPlotDict_PrivateData): def __init__(self): super(QwtPlot_PrivateData, self).__init__() self.titleLabel = None self.footerLabel = None self.canvas = None self.legend = None self.layout = None self.autoReplot = None class AxisData(object): def __init__(self): self.isEnabled = None self.doAutoScale = None self.minValue = None self.maxValue = None self.stepSize = None self.maxMajor = None self.maxMinor = None self.isValid = None self.scaleDiv = None # QwtScaleDiv self.scaleEngine = None # QwtScaleEngine self.scaleWidget = None # QwtScaleWidget class QwtPlot(QFrame, QwtPlotDict): """ A 2-D plotting widget QwtPlot is a widget for plotting two-dimensional graphs. An unlimited number of plot items can be displayed on its canvas. Plot items might be curves (:py:class:`qwt.plot_curve.QwtPlotCurve`), markers (:py:class:`qwt.plot_marker.QwtPlotMarker`), the grid (:py:class:`qwt.plot_grid.QwtPlotGrid`), or anything else derived from :py:class:`QwtPlotItem`. A plot can have up to four axes, with each plot item attached to an x- and a y axis. The scales at the axes can be explicitly set (`QwtScaleDiv`), or are calculated from the plot items, using algorithms (`QwtScaleEngine`) which can be configured separately for each axis. The following example is a good starting point to see how to set up a plot widget:: from qwt.qt.QtGui import QApplication from qwt import QwtPlot, QwtPlotCurve import numpy as np app = QApplication([]) x = np.linspace(-10, 10, 500) y1, y2 = np.cos(x), np.sin(x) my_plot = QwtPlot("Two curves") curve1, curve2 = QwtPlotCurve("Curve 1"), QwtPlotCurve("Curve 2") curve1.setData(x, y1) curve2.setData(x, y2) curve1.attach(my_plot) curve2.attach(my_plot) my_plot.resize(600, 300) my_plot.replot() my_plot.show() app.exec_() .. image:: /images/QwtPlot_example.png .. py:class:: QwtPlot([title=""], [parent=None]) :param str title: Title text :param QWidget parent: Parent widget .. py:data:: itemAttached A signal indicating, that an item has been attached/detached :param plotItem: Plot item :param on: Attached/Detached .. py:data:: legendDataChanged A signal with the attributes how to update the legend entries for a plot item. :param itemInfo: Info about a plot item, build from itemToInfo() :param data: Attributes of the entries (usually <= 1) for the plot item. """ itemAttached = Signal("PyQt_PyObject", bool) legendDataChanged = Signal("PyQt_PyObject", "PyQt_PyObject") # enum Axis validAxes = yLeft, yRight, xBottom, xTop = list(range(4)) axisCnt = len(validAxes) # enum LegendPosition LeftLegend, RightLegend, BottomLegend, TopLegend = list(range(4)) def __init__(self, *args): if len(args) == 0: title, parent = "", None elif len(args) == 1: if isinstance(args[0], QWidget) or args[0] is None: title = "" parent, = args else: title, = args parent = None elif len(args) == 2: title, parent = args else: raise TypeError("%s() takes 0, 1 or 2 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) QwtPlotDict.__init__(self) QFrame.__init__(self, parent) self.__layout_state = None self.__data = QwtPlot_PrivateData() from qwt.plot_layout import QwtPlotLayout self.__data.layout = QwtPlotLayout() self.__data.autoReplot = False self.setAutoReplot(True) # self.setPlotLayout(self.__data.layout) # title self.__data.titleLabel = QwtTextLabel(self) self.__data.titleLabel.setObjectName("QwtPlotTitle") self.__data.titleLabel.setFont(QFont(self.fontInfo().family(), 14, QFont.Bold)) text = QwtText(title) text.setRenderFlags(Qt.AlignCenter|Qt.TextWordWrap) self.__data.titleLabel.setText(text) # footer self.__data.footerLabel = QwtTextLabel(self) self.__data.footerLabel.setObjectName("QwtPlotFooter") footer = QwtText() footer.setRenderFlags(Qt.AlignCenter|Qt.TextWordWrap) self.__data.footerLabel.setText(footer) # legend self.__data.legend = None # axis self.__axisData = [] self.initAxesData() # canvas self.__data.canvas = QwtPlotCanvas(self) self.__data.canvas.setObjectName("QwtPlotCanvas") self.__data.canvas.installEventFilter(self) self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) self.resize(200, 200) focusChain = [self, self.__data.titleLabel, self.axisWidget(self.xTop), self.axisWidget(self.yLeft), self.__data.canvas, self.axisWidget(self.yRight), self.axisWidget(self.xBottom), self.__data.footerLabel] for idx in range(len(focusChain)-1): qwtSetTabOrder(focusChain[idx], focusChain[idx+1], False) qwtEnableLegendItems(self, True) def __del__(self): #XXX Is is really necessary in Python? (pure transcription of C++) self.setAutoReplot(False) self.detachItems(QwtPlotItem.Rtti_PlotItem, self.autoDelete()) self.__data.layout = None self.deleteAxesData() self.__data = None def initAxesData(self): """Initialize axes""" self.__axisData = [AxisData() for axisId in self.validAxes] self.__axisData[self.yLeft].scaleWidget = \ QwtScaleWidget(QwtScaleDraw.LeftScale, self) self.__axisData[self.yRight].scaleWidget = \ QwtScaleWidget(QwtScaleDraw.RightScale, self) self.__axisData[self.xTop].scaleWidget = \ QwtScaleWidget(QwtScaleDraw.TopScale, self) self.__axisData[self.xBottom].scaleWidget = \ QwtScaleWidget(QwtScaleDraw.BottomScale, self) self.__axisData[self.yLeft ].scaleWidget.setObjectName("QwtPlotAxisYLeft") self.__axisData[self.yRight ].scaleWidget.setObjectName("QwtPlotAxisYRight") self.__axisData[self.xTop ].scaleWidget.setObjectName("QwtPlotAxisXTop") self.__axisData[self.xBottom ].scaleWidget.setObjectName("QwtPlotAxisXBottom") fscl = QFont(self.fontInfo().family(), 10) fttl = QFont(self.fontInfo().family(), 12, QFont.Bold) for axisId in self.validAxes: d = self.__axisData[axisId] d.scaleEngine = QwtLinearScaleEngine() d.scaleWidget.setTransformation(d.scaleEngine.transformation()) d.scaleWidget.setFont(fscl) d.scaleWidget.setMargin(2) text = d.scaleWidget.title() text.setFont(fttl) d.scaleWidget.setTitle(text) d.doAutoScale = True d.minValue = 0.0 d.maxValue = 1000.0 d.stepSize = 0.0 d.maxMinor = 5 d.maxMajor = 8 d.isValid = False self.__axisData[self.yLeft].isEnabled = True self.__axisData[self.yRight].isEnabled = False self.__axisData[self.xBottom].isEnabled = True self.__axisData[self.xTop].isEnabled = False def deleteAxesData(self): #XXX Is is really necessary in Python? (pure transcription of C++) for axisId in self.validAxes: self.__axisData[axisId].scaleEngine = None self.__axisData[axisId] = None def axisWidget(self, axisId): """ :param int axisId: Axis index :return: Scale widget of the specified axis, or None if axisId is invalid. """ if self.axisValid(axisId): return self.__axisData[axisId].scaleWidget def setAxisScaleEngine(self, axisId, scaleEngine): """ Change the scale engine for an axis :param int axisId: Axis index :param qwt.scale_engine.QwtScaleEngine scaleEngine: Scale engine .. seealso:: :py:meth:`axisScaleEngine()` """ if self.axisValid(axisId) and scaleEngine is not None: d = self.__axisData[axisId] d.scaleEngine = scaleEngine self.__axisData[axisId].scaleWidget.setTransformation( scaleEngine.transformation()) d.isValid = False self.autoRefresh() def axisScaleEngine(self, axisId): """ :param int axisId: Axis index :return: Scale engine for a specific axis .. seealso:: :py:meth:`setAxisScaleEngine()` """ if self.axisValid(axisId): return self.__axisData[axisId].scaleEngine def axisAutoScale(self, axisId): """ :param int axisId: Axis index :return: True, if autoscaling is enabled """ if self.axisValid(axisId): return self.__axisData[axisId].doAutoScale def axisEnabled(self, axisId): """ :param int axisId: Axis index :return: True, if a specified axis is enabled """ if self.axisValid(axisId): return self.__axisData[axisId].isEnabled def axisFont(self, axisId): """ :param int axisId: Axis index :return: The font of the scale labels for a specified axis """ if self.axisValid(axisId): return self.axisWidget(axisId).font() else: return QFont() def axisMaxMajor(self, axisId): """ :param int axisId: Axis index :return: The maximum number of major ticks for a specified axis .. seealso:: :py:meth:`setAxisMaxMajor()`, :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` """ if self.axisValid(axisId): return self.axisWidget(axisId).maxMajor else: return 0 def axisMaxMinor(self, axisId): """ :param int axisId: Axis index :return: The maximum number of minor ticks for a specified axis .. seealso:: :py:meth:`setAxisMaxMinor()`, :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` """ if self.axisValid(axisId): return self.axisWidget(axisId).maxMinor else: return 0 def axisScaleDiv(self, axisId): """ :param int axisId: Axis index :return: The scale division of a specified axis axisScaleDiv(axisId).lowerBound(), axisScaleDiv(axisId).upperBound() are the current limits of the axis scale. .. seealso:: :py:class:`qwt.scale_div.QwtScaleDiv`, :py:meth:`setAxisScaleDiv()`, :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` """ return self.__axisData[axisId].scaleDiv def axisScaleDraw(self, axisId): """ :param int axisId: Axis index :return: Specified scaleDraw for axis, or NULL if axis is invalid. """ if self.axisValid(axisId): return self.axisWidget(axisId).scaleDraw() def axisStepSize(self, axisId): """ :param int axisId: Axis index :return: step size parameter value This doesn't need to be the step size of the current scale. .. seealso:: :py:meth:`setAxisScale()`, :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` """ if self.axisValid(axisId): return self.axisWidget(axisId).stepSize else: return 0 def axisInterval(self, axisId): """ :param int axisId: Axis index :return: The current interval of the specified axis This is only a convenience function for axisScaleDiv(axisId).interval() .. seealso:: :py:class:`qwt.scale_div.QwtScaleDiv`, :py:meth:`axisScaleDiv()` """ if self.axisValid(axisId): return self.axisWidget(axisId).scaleDiv.interval() else: return QwtInterval() def axisTitle(self, axisId): """ :param int axisId: Axis index :return: Title of a specified axis """ if self.axisValid(axisId): return self.axisWidget(axisId).title() else: return QwtText() def enableAxis(self, axisId, tf=True): """ Enable or disable a specified axis When an axis is disabled, this only means that it is not visible on the screen. Curves, markers and can be attached to disabled axes, and transformation of screen coordinates into values works as normal. Only xBottom and yLeft are enabled by default. :param int axisId: Axis index :param bool tf: True (enabled) or False (disabled) """ if self.axisValid(axisId) and tf != self.__axisData[axisId].isEnabled: self.__axisData[axisId].isEnabled = tf self.updateLayout() def invTransform(self, axisId, pos): """ Transform the x or y coordinate of a position in the drawing region into a value. :param int axisId: Axis index :param int pos: position .. warning:: The position can be an x or a y coordinate, depending on the specified axis. """ if self.axisValid(axisId): return self.canvasMap(axisId).invTransform(pos) else: return 0. def transform(self, axisId, value): """ Transform a value into a coordinate in the plotting region :param int axisId: Axis index :param fload value: Value :return: X or Y coordinate in the plotting region corresponding to the value. """ if self.axisValid(axisId): return self.canvasMap(axisId).transform(value) else: return 0. def setAxisFont(self, axisId, font): """ Change the font of an axis :param int axisId: Axis index :param QFont font: Font .. warning:: This function changes the font of the tick labels, not of the axis title. """ if self.axisValid(axisId): return self.axisWidget(axisId).setFont(font) def setAxisAutoScale(self, axisId, on=True): """ Enable autoscaling for a specified axis This member function is used to switch back to autoscaling mode after a fixed scale has been set. Autoscaling is enabled by default. :param int axisId: Axis index :param bool on: On/Off .. seealso:: :py:meth:`setAxisScale()`, :py:meth:`setAxisScaleDiv()`, :py:meth:`updateAxes()` .. note:: The autoscaling flag has no effect until updateAxes() is executed ( called by replot() ). """ if self.axisValid(axisId) and self.__axisData[axisId].doAutoScale != on: self.__axisData[axisId].doAutoScale = on self.autoRefresh() def setAxisScale(self, axisId, min_, max_, stepSize=0): """ Disable autoscaling and specify a fixed scale for a selected axis. In updateAxes() the scale engine calculates a scale division from the specified parameters, that will be assigned to the scale widget. So updates of the scale widget usually happen delayed with the next replot. :param int axisId: Axis index :param float min_: Minimum of the scale :param float max_: Maximum of the scale :param float stepSize: Major step size. If step == 0, the step size is calculated automatically using the maxMajor setting. .. seealso:: :py:meth:`setAxisMaxMajor()`, :py:meth:`setAxisAutoScale()`, :py:meth:`axisStepSize()`, :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()` """ if self.axisValid(axisId): d = self.__axisData[axisId] d.doAutoScale = False d.isValid = False d.minValue = min_ d.maxValue = max_ d.stepSize = stepSize self.autoRefresh() def setAxisScaleDiv(self, axisId, scaleDiv): """ Disable autoscaling and specify a fixed scale for a selected axis. The scale division will be stored locally only until the next call of updateAxes(). So updates of the scale widget usually happen delayed with the next replot. :param int axisId: Axis index :param qwt.scale_div.QwtScaleDiv scaleDiv: Scale division .. seealso:: :py:meth:`setAxisScale()`, :py:meth:`setAxisAutoScale()` """ if self.axisValid(axisId): d = self.__axisData[axisId] d.doAutoScale = False d.scaleDiv = scaleDiv d.isValid = True self.autoRefresh() def setAxisScaleDraw(self, axisId, scaleDraw): """ Set a scale draw :param int axisId: Axis index :param qwt.scale_draw.QwtScaleDraw scaleDraw: Object responsible for drawing scales. By passing scaleDraw it is possible to extend QwtScaleDraw functionality and let it take place in QwtPlot. Please note that scaleDraw has to be created with new and will be deleted by the corresponding QwtScale member ( like a child object ). .. seealso:: :py:class:`qwt.scale_draw.QwtScaleDraw`, :py:class:`qwt.scale_widget.QwtScaleWigdet` .. warning:: The attributes of scaleDraw will be overwritten by those of the previous QwtScaleDraw. """ if self.axisValid(axisId): self.axisWidget(axisId).setScaleDraw(scaleDraw) self.autoRefresh() def setAxisLabelAlignment(self, axisId, alignment): """ Change the alignment of the tick labels :param int axisId: Axis index :param Qt.Alignment alignment: Or'd Qt.AlignmentFlags .. seealso:: :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAlignment()` """ if self.axisValid(axisId): self.axisWidget(axisId).setLabelAlignment(alignment) def setAxisLabelRotation(self, axisId, rotation): """ Rotate all tick labels :param int axisId: Axis index :param float rotation: Angle in degrees. When changing the label rotation, the label alignment might be adjusted too. .. seealso:: :py:meth:`setLabelRotation()`, :py:meth:`setAxisLabelAlignment()` """ if self.axisValid(axisId): self.axisWidget(axisId).setLabelRotation(rotation) def setAxisLabelAutoSize(self, axisId, state): """ Set tick labels automatic size option (default: on) :param int axisId: Axis index :param bool state: On/off .. seealso:: :py:meth:`qwt.scale_draw.QwtScaleDraw.setLabelAutoSize()` """ if self.axisValid(axisId): self.axisWidget(axisId).setLabelAutoSize(state) def setAxisMaxMinor(self, axisId, maxMinor): """ Set the maximum number of minor scale intervals for a specified axis :param int axisId: Axis index :param int maxMinor: Maximum number of minor steps .. seealso:: :py:meth:`axisMaxMinor()` """ if self.axisValid(axisId): maxMinor = max([0, min([maxMinor, 100])]) d = self.__axisData[axisId] if maxMinor != d.maxMinor: d.maxMinor = maxMinor d.isValid = False self.autoRefresh() def setAxisMaxMajor(self, axisId, maxMajor): """ Set the maximum number of major scale intervals for a specified axis :param int axisId: Axis index :param int maxMajor: Maximum number of major steps .. seealso:: :py:meth:`axisMaxMajor()` """ if self.axisValid(axisId): maxMajor = max([1, min([maxMajor, 10000])]) d = self.__axisData[axisId] if maxMajor != d.maxMajor: d.maxMajor = maxMajor d.isValid = False self.autoRefresh() def setAxisTitle(self, axisId, title): """ Change the title of a specified axis :param int axisId: Axis index :param title: axis title :type title: qwt.text.QwtText or str """ if self.axisValid(axisId): self.axisWidget(axisId).setTitle(title) self.updateLayout() def updateAxes(self): """ Rebuild the axes scales In case of autoscaling the boundaries of a scale are calculated from the bounding rectangles of all plot items, having the `QwtPlotItem.AutoScale` flag enabled (`QwtScaleEngine.autoScale()`). Then a scale division is calculated (`QwtScaleEngine.didvideScale()`) and assigned to scale widget. When the scale boundaries have been assigned with `setAxisScale()` a scale division is calculated (`QwtScaleEngine.didvideScale()`) for this interval and assigned to the scale widget. When the scale has been set explicitly by `setAxisScaleDiv()` the locally stored scale division gets assigned to the scale widget. The scale widget indicates modifications by emitting a `QwtScaleWidget.scaleDivChanged()` signal. `updateAxes()` is usually called by `replot()`. .. seealso:: :py:meth:`setAxisAutoScale()`, :py:meth:`setAxisScale()`, :py:meth:`setAxisScaleDiv()`, :py:meth:`replot()`, :py:meth:`QwtPlotItem.boundingRect()` """ intv = [QwtInterval() for _i in self.validAxes] itmList = self.itemList() for item in itmList: if not item.testItemAttribute(QwtPlotItem.AutoScale): continue if not item.isVisible(): continue if self.axisAutoScale(item.xAxis()) or self.axisAutoScale(item.yAxis()): rect = item.boundingRect() if rect.width() >= 0.: intv[item.xAxis()] |= QwtInterval(rect.left(), rect.right()) if rect.height() >= 0.: intv[item.yAxis()] |= QwtInterval(rect.top(), rect.bottom()) for axisId in self.validAxes: d = self.__axisData[axisId] minValue = d.minValue maxValue = d.maxValue stepSize = d.stepSize if d.doAutoScale and intv[axisId].isValid(): d.isValid = False minValue = intv[axisId].minValue() maxValue = intv[axisId].maxValue() d.scaleEngine.autoScale(d.maxMajor, minValue, maxValue, stepSize) if not d.isValid: d.scaleDiv = d.scaleEngine.divideScale(minValue, maxValue, d.maxMajor, d.maxMinor, stepSize) d.isValid = True scaleWidget = self.axisWidget(axisId) scaleWidget.setScaleDiv(d.scaleDiv) # It is *really* necessary to update border dist! # Otherwise, when tick labels are large enough, the ticks # may not be aligned with canvas grid. # See the following issues for more details: # https://github.com/PierreRaybaut/guiqwt/issues/57 # https://github.com/PierreRaybaut/PythonQwt/issues/30 startDist, endDist = scaleWidget.getBorderDistHint() scaleWidget.setBorderDist(startDist, endDist) for item in itmList: if item.testItemInterest(QwtPlotItem.ScaleInterest): item.updateScaleDiv(self.axisScaleDiv(item.xAxis()), self.axisScaleDiv(item.yAxis())) def setCanvas(self, canvas): """ Set the drawing canvas of the plot widget. The default canvas is a `QwtPlotCanvas`. :param QWidget canvas: Canvas Widget .. seealso:: :py:meth:`canvas()` """ if canvas == self.__data.canvas: return self.__data.canvas = canvas if canvas is not None: canvas.setParent(self) canvas.installEventFilter(self) if self.isVisible(): canvas.show() def event(self, event): ok = QFrame.event(self, event) if event.type() == QEvent.LayoutRequest: self.updateLayout() elif event.type() == QEvent.PolishRequest: self.replot() return ok def eventFilter(self, obj, event): if obj is self.__data.canvas: if event.type() == QEvent.Resize: self.updateCanvasMargins() elif event.type() == 178:#QEvent.ContentsRectChange: self.updateLayout() return QFrame.eventFilter(self, obj, event) def autoRefresh(self): """Replots the plot if :py:meth:`autoReplot()` is True.""" if self.__data.autoReplot: self.replot() def setAutoReplot(self, tf=True): """ Set or reset the autoReplot option If the autoReplot option is set, the plot will be updated implicitly by manipulating member functions. Since this may be time-consuming, it is recommended to leave this option switched off and call :py:meth:`replot()` explicitly if necessary. The autoReplot option is set to false by default, which means that the user has to call :py:meth:`replot()` in order to make changes visible. :param bool tf: True or False. Defaults to True. .. seealso:: :py:meth:`canvas()` """ self.__data.autoReplot = tf def autoReplot(self): """ :return: True if the autoReplot option is set. .. seealso:: :py:meth:`setAutoReplot()` """ return self.__data.autoReplot def setTitle(self, title): """ Change the plot's title :param title: New title :type title: str or qwt.text.QwtText .. seealso:: :py:meth:`title()` """ current_title = self.__data.titleLabel.text() if isinstance(title, QwtText) and current_title == title: return elif not isinstance(title, QwtText) and current_title.text() == title: return self.__data.titleLabel.setText(title) self.updateLayout() def title(self): """ :return: Title of the plot .. seealso:: :py:meth:`setTitle()` """ return self.__data.titleLabel.text() def titleLabel(self): """ :return: Title label widget. """ return self.__data.titleLabel def setFooter(self, text): """ Change the text the footer :param text: New text of the footer :type text: str or qwt.text.QwtText .. seealso:: :py:meth:`footer()` """ current_footer = self.__data.footerLabel.text() if isinstance(text, QwtText) and current_footer == text: return elif not isinstance(text, QwtText) and current_footer.text() == text: return self.__data.footerLabel.setText(text) self.updateLayout() def footer(self): """ :return: Text of the footer .. seealso:: :py:meth:`setFooter()` """ return self.__data.footerLabel.text() def footerLabel(self): """ :return: Footer label widget. """ return self.__data.footerLabel def setPlotLayout(self, layout): """ Assign a new plot layout :param layout: Layout :type layout: qwt.plot_layout.QwtPlotLayout .. seealso:: :py:meth:`plotLayout()` """ if layout != self.__data.layout: self.__data.layout = layout self.updateLayout() def plotLayout(self): """ :return: the plot's layout .. seealso:: :py:meth:`setPlotLayout()` """ return self.__data.layout def legend(self): """ :return: the plot's legend .. seealso:: :py:meth:`insertLegend()` """ return self.__data.legend def canvas(self): """ :return: the plot's canvas """ return self.__data.canvas def sizeHint(self): """ :return: Size hint for the plot widget .. seealso:: :py:meth:`minimumSizeHint()` """ dw = dh = 0 for axisId in self.validAxes: if self.axisEnabled(axisId): niceDist = 40 scaleWidget = self.axisWidget(axisId) scaleDiv = scaleWidget.scaleDraw().scaleDiv() majCnt = len(scaleDiv.ticks(QwtScaleDiv.MajorTick)) if axisId in (self.yLeft, self.yRight): hDiff = (majCnt-1)*niceDist-scaleWidget.minimumSizeHint().height() if hDiff > dh: dh = hDiff else: wDiff = (majCnt-1)*niceDist-scaleWidget.minimumSizeHint().width() if wDiff > dw: dw = wDiff return self.minimumSizeHint() + QSize(dw, dh) def minimumSizeHint(self): """ :return: Return a minimum size hint """ hint = self.__data.layout.minimumSizeHint(self) hint += QSize(2*self.frameWidth(), 2*self.frameWidth()) return hint def resizeEvent(self, e): QFrame.resizeEvent(self, e) self.updateLayout() def replot(self): """ Redraw the plot If the `autoReplot` option is not set (which is the default) or if any curves are attached to raw data, the plot has to be refreshed explicitly in order to make changes visible. .. seealso:: :py:meth:`updateAxes()`, :py:meth:`setAutoReplot()` """ doAutoReplot = self.autoReplot() self.setAutoReplot(False) self.updateAxes() # Maybe the layout needs to be updated, because of changed # axes labels. We need to process them here before painting # to avoid that scales and canvas get out of sync. QApplication.sendPostedEvents(self, QEvent.LayoutRequest) if self.__data.canvas: try: self.__data.canvas.replot() except (AttributeError, TypeError): self.__data.canvas.update(self.__data.canvas.contentsRect()) self.setAutoReplot(doAutoReplot) def get_layout_state(self): return (self.contentsRect(), self.__data.titleLabel.text(), self.__data.footerLabel.text(), [(self.axisEnabled(axisId), self.axisTitle(axisId).text()) for axisId in self.validAxes], self.__data.legend) def updateLayout(self): """ Adjust plot content to its current size. .. seealso:: :py:meth:`resizeEvent()` """ # state = self.get_layout_state() # if self.__layout_state is not None and\ # state == self.__layout_state: # return # self.__layout_state = state self.__data.layout.activate(self, self.contentsRect()) titleRect = self.__data.layout.titleRect().toRect() footerRect = self.__data.layout.footerRect().toRect() scaleRect = [self.__data.layout.scaleRect(axisId).toRect() for axisId in self.validAxes] legendRect = self.__data.layout.legendRect().toRect() canvasRect = self.__data.layout.canvasRect().toRect() if self.__data.titleLabel.text(): self.__data.titleLabel.setGeometry(titleRect) if not self.__data.titleLabel.isVisibleTo(self): self.__data.titleLabel.show() else: self.__data.titleLabel.hide() if self.__data.footerLabel.text(): self.__data.footerLabel.setGeometry(footerRect) if not self.__data.footerLabel.isVisibleTo(self): self.__data.footerLabel.show() else: self.__data.footerLabel.hide() for axisId in self.validAxes: if self.axisEnabled(axisId): self.axisWidget(axisId).setGeometry(scaleRect[axisId]) if axisId in (self.xBottom, self.xTop): r = QRegion(scaleRect[axisId]) if self.axisEnabled(self.yLeft): r = r.subtracted(QRegion(scaleRect[self.yLeft])) if self.axisEnabled(self.yRight): r = r.subtracted(QRegion(scaleRect[self.yRight])) r.translate(-scaleRect[axisId].x(), -scaleRect[axisId].y()) self.axisWidget(axisId).setMask(r) if not self.axisWidget(axisId).isVisibleTo(self): self.axisWidget(axisId).show() else: self.axisWidget(axisId).hide() if self.__data.legend: if self.__data.legend.isEmpty(): self.__data.legend.hide() else: self.__data.legend.setGeometry(legendRect) self.__data.legend.show() self.__data.canvas.setGeometry(canvasRect) def getCanvasMarginsHint(self, maps, canvasRect): """ Calculate the canvas margins :param list maps: `QwtPlot.axisCnt` maps, mapping between plot and paint device coordinates :param QRectF canvasRect: Bounding rectangle where to paint Plot items might indicate, that they need some extra space at the borders of the canvas by the `QwtPlotItem.Margins` flag. .. seealso:: :py:meth:`updateCanvasMargins()`, :py:meth:`getCanvasMarginHint()` """ left = top = right = bottom = -1. for item in self.itemList(): if item.testItemAttribute(QwtPlotItem.Margins): m = item.getCanvasMarginHint(maps[item.xAxis()], maps[item.yAxis()], canvasRect) left = max([left, m[self.yLeft]]) top = max([top, m[self.xTop]]) right = max([right, m[self.yRight]]) bottom = max([bottom, m[self.xBottom]]) return left, top, right, bottom def updateCanvasMargins(self): """ Update the canvas margins Plot items might indicate, that they need some extra space at the borders of the canvas by the `QwtPlotItem.Margins` flag. .. seealso:: :py:meth:`getCanvasMarginsHint()`, :py:meth:`QwtPlotItem.getCanvasMarginHint()` """ maps = [self.canvasMap(axisId) for axisId in self.validAxes] margins = self.getCanvasMarginsHint(maps, self.canvas().contentsRect()) doUpdate = False for axisId in self.validAxes: if margins[axisId] >= 0.: m = np.ceil(margins[axisId]) self.plotLayout().setCanvasMargin(m, axisId) doUpdate = True if doUpdate: self.updateLayout() def drawCanvas(self, painter): """ Redraw the canvas. :param QPainter painter: Painter used for drawing .. warning:: drawCanvas calls drawItems what is also used for printing. Applications that like to add individual plot items better overload drawItems() .. seealso:: :py:meth:`getCanvasMarginsHint()`, :py:meth:`QwtPlotItem.getCanvasMarginHint()` """ maps = [self.canvasMap(axisId) for axisId in self.validAxes] self.drawItems(painter, self.__data.canvas.contentsRect(), maps) def drawItems(self, painter, canvasRect, maps): """ Redraw the canvas. :param QPainter painter: Painter used for drawing :param QRectF canvasRect: Bounding rectangle where to paint :param list maps: `QwtPlot.axisCnt` maps, mapping between plot and paint device coordinates .. note:: Usually canvasRect is `contentsRect()` of the plot canvas. Due to a bug in Qt this rectangle might be wrong for certain frame styles ( f.e `QFrame.Box` ) and it might be necessary to fix the margins manually using `QWidget.setContentsMargins()` """ for item in self.itemList(): if item and item.isVisible(): painter.save() painter.setRenderHint(QPainter.Antialiasing, item.testRenderHint(QwtPlotItem.RenderAntialiased)) painter.setRenderHint(QPainter.HighQualityAntialiasing, item.testRenderHint(QwtPlotItem.RenderAntialiased)) item.draw(painter, maps[item.xAxis()], maps[item.yAxis()], canvasRect) painter.restore() def canvasMap(self, axisId): """ :param int axisId: Axis :return: Map for the axis on the canvas. With this map pixel coordinates can translated to plot coordinates and vice versa. .. seealso:: :py:class:`qwt.scale_map.QwtScaleMap`, :py:meth:`transform()`, :py:meth:`invTransform()` """ map_ = QwtScaleMap() if not self.__data.canvas: return map_ map_.setTransformation(self.axisScaleEngine(axisId).transformation()) sd = self.axisScaleDiv(axisId) map_.setScaleInterval(sd.lowerBound(), sd.upperBound()) if self.axisEnabled(axisId): s = self.axisWidget(axisId) if axisId in (self.yLeft, self.yRight): y = s.y() + s.startBorderDist() - self.__data.canvas.y() h = s.height() - s.startBorderDist() - s.endBorderDist() map_.setPaintInterval(y+h, y) else: x = s.x() + s.startBorderDist() - self.__data.canvas.x() w = s.width() - s.startBorderDist() - s.endBorderDist() map_.setPaintInterval(x, x+w) else: canvasRect = self.__data.canvas.contentsRect() if axisId in (self.yLeft, self.yRight): top = 0 if not self.plotLayout().alignCanvasToScale(self.xTop): top = self.plotLayout().canvasMargin(self.xTop) bottom = 0 if not self.plotLayout().alignCanvasToScale(self.xBottom): bottom = self.plotLayout().canvasMargin(self.xBottom) map_.setPaintInterval(canvasRect.bottom()-bottom, canvasRect.top()+top) else: left = 0 if not self.plotLayout().alignCanvasToScale(self.yLeft): left = self.plotLayout().canvasMargin(self.yLeft) right = 0 if not self.plotLayout().alignCanvasToScale(self.yRight): right = self.plotLayout().canvasMargin(self.yRight) map_.setPaintInterval(canvasRect.left()+left, canvasRect.right()-right) return map_ def setCanvasBackground(self, brush): """ Change the background of the plotting area Sets brush to `QPalette.Window` of all color groups of the palette of the canvas. Using `canvas().setPalette()` is a more powerful way to set these colors. :param QBrush brush: New background brush .. seealso:: :py:meth:`canvasBackground()` """ pal = self.__data.canvas.palette() pal.setBrush(QPalette.Window, brush) self.canvas().setPalette(pal) def canvasBackground(self): """ :return: Background brush of the plotting area. .. seealso:: :py:meth:`setCanvasBackground()` """ return self.canvas().palette().brush(QPalette.Normal, QPalette.Window) def axisValid(self, axisId): """ :param int axisId: Axis :return: True if the specified axis exists, otherwise False """ return axisId in QwtPlot.validAxes def insertLegend(self, legend, pos=None, ratio=-1): """ Insert a legend If the position legend is `QwtPlot.LeftLegend` or `QwtPlot.RightLegend` the legend will be organized in one column from top to down. Otherwise the legend items will be placed in a table with a best fit number of columns from left to right. insertLegend() will set the plot widget as parent for the legend. The legend will be deleted in the destructor of the plot or when another legend is inserted. Legends, that are not inserted into the layout of the plot widget need to connect to the legendDataChanged() signal. Calling updateLegend() initiates this signal for an initial update. When the application code wants to implement its own layout this also needs to be done for rendering plots to a document ( see QwtPlotRenderer ). :param qwt.legend.QwtAbstractLegend legend: Legend :param QwtPlot.LegendPosition pos: The legend's position. :param float ratio: Ratio between legend and the bounding rectangle of title, canvas and axes .. note:: For top/left position the number of columns will be limited to 1, otherwise it will be set to unlimited. .. note:: The legend will be shrunk if it would need more space than the given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of <= 0.0 it will be reset to the default ratio. The default vertical/horizontal ratio is 0.33/0.5. .. seealso:: :py:meth:`legend()`, :py:meth:`qwt.plot_layout.QwtPlotLayout.legendPosition()`, :py:meth:`qwt.plot_layout.QwtPlotLayout.setLegendPosition()` """ if pos is None: pos = self.RightLegend self.__data.layout.setLegendPosition(pos, ratio) if legend != self.__data.legend: if self.__data.legend and self.__data.legend.parent() is self: del self.__data.legend self.__data.legend = legend if self.__data.legend: self.legendDataChanged.connect(self.__data.legend.updateLegend) if self.__data.legend.parent() is not self: self.__data.legend.setParent(self) qwtEnableLegendItems(self, False) self.updateLegend() qwtEnableLegendItems(self, True) lpos = self.__data.layout.legendPosition() if legend is not None: if lpos in (self.LeftLegend, self.RightLegend): if legend.maxColumns() == 0: legend.setMaxColumns(1) elif lpos in (self.TopLegend, self.BottomLegend): legend.setMaxColumns(0) previousInChain = None if lpos == self.LeftLegend: previousInChain = self.axisWidget(QwtPlot.xTop) elif lpos == self.TopLegend: previousInChain = self elif lpos == self.RightLegend: previousInChain = self.axisWidget(QwtPlot.yRight) elif lpos == self.BottomLegend: previousInChain = self.footerLabel() if previousInChain: qwtSetTabOrder(previousInChain, legend, True) self.updateLayout() def updateLegend(self, plotItem=None): """ If plotItem is None, emit QwtPlot.legendDataChanged for all plot item. Otherwise, emit the signal for passed plot item. :param qwt.plot.QwtPlotItem plotItem: Plot item .. seealso:: :py:meth:`QwtPlotItem.legendData()`, :py:data:`QwtPlot.legendDataChanged` """ if plotItem is None: items = list(self.itemList()) else: items = [plotItem] for plotItem in items: if plotItem is None: continue legendData = [] if plotItem.testItemAttribute(QwtPlotItem.Legend): legendData = plotItem.legendData() self.legendDataChanged.emit(plotItem, legendData) def updateLegendItems(self, plotItem, legendData): """ Update all plot items interested in legend attributes Call `QwtPlotItem.updateLegend()`, when the `QwtPlotItem.LegendInterest` flag is set. :param qwt.plot.QwtPlotItem plotItem: Plot item :param list legendData: Entries to be displayed for the plot item ( usually 1 ) .. seealso:: :py:meth:`QwtPlotItem.LegendInterest()`, :py:meth:`QwtPlotItem.updateLegend` """ if plotItem is not None: for item in self.itemList(): if item.testItemInterest(QwtPlotItem.LegendInterest): item.updateLegend(plotItem, legendData) def attachItem(self, plotItem, on): """ Attach/Detach a plot item :param qwt.plot.QwtPlotItem plotItem: Plot item :param bool on: When true attach the item, otherwise detach it """ if plotItem.testItemInterest(QwtPlotItem.LegendInterest): for item in self.itemList(): legendData = [] if on and item.testItemAttribute(QwtPlotItem.Legend): legendData = item.legendData() plotItem.updateLegend(item, legendData) if on: self.insertItem(plotItem) else: self.removeItem(plotItem) self.itemAttached.emit(plotItem, on) if plotItem.testItemAttribute(QwtPlotItem.Legend): if on: self.updateLegend(plotItem) else: self.legendDataChanged.emit(plotItem, []) self.autoRefresh() def print_(self, printer): """ Print plot to printer :param printer: Printer :type printer: QPaintDevice or QPrinter or QSvgGenerator """ from qwt.plot_renderer import QwtPlotRenderer renderer = QwtPlotRenderer(self) renderer.renderTo(self, printer) def exportTo(self, filename, size=(800, 600), size_mm=None, resolution=72., format_=None): """ Export plot to PDF or image file (SVG, PNG, ...) :param str filename: Filename :param tuple size: (width, height) size in pixels :param tuple size_mm: (width, height) size in millimeters :param float resolution: Image resolution :param str format_: File format (PDF, SVG, PNG, ...) """ if size_mm is None: size_mm = tuple(25.4*np.array(size)/resolution) from qwt.plot_renderer import QwtPlotRenderer renderer = QwtPlotRenderer(self) renderer.renderDocument(self, filename, size_mm, resolution, format_) class QwtPlotItem_PrivateData(object): def __init__(self): self.plot = None self.isVisible = True self.attributes = 0 self.interests = 0 self.renderHints = 0 self.z = 0. self.xAxis = QwtPlot.xBottom self.yAxis = QwtPlot.yLeft self.legendIconSize = QSize(8, 8) self.title = None # QwtText class QwtPlotItem(object): """ Base class for items on the plot canvas A plot item is "something", that can be painted on the plot canvas, or only affects the scales of the plot widget. They can be categorized as: - Representator A "Representator" is an item that represents some sort of data on the plot canvas. The different representator classes are organized according to the characteristics of the data: - :py:class:`qwt.plot_marker.QwtPlotMarker`: Represents a point or a horizontal/vertical coordinate - :py:class:`qwt.plot_curve.QwtPlotCurve`: Represents a series of points - Decorators A "Decorator" is an item, that displays additional information, that is not related to any data: - :py:class:`qwt.plot_grid.QwtPlotGrid` Depending on the `QwtPlotItem.ItemAttribute` flags, an item is included into autoscaling or has an entry on the legend. Before misusing the existing item classes it might be better to implement a new type of plot item ( don't implement a watermark as spectrogram ). Deriving a new type of `QwtPlotItem` primarily means to implement the `YourPlotItem.draw()` method. .. seealso:: The cpuplot example shows the implementation of additional plot items. .. py:class:: QwtPlotItem([title=None]) Constructor :param title: Title of the item :type title: qwt.text.QwtText or str """ # enum RttiValues (Rtti_PlotItem, Rtti_PlotGrid, Rtti_PlotScale, Rtti_PlotLegend, Rtti_PlotMarker, Rtti_PlotCurve, Rtti_PlotSpectroCurve, Rtti_PlotIntervalCurve, Rtti_PlotHistogram, Rtti_PlotSpectrogram, Rtti_PlotSVG, Rtti_PlotTradingCurve, Rtti_PlotBarChart, Rtti_PlotMultiBarChart, Rtti_PlotShape, Rtti_PlotTextLabel, Rtti_PlotZone) = list(range(17)) Rtti_PlotUserItem = 1000 # enum ItemAttribute Legend = 0x01 AutoScale = 0x02 Margins = 0x04 # enum ItemInterest ScaleInterest = 0x01 LegendInterest = 0x02 # enum RenderHint RenderAntialiased = 0x1 def __init__(self, title=None): """title: QwtText""" if title is None: title = QwtText("") if hasattr(title, 'capitalize'): # avoids dealing with Py3K compat. title = QwtText(title) assert isinstance(title, QwtText) self.__data = QwtPlotItem_PrivateData() self.__data.title = title def attach(self, plot): """ Attach the item to a plot. This method will attach a `QwtPlotItem` to the `QwtPlot` argument. It will first detach the `QwtPlotItem` from any plot from a previous call to attach (if necessary). If a None argument is passed, it will detach from any `QwtPlot` it was attached to. :param qwt.plot.QwtPlot plot: Plot widget .. seealso:: :py:meth:`detach()` """ if plot is self.__data.plot: return if self.__data.plot: self.__data.plot.attachItem(self, False) self.__data.plot = plot if self.__data.plot: self.__data.plot.attachItem(self, True) def detach(self): """ Detach the item from a plot. This method detaches a `QwtPlotItem` from any `QwtPlot` it has been associated with. .. seealso:: :py:meth:`attach()` """ self.attach(None) def rtti(self): """ Return rtti for the specific class represented. `QwtPlotItem` is simply a virtual interface class, and base classes will implement this method with specific rtti values so a user can differentiate them. :return: rtti value """ return self.Rtti_PlotItem def plot(self): """ :return: attached plot """ return self.__data.plot def z(self): """ Plot items are painted in increasing z-order. :return: item z order .. seealso:: :py:meth:`setZ()`, :py:meth:`QwtPlotDict.itemList()` """ return self.__data.z def setZ(self, z): """ Set the z value Plot items are painted in increasing z-order. :param float z: Z-value .. seealso:: :py:meth:`z()`, :py:meth:`QwtPlotDict.itemList()` """ if self.__data.z != z: if self.__data.plot: self.__data.plot.attachItem(self, False) self.__data.z = z if self.__data.plot: self.__data.plot.attachItem(self, True) self.itemChanged() def setTitle(self, title): """ Set a new title :param title: Title :type title: qwt.text.QwtText or str .. seealso:: :py:meth:`title()` """ if not isinstance(title, QwtText): title = QwtText(title) if self.__data.title != title: self.__data.title = title self.legendChanged() def title(self): """ :return: Title of the item .. seealso:: :py:meth:`setTitle()` """ return self.__data.title def setItemAttribute(self, attribute, on=True): """ Toggle an item attribute :param int attribute: Attribute type :param bool on: True/False .. seealso:: :py:meth:`testItemAttribute()` """ if bool(self.__data.attributes & attribute) != on: if on: self.__data.attributes |= attribute else: self.__data.attributes &= ~attribute if attribute == QwtPlotItem.Legend: self.legendChanged() self.itemChanged() def testItemAttribute(self, attribute): """ Test an item attribute :param int attribute: Attribute type :return: True/False .. seealso:: :py:meth:`setItemAttribute()` """ return bool(self.__data.attributes & attribute) def setItemInterest(self, interest, on=True): """ Toggle an item interest :param int attribute: Interest type :param bool on: True/False .. seealso:: :py:meth:`testItemInterest()` """ if bool(self.__data.interests & interest) != on: if on: self.__data.interests |= interest else: self.__data.interests &= ~interest self.itemChanged() def testItemInterest(self, interest): """ Test an item interest :param int attribute: Interest type :return: True/False .. seealso:: :py:meth:`setItemInterest()` """ return bool(self.__data.interests & interest) def setRenderHint(self, hint, on=True): """ Toggle a render hint :param int hint: Render hint :param bool on: True/False .. seealso:: :py:meth:`testRenderHint()` """ if bool(self.__data.renderHints & hint) != on: if on: self.__data.renderHints |= hint else: self.__data.renderHints &= ~hint self.itemChanged() def testRenderHint(self, hint): """ Test a render hint :param int attribute: Render hint :return: True/False .. seealso:: :py:meth:`setRenderHint()` """ return bool(self.__data.renderHints & hint) def setLegendIconSize(self, size): """ Set the size of the legend icon The default setting is 8x8 pixels :param QSize size: Size .. seealso:: :py:meth:`legendIconSize()`, :py:meth:`legendIcon()` """ if self.__data.legendIconSize != size: self.__data.legendIconSize = size self.legendChanged() def legendIconSize(self): """ :return: Legend icon size .. seealso:: :py:meth:`setLegendIconSize()`, :py:meth:`legendIcon()` """ return self.__data.legendIconSize def legendIcon(self, index, size): """ :param int index: Index of the legend entry (usually there is only one) :param QSizeF size: Icon size :return: Icon representing the item on the legend The default implementation returns an invalid icon .. seealso:: :py:meth:`setLegendIconSize()`, :py:meth:`legendData()` """ return QwtGraphic() def defaultIcon(self, brush, size): """ Return a default icon from a brush The default icon is a filled rectangle used in several derived classes as legendIcon(). :param QBrush brush: Fill brush :param QSizeF size: Icon size :return: A filled rectangle """ icon = QwtGraphic() if not size.isEmpty(): icon.setDefaultSize(size) r = QRectF(0, 0, size.width(), size.height()) painter = QPainter(icon) painter.fillRect(r, brush) return icon def show(self): """Show the item""" self.setVisible(True) def hide(self): """Hide the item""" self.setVisible(False) def setVisible(self, on): """ Show/Hide the item :param bool on: Show if True, otherwise hide .. seealso:: :py:meth:`isVisible()`, :py:meth:`show()`, :py:meth:`hide()` """ if on != self.__data.isVisible: self.__data.isVisible = on self.itemChanged() def isVisible(self): """ :return: True if visible .. seealso:: :py:meth:`setVisible()`, :py:meth:`show()`, :py:meth:`hide()` """ return self.__data.isVisible def itemChanged(self): """ Update the legend and call `QwtPlot.autoRefresh()` for the parent plot. .. seealso:: :py:meth:`QwtPlot.legendChanged()`, :py:meth:`QwtPlot.autoRefresh()` """ if self.__data.plot: self.__data.plot.autoRefresh() def legendChanged(self): """ Update the legend of the parent plot. .. seealso:: :py:meth:`QwtPlot.updateLegend()`, :py:meth:`itemChanged()` """ if self.testItemAttribute(QwtPlotItem.Legend) and self.__data.plot: self.__data.plot.updateLegend(self) def setAxes(self, xAxis, yAxis): """ Set X and Y axis The item will painted according to the coordinates of its Axes. :param int xAxis: X Axis (`QwtPlot.xBottom` or `QwtPlot.xTop`) :param int yAxis: Y Axis (`QwtPlot.yLeft` or `QwtPlot.yRight`) .. seealso:: :py:meth:`setXAxis()`, :py:meth:`setYAxis()`, :py:meth:`xAxis()`, :py:meth:`yAxis()` """ if xAxis == QwtPlot.xBottom or xAxis == QwtPlot.xTop: self.__data.xAxis = xAxis if yAxis == QwtPlot.yLeft or yAxis == QwtPlot.yRight: self.__data.yAxis = yAxis self.itemChanged() def setAxis(self, xAxis, yAxis): """ Set X and Y axis .. warning:: `setAxis` has been removed in Qwt6: please use :py:meth:`setAxes()` instead """ import warnings warnings.warn("`setAxis` has been removed in Qwt6: "\ "please use `setAxes` instead", RuntimeWarning) self.setAxes(xAxis, yAxis) def setXAxis(self, axis): """ Set the X axis The item will painted according to the coordinates its Axes. :param int axis: X Axis (`QwtPlot.xBottom` or `QwtPlot.xTop`) .. seealso:: :py:meth:`setAxes()`, :py:meth:`setYAxis()`, :py:meth:`xAxis()`, :py:meth:`yAxis()` """ if axis in (QwtPlot.xBottom, QwtPlot.xTop): self.__data.xAxis = axis self.itemChanged() def setYAxis(self, axis): """ Set the Y axis The item will painted according to the coordinates its Axes. :param int axis: Y Axis (`QwtPlot.yLeft` or `QwtPlot.yRight`) .. seealso:: :py:meth:`setAxes()`, :py:meth:`setXAxis()`, :py:meth:`xAxis()`, :py:meth:`yAxis()` """ if axis in (QwtPlot.yLeft, QwtPlot.yRight): self.__data.yAxis = axis self.itemChanged() def xAxis(self): """ :return: xAxis """ return self.__data.xAxis def yAxis(self): """ :return: yAxis """ return self.__data.yAxis def boundingRect(self): """ :return: An invalid bounding rect: QRectF(1.0, 1.0, -2.0, -2.0) .. note:: A width or height < 0.0 is ignored by the autoscaler """ return QRectF(1.0, 1.0, -2.0, -2.0) def getCanvasMarginHint(self, xMap, yMap, canvasRect): """ Calculate a hint for the canvas margin When the QwtPlotItem::Margins flag is enabled the plot item indicates, that it needs some margins at the borders of the canvas. This is f.e. used by bar charts to reserve space for displaying the bars. The margins are in target device coordinates ( pixels on screen ) :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas in painter coordinates .. seealso:: :py:meth:`QwtPlot.getCanvasMarginsHint()`, :py:meth:`QwtPlot.updateCanvasMargins()`, """ left = top = right = bottom = 0. return left, top, right, bottom def legendData(self): """ Return all information, that is needed to represent the item on the legend `QwtLegendData` is basically a list of QVariants that makes it possible to overload and reimplement legendData() to return almost any type of information, that is understood by the receiver that acts as the legend. The default implementation returns one entry with the title() of the item and the legendIcon(). :return: Data, that is needed to represent the item on the legend .. seealso:: :py:meth:`title()`, :py:meth:`legendIcon()`, :py:class:`qwt.legend.QwtLegend` """ data = QwtLegendData() label = self.title() label.setRenderFlags(label.renderFlags() & Qt.AlignLeft) data.setValue(QwtLegendData.TitleRole, label) graphic = self.legendIcon(0, self.legendIconSize()) if not graphic.isNull(): data.setValue(QwtLegendData.IconRole, graphic) return [data] def updateLegend(self, item, data): """ Update the item to changes of the legend info Plot items that want to display a legend ( not those, that want to be displayed on a legend ! ) will have to implement updateLegend(). updateLegend() is only called when the LegendInterest interest is enabled. The default implementation does nothing. :param qwt.plot.QwtPlotItem item: Plot item to be displayed on a legend :param list data: Attributes how to display item on the legend .. note:: Plot items, that want to be displayed on a legend need to enable the `QwtPlotItem.Legend` flag and to implement legendData() and legendIcon() """ pass def scaleRect(self, xMap, yMap): """ Calculate the bounding scale rectangle of 2 maps :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :return: Bounding scale rect of the scale maps, not normalized """ return QRectF(xMap.s1(), yMap.s1(), xMap.sDist(), yMap.sDist()) def paintRect(self, xMap, yMap): """ Calculate the bounding paint rectangle of 2 maps :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :return: Bounding paint rectangle of the scale maps, not normalized """ return QRectF(xMap.p1(), yMap.p1(), xMap.pDist(), yMap.pDist()) PythonQwt-0.5.5/qwt/qt/0000755000000000000000000000000012651077706013446 5ustar rootrootPythonQwt-0.5.5/qwt/qt/QtCore.py0000666000000000000000000000263512605720034015213 0ustar rootroot# -*- coding: utf-8 -*- # # Copyright © 2011 Pierre Raybaut # Licensed under the terms of the MIT License # (see LICENSE file for details) import os if os.environ['QT_API'] == 'pyqt5': from PyQt5.QtCore import * # analysis:ignore from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import pyqtSignal as Signal from PyQt5.QtCore import pyqtSlot as Slot from PyQt5.QtCore import pyqtProperty as Property from PyQt5.QtCore import QT_VERSION_STR as __version__ elif os.environ['QT_API'] == 'pyqt': from PyQt4.QtCore import * # analysis:ignore from PyQt4.Qt import QCoreApplication # analysis:ignore from PyQt4.Qt import Qt # analysis:ignore from PyQt4.QtCore import pyqtSignal as Signal # analysis:ignore from PyQt4.QtCore import pyqtSlot as Slot # analysis:ignore from PyQt4.QtCore import pyqtProperty as Property # analysis:ignore from PyQt4.QtCore import QT_VERSION_STR as __version__ # analysis:ignore # Forces new modules written by PyQt4 developers to be PyQt5-compatible del SIGNAL, SLOT else: import PySide.QtCore __version__ = PySide.QtCore.__version__ # analysis:ignore from PySide.QtCore import * # analysis:ignore PythonQwt-0.5.5/qwt/qt/QtWebKit.py0000666000000000000000000000114512605720032015501 0ustar rootroot# -*- coding: utf-8 -*- # # Copyright © 2011 Pierre Raybaut # Licensed under the terms of the MIT License # (see LICENSE file for details) import os if os.environ['QT_API'] == 'pyqt5': from PyQt5.QtWebKitWidgets import QWebPage, QWebView # analysis:ignore from PyQt5.QtWebKit import QWebSettings # analysis:ignore elif os.environ['QT_API'] == 'pyqt': from PyQt4.QtWebKit import (QWebPage, QWebView, # analysis:ignore QWebSettings) else: from PySide.QtWebKit import * # analysis:ignore PythonQwt-0.5.5/qwt/qt/QtGui.py0000666000000000000000000000166712605720032015051 0ustar rootroot# -*- coding: utf-8 -*- # # Copyright © 2011 Pierre Raybaut # Licensed under the terms of the MIT License # (see LICENSE file for details) import os if os.environ['QT_API'] == 'pyqt5': from PyQt5.QtCore import QSortFilterProxyModel # analysis:ignore from PyQt5.QtPrintSupport import (QPrinter, QPrintDialog, # analysis:ignore QAbstractPrintDialog) from PyQt5.QtPrintSupport import QPrintPreviewDialog # analysis:ignore from PyQt5.QtGui import * # analysis:ignore from PyQt5.QtWidgets import * # analysis:ignore elif os.environ['QT_API'] == 'pyqt': from PyQt4.Qt import QKeySequence, QTextCursor # analysis:ignore from PyQt4.QtGui import * # analysis:ignore else: from PySide.QtGui import * # analysis:ignore PythonQwt-0.5.5/qwt/qt/compat.py0000666000000000000000000002061412605040216015273 0ustar rootroot# -*- coding: utf-8 -*- # # Copyright © 2011-2012 Pierre Raybaut # Licensed under the terms of the MIT License # (see LICENSE file for details) """ spyderlib.qt.compat ------------------- Transitional module providing compatibility functions intended to help migrating from PyQt to PySide. This module should be fully compatible with: * PyQt >=v4.4 * both PyQt API #1 and API #2 * PySide """ from __future__ import print_function import os import sys import collections from qwt.qt.QtGui import QFileDialog from qwt.py3compat import is_text_string, to_text_string, TEXT_TYPES #============================================================================== # QVariant conversion utilities #============================================================================== PYQT_API_1 = False if os.environ['QT_API'] == 'pyqt': import sip try: PYQT_API_1 = sip.getapi('QVariant') == 1 # PyQt API #1 except AttributeError: # PyQt =v4.4 (API #1 and #2) and PySide >=v1.0""" # Calling QFileDialog static method if sys.platform == "win32": # On Windows platforms: redirect standard outputs _temp1, _temp2 = sys.stdout, sys.stderr sys.stdout, sys.stderr = None, None try: result = QFileDialog.getExistingDirectory(parent, caption, basedir, options) finally: if sys.platform == "win32": # On Windows platforms: restore standard outputs sys.stdout, sys.stderr = _temp1, _temp2 if not is_text_string(result): # PyQt API #1 result = to_text_string(result) return result def _qfiledialog_wrapper(attr, parent=None, caption='', basedir='', filters='', selectedfilter='', options=None): if options is None: options = QFileDialog.Options(0) try: # PyQt =v4.6 QString = None # analysis:ignore tuple_returned = True try: # PyQt >=v4.6 func = getattr(QFileDialog, attr+'AndFilter') except AttributeError: # PySide or PyQt =v4.6 output, selectedfilter = result else: # PyQt =v4.4 (API #1 and #2) and PySide >=v1.0""" return _qfiledialog_wrapper('getOpenFileName', parent=parent, caption=caption, basedir=basedir, filters=filters, selectedfilter=selectedfilter, options=options) def getopenfilenames(parent=None, caption='', basedir='', filters='', selectedfilter='', options=None): """Wrapper around QtGui.QFileDialog.getOpenFileNames static method Returns a tuple (filenames, selectedfilter) -- when dialog box is canceled, returns a tuple (empty list, empty string) Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" return _qfiledialog_wrapper('getOpenFileNames', parent=parent, caption=caption, basedir=basedir, filters=filters, selectedfilter=selectedfilter, options=options) def getsavefilename(parent=None, caption='', basedir='', filters='', selectedfilter='', options=None): """Wrapper around QtGui.QFileDialog.getSaveFileName static method Returns a tuple (filename, selectedfilter) -- when dialog box is canceled, returns a tuple of empty strings Compatible with PyQt >=v4.4 (API #1 and #2) and PySide >=v1.0""" return _qfiledialog_wrapper('getSaveFileName', parent=parent, caption=caption, basedir=basedir, filters=filters, selectedfilter=selectedfilter, options=options) if __name__ == '__main__': from spyderlib.utils.qthelpers import qapplication _app = qapplication() print(repr(getexistingdirectory())) print(repr(getopenfilename(filters='*.py;;*.txt'))) print(repr(getopenfilenames(filters='*.py;;*.txt'))) print(repr(getsavefilename(filters='*.py;;*.txt'))) sys.exit() PythonQwt-0.5.5/qwt/qt/QtSvg.py0000666000000000000000000000074512605720032015060 0ustar rootroot# -*- coding: utf-8 -*- # # Copyright © 2012 Pierre Raybaut # Licensed under the terms of the MIT License # (see LICENSE file for details) import os if os.environ['QT_API'] == 'pyqt5': from PyQt5.QtSvg import * # analysis:ignore elif os.environ['QT_API'] == 'pyqt': from PyQt4.QtSvg import * # analysis:ignore else: from PySide.QtSvg import * # analysis:ignore PythonQwt-0.5.5/qwt/qt/__init__.py0000666000000000000000000000472112605720034015553 0ustar rootroot# -*- coding: utf-8 -*- # # Copyright © 2011-2012 Pierre Raybaut # © 2012-2014 anatoly techtonik # Licensed under the terms of the MIT License # (see LICENSE file for details) """Compatibility package (PyQt4/PyQt5/PySide)""" import os os.environ.setdefault('QT_API', 'pyqt') assert os.environ['QT_API'] in ('pyqt5', 'pyqt', 'pyside') API = os.environ['QT_API'] API_NAME = {'pyqt5': 'PyQt5', 'pyqt': 'PyQt4', 'pyside': 'PySide'}[API] if API == 'pyqt': # Spyder 2.3 is compatible with both #1 and #2 PyQt API, # but to avoid issues with IPython and other Qt plugins # we choose to support only API #2 for 2.4+ import sip try: sip.setapi('QString', 2) sip.setapi('QVariant', 2) sip.setapi('QDate', 2) sip.setapi('QDateTime', 2) sip.setapi('QTextStream', 2) sip.setapi('QTime', 2) sip.setapi('QUrl', 2) except AttributeError: # PyQt < v4.6. The actual check is done by requirements.check_qt() # call from spyder.py pass try: from PyQt4.QtCore import PYQT_VERSION_STR as __version__ # analysis:ignore except ImportError: # Trying PyQt5 before switching to PySide (at this point, PyQt4 may # not be installed but PyQt5 or Pyside could still be if the QT_API # environment variable hasn't been set-up) try: import PyQt5 # analysis:ignore API = os.environ['QT_API'] = 'pyqt5' API_NAME = 'PyQt5' except ImportError: API = os.environ['QT_API'] = 'pyside' API_NAME = 'PySide' else: is_old_pyqt = __version__.startswith(('4.4', '4.5', '4.6', '4.7')) is_pyqt46 = __version__.startswith('4.6') import sip try: API_NAME += (" (API v%d)" % sip.getapi('QString')) except AttributeError: pass from PyQt4 import uic # analysis:ignore PYQT5 = False if API == 'pyqt5': try: from PyQt5.QtCore import PYQT_VERSION_STR as __version__ from PyQt5 import uic # analysis:ignore PYQT5 = True is_old_pyqt = is_pyqt46 = False except ImportError: pass if API == 'pyside': try: from PySide import __version__ # analysis:ignore except ImportError: raise ImportError("Spyder requires PySide or PyQt to be installed") else: is_old_pyqt = is_pyqt46 = False PythonQwt-0.5.5/qwt/painter.py0000666000000000000000000004512712615225450015042 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPainterClass --------------- .. autoclass:: QwtPainterClass :members: """ from qwt.color_map import QwtColorMap from qwt.scale_map import QwtScaleMap from qwt.qt.QtGui import (QPaintEngine, QFrame, QPixmap, QPainter, QPalette, QStyle, QPen, QStyleOptionFocusRect, QBrush, QLinearGradient, QPainterPath, QColor, QStyleOption) from qwt.qt.QtCore import Qt, QRect, QPoint, QT_VERSION QWIDGETSIZE_MAX = (1<<24)-1 def isX11GraphicsSystem(): pm = QPixmap(1, 1) painter = QPainter(pm) isX11 = painter.paintEngine().type() == QPaintEngine.X11 del painter return isX11 def qwtFillRect(widget, painter, rect, brush): if brush.style() == Qt.TexturePattern: painter.save() painter.setClipRect(rect) painter.drawTiledPixmap(rect, brush.texture(), rect.topLeft()) painter.restore() elif brush.gradient(): painter.save() painter.setClipRect(rect) painter.fillRect(0, 0, widget.width(), widget.height(), brush) painter.restore() else: painter.fillRect(rect, brush) class QwtPainterClass(object): """A collection of `QPainter` workarounds""" def drawImage(self, painter, rect, image): alignedRect = rect.toAlignedRect() if alignedRect != rect: clipRect = rect.adjusted(0., 0., -1., -1.) painter.save() painter.setClipRect(clipRect, Qt.IntersectClip) painter.drawImage(alignedRect, image) painter.restore() else: painter.drawImage(alignedRect, image) def drawPixmap(self, painter, rect, pixmap): alignedRect = rect.toAlignedRect() if alignedRect != rect: clipRect = rect.adjusted(0., 0., -1., -1.) painter.save() painter.setClipRect(clipRect, Qt.IntersectClip) painter.drawPixmap(alignedRect, pixmap) painter.restore() else: painter.drawPixmap(alignedRect, pixmap) def drawFocusRect(self, *args): if len(args) == 2: painter, widget = args self.drawFocusRect(painter, widget, widget.rect()) elif len(args) == 3: painter, widget, rect = args opt = QStyleOptionFocusRect() opt.initFrom(widget) opt.rect = rect opt.state |= QStyle.State_HasFocus widget.style().drawPrimitive(QStyle.PE_FrameFocusRect, opt, painter, widget) else: raise TypeError("QwtPainter.drawFocusRect() takes 2 or 3 argument"\ "(s) (%s given)" % len(args)) def drawRoundFrame(self, painter, rect, palette, lineWidth, frameStyle): """ Draw a round frame :param QPainter painter: Painter :param QRectF rect: Target rectangle :param QPalette palette: `QPalette.WindowText` is used for plain borders, `QPalette.Dark` and `QPalette.Light` for raised or sunken borders :param int lineWidth: Line width :param int frameStyle: bitwise OR´ed value of `QFrame.Shape` and `QFrame.Shadow` """ Plain, Sunken, Raised = list(range(3)) style = Plain if (frameStyle & QFrame.Sunken) == QFrame.Sunken: style = Sunken elif (frameStyle & QFrame.Raised) == QFrame.Raised: style = Raised lw2 = .5*lineWidth r = rect.adjusted(lw2, lw2, -lw2, -lw2) if style != Plain: c1 = palette.color(QPalette.Light) c2 = palette.color(QPalette.Dark) if style == Sunken: c1, c2 = c2, c1 gradient = QLinearGradient(r.topLeft(), r.bottomRight()) gradient.setColorAt(0., c1) gradient.setColorAt(1., c2) brush = QBrush(gradient) else: brush = palette.brush(QPalette.WindowText) painter.save() painter.setPen(QPen(brush, lineWidth)) painter.drawEllipse(r) painter.restore() def drawFrame(self, painter, rect, palette, foregroundRole, frameWidth, midLineWidth, frameStyle): """ Draw a rectangular frame :param QPainter painter: Painter :param QRectF rect: Frame rectangle :param QPalette palette: Palette :param QPalette.ColorRole foregroundRole: Palette :param int frameWidth: Frame width :param int midLineWidth: Used for `QFrame.Box` :param int frameStyle: bitwise OR´ed value of `QFrame.Shape` and `QFrame.Shadow` """ if frameWidth <= 0 or rect.isEmpty(): return shadow = frameStyle & QFrame.Shadow_Mask painter.save() if shadow == QFrame.Plain: outerRect = rect.adjusted(0., 0., -1., -1.) innerRect = outerRect.adjusted( frameWidth, frameWidth, -frameWidth, -frameWidth) path = QPainterPath() path.addRect(outerRect) path.addRect(innerRect) painter.setPen(Qt.NoPen) painter.setBrush(palette.color(foregroundRole)) painter.drawPath(path) else: shape = frameStyle & QFrame.Shape_Mask if shape == QFrame.Box: outerRect = rect.adjusted(0., 0., -1., -1.) midRect1 = outerRect.adjusted( frameWidth, frameWidth, -frameWidth, -frameWidth) midRect2 = midRect1.adjusted( midLineWidth, midLineWidth, -midLineWidth, -midLineWidth) innerRect = midRect2.adjusted( frameWidth, frameWidth, -frameWidth, -frameWidth) path1 = QPainterPath() path1.moveTo(outerRect.bottomLeft()) path1.lineTo(outerRect.topLeft()) path1.lineTo(outerRect.topRight()) path1.lineTo(midRect1.topRight()) path1.lineTo(midRect1.topLeft()) path1.lineTo(midRect1.bottomLeft()) path2 = QPainterPath() path2.moveTo(outerRect.bottomLeft()) path2.lineTo(outerRect.bottomRight()) path2.lineTo(outerRect.topRight()) path2.lineTo(midRect1.topRight()) path2.lineTo(midRect1.bottomRight()) path2.lineTo(midRect1.bottomLeft()) path3 = QPainterPath() path3.moveTo(midRect2.bottomLeft()) path3.lineTo(midRect2.topLeft()) path3.lineTo(midRect2.topRight()) path3.lineTo(innerRect.topRight()) path3.lineTo(innerRect.topLeft()) path3.lineTo(innerRect.bottomLeft()) path4 = QPainterPath() path4.moveTo(midRect2.bottomLeft()) path4.lineTo(midRect2.bottomRight()) path4.lineTo(midRect2.topRight()) path4.lineTo(innerRect.topRight()) path4.lineTo(innerRect.bottomRight()) path4.lineTo(innerRect.bottomLeft()) path5 = QPainterPath() path5.addRect(midRect1) path5.addRect(midRect2) painter.setPen(Qt.NoPen) brush1 = palette.dark().color() brush2 = palette.light().color() if shadow == QFrame.Raised: brush1, brush2 = brush2, brush1 painter.setBrush(brush1) painter.drawPath(path1) painter.drawPath(path4) painter.setBrush(brush2) painter.drawPath(path2) painter.drawPath(path3) painter.setBrush(palette.mid()) painter.drawPath(path5) else: outerRect = rect.adjusted(0., 0., -1., -1.) innerRect = outerRect.adjusted(frameWidth-1., frameWidth-1., -(frameWidth-1.), -(frameWidth-1.)) path1 = QPainterPath() path1.moveTo(outerRect.bottomLeft()) path1.lineTo(outerRect.topLeft()) path1.lineTo(outerRect.topRight()) path1.lineTo(innerRect.topRight()) path1.lineTo(innerRect.topLeft()) path1.lineTo(innerRect.bottomLeft()) path2 = QPainterPath() path2.moveTo(outerRect.bottomLeft()) path2.lineTo(outerRect.bottomRight()) path2.lineTo(outerRect.topRight()) path2.lineTo(innerRect.topRight()) path2.lineTo(innerRect.bottomRight()) path2.lineTo(innerRect.bottomLeft()) painter.setPen(Qt.NoPen) brush1 = palette.dark().color() brush2 = palette.light().color() if shadow == QFrame.Raised: brush1, brush2 = brush2, brush1 painter.setBrush(brush1) painter.drawPath(path1) painter.setBrush(brush2) painter.drawPath(path2) painter.restore() def drawRoundedFrame(self, painter, rect, xRadius, yRadius, palette, lineWidth, frameStyle): """ Draw a rectangular frame with rounded borders :param QPainter painter: Painter :param QRectF rect: Frame rectangle :param float xRadius: x-radius of the ellipses defining the corners :param float yRadius: y-radius of the ellipses defining the corners :param QPalette palette: `QPalette.WindowText` is used for plain borders, `QPalette.Dark` and `QPalette.Light` for raised or sunken borders :param int lineWidth: Line width :param int frameStyle: bitwise OR´ed value of `QFrame.Shape` and `QFrame.Shadow` """ painter.save() painter.setRenderHint(QPainter.Antialiasing, True) painter.setBrush(Qt.NoBrush) lw2 = lineWidth*.5 r = rect.adjusted(lw2, lw2, -lw2, -lw2) path = QPainterPath() path.addRoundedRect(r, xRadius, yRadius) Plain, Sunken, Raised = list(range(3)) style = Plain if (frameStyle & QFrame.Sunken) == QFrame.Sunken: style = Sunken if (frameStyle & QFrame.Raised) == QFrame.Raised: style = Raised if style != Plain and path.elementCount() == 17: pathList = [QPainterPath() for _i in range(8)] for i in range(4): j = i*4+1 pathList[2*i].moveTo(path.elementAt(j-1).x, path.elementAt(j-1).y) pathList[2*i].cubicTo( path.elementAt(j+0).x, path.elementAt(j+0).y, path.elementAt(j+1).x, path.elementAt(j+1).y, path.elementAt(j+2).x, path.elementAt(j+2).y) pathList[2*i+1].moveTo(path.elementAt(j+2).x, path.elementAt(j+2).y) pathList[2*i+1].lineTo(path.elementAt(j+3).x, path.elementAt(j+3).y) c1 = QColor(palette.color(QPalette.Dark)) c2 = QColor(palette.color(QPalette.Light)) if style == Raised: c1, c2 = c2, c1 for i in range(5): r = pathList[2*i].controlPointRect() arcPen = QPen() arcPen.setCapStyle(Qt.FlatCap) arcPen.setWidth(lineWidth) linePen = QPen() linePen.setCapStyle(Qt.FlatCap) linePen.setWidth(lineWidth) if i == 0: arcPen.setColor(c1) linePen.setColor(c1) elif i == 1: gradient = QLinearGradient() gradient.setStart(r.topLeft()) gradient.setFinalStop(r.bottomRight()) gradient.setColorAt(0., c1) gradient.setColorAt(1., c2) arcPen.setBrush(gradient) linePen.setColor(c2) elif i == 2: arcPen.setColor(c2) linePen.setColor(c2) elif i == 3: gradient = QLinearGradient() gradient.setStart(r.bottomRight()) gradient.setFinalStop(r.topLeft()) gradient.setColorAt(0., c2) gradient.setColorAt(1., c1) arcPen.setBrush(gradient) linePen.setColor(c1) painter.setPen(arcPen) painter.drawPath(pathList[2*i]) painter.setPen(linePen) painter.drawPath(pathList[2*i+1]) else: pen = QPen(palette.color(QPalette.WindowText), lineWidth) painter.setPen(pen) painter.drawPath(path) painter.restore() def drawColorBar(self, painter, colorMap, interval, scaleMap, orientation, rect): """ Draw a color bar into a rectangle :param QPainter painter: Painter :param qwt.color_map.QwtColorMap colorMap: Color map :param qwt.interval.QwtInterval interval: Value range :param qwt.scalemap.QwtScaleMap scaleMap: Scale map :param Qt.Orientation orientation: Orientation :param QRectF rect: Target rectangle """ colorTable = [] if colorMap.format() == QwtColorMap.Indexed: colorTable = colorMap.colorTable(interval) c = QColor() devRect = rect.toAlignedRect() pixmap = QPixmap(devRect.size()) pixmap.fill(Qt.transparent) pmPainter = QPainter(pixmap) pmPainter.translate(-devRect.x(), -devRect.y()) if orientation == Qt.Horizontal: sMap = QwtScaleMap(scaleMap) sMap.setPaintInterval(rect.left(), rect.right()) for x in range(devRect.left(), devRect.right()+1): value = sMap.invTransform(x) if colorMap.format() == QwtColorMap.RGB: c.setRgba(colorMap.rgb(interval, value)) else: c = colorTable[colorMap.colorIndex(interval, value)] pmPainter.setPen(c) pmPainter.drawLine(x, devRect.top(), x, devRect.bottom()) else: sMap = QwtScaleMap(scaleMap) sMap.setPaintInterval(rect.bottom(), rect.top()) for y in range(devRect.top(), devRect.bottom()+1): value = sMap.invTransform(y) if colorMap.format() == QwtColorMap.RGB: c.setRgba(colorMap.rgb(interval, value)) else: c = colorTable[colorMap.colorIndex(interval, value)] pmPainter.setPen(c) pmPainter.drawLine(devRect.left(), y, devRect.right(), y) pmPainter.end() self.drawPixmap(painter, rect, pixmap) def fillPixmap(self, widget, pixmap, offset=None): """ Fill a pixmap with the content of a widget In Qt >= 5.0 `QPixmap.fill()` is a nop, in Qt 4.x it is buggy for backgrounds with gradients. Thus `fillPixmap()` offers an alternative implementation. :param QWidget widget: Widget :param QPixmap pixmap: Pixmap to be filled :param QPoint offset: Offset .. seealso:: :py:meth:`QPixmap.fill()` """ if offset is None: offset = QPoint() rect = QRect(offset, pixmap.size()) painter = QPainter(pixmap) painter.translate(-offset) autoFillBrush = widget.palette().brush(widget.backgroundRole()) if not (widget.autoFillBackground() and autoFillBrush.isOpaque()): bg = widget.palette().brush(QPalette.Window) qwtFillRect(widget, painter, rect, bg) if widget.autoFillBackground(): qwtFillRect(widget, painter, rect, autoFillBrush) if widget.testAttribute(Qt.WA_StyledBackground): painter.setClipRegion(rect) opt = QStyleOption() opt.initFrom(widget) widget.style().drawPrimitive(QStyle.PE_Widget, opt, painter, widget) def drawBackground(self, painter, rect, widget): """ Fill rect with the background of a widget :param QPainter painter: Painter :param QRectF rect: Rectangle to be filled :param QWidget widget: Widget .. seealso:: :py:data:`QStyle.PE_Widget`, :py:meth:`QWidget.backgroundRole()` """ if widget.testAttribute(Qt.WA_StyledBackground): opt = QStyleOption() opt.initFrom(widget) opt.rect = rect.toAlignedRect() widget.style().drawPrimitive(QStyle.PE_Widget, opt, painter, widget) else: brush = widget.palette().brush(widget.backgroundRole()) painter.fillRect(rect, brush) def backingStore(self, widget, size): """ :param QWidget widget: Widget, for which the backinstore is intended :param QSize size: Size of the pixmap :return: A pixmap that can be used as backing store """ if QT_VERSION >= 0x050000: pixelRatio = 1. if widget and widget.windowHandle(): pixelRatio = widget.windowHandle().devicePixelRatio() else: from qwt.qt.QtGui import qApp if qApp is not None: try: pixelRatio = qApp.devicePixelRatio() except RuntimeError: pass pm = QPixmap(size*pixelRatio) pm.setDevicePixelRatio(pixelRatio) else: pm = QPixmap(size) if QT_VERSION < 0x050000 and widget and isX11GraphicsSystem(): if pm.x11Info().screen() != widget.x11Info().screen(): pm.x11SetScreen(widget.x11Info().screen()) return pm QwtPainter = QwtPainterClass()PythonQwt-0.5.5/qwt/plot_directpainter.py0000666000000000000000000002442112615225450017265 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPlotDirectPainter -------------------- .. autoclass:: QwtPlotDirectPainter :members: """ from qwt.qt.QtGui import QPainter, QRegion from qwt.qt.QtCore import QObject, QT_VERSION, Qt, QEvent from qwt.plot import QwtPlotItem from qwt.plot_canvas import QwtPlotCanvas def qwtRenderItem(painter, canvasRect, seriesItem, from_, to): #TODO: A minor performance improvement is possible with caching the maps plot = seriesItem.plot() xMap = plot.canvasMap(seriesItem.xAxis()) yMap = plot.canvasMap(seriesItem.yAxis()) painter.setRenderHint(QPainter.Antialiasing, seriesItem.testRenderHint(QwtPlotItem.RenderAntialiased)) seriesItem.drawSeries(painter, xMap, yMap, canvasRect, from_, to) def qwtHasBackingStore(canvas): return canvas.testPaintAttribute(QwtPlotCanvas.BackingStore)\ and canvas.backingStore() class QwtPlotDirectPainter_PrivateData(object): def __init__(self): self.attributes = 0 self.hasClipping = False self.seriesItem = None # QwtPlotSeriesItem self.clipRegion = QRegion() self.painter = QPainter() self.from_ = None self.to = None class QwtPlotDirectPainter(QObject): """ Painter object trying to paint incrementally Often applications want to display samples while they are collected. When there are too many samples complete replots will be expensive to be processed in a collection cycle. `QwtPlotDirectPainter` offers an API to paint subsets (f.e all additions points) without erasing/repainting the plot canvas. On certain environments it might be important to calculate a proper clip region before painting. F.e. for Qt Embedded only the clipped part of the backing store will be copied to a (maybe unaccelerated) frame buffer. .. warning:: Incremental painting will only help when no replot is triggered by another operation (like changing scales) and nothing needs to be erased. Paint attributes: * `QwtPlotDirectPainter.AtomicPainter`: Initializing a `QPainter` is an expensive operation. When `AtomicPainter` is set each call of `drawSeries()` opens/closes a temporary `QPainter`. Otherwise `QwtPlotDirectPainter` tries to use the same `QPainter` as long as possible. * `QwtPlotDirectPainter.FullRepaint`: When `FullRepaint` is set the plot canvas is explicitly repainted after the samples have been rendered. * `QwtPlotDirectPainter.CopyBackingStore`: When `QwtPlotCanvas.BackingStore` is enabled the painter has to paint to the backing store and the widget. In certain situations/environments it might be faster to paint to the backing store only and then copy the backing store to the canvas. This flag can also be useful for settings, where Qt fills the the clip region with the widget background. """ # enum Attribute AtomicPainter = 0x01 FullRepaint = 0x02 CopyBackingStore = 0x04 def __init__(self, parent=None): QObject.__init__(self, parent) self.__data = QwtPlotDirectPainter_PrivateData() def setAttribute(self, attribute, on=True): """ Change an attribute :param int attribute: Attribute to change :param bool on: On/Off .. seealso:: :py:meth:`testAttribute()` """ if self.testAttribute(attribute) != on: self.__data.attributes |= attribute else: self.__data.attributes &= ~attribute if attribute == self.AtomicPainter and on: self.reset() def testAttribute(self, attribute): """ :param int attribute: Attribute to be tested :return: True, when attribute is enabled .. seealso:: :py:meth:`setAttribute()` """ return self.__data.attributes & attribute def setClipping(self, enable): """ En/Disables clipping :param bool enable: Enables clipping is true, disable it otherwise .. seealso:: :py:meth:`hasClipping()`, :py:meth:`clipRegion()`, :py:meth:`setClipRegion()` """ self.__data.hasClipping = enable def hasClipping(self): """ :return: Return true, when clipping is enabled .. seealso:: :py:meth:`setClipping()`, :py:meth:`clipRegion()`, :py:meth:`setClipRegion()` """ return self.__data.hasClipping def setClipRegion(self, region): """ Assign a clip region and enable clipping Depending on the environment setting a proper clip region might improve the performance heavily. F.e. on Qt embedded only the clipped part of the backing store will be copied to a (maybe unaccelerated) frame buffer device. :param QRegion region: Clip region .. seealso:: :py:meth:`hasClipping()`, :py:meth:`setClipping()`, :py:meth:`clipRegion()` """ self.__data.clipRegion = region self.__data.hasClipping = True def clipRegion(self): """ :return: Return Currently set clip region. .. seealso:: :py:meth:`hasClipping()`, :py:meth:`setClipping()`, :py:meth:`setClipRegion()` """ return self.__data.clipRegion def drawSeries(self, seriesItem, from_, to): """ Draw a set of points of a seriesItem. When observing a measurement while it is running, new points have to be added to an existing seriesItem. drawSeries() can be used to display them avoiding a complete redraw of the canvas. Setting `plot().canvas().setAttribute(Qt.WA_PaintOutsidePaintEvent, True)` will result in faster painting, if the paint engine of the canvas widget supports this feature. :param qwt.plot_series.QwtPlotSeriesItem seriesItem: Item to be painted :param int from_: Index of the first point to be painted :param int to: Index of the last point to be painted. If to < 0 the series will be painted to its last point. """ if seriesItem is None or seriesItem.plot() is None: return canvas = seriesItem.plot().canvas() canvasRect = canvas.contentsRect() plotCanvas = canvas #XXX: cast to QwtPlotCanvas if plotCanvas and qwtHasBackingStore(plotCanvas): painter = QPainter(plotCanvas.backingStore()) #XXX: cast plotCanvas.backingStore() to QPixmap if self.__data.hasClipping: painter.setClipRegion(self.__data.clipRegion) qwtRenderItem(painter, canvasRect, seriesItem, from_, to) if self.testAttribute(self.FullRepaint): plotCanvas.repaint() return immediatePaint = True if not canvas.testAttribute(Qt.WA_WState_InPaintEvent): if QT_VERSION >= 0x050000 or\ not canvas.testAttribute(Qt.WA_PaintOutsidePaintEvent): immediatePaint = False if immediatePaint: if not self.__data.painter.isActive(): self.reset() self.__data.painter.begin(canvas) canvas.installEventFilter(self) if self.__data.hasClipping: self.__data.painter.setClipRegion( QRegion(canvasRect) & self.__data.clipRegion) elif not self.__data.painter.hasClipping(): self.__data.painter.setClipRect(canvasRect) qwtRenderItem(self.__data.painter, canvasRect, seriesItem, from_, to) if self.__data.attributes & self.AtomicPainter: self.reset() elif self.__data.hasClipping: self.__data.painter.setClipping(False) else: self.reset() self.__data.seriesItem = seriesItem self.__data.from_ = from_ self.__data.to = to clipRegion = QRegion(canvasRect) if self.__data.hasClipping: clipRegion &= self.__data.clipRegion canvas.installEventFilter(self) canvas.repaint(clipRegion) canvas.removeEventFilter(self) self.__data.seriesItem = None def reset(self): """Close the internal QPainter""" if self.__data.painter.isActive(): w = self.__data.painter.device() #XXX: cast to QWidget if w: w.removeEventFilter(self) self.__data.painter.end() def eventFilter(self, obj_, event): if event.type() == QEvent.Paint: self.reset() if self.__data.seriesItem: pe = event #XXX: cast to QPaintEvent canvas = self.__data.seriesItem.plot().canvas() painter = QPainter(canvas) painter.setClipRegion(pe.region()) doCopyCache = self.testAttribute(self.CopyBackingStore) if doCopyCache: plotCanvas = canvas #XXX: cast to QwtPlotCanvas if plotCanvas: doCopyCache = qwtHasBackingStore(plotCanvas) if doCopyCache: painter.drawPixmap(plotCanvas.contentsRect().topLeft(), plotCanvas.backingStore()) if not doCopyCache: qwtRenderItem(painter, canvas.contentsRect(), self.__data.seriesItem, self.__data.from_, self.__data.to) return True return False PythonQwt-0.5.5/qwt/tests/0000755000000000000000000000000012651077706014164 5ustar rootrootPythonQwt-0.5.5/qwt/tests/ImagePlotDemo.py0000666000000000000000000001536512617623060017232 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import QApplication, QPen, qRgb from qwt.qt.QtCore import Qt from qwt import (QwtPlot, QwtPlotMarker, QwtLegend, QwtPlotGrid, QwtPlotCurve, QwtPlotItem, QwtText, QwtLegendData, QwtLinearColorMap, QwtInterval, QwtScaleMap, toQImage) def bytescale(data, cmin=None, cmax=None, high=255, low=0): if ((hasattr(data, 'dtype') and data.dtype.char == np.uint8) or (hasattr(data, 'typecode') and data.typecode == np.uint8) ): return data high = high - low if cmin is None: cmin = min(np.ravel(data)) if cmax is None: cmax = max(np.ravel(data)) scale = high * 1.0 / (cmax-cmin or 1) bytedata = ((data*1.0-cmin)*scale + 0.4999).astype(np.uint8) return bytedata + np.asarray(low).astype(np.uint8) def linearX(nx, ny): return np.repeat(np.arange(nx, typecode = np.float32)[:, np.newaxis], ny, -1) def linearY(nx, ny): return np.repeat(np.arange(ny, typecode = np.float32)[np.newaxis, :], nx, 0) def square(n, min, max): t = np.arange(min, max, float(max-min)/(n-1)) #return outer(cos(t), sin(t)) return np.cos(t)*np.sin(t)[:,np.newaxis] class PlotImage(QwtPlotItem): def __init__(self, title = QwtText()): QwtPlotItem.__init__(self) self.setTitle(title) self.setItemAttribute(QwtPlotItem.Legend); self.xyzs = None def setData(self, xyzs, xRange = None, yRange = None): self.xyzs = xyzs shape = xyzs.shape if not xRange: xRange = (0, shape[0]) if not yRange: yRange = (0, shape[1]) self.xMap = QwtScaleMap(0, xyzs.shape[0], *xRange) self.plot().setAxisScale(QwtPlot.xBottom, *xRange) self.yMap = QwtScaleMap(0, xyzs.shape[1], *yRange) self.plot().setAxisScale(QwtPlot.yLeft, *yRange) self.image = toQImage(bytescale(self.xyzs)).mirrored(False, True) for i in range(0, 256): self.image.setColor(i, qRgb(i, 0, 255-i)) def updateLegend(self, legend): QwtPlotItem.updateLegend(self, legend) legend.find(self).setText(self.title()) def draw(self, painter, xMap, yMap, rect): """Paint image zoomed to xMap, yMap Calculate (x1, y1, x2, y2) so that it contains at least 1 pixel, and copy the visible region to scale it to the canvas. """ assert(isinstance(self.plot(), QwtPlot)) # calculate y1, y2 # the scanline order (index y) is inverted with respect to the y-axis y1 = y2 = self.image.height() y1 *= (self.yMap.s2() - yMap.s2()) y1 /= (self.yMap.s2() - self.yMap.s1()) y1 = max(0, int(y1-0.5)) y2 *= (self.yMap.s2() - yMap.s1()) y2 /= (self.yMap.s2() - self.yMap.s1()) y2 = min(self.image.height(), int(y2+0.5)) # calculate x1, x2 -- the pixel order (index x) is normal x1 = x2 = self.image.width() x1 *= (xMap.s1() - self.xMap.s1()) x1 /= (self.xMap.s2() - self.xMap.s1()) x1 = max(0, int(x1-0.5)) x2 *= (xMap.s2() - self.xMap.s1()) x2 /= (self.xMap.s2() - self.xMap.s1()) x2 = min(self.image.width(), int(x2+0.5)) # copy image = self.image.copy(x1, y1, x2-x1, y2-y1) # zoom image = image.scaled(xMap.p2()-xMap.p1()+1, yMap.p1()-yMap.p2()+1) # draw painter.drawImage(xMap.p1(), yMap.p2(), image) class ImagePlot(QwtPlot): def __init__(self, *args): QwtPlot.__init__(self, *args) # set plot title self.setTitle('ImagePlot') # set plot layout self.plotLayout().setCanvasMargin(0) self.plotLayout().setAlignCanvasToScales(True) # set legend legend = QwtLegend() legend.setDefaultItemMode(QwtLegendData.Clickable) self.insertLegend(legend, QwtPlot.RightLegend) # set axis titles self.setAxisTitle(QwtPlot.xBottom, 'time (s)') self.setAxisTitle(QwtPlot.yLeft, 'frequency (Hz)') colorMap = QwtLinearColorMap(Qt.blue, Qt.red) interval = QwtInterval(-1, 1) self.enableAxis(QwtPlot.yRight) self.setAxisScale(QwtPlot.yRight, -1, 1) self.axisWidget(QwtPlot.yRight).setColorBarEnabled(True) self.axisWidget(QwtPlot.yRight).setColorMap(interval, colorMap) # calculate 3 NumPy arrays x = np.arange(-2*np.pi, 2*np.pi, 0.01) y = np.pi*np.sin(x) z = 4*np.pi*np.cos(x)*np.cos(x)*np.sin(x) # attach a curve curve = QwtPlotCurve('y = pi*sin(x)') curve.attach(self) curve.setPen(QPen(Qt.green, 2)) curve.setData(x, y) # attach another curve curve = QwtPlotCurve('y = 4*pi*sin(x)*cos(x)**2') curve.attach(self) curve.setPen(QPen(Qt.black, 2)) curve.setData(x, z) # attach a grid grid = QwtPlotGrid() grid.attach(self) grid.setPen(QPen(Qt.black, 0, Qt.DotLine)) # attach a horizontal marker at y = 0 marker = QwtPlotMarker() marker.attach(self) marker.setValue(0.0, 0.0) marker.setLineStyle(QwtPlotMarker.HLine) marker.setLabelAlignment(Qt.AlignRight | Qt.AlignTop) marker.setLabel(QwtText('y = 0')) # attach a vertical marker at x = pi marker = QwtPlotMarker() marker.attach(self) marker.setValue(np.pi, 0.0) marker.setLineStyle(QwtPlotMarker.VLine) marker.setLabelAlignment(Qt.AlignRight | Qt.AlignBottom) marker.setLabel(QwtText('x = pi')) # attach a plot image plotImage = PlotImage('Image') plotImage.attach(self) plotImage.setData(square(512, -2*np.pi, 2*np.pi), (-2*np.pi, 2*np.pi), (-2*np.pi, 2*np.pi)) legend.clicked.connect(self.toggleVisibility) # replot self.replot() def toggleVisibility(self, plotItem, idx): """Toggle the visibility of a plot item """ plotItem.setVisible(not plotItem.isVisible()) self.replot() def make(): demo = ImagePlot() demo.resize(600, 400) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/CurveBenchmark.py0000666000000000000000000001403012632324736017434 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the MIT License # Copyright (c) 2015 Pierre Raybaut # (see LICENSE file for more details) """Curve benchmark example""" SHOW = True # Show test in GUI-based test launcher import time import numpy as np import sys from qwt.qt.QtGui import (QApplication, QPen, QMainWindow, QGridLayout, QTabWidget, QWidget, QTextEdit, QLineEdit, QFont, QFontDatabase) from qwt.qt.QtCore import Qt import os if os.environ.get('USE_PYQWT5', False): USE_PYQWT5 = True from PyQt4.Qwt5 import QwtPlot, QwtPlotCurve else: USE_PYQWT5 = False from qwt import QwtPlot, QwtPlotCurve # analysis:ignore COLOR_INDEX = None def get_curve_color(): global COLOR_INDEX colors = (Qt.blue, Qt.red, Qt.green, Qt.yellow, Qt.magenta, Qt.cyan) if COLOR_INDEX is None: COLOR_INDEX = 0 else: COLOR_INDEX = (COLOR_INDEX + 1) % len(colors) return colors[COLOR_INDEX] class BMPlot(QwtPlot): def __init__(self, title, xdata, ydata, style, symbol=None, *args): super(BMPlot, self).__init__(*args) self.setMinimumSize(200, 200) self.setTitle(title) self.setAxisTitle(QwtPlot.xBottom, 'x') self.setAxisTitle(QwtPlot.yLeft, 'y') self.curve_nb = 0 for idx in range(1, 11): self.curve_nb += 1 curve = QwtPlotCurve() curve.setPen(QPen(get_curve_color())) curve.setStyle(style) curve.setRenderHint(QwtPlotCurve.RenderAntialiased) if symbol is not None: curve.setSymbol(symbol) curve.attach(self) curve.setData(xdata, ydata*idx, finite=False) # self.setAxisScale(self.yLeft, -1.5, 1.5) # self.setAxisScale(self.xBottom, 9.9, 10.) self.replot() class BMWidget(QWidget): def __init__(self, points, *args, **kwargs): super(BMWidget, self).__init__() self.plot_nb = 0 self.curve_nb = 0 self.setup(points, *args, **kwargs) def params(self, *args, **kwargs): if kwargs.get('only_lines', False): return (('Lines', None),) else: return ( ('Lines', None), ('Dots', None), ) def setup(self, points, *args, **kwargs): x = np.linspace(.001, 20., points) y = (np.sin(x)/x)*np.cos(20*x) layout = QGridLayout() nbcol, col, row = 2, 0, 0 for style, symbol in self.params(*args, **kwargs): plot = BMPlot(style, x, y, getattr(QwtPlotCurve, style), symbol=symbol) layout.addWidget(plot, row, col) self.plot_nb += 1 self.curve_nb += plot.curve_nb col += 1 if col >= nbcol: row +=1 col = 0 self.text = QLineEdit() self.text.setReadOnly(True) self.text.setAlignment(Qt.AlignCenter) self.text.setText("Rendering plot...") layout.addWidget(self.text, row+1, 0, 1, 2) self.setLayout(layout) class BMText(QTextEdit): def __init__(self, parent=None, title=None): super(BMText, self).__init__(parent) self.setReadOnly(True) library = 'PyQwt5' if USE_PYQWT5 else 'PythonQwt' wintitle = self.parent().windowTitle() if not wintitle: wintitle = "Benchmark" if title is None: title = '%s example' % wintitle self.parent().setWindowTitle('%s [%s]' % (wintitle, library)) self.setText("""\ %s:
(base plotting library: %s)

Click on each tab to test if plotting performance is acceptable in terms of GUI response time (switch between tabs, resize main windows, ...).


Benchmarks results: """ % (title, library)) class BMDemo(QMainWindow): TITLE = 'Curve benchmark' SIZE = (1000, 800) def __init__(self, max_n, parent=None, **kwargs): super(BMDemo, self).__init__(parent=parent) title = self.TITLE if kwargs.get('only_lines', False): title = '%s (%s)' % (title, 'only lines') self.setWindowTitle(title) self.tabs = QTabWidget() self.setCentralWidget(self.tabs) self.text = BMText(self) self.tabs.addTab(self.text, 'Contents') self.resize(*self.SIZE) # Force window to show up and refresh (for test purpose only) self.show() QApplication.processEvents() t0g = time.time() self.run_benchmark(max_n, **kwargs) dt = time.time()-t0g self.text.append("

Total elapsed time: %d ms" % (dt*1e3)) self.tabs.setCurrentIndex(0) def process_iteration(self, title, description, widget, t0): self.tabs.addTab(widget, title) self.tabs.setCurrentWidget(widget) # Force widget to refresh (for test purpose only) QApplication.processEvents() time_str = "Elapsed time: %d ms" % ((time.time()-t0)*1000) widget.text.setText(time_str) self.text.append("
%s:
%s" % (description, time_str)) def run_benchmark(self, max_n, **kwargs): for idx in range(4, -1, -1): points = max_n/10**idx t0 = time.time() widget = BMWidget(points, **kwargs) title = '%d points' % points description = '%d plots with %d curves of %d points' % ( widget.plot_nb, widget.curve_nb, points) self.process_iteration(title, description, widget, t0) if __name__ == '__main__': app = QApplication([]) for name in ('Calibri', 'Verdana', 'Arial'): if name in QFontDatabase().families(): app.setFont(QFont(name)) break kwargs = {} for arg in sys.argv[1:]: kwargs[arg] = True demo = BMDemo(1000000, **kwargs) app.exec_() PythonQwt-0.5.5/qwt/tests/MultiDemo.py0000666000000000000000000000511112605720026016424 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import QApplication, QPen, QGridLayout, QWidget from qwt.qt.QtCore import Qt from qwt import QwtPlot, QwtPlotCurve def drange(start, stop, step): start, stop, step = float(start), float(stop), float(step) size = int(round((stop-start)/step)) result = [start]*size for i in range(size): result[i] += i*step return result def lorentzian(x): return 1.0/(1.0+(x-5.0)**2) class MultiDemo(QWidget): def __init__(self, *args): QWidget.__init__(self, *args) layout = QGridLayout(self) # try to create a plot for SciPy arrays # make a curve and copy the data numpy_curve = QwtPlotCurve('y = lorentzian(x)') x = np.arange(0.0, 10.0, 0.01) y = lorentzian(x) numpy_curve.setData(x, y) # here, we know we can plot NumPy arrays numpy_plot = QwtPlot(self) numpy_plot.setTitle('numpy array') numpy_plot.setCanvasBackground(Qt.white) numpy_plot.plotLayout().setCanvasMargin(0) numpy_plot.plotLayout().setAlignCanvasToScales(True) # insert a curve and make it red numpy_curve.attach(numpy_plot) numpy_curve.setPen(QPen(Qt.red)) layout.addWidget(numpy_plot, 0, 0) numpy_plot.replot() # create a plot widget for lists of Python floats list_plot = QwtPlot(self) list_plot.setTitle('Python list') list_plot.setCanvasBackground(Qt.white) list_plot.plotLayout().setCanvasMargin(0) list_plot.plotLayout().setAlignCanvasToScales(True) x = drange(0.0, 10.0, 0.01) y = [lorentzian(item) for item in x] # insert a curve, make it red and copy the lists list_curve = QwtPlotCurve('y = lorentzian(x)') list_curve.attach(list_plot) list_curve.setPen(QPen(Qt.red)) list_curve.setData(x, y) layout.addWidget(list_plot, 0, 1) list_plot.replot() def make(): demo = MultiDemo() demo.resize(400, 300) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/comparative_benchmarks.py0000666000000000000000000000204312615225450021237 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the MIT License # Copyright (c) 2015 Pierre Raybaut # (see LICENSE file for more details) """ PyQwt5 vs. PythonQwt ==================== """ import os import os.path as osp import sys import subprocess import time def run_script(filename, args=None, wait=True): """Run Python script""" os.environ['PYTHONPATH'] = os.pathsep.join(sys.path) command = [sys.executable, '"'+filename+'"'] if args is not None: command.append(args) proc = subprocess.Popen(" ".join(command), shell=True) if wait: proc.wait() def main(): for name in ('CurveBenchmark.py', 'CurveStyles.py',): for args in (None, 'only_lines'): for value in ('', '1'): os.environ['USE_PYQWT5'] = value filename = osp.join(osp.dirname(osp.abspath(__file__)), name) run_script(filename, wait=False, args=args) time.sleep(4) if __name__ == '__main__': main() PythonQwt-0.5.5/qwt/tests/ReallySimpleDemo.py0000666000000000000000000000430312605720026017736 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import QApplication, QPen from qwt.qt.QtCore import Qt from qwt import QwtPlot, QwtPlotMarker, QwtLegend, QwtPlotCurve, QwtText class SimplePlot(QwtPlot): def __init__(self, *args): QwtPlot.__init__(self, *args) self.setTitle('ReallySimpleDemo.py') self.insertLegend(QwtLegend(), QwtPlot.RightLegend) self.setAxisTitle(QwtPlot.xBottom, 'x -->') self.setAxisTitle(QwtPlot.yLeft, 'y -->') self.enableAxis(self.xBottom) # insert a few curves cSin = QwtPlotCurve('y = sin(x)') cSin.setPen(QPen(Qt.red)) cSin.attach(self) cCos = QwtPlotCurve('y = cos(x)') cCos.setPen(QPen(Qt.blue)) cCos.attach(self) # make a Numeric array for the horizontal data x = np.arange(0.0, 10.0, 0.1) # initialize the data cSin.setData(x, np.sin(x)) cCos.setData(x, np.cos(x)) # insert a horizontal marker at y = 0 mY = QwtPlotMarker() mY.setLabel(QwtText('y = 0')) mY.setLabelAlignment(Qt.AlignRight | Qt.AlignTop) mY.setLineStyle(QwtPlotMarker.HLine) mY.setYValue(0.0) mY.attach(self) # insert a vertical marker at x = 2 pi mX = QwtPlotMarker() mX.setLabel(QwtText('x = 2 pi')) mX.setLabelAlignment(Qt.AlignRight | Qt.AlignTop) mX.setLineStyle(QwtPlotMarker.VLine) mX.setXValue(2*np.pi) mX.attach(self) # replot self.replot() def make(): demo = SimplePlot() demo.resize(800, 500) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() demo.exportTo("demo.png", size=(1600, 900), resolution=200) sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/CurveDemo1.py0000666000000000000000000001103012605720030016467 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import (QApplication, QPen, QBrush, QFrame, QFont, QPainter, QPaintEngine) from qwt.qt.QtCore import QSize from qwt.qt.QtCore import Qt from qwt import QwtSymbol, QwtPlotCurve, QwtPlotItem, QwtScaleMap class CurveDemo(QFrame): def __init__(self, *args): QFrame.__init__(self, *args) self.xMap = QwtScaleMap() self.xMap.setScaleInterval(-0.5, 10.5) self.yMap = QwtScaleMap() self.yMap.setScaleInterval(-1.1, 1.1) # frame style self.setFrameStyle(QFrame.Box | QFrame.Raised) self.setLineWidth(2) self.setMidLineWidth(3) # calculate values self.x = np.arange(0, 10.0, 10.0/27) self.y = np.sin(self.x)*np.cos(2*self.x) # make curves with different styles self.curves = [] self.titles = [] # curve 1 self.titles.append('Style: Sticks, Symbol: Ellipse') curve = QwtPlotCurve() curve.setPen(QPen(Qt.red)) curve.setStyle(QwtPlotCurve.Sticks) curve.setSymbol(QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.yellow), QPen(Qt.blue), QSize(5, 5))) self.curves.append(curve) # curve 2 self.titles.append('Style: Lines, Symbol: None') curve = QwtPlotCurve() curve.setPen(QPen(Qt.darkBlue)) curve.setStyle(QwtPlotCurve.Lines) self.curves.append(curve) # curve 3 self.titles.append('Style: Lines, Symbol: None, Antialiased') curve = QwtPlotCurve() curve.setPen(QPen(Qt.darkBlue)) curve.setStyle(QwtPlotCurve.Lines) curve.setRenderHint(QwtPlotItem.RenderAntialiased) self.curves.append(curve) # curve 4 self.titles.append('Style: Steps, Symbol: None') curve = QwtPlotCurve() curve.setPen(QPen(Qt.darkCyan)) curve.setStyle(QwtPlotCurve.Steps) self.curves.append(curve) # curve 5 self.titles.append('Style: NoCurve, Symbol: XCross') curve = QwtPlotCurve() curve.setStyle(QwtPlotCurve.NoCurve) curve.setSymbol(QwtSymbol(QwtSymbol.XCross, QBrush(), QPen(Qt.darkMagenta), QSize(5, 5))) self.curves.append(curve) # attach data, using Numeric for curve in self.curves: curve.setData(self.x, self.y) def shiftDown(self, rect, offset): rect.translate(0, offset) def paintEvent(self, event): QFrame.paintEvent(self, event) painter = QPainter(self) painter.setClipRect(self.contentsRect()) self.drawContents(painter) def drawContents(self, painter): # draw curves r = self.contentsRect() dy = r.height()/len(self.curves) r.setHeight(dy) for curve in self.curves: self.xMap.setPaintInterval(r.left(), r.right()) self.yMap.setPaintInterval(r.top(), r.bottom()) engine = painter.device().paintEngine() if engine is not None and engine.hasFeature(QPaintEngine.Antialiasing): painter.setRenderHint( QPainter.Antialiasing, curve.testRenderHint(QwtPlotItem.RenderAntialiased)) curve.draw(painter, self.xMap, self.yMap, r) self.shiftDown(r, dy) # draw titles r = self.contentsRect() r.setHeight(dy) painter.setFont(QFont('Helvetica', 8)) painter.setPen(Qt.black) for title in self.titles: painter.drawText( 0, r.top(), r.width(), painter.fontMetrics().height(), Qt.AlignTop | Qt.AlignHCenter, title) self.shiftDown(r, dy) def make(): demo = CurveDemo() demo.resize(300, 600) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/DataDemo.py0000666000000000000000000000647612605720030016215 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import random import sys import numpy as np from qwt.qt.QtGui import QApplication, QPen, QBrush, QFrame from qwt.qt.QtCore import QSize from qwt.qt.QtCore import Qt from qwt import (QwtPlot, QwtPlotMarker, QwtSymbol, QwtLegend, QwtPlotCurve, QwtAbstractScaleDraw) class DataPlot(QwtPlot): def __init__(self, *args): QwtPlot.__init__(self, *args) self.setCanvasBackground(Qt.white) self.alignScales() # Initialize data self.x = np.arange(0.0, 100.1, 0.5) self.y = np.zeros(len(self.x), np.float) self.z = np.zeros(len(self.x), np.float) self.setTitle("A Moving QwtPlot Demonstration") self.insertLegend(QwtLegend(), QwtPlot.BottomLegend); self.curveR = QwtPlotCurve("Data Moving Right") self.curveR.attach(self) self.curveL = QwtPlotCurve("Data Moving Left") self.curveL.attach(self) self.curveL.setSymbol(QwtSymbol(QwtSymbol.Ellipse, QBrush(), QPen(Qt.yellow), QSize(7, 7))) self.curveR.setPen(QPen(Qt.red)) self.curveL.setPen(QPen(Qt.blue)) mY = QwtPlotMarker() mY.setLabelAlignment(Qt.AlignRight | Qt.AlignTop) mY.setLineStyle(QwtPlotMarker.HLine) mY.setYValue(0.0) mY.attach(self) self.setAxisTitle(QwtPlot.xBottom, "Time (seconds)") self.setAxisTitle(QwtPlot.yLeft, "Values") self.startTimer(50) self.phase = 0.0 def alignScales(self): self.canvas().setFrameStyle(QFrame.Box | QFrame.Plain) self.canvas().setLineWidth(1) for i in range(QwtPlot.axisCnt): scaleWidget = self.axisWidget(i) if scaleWidget: scaleWidget.setMargin(0) scaleDraw = self.axisScaleDraw(i) if scaleDraw: scaleDraw.enableComponent(QwtAbstractScaleDraw.Backbone, False) def timerEvent(self, e): if self.phase > np.pi - 0.0001: self.phase = 0.0 # y moves from left to right: # shift y array right and assign new value y[0] self.y = np.concatenate((self.y[:1], self.y[:-1]), 1) self.y[0] = np.sin(self.phase) * (-1.0 + 2.0*random.random()) # z moves from right to left: # Shift z array left and assign new value to z[n-1]. self.z = np.concatenate((self.z[1:], self.z[:1]), 1) self.z[-1] = 0.8 - (2.0 * self.phase/np.pi) + 0.4*random.random() self.curveR.setData(self.x, self.y) self.curveL.setData(self.x, self.z) self.replot() self.phase += np.pi*0.02 def make(): demo = DataPlot() demo.resize(500, 300) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/CurveStyles.py0000666000000000000000000000500512617373374017034 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the MIT License # Copyright (c) 2015 Pierre Raybaut # (see LICENSE file for more details) """Curve styles""" SHOW = True # Show test in GUI-based test launcher import time import sys from qwt.qt.QtGui import QApplication, QPen, QBrush, QFont, QFontDatabase from qwt.qt.QtCore import QSize from qwt.qt.QtCore import Qt from qwt.tests import CurveBenchmark as cb if cb.USE_PYQWT5: from PyQt4.Qwt5 import QwtSymbol else: from qwt import QwtSymbol # analysis:ignore class CSWidget(cb.BMWidget): def params(self, *args, **kwargs): symbols, = args symb1 = QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.yellow), QPen(Qt.blue), QSize(5, 5)) symb2 = QwtSymbol(QwtSymbol.XCross, QBrush(), QPen(Qt.darkMagenta), QSize(5, 5)) if symbols: if kwargs.get('only_lines', False): return (('Lines', symb1), ('Lines', symb1), ('Lines', symb2), ('Lines', symb2),) else: return (('Sticks', symb1), ('Lines', symb1), ('Steps', symb2), ('Dots', symb2),) else: if kwargs.get('only_lines', False): return (('Lines', None), ('Lines', None), ('Lines', None), ('Lines', None),) else: return (('Sticks', None), ('Lines', None), ('Steps', None), ('Dots', None),) class BMDemo(cb.BMDemo): TITLE = 'Curve styles' SIZE = (1000, 800) def run_benchmark(self, max_n, **kwargs): for points, symbols in zip((max_n/10, max_n/10, max_n, max_n), (True, False)*2): t0 = time.time() symtext = "with%s symbols" % ("" if symbols else "out") widget = CSWidget(points, symbols, **kwargs) title = '%d points' % points description = '%d plots with %d curves of %d points, %s' % ( widget.plot_nb, widget.curve_nb, points, symtext) self.process_iteration(title, description, widget, t0) if __name__ == '__main__': app = QApplication([]) for name in ('Calibri', 'Verdana', 'Arial'): if name in QFontDatabase().families(): app.setFont(QFont(name)) break kwargs = {} for arg in sys.argv[1:]: kwargs[arg] = True demo = BMDemo(1000, **kwargs) app.exec_() PythonQwt-0.5.5/qwt/tests/MapDemo.py0000666000000000000000000000736312605720026016062 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import random import sys import time import numpy as np from qwt.qt.QtGui import QApplication, QPen, QBrush, QMainWindow, QToolBar from qwt.qt.QtCore import QSize from qwt.qt.QtCore import Qt from qwt import QwtPlot, QwtSymbol, QwtPlotCurve def standard_map(x, y, kappa): """provide one interate of the inital conditions (x, y) for the standard map with parameter kappa.""" y_new = y-kappa*np.sin(2.0*np.pi*x) x_new = x+y_new # bring back to [0,1.0]^2 if( (x_new>1.0) or (x_new<0.0) ): x_new = x_new - np.floor(x_new) if( (y_new>1.0) or (y_new<0.0) ): y_new = y_new - np.floor(y_new) return x_new, y_new class MapDemo(QMainWindow): def __init__(self, *args): QMainWindow.__init__(self, *args) self.plot = QwtPlot(self) self.plot.setTitle("A Simple Map Demonstration") self.plot.setCanvasBackground(Qt.white) self.plot.setAxisTitle(QwtPlot.xBottom, "x") self.plot.setAxisTitle(QwtPlot.yLeft, "y") self.plot.setAxisScale(QwtPlot.xBottom, 0.0, 1.0) self.plot.setAxisScale(QwtPlot.yLeft, 0.0, 1.0) self.setCentralWidget(self.plot) # Initialize map data self.count = self.i = 1000 self.xs = np.zeros(self.count, np.float) self.ys = np.zeros(self.count, np.float) self.kappa = 0.2 self.curve = QwtPlotCurve("Map") self.curve.attach(self.plot) self.curve.setSymbol(QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.red), QPen(Qt.blue), QSize(5, 5))) self.curve.setPen(QPen(Qt.cyan)) toolBar = QToolBar(self) self.addToolBar(toolBar) # 1 tick = 1 ms, 10 ticks = 10 ms (Linux clock is 100 Hz) self.ticks = 10 self.tid = self.startTimer(self.ticks) self.timer_tic = None self.user_tic = None self.system_tic = None self.plot.replot() def setTicks(self, ticks): self.i = self.count self.ticks = int(ticks) self.killTimer(self.tid) self.tid = self.startTimer(ticks) def resizeEvent(self, event): self.plot.resize(event.size()) self.plot.move(0, 0) def moreData(self): if self.i == self.count: self.i = 0 self.x = random.random() self.y = random.random() self.xs[self.i] = self.x self.ys[self.i] = self.y self.i += 1 chunks = [] self.timer_toc = time.time() if self.timer_tic: chunks.append("wall: %s s." % (self.timer_toc-self.timer_tic)) print(' '.join(chunks)) self.timer_tic = self.timer_toc else: self.x, self.y = standard_map(self.x, self.y, self.kappa) self.xs[self.i] = self.x self.ys[self.i] = self.y self.i += 1 def timerEvent(self, e): self.moreData() self.curve.setData(self.xs[:self.i], self.ys[:self.i]) self.plot.replot() def make(): demo = MapDemo() demo.resize(600, 600) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/CPUplot.py0000666000000000000000000003215512617446334016075 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import os import sys import numpy as np from qwt.qt.QtGui import (QApplication, QColor, QBrush, QWidget, QVBoxLayout, QLabel) from qwt.qt.QtCore import QRect, QTime from qwt.qt.QtCore import Qt from qwt import (QwtPlot, QwtPlotMarker, QwtScaleDraw, QwtLegend, QwtPlotCurve, QwtPlotItem, QwtLegendData, QwtText) class CpuStat: User = 0 Nice = 1 System = 2 Idle = 3 counter = 0 dummyValues = ( ( 103726, 0, 23484, 819556 ), ( 103783, 0, 23489, 819604 ), ( 103798, 0, 23490, 819688 ), ( 103820, 0, 23490, 819766 ), ( 103840, 0, 23493, 819843 ), ( 103875, 0, 23499, 819902 ), ( 103917, 0, 23504, 819955 ), ( 103950, 0, 23508, 820018 ), ( 103987, 0, 23510, 820079 ), ( 104020, 0, 23513, 820143 ), ( 104058, 0, 23514, 820204 ), ( 104099, 0, 23520, 820257 ), ( 104121, 0, 23525, 820330 ), ( 104159, 0, 23530, 820387 ), ( 104176, 0, 23534, 820466 ), ( 104215, 0, 23538, 820523 ), ( 104245, 0, 23541, 820590 ), ( 104267, 0, 23545, 820664 ), ( 104311, 0, 23555, 820710 ), ( 104355, 0, 23565, 820756 ), ( 104367, 0, 23567, 820842 ), ( 104383, 0, 23572, 820921 ), ( 104396, 0, 23577, 821003 ), ( 104413, 0, 23579, 821084 ), ( 104446, 0, 23588, 821142 ), ( 104521, 0, 23594, 821161 ), ( 104611, 0, 23604, 821161 ), ( 104708, 0, 23607, 821161 ), ( 104804, 0, 23611, 821161 ), ( 104895, 0, 23620, 821161 ), ( 104993, 0, 23622, 821161 ), ( 105089, 0, 23626, 821161 ), ( 105185, 0, 23630, 821161 ), ( 105281, 0, 23634, 821161 ), ( 105379, 0, 23636, 821161 ), ( 105472, 0, 23643, 821161 ), ( 105569, 0, 23646, 821161 ), ( 105666, 0, 23649, 821161 ), ( 105763, 0, 23652, 821161 ), ( 105828, 0, 23661, 821187 ), ( 105904, 0, 23666, 821206 ), ( 105999, 0, 23671, 821206 ), ( 106094, 0, 23676, 821206 ), ( 106184, 0, 23686, 821206 ), ( 106273, 0, 23692, 821211 ), ( 106306, 0, 23700, 821270 ), ( 106341, 0, 23703, 821332 ), ( 106392, 0, 23709, 821375 ), ( 106423, 0, 23715, 821438 ), ( 106472, 0, 23721, 821483 ), ( 106531, 0, 23727, 821517 ), ( 106562, 0, 23732, 821582 ), ( 106597, 0, 23736, 821643 ), ( 106633, 0, 23737, 821706 ), ( 106666, 0, 23742, 821768 ), ( 106697, 0, 23744, 821835 ), ( 106730, 0, 23748, 821898 ), ( 106765, 0, 23751, 821960 ), ( 106799, 0, 23754, 822023 ), ( 106831, 0, 23758, 822087 ), ( 106862, 0, 23761, 822153 ), ( 106899, 0, 23763, 822214 ), ( 106932, 0, 23766, 822278 ), ( 106965, 0, 23768, 822343 ), ( 107009, 0, 23771, 822396 ), ( 107040, 0, 23775, 822461 ), ( 107092, 0, 23780, 822504 ), ( 107143, 0, 23787, 822546 ), ( 107200, 0, 23795, 822581 ), ( 107250, 0, 23803, 822623 ), ( 107277, 0, 23810, 822689 ), ( 107286, 0, 23810, 822780 ), ( 107313, 0, 23817, 822846 ), ( 107325, 0, 23818, 822933 ), ( 107332, 0, 23818, 823026 ), ( 107344, 0, 23821, 823111 ), ( 107357, 0, 23821, 823198 ), ( 107368, 0, 23823, 823284 ), ( 107375, 0, 23824, 823377 ), ( 107386, 0, 23825, 823465 ), ( 107396, 0, 23826, 823554 ), ( 107422, 0, 23830, 823624 ), ( 107434, 0, 23831, 823711 ), ( 107456, 0, 23835, 823785 ), ( 107468, 0, 23838, 823870 ), ( 107487, 0, 23840, 823949 ), ( 107515, 0, 23843, 824018 ), ( 107528, 0, 23846, 824102 ), ( 107535, 0, 23851, 824190 ), ( 107548, 0, 23853, 824275 ), ( 107562, 0, 23857, 824357 ), ( 107656, 0, 23863, 824357 ), ( 107751, 0, 23868, 824357 ), ( 107849, 0, 23870, 824357 ), ( 107944, 0, 23875, 824357 ), ( 108043, 0, 23876, 824357 ), ( 108137, 0, 23882, 824357 ), ( 108230, 0, 23889, 824357 ), ( 108317, 0, 23902, 824357 ), ( 108412, 0, 23907, 824357 ), ( 108511, 0, 23908, 824357 ), ( 108608, 0, 23911, 824357 ), ( 108704, 0, 23915, 824357 ), ( 108801, 0, 23918, 824357 ), ( 108891, 0, 23928, 824357 ), ( 108987, 0, 23932, 824357 ), ( 109072, 0, 23943, 824361 ), ( 109079, 0, 23943, 824454 ), ( 109086, 0, 23944, 824546 ), ( 109098, 0, 23950, 824628 ), ( 109108, 0, 23955, 824713 ), ( 109115, 0, 23957, 824804 ), ( 109122, 0, 23958, 824896 ), ( 109132, 0, 23959, 824985 ), ( 109142, 0, 23961, 825073 ), ( 109146, 0, 23962, 825168 ), ( 109153, 0, 23964, 825259 ), ( 109162, 0, 23966, 825348 ), ( 109168, 0, 23969, 825439 ), ( 109176, 0, 23971, 825529 ), ( 109185, 0, 23974, 825617 ), ( 109193, 0, 23977, 825706 ), ( 109198, 0, 23978, 825800 ), ( 109206, 0, 23978, 825892 ), ( 109212, 0, 23981, 825983 ), ( 109219, 0, 23981, 826076 ), ( 109225, 0, 23981, 826170 ), ( 109232, 0, 23984, 826260 ), ( 109242, 0, 23984, 826350 ), ( 109255, 0, 23986, 826435 ), ( 109268, 0, 23987, 826521 ), ( 109283, 0, 23990, 826603 ), ( 109288, 0, 23991, 826697 ), ( 109295, 0, 23993, 826788 ), ( 109308, 0, 23994, 826874 ), ( 109322, 0, 24009, 826945 ), ( 109328, 0, 24011, 827037 ), ( 109338, 0, 24012, 827126 ), ( 109347, 0, 24012, 827217 ), ( 109354, 0, 24017, 827305 ), ( 109367, 0, 24017, 827392 ), ( 109371, 0, 24019, 827486 ), ) def __init__(self): self.procValues = self.__lookup() def statistic(self): values = self.__lookup() userDelta = 0.0 for i in [CpuStat.User, CpuStat.Nice]: userDelta += (values[i] - self.procValues[i]) systemDelta = values[CpuStat.System] - self.procValues[CpuStat.System] totalDelta = 0.0 for i in range(len(self.procValues)): totalDelta += (values[i] - self.procValues[i]) self.procValues = values return 100.0*userDelta/totalDelta, 100.0*systemDelta/totalDelta def upTime(self): result = QTime() for item in self.procValues: result = result.addSecs(item/100) return result def __lookup(self): if os.path.exists("/proc/stat"): for line in open("/proc/stat"): words = line.split() if words[0] == "cpu" and len(words) >= 5: return [float(w) for w in words[1:]] else: result = CpuStat.dummyValues[CpuStat.counter] CpuStat.counter += 1 CpuStat.counter %= len(CpuStat.dummyValues) return result class CpuPieMarker(QwtPlotMarker): def __init__(self, *args): QwtPlotMarker.__init__(self, *args) self.setZ(1000.0) self.setRenderHint(QwtPlotItem.RenderAntialiased, True) def rtti(self): return QwtPlotItem.Rtti_PlotUserItem def draw(self, painter, xMap, yMap, rect): margin = 5 pieRect = QRect() pieRect.setX(rect.x() + margin) pieRect.setY(rect.y() + margin) pieRect.setHeight(yMap.transform(80.0)) pieRect.setWidth(pieRect.height()) angle = 3*5760/4 for key in ["User", "System", "Idle"]: curve = self.plot().cpuPlotCurve(key) if curve.dataSize(): value = int(5760*curve.sample(0).y()/100.0) painter.save() painter.setBrush(QBrush(curve.pen().color(), Qt.SolidPattern)) painter.drawPie(pieRect, -angle, -value) painter.restore() angle += value class TimeScaleDraw(QwtScaleDraw): def __init__(self, baseTime, *args): QwtScaleDraw.__init__(self, *args) self.baseTime = baseTime def label(self, value): upTime = self.baseTime.addSecs(int(value)) return QwtText(upTime.toString()) class Background(QwtPlotItem): def __init__(self): QwtPlotItem.__init__(self) self.setZ(0.0) def rtti(self): return QwtPlotItem.Rtti_PlotUserItem def draw(self, painter, xMap, yMap, rect): c = QColor(Qt.white) r = QRect(rect) for i in range(100, 0, -10): r.setBottom(yMap.transform(i - 10)) r.setTop(yMap.transform(i)) painter.fillRect(r, c) c = c.darker(110) class CpuCurve(QwtPlotCurve): def __init__(self, *args): QwtPlotCurve.__init__(self, *args) self.setRenderHint(QwtPlotItem.RenderAntialiased) def setColor(self, color): c = QColor(color) c.setAlpha(150) self.setPen(c) self.setBrush(c) HISTORY = 60 class CpuPlot(QwtPlot): def __init__(self, *args): QwtPlot.__init__(self, *args) self.curves = {} self.data = {} self.timeData = 1.0 * np.arange(HISTORY-1, -1, -1) self.cpuStat = CpuStat() self.setAutoReplot(False) self.plotLayout().setAlignCanvasToScales(True) legend = QwtLegend() legend.setDefaultItemMode(QwtLegendData.Checkable) self.insertLegend(legend, QwtPlot.RightLegend) self.setAxisTitle(QwtPlot.xBottom, "System Uptime [h:m:s]") self.setAxisScaleDraw( QwtPlot.xBottom, TimeScaleDraw(self.cpuStat.upTime())) self.setAxisScale(QwtPlot.xBottom, 0, HISTORY) self.setAxisLabelRotation(QwtPlot.xBottom, -50.0) self.setAxisLabelAlignment( QwtPlot.xBottom, Qt.AlignLeft | Qt.AlignBottom) self.setAxisTitle(QwtPlot.yLeft, "Cpu Usage [%]") self.setAxisScale(QwtPlot.yLeft, 0, 100) background = Background() background.attach(self) pie = CpuPieMarker() pie.attach(self) curve = CpuCurve('System') curve.setColor(Qt.red) curve.attach(self) self.curves['System'] = curve self.data['System'] = np.zeros(HISTORY, np.float) curve = CpuCurve('User') curve.setColor(Qt.blue) curve.setZ(curve.z() - 1.0) curve.attach(self) self.curves['User'] = curve self.data['User'] = np.zeros(HISTORY, np.float) curve = CpuCurve('Total') curve.setColor(Qt.black) curve.setZ(curve.z() - 2.0) curve.attach(self) self.curves['Total'] = curve self.data['Total'] = np.zeros(HISTORY, np.float) curve = CpuCurve('Idle') curve.setColor(Qt.darkCyan) curve.setZ(curve.z() - 3.0) curve.attach(self) self.curves['Idle'] = curve self.data['Idle'] = np.zeros(HISTORY, np.float) self.showCurve(self.curves['System'], True) self.showCurve(self.curves['User'], True) self.showCurve(self.curves['Total'], False) self.showCurve(self.curves['Idle'], False) self.startTimer(1000) legend.checked.connect(self.showCurve) self.replot() def timerEvent(self, e): for data in self.data.values(): data[1:] = data[0:-1] self.data["User"][0], self.data["System"][0] = self.cpuStat.statistic() self.data["Total"][0] = self.data["User"][0] + self.data["System"][0] self.data["Idle"][0] = 100.0 - self.data["Total"][0] self.timeData += 1.0 self.setAxisScale( QwtPlot.xBottom, self.timeData[-1], self.timeData[0]) for key in self.curves.keys(): self.curves[key].setData(self.timeData, self.data[key]) self.replot() def showCurve(self, item, on, index=None): item.setVisible(on) self.legend().legendWidget(item).setChecked(on) self.replot() def cpuPlotCurve(self, key): return self.curves[key] def make(): demo = QWidget() demo.setWindowTitle('Cpu Plot') plot = CpuPlot(demo) plot.setTitle("History") label = QLabel("Press the legend to en/disable a curve", demo) layout = QVBoxLayout(demo) layout.addWidget(plot) layout.addWidget(label) demo.resize(600, 400) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/BodeDemo.py0000666000000000000000000002454212605720032016211 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) from __future__ import unicode_literals SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import (QApplication, QPen, QBrush, QFrame, QFont, QWidget, QMainWindow, QToolButton, QIcon, QPixmap, QToolBar, QHBoxLayout, QLabel, QPrinter, QPrintDialog, QFontDatabase) from qwt.qt.QtCore import QSize from qwt.qt.QtCore import Qt from qwt import (QwtPlot, QwtPlotMarker, QwtSymbol, QwtLegend, QwtPlotGrid, QwtPlotCurve, QwtPlotItem, QwtLogScaleEngine, QwtText, QwtPlotRenderer) print_xpm = ['32 32 12 1', 'a c #ffffff', 'h c #ffff00', 'c c #ffffff', 'f c #dcdcdc', 'b c #c0c0c0', 'j c #a0a0a4', 'e c #808080', 'g c #808000', 'd c #585858', 'i c #00ff00', '# c #000000', '. c None', '................................', '................................', '...........###..................', '..........#abb###...............', '.........#aabbbbb###............', '.........#ddaaabbbbb###.........', '........#ddddddaaabbbbb###......', '.......#deffddddddaaabbbbb###...', '......#deaaabbbddddddaaabbbbb###', '.....#deaaaaaaabbbddddddaaabbbb#', '....#deaaabbbaaaa#ddedddfggaaad#', '...#deaaaaaaaaaa#ddeeeeafgggfdd#', '..#deaaabbbaaaa#ddeeeeabbbbgfdd#', '.#deeefaaaaaaa#ddeeeeabbhhbbadd#', '#aabbbeeefaaa#ddeeeeabbbbbbaddd#', '#bbaaabbbeee#ddeeeeabbiibbadddd#', '#bbbbbaaabbbeeeeeeabbbbbbaddddd#', '#bjbbbbbbaaabbbbeabbbbbbadddddd#', '#bjjjjbbbbbbaaaeabbbbbbaddddddd#', '#bjaaajjjbbbbbbaaabbbbadddddddd#', '#bbbbbaaajjjbbbbbbaaaaddddddddd#', '#bjbbbbbbaaajjjbbbbbbddddddddd#.', '#bjjjjbbbbbbaaajjjbbbdddddddd#..', '#bjaaajjjbbbbbbjaajjbddddddd#...', '#bbbbbaaajjjbbbjbbaabdddddd#....', '###bbbbbbaaajjjjbbbbbddddd#.....', '...###bbbbbbaaajbbbbbdddd#......', '......###bbbbbbjbbbbbddd#.......', '.........###bbbbbbbbbdd#........', '............###bbbbbbd#.........', '...............###bbb#..........', '..................###...........'] class BodePlot(QwtPlot): def __init__(self, *args): QwtPlot.__init__(self, *args) self.setTitle('Frequency Response of a 2nd-order System') self.setCanvasBackground(Qt.darkBlue) # legend legend = QwtLegend() legend.setFrameStyle(QFrame.Box | QFrame.Sunken) self.insertLegend(legend, QwtPlot.BottomLegend) # grid self.grid = QwtPlotGrid() self.grid.enableXMin(True) self.grid.attach(self) # axes self.enableAxis(QwtPlot.yRight) self.setAxisTitle(QwtPlot.xBottom, '\u03c9/\u03c90') self.setAxisTitle(QwtPlot.yLeft, 'Amplitude [dB]') self.setAxisTitle(QwtPlot.yRight, 'Phase [\u00b0]') self.setAxisMaxMajor(QwtPlot.xBottom, 6) self.setAxisMaxMinor(QwtPlot.xBottom, 10) self.setAxisScaleEngine(QwtPlot.xBottom, QwtLogScaleEngine()) # curves self.curve1 = QwtPlotCurve('Amplitude') self.curve1.setRenderHint(QwtPlotItem.RenderAntialiased); self.curve1.setPen(QPen(Qt.yellow)) self.curve1.setYAxis(QwtPlot.yLeft) self.curve1.attach(self) self.curve2 = QwtPlotCurve('Phase') self.curve2.setRenderHint(QwtPlotItem.RenderAntialiased); self.curve2.setPen(QPen(Qt.cyan)) self.curve2.setYAxis(QwtPlot.yRight) self.curve2.attach(self) # alias fn = self.fontInfo().family() # marker self.dB3Marker = m = QwtPlotMarker() m.setValue(0.0, 0.0) m.setLineStyle(QwtPlotMarker.VLine) m.setLabelAlignment(Qt.AlignRight | Qt.AlignBottom) m.setLinePen(QPen(Qt.green, 2, Qt.DashDotLine)) text = QwtText('') text.setColor(Qt.green) text.setBackgroundBrush(Qt.red) text.setFont(QFont(fn, 12, QFont.Bold)) m.setLabel(text) m.attach(self) self.peakMarker = m = QwtPlotMarker() m.setLineStyle(QwtPlotMarker.HLine) m.setLabelAlignment(Qt.AlignRight | Qt.AlignBottom) m.setLinePen(QPen(Qt.red, 2, Qt.DashDotLine)) text = QwtText('') text.setColor(Qt.red) text.setBackgroundBrush(QBrush(self.canvasBackground())) text.setFont(QFont(fn, 12, QFont.Bold)) m.setLabel(text) m.setSymbol(QwtSymbol(QwtSymbol.Diamond, QBrush(Qt.yellow), QPen(Qt.green), QSize(7,7))) m.attach(self) # text marker m = QwtPlotMarker() m.setValue(0.1, -20.0) m.setLabelAlignment(Qt.AlignRight | Qt.AlignBottom) text = QwtText( '[1-(\u03c9/\u03c90)2+2j\u03c9/Q]' '-1' ) text.setFont(QFont(fn, 12, QFont.Bold)) text.setColor(Qt.blue) text.setBackgroundBrush(QBrush(Qt.yellow)) text.setBorderPen(QPen(Qt.red, 2)) m.setLabel(text) m.attach(self) self.setDamp(0.01) def showData(self, frequency, amplitude, phase): self.curve1.setData(frequency, amplitude) self.curve2.setData(frequency, phase) def showPeak(self, frequency, amplitude): self.peakMarker.setValue(frequency, amplitude) label = self.peakMarker.label() label.setText('Peak: %4g dB' % amplitude) self.peakMarker.setLabel(label) def show3dB(self, frequency): self.dB3Marker.setValue(frequency, 0.0) label = self.dB3Marker.label() label.setText('-3dB at f = %4g' % frequency) self.dB3Marker.setLabel(label) def setDamp(self, d): self.damping = d # Numerical Python: f, g, a and p are NumPy arrays! f = np.exp(np.log(10.0)*np.arange(-2, 2.02, 0.04)) g = 1.0/(1.0-f*f+2j*self.damping*f) a = 20.0*np.log10(abs(g)) p = 180*np.arctan2(g.imag, g.real)/np.pi # for show3dB i3 = np.argmax(np.where(np.less(a, -3.0), a, -100.0)) f3 = f[i3] - (a[i3]+3.0)*(f[i3]-f[i3-1])/(a[i3]-a[i3-1]) # for showPeak imax = np.argmax(a) self.showPeak(f[imax], a[imax]) self.show3dB(f3) self.showData(f, a, p) self.replot() class BodeDemo(QMainWindow): def __init__(self, *args): QMainWindow.__init__(self, *args) self.plot = BodePlot(self) self.plot.setContentsMargins(5, 5, 5, 0) self.setContextMenuPolicy(Qt.NoContextMenu) self.setCentralWidget(self.plot) toolBar = QToolBar(self) self.addToolBar(toolBar) btnPrint = QToolButton(toolBar) btnPrint.setText("Print") btnPrint.setIcon(QIcon(QPixmap(print_xpm))) btnPrint.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) toolBar.addWidget(btnPrint) btnPrint.clicked.connect(self.print_) btnExport = QToolButton(toolBar) btnExport.setText("Export") btnExport.setIcon(QIcon(QPixmap(print_xpm))) btnExport.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) toolBar.addWidget(btnExport) btnExport.clicked.connect(self.exportDocument) toolBar.addSeparator() dampBox = QWidget(toolBar) dampLayout = QHBoxLayout(dampBox) dampLayout.setSpacing(0) dampLayout.addWidget(QWidget(dampBox), 10) # spacer dampLayout.addWidget(QLabel("Damping Factor", dampBox), 0) dampLayout.addSpacing(10) toolBar.addWidget(dampBox) self.statusBar() self.showInfo() def print_(self): printer = QPrinter(QPrinter.HighResolution) printer.setCreator('Bode example') printer.setOrientation(QPrinter.Landscape) printer.setColorMode(QPrinter.Color) docName = str(self.plot.title().text()) if not docName: docName.replace('\n', ' -- ') printer.setDocName(docName) dialog = QPrintDialog(printer) if dialog.exec_(): renderer = QwtPlotRenderer() if (QPrinter.GrayScale == printer.colorMode()): renderer.setDiscardFlag(QwtPlotRenderer.DiscardBackground) renderer.setDiscardFlag(QwtPlotRenderer.DiscardCanvasBackground) renderer.setDiscardFlag(QwtPlotRenderer.DiscardCanvasFrame) renderer.setLayoutFlag(QwtPlotRenderer.FrameWithScales) renderer.renderTo(self.plot, printer) def exportDocument(self): renderer = QwtPlotRenderer(self.plot) renderer.exportTo(self.plot, "bode") def showInfo(self, text=""): self.statusBar().showMessage(text) def moved(self, point): info = "Freq=%g, Ampl=%g, Phase=%g" % ( self.plot.invTransform(QwtPlot.xBottom, point.x()), self.plot.invTransform(QwtPlot.yLeft, point.y()), self.plot.invTransform(QwtPlot.yRight, point.y())) self.showInfo(info) def selected(self, _): self.showInfo() def make(): demo = BodeDemo() demo.resize(540, 400) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) fonts = QFontDatabase() for name in ('Verdana', 'STIXGeneral'): if name in fonts.families(): app.setFont(QFont(name)) break demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/demo.png0000666000000000000000000010747512641752170015632 0ustar rootrootPNG  IHDR@MsBIT|d pHYsnu> IDATxQh[g0g>z 30;&B1e&P d E"Ԕax@Æ18,:! N?vvY|j/vT;#Y9~?08GGG`$@fn70|x7bfff#*O?JqRtaFܾ}c2lVDĥI/ [W M Wǭ[r,(W:FV[dĿR˒aHjloo=wΝ+'?.B!]O>M{(@ )+0n)۷oG\N{@à:ٳgLUV͛ىwy'[NNN??ҧqj>k~KR @t X^SVcggnǏcmm\#"bvv6ݻqttt+GGGŋB a@H\~}~ߍpDDt̗.}Lŋ/"rhdLݻwZv{}v4X}w:F׮]MQVVϭ_?'?IFg{O0S333[ośo];);.PmڡrBcqN˱oR)"N͛7;&k>}Ϟ=k}~87OӮU?AWI|׍I:VgϫF|ꫯZq1>y7bff&1{1yG @rjs7pJ:n B|'177q{Mr}||gmw^ UtdZvǛ@j_B!~_\Hd]6O.w޽}\$ɘZׯ_QiQ?g=y$VWW;>uV<#Xѩ:8f4T*q(˃9R)ZدvcRDVgoDDϼP(Ľ{0NNNz`yfכb1vww#sR)qrFDlnnvO$k}|lllt|믿n=S#"buuu`M>`$@Fͱzrij B׽7od̓n ˗]լοqF-JqΝݍÑ-Isf&>WbO vSu;Uy"ڟiQ?ݦJrQ%Ar>O~ jZ;;;qttz=ztp?ؿۿ8?O<;40=}I ITոw^ǛBbvX[[;W9?nRx~kk+?~j܍zGGGŋ<c|cN;h4:~|Zv|~|͎n]zwލ0Lϟ[ Ugp$?;QOǸ{7)- r? l^޽{n!X[[@F78G)3~[[[~N Ua}0X ){Yjn7:ggg{~񚎏ӧݍݸw^rK;lnN;mZNS^ݿke*J|}?goc^ݺuc|MKa?ֹxz~)<#G %E;MF]&g:4 qE.}vܾ};677csssdig~ߵݧP(tֵZ2JeUYc)1~T&tm:k׮ SyG @ 666]+Gqsoo/b}}}5?;>?1jZT*///_zXYYz/^xIiW:>>sf\UuKv:G;i%^{9}IX,-=6X\>_kǭ[ݻeɐ~:Oݦ~0‘Q~ 뭟Z|~]o SZZQVVE^:阄T*6IݻwcJa2t|7HHKG$ZdqӠ򗿌_mo \5!cxS;4̝;w:~t[$yu jcc  |>n]qx?}6SK5KC < CбkR??}쭷xjz1u)ɏϳSWM'*+J|__;> n~?nx<#O`nݺiX t\.lc`ee%;;;]6T*]%Kfшv|9s>ONN޽{;,Az7;>֩ɓ'=/u~A~0X #rnv,T*1??ZhթdNNNZ^\v-n߾x{{{WV޽{? ?я&f,,,v^HG\:}ox\j5>裮ɀSA%yMVry87,a9y{h^h<}`|^9An?>>zSٳg?<<|ͱvq~>~z3:g}AVZcy {A~?1>XU*:v~Fy<}t Ik rܽz|~ < #477uJKo{& ^sɏifeuKG~---]yɏ4%9}w$@Flqq1vvv>VTWչm+++ぎaaa!zQqƥ\65Mvi|a эqx?p |'7 KRloo۶Zc2B׻w&Jj}0tZ(b~~1>888mP{nGB}Ç1;;;y?|>JR<|*~sG<# }M'''w\tӴ:F#8::oZ0|7jp|>+++1 cM~ݞiN˗/~7n܈OcWV/5S>;tLw=󾗵!oV_@ndi0^|y8M[oo333]3q_|u|~0 j7oxVX'´Dbb#l2cT@n6# $@;'''Q,;><@vI0v^x7xcD##`{n0T}m / # 0|dchzKGL3Gz0H:@#L `HG8 đ&0q$@#L `H絴x0>Ȭ="C _O{L đ`jrHG8 đzh4Ν;\.}r\.ܹFc`)" XQTT*JYYYRJ%vww2v$`*<}4blll\)Ѵ_uŸyf,--]:@`JW>Ç#"?򱚚jt0Tmߎx$bCT61 ӨX,q tyIk꫹wnn.JR xIܺuJǓa: #ݞSk`8 z7r1b.x*qƫq݋RjZׯ_B1qR(T*EVo o2l .d0\,iFt jZgss}kk+~X\\ZAJ6Yw888Ѝppppn$r=N7֔T˭IOjB?h4b1Z _)=wﶶ?z(*JTKL H0ZDt2cu.Z vr|| Yd믟sO$uV6 ^H5766?qŵk.$*-N75?^?AL~&WRлzۨ׿\  5qo\JmԼo^^rSF.I;fiv$r9˵n4Tmmny}i^Y9 VXFF#bfXZZǏG>bq͸vڥ,2;;ۺ)J|V:@2c"YyUULIBLfc4վΝ;sUF#>裈z|,TN6:Y禮z&~Rpj;@""gUҔWݺu+ B|QVcyy9 BS]D\\ aՔ(JsӍw}7"N;DiYq @SHED,//8ݻ:5F#K;>>ڊň8-JM<)a  h4,҉nQIt޴Te: MW:Y( aϳ\,` iqMp UVq( b-{ET/^\) b J4V5|.0ӵlkR4Yj_IEg;A|.}ߏ/^xyJ87byycg?"GTO>\^T*xمלO>$coo/8JR+wH05 Ynw&i EC B}HZhܭ¶~;?zuHIFҙ ˿KDdoөceemWY^g\v?0*ʅ:&6"Nº?^㪬T6.0.0hY^룛_Wٳs^cQ$?jZD\}1R.'fIFoۈ4XƍP($Z#( r!T^(Ah&>-ɓ(Jqpp qppR)h vI0$A빜@O *Ʊ#puu5bqq1"NlڊՁV:vҌY-۱R)޽;=<<֟ONNbww7677P(@^,ҲQ,\._xr9blllFz+9;U1\.q yDD|Pk@Fr.Ыqܹs'"">|xh? 2:I<ּX|>~_zm>'8vܹ #_?Mm^uΝT*QtѼ6T*Q*bee4B$#$@5<)ĸ\cb}}=={ֶ0cgg"3Fٳg>677#t7n!rug 6 ƓHJy:hӘ\f?Y4V2c\WKQ 2Y\9vN)1-C2\b""^K{JEM6 &mK 1̀g 4I~F3nY/ѣ!f@^VUsq;Z&7GK2,%fn Ёb$!˽.1C0$@ HK@өH czn!K0$@ 'L]!f>1K6Y$ .D(!f.1KY$$D6 (`&1KYI0=^K{^}k $P;z8,z8,Ad ]ţ IDAT Y"u<(`>$A:x|~99?8*Jc7[(sNDNc533h4⣏>>kD&JRx"Fn+\Mmz&_3QppppnɄW5'◿eDDQ8::gϞVD&JsGDv4q[Z5o&<bcc#"N+mW,R\xQT*(汛u T3Bw`B `9fq 0^Ol (J_"fgg#+T*EPhmKӧORVBQTbgg'VVVw֭ىxyess3"l,..FwI#g~oqxxh͐汓&@t##*F6W3 b!ftTOSk˗/=|~zYDĥSq""oxs{s|>'?qnzWݺu+ B|QVcyy9 B)Ěnei D\7MלHwt䪜C̸q1??q8&X,FVDHsˮk>O>۷oGDF?vڹdG14?Ϧ^?äR5UUw`h4h4X,ffa[ZZǏG>bq͸vZ|(Jq0?CkC AniXu& fɶZ[zYT]ZZ8>>o&^|뱾ya}nNNNP(DDDPw}whI IrIa "fatjN_~zC:gcc#"._qǛۛjnn.cmmZdFsFu կ"uKNyk]F2`ғ 0( HFA0#ϷjX?/"~߶77ƍyIJ888888RO<ZF% ILY1 L<305Q.%!jk&rh4\.zs {Ṯ;;;zjlmmbDD,..VvJ9y쳋wzD{25r\|(45&RO 09InM庒ar~A{~LVj[Z͛7;>޼lgϞB-_:fgg[- ޹s'"">|333F#>裈:Vi2T*ŋ/%At@LbE *honN=9 @GD=հ-,,DZ|>JҹjىZv.ŋ(J>"N?R>l} {coo/*Jܿ\#"bff&>䓨T*GT|>I߸ FŹudtg5בs .O۷c?nݺp2ɓ'RܔaP.P(@:4@hME5F B5=:>>Bpa}HL\.MR[9 11-FI5 Tʊz=q裏.LkcnI% ̀¹4AIq:\71 LG}KKKb1x+OU.X,F,--\ Ȩq &=׌W.#WIݽ{7jk~ u?|ܽ{[ ,-Ъf!mAո\/fEЯn\A׋9ytƏ5@+sG[i4.7!mb@0z q㼸 0,@VIW,QQ4Ad9 "f!K,%c @ , YM@&!fFJ*LIebѐ1 `I~ub K$@bHG bƁ`$@`̤L$ҡM 0NPQ1 $#f.  1TɈYG #cJE$#`I~0,@$@DTR1$A&q&fhVT "P Q,HL%7%HƋiVTI(byL1  @:H $c^]t 3fQ$ Б@ ? 1 H`p$@`Bd&I& @` 2 "faYC&*&ZZ_|E;=/cJUJ/0l*͊*:h5 s4\T$@r ۛɐR+++ F5 OVcgg'^QDDDPÞ[/9yuq{,L1 L|ÇqX[[VrO?VըT*qao0!H:D'@&!^TV \x % C2 t(z1 m9}P jiu@ $ҡ0ng gvyq DŽ~d4 ϟ~mgqňڊKZ?݋rFcTRC$#f[Hsa֭[=?T*EPk׮EZx@E$#HNZш͈( 1??qz~VNk7oaTRw$A%#f~ۿ۞?;;kkk g RQdh4Q("""B~q.JOϩ !TpQʿ I$ .$7 |>Ӻ|:G}q"/^*==~%P&ShXHjpU=֍7ZwZKzQD&Q>3.@mAgbd&:rvN i/>}4:YgڜV R݉YAHZmsss}Ç[_hǃW˩H.'f'@zݧr9r\rmj(J133svww;vDD7io_z䓈8MNF#T*EDr{&+iss3]r9jZkqlooT*JNT@r*F\TX-..FTB뱾~aRta򓓓VgGPQZɊDgu[ߣT*JOǃ Dp7 я~_|ŹN;m?emɓ|A8<tmіDd /NdBGGGQ(vvv""P(_^wI21 ? >|IX[[k%G>xQI_A2b_@iZFR4W*j#LU` bo m~_XXh \F%@:tilB//svæ &E[d,@Iﳳm9r{E\Fqc¸LâH?7R) B\v-KzTR 1 0(b k$@RR/jisM L] @Y`<lEDzDDJOɴgJ*t d ~Eu@vi)H.? m JQ0(? nܸ'''ma -`X,@VH1??S?g_P( ؐe@,xilBZQw%I%@:LɈYadHu=ܞcaar9r\r=nΒ7~LE$#HƇH|ID.BJX4k-NG}~\n{{lss3]r9jZkqloo]*]H?=*](JraIkB<8x.AncB.zyDD|X:7/ 99q A??`΁d|_?[ߡ ki VVVG?Q|ھN_|?z˗/㫯ju|OZ\0K1??kkkW_EiBcii>qC ' 1 0j }ilnnFD6L B2CHOd.jH5 #2`V̤<. E[ iR ?/ {AK{(J*t iA4] ` bȮslTRCЍ_r^`<5@$e\ _ DRf , +Pd@iY#fFI @,@H@TRCL-@4d,HcC0$%}PI_A2b t ] ` b J*tz!JL'E[ɈYaƎ`@%@:LɈYq & `,d@$@ !TC&.q%fE[Z,@$@ T0$#fƝ.`^K{^&LfE#]3pc`, )T Y!pK&rLb`RYAƞ,@:$@ T0L'] ILdQ5>tIA0C0 ЁJ*`R f& `bL%mHA)ƛ  L!H 1 0-tW%L] ` b 8C%@:t&SLE[C$#f⵴h*I`:IL%BӚ QIE V.߁@LSNK9@:L%L,d`X$@ lNC) IDATh@,H0TRW0IJɈY^IO0G d,z!L] ` b< J*tFA8Gd-,@R RdK Ap`ҥhkz`ң^TOh[g?Ȣ+r/wc])A656]\jB0b0!nD,ˆ3k%-D`@pBUw[dʉlIG:^/0#?R>|L̨s{&pH Pz@B@@)&ftXJq# n&.d3tX-ff.#*f@Y`q*L*lX X&] E &m"] PY X>@V TT@YsZX`?vn݊z>u~t:j͛qƍؘ oCu nWFz( t:l6g^Ng|9IvpR:::v;c8qxޱɓhė_~ a 8<:Ad&@6,ŵPFoǏ%~zDDnDDFٜ״dra3(rY {x)Nz}k[K2d/2-@Y R Fch|0sNg1Ut f@7o_gߎLJ~80X.HS ?gqzzoߎtɱZN1Me~?3x#"ΏERL* ,sh_é猎7<\ۍZZ-=zkGGכ)xх)~7]H.HF1 5 \HDĽ{"f,ƛ{//=?Oׯ_n`|$>:\])Ue&@6,φ߳IJuKt:l6;==/jÇ/6?n'yݴ1JVEyDDܽ{wCJDB́C̤.y{o]zl6W_Ǐxݎ[nMٳgNɓxY<}th47ߌ7xC\A䑚.W$~ z=vvvӈxJܹsg9ڊ%F 7* ۋl*(LbYlɨY SLnLH)& SX[[x4\L*lX Q.q݌G.HF1 5 L'Ç1ccc#@.Z`j% _F] ޛ9fjT72$QK@Yep (*s&mj6Cd,PP L*lX Q.:@6tO 3PLPe?"P8ZkGV Qy/_}Ar3 ~[DDՊzz=Z^~!r ,툈HGs׿5""yB Of@sW/3a+kEDKύIVh/oll;d?p8ښ[[[^:,@^E[__VN'NOOOOODպ0xv-TŨ0,,ȗt:ٳgf19sm2 35 Y2X~@VHK9$c,lY`F`=x ,,a&RKuJ~M !,02pEe@B~Y.d VMb~?z^DDlooO=g{{;:Nz QW9ę\F;$j57c +#%&7NP!'''ǛSϙ<^ ѽC2ZP\ HMK+e+HfA %u%)oN1Q纰"@X E!Ȋ5UacA楋UP@ +v㳳眞N=] @Y( Ȋqp_'Y@O=gEYY UR@R@/Ϋ}=3:h4bsssecCK9gj@HݻN'KaAt:x2#a+ @259vwwQ뱻Ñ;wd9L(-$e+H.HFdlvv}4͌Fbw-TY^z;;;Y$,@|j OK9@6tgSLpPj t @rN$Ȇ@Y rײW3;uX 䉚VKRA+`$?r3}~;"">S rˡ{] gWL7h6_">|݋?<@e(&bKE|A|gE<~lllna7$c,Ef3tX7A*kkkl6lѲTrl\ K =677֝;wR@y)&{<R @ 5t@ tC @`uW HXt@2 lYBȇ~8 n,z2NV+n޼7n܈͐͂($_*.J n7ZKGaHӉf9u}z/t:0o͙ b`yuTJ8hq||0nG'O|^F# 1c8a4}v\z.X&-F @J|'<ىX[[q8|v}Yܹs'Ƿ޽{?/PZ? =6CJUSz1 f΅mmm3]`t@2 lYGɥ.ڇcHR ,R +2 @_@,R }&O~?~鹵ZmuvqvvXR䙚Y:u4oƏoܸuN'ZV\~\@1X e=2 F܆#)mާ?)vww#"q||kkk37XQKe[}Ǩ d+\riB~ߎwKى l6g @RL,{(@~z ,5z4%3R`l @_/j43]{_ttt4^>scdrcNИfecAHF1 5 I.͛7ǏΦszz:Yx#y}-WRdrq8==۷oGD:X[BG,f*Lؘgu~/ҕ0/-@YHh_é猎7<\ۍZZ-=zkGGכ)xх)~7]X JDDܻw/"o&~pp0`{k믿{{{qv1 ONN⣏>Օr,I$f~6 ^FӉfyx V>|u8~MӪjX_GDݻwW9$ #f4C2nUJH.?@d?.7,ߵ f3^}xquϟ={6~;:pO|0666 P l\`WRL  )_@YkYիy01#5 /TXP Hr_ٰ!$f* $( @(&s3QLUX K "_@qY(;3䙚\W.LD J,. E1Tb0P\:)+s(&f6\W%X] @Y 3] 2X72 Eb ilWIul,,´C2Zf&W'X!] @X"GB@Hr ,# tPRcF$W@YE< (?5 e  UZ25 $%(-7,KP6Ie$ S6LP\JWJ@̨d, 5 @1]zy7 DZ;>n֭[QnmCf vk ?""vww^nW(jEq||v;""ZV!NEDZÑ?81դ rdtPv)~zޞzx׋`. ,"dxsss9 +2`,,WP)L<Zmuvqvv5J1K SXt:jS.P>@y*e=N)8NOOOSFD۷8fv͍F] fryf(J U[__qrxxfs,"@ HFUb z뭷Ə_(7`dWd8^5htYϿN'kbHJT*I͛gggS9==z>@L7z}z~&fJE@ 4s&oll󏏏Ǐu@ @}=3:h4bssnjjѣs=zΒO?tƍ3Lt@2Ȇ  w^D<߄`Xxs{o믿~~zt '''GtR.( _A2? ÿg=vtlttVՊ{`vE^Z ?<""޽! UFHJ @2?󎩿 w-YٌW_}5?~v;nݺuϞ=?~w^z~8Ɠ'OٳgqGш7|3x p@Pף^N<}4"wܙzVlmmED!0^ f{?ViIa%ۋl +2 0I@R6v#LU'I(ߏGP.@ 2X _ );<L0  5 @rR!QȆ kYիQL$VGRA+ . O3ЅP)jY ,e= =ZuuO"yȱkkk;;;p?NzSLEb{{{9^/Aj?O?}gT.NY1q '@NNNƏ7773y<>SLPe?(R +2i _~o5FU\nx}}}9'ϟI>y @t@2? _ \vsZJǿg~zDDCh63]g(ɌfT1 u1 ZjJ*EՊFs_뭷?Nc_y)!!Opx׋LמIA{7u"ote]],^ PUVd7o?>;;zg?1"(wܙ:e%twBJLnB~Q?N=GGG""Ƥɱ ( ~97669|"]$#ǩ^@2@( }=3:>mnZ-jZ쳬RZ-f#bjUw͢-2S2l,_^Deuu)"0e#brc$!$cCtFef@2(gcs?X SF}.XU.lYLP*/BNe%LK ~2@] -e=VJ 3'!Wp A QfXRX0M+b]] Qe#RP@2@ȚJ"t@2(Ud.lY@UϩY2\? (dČ*V .lY@eөY"T?`^ȘU.E\N!!mHFr@1A~@ڄPn !$  jhe=VVU?F!(¿9TBO@!X35K1Y("XR,(5KY(*dCR jL%7D~AOX&"#/o~B?BK)^Bm؅b(,-MB @ jqZ|YB C SB?!_DRe 1YLJkmIS t=ODgo4}}G|fQ9. 2|4? ,GBh@N((5 CE2<4? )$F&i~@>YOBh@((G!1Z4AGM28jFrHA Ѥ @e,d䔂? #N?+4?},f!4@ WD($F&Ho5>4?7YzKBi@)(zT٣ f}Q􆚅KB$$TV)$~&;K͒5 y$O!/{?3>7Sjqr_i~iPO<RPF^D#p@ju>#u yX@S{0E{\`KbBiRP47Hg[w9 vi-QPRHpAvg.uв<y}tno6Hޚf}jxЖYIoXyl)o3^ @GQD<\+OM`ta{tKJ+"腬rc}oU(S@k4@TdQ?ï~6ț,6s?,|fT/5je7 F (-YnT a欐al, :5utOan8" akŰ5Be @5nzaX/ЪF 4=|Ac>A?q g01=a =1 ~q , s4@2G  s4@2jA~ V9& 1h2 $ 2 3@2G  s4@2GȜw׃@2G  s4@2G  s4@2GȜw=gkk+>}ssscKKKGGƨ܌V]mRꫯT*EDl:u*N<GmwBv N3 9#qƊb?>;molE4Η| ϟ?/"={1??7oh{etnWa֭[c,N:cccmm(0JީV_zG\٦ϗJE7nHn.]_~ewׯ_fjuVY~Yh.--űcR+,kZcݻwކ| ٸrJ}WٳgGvee%te"ٟ6"_UT 7o:fWFaXo}v?;rHRFcc4tVY~UըVX[[8}t,//]rm's}ޱpB2+d}}=Kve"ٟ6"_T.Q,ckk+NrS4M#&P+䌴lnn&||r*۔O:!7~,>tNgb.ԋi#E4n0vve6 E}CR4\G|L[wׯ_'f3e_/2 9ckk+Y?S]+++rZYO:!7;vX:߃ϟo5z|ʏA{lH;2  b||<9+kcc:{fqO\BB!z~]__o]^^>R OO<f(|eѣGrJE(tBnhDR?٩<{[>\ySӃ)_?kv/^$/^|^ (0j4@ׯGNuuu5ajX]]M_ޫő?r9Y>s[wZ޺u+(<ގ$˥Rl4CSΨ@Y;;;f'NhrV_6k66UrCX^^حr=,3}vel{.\4esssݻǂ2 w׃Q.Y*bzz#b ׭Vޥfggݻ]m~ۭ,CJ+rm.]JkV?~<""ڵk-M>ЉGf9;}x+4lW~Vk`e5zt'?O\R|Gn~Nj^|,7 ܍jϟ?/_W_}R,qYgpV)gЉ .DRǏǎk=X~iwzX6zG/HfE~<ثcAF @&]X,ƃ=G>9w J6Yإd޴\SoFI?gVI.7A2FlA>9w J6Y  sblll~3 |2d~52gcc#"ڿ/|r@l/?d @hd9 @hd9 @hd9 @hd0R zzZ~=A4R(""X,˗ԩS1666Q@Z{ z q…QX0VWWP(4oss777:w[nƍ ? j3331;;𹕕jɿkZ4\wvv6fffz2^hKRLMMEPr}0\ vLLL4|T*tDD͂JG{mkk+>}sss7??SSSM?/ ?4@`lnnT*JDDbrrgcZ/^X__o^X8w@NЃx[}ߏ?7M?7>/?_݋|;o;`#VիWPX,J*3 BVծ;;;׿5l\x13aiT*q؈'NWTQ*z:Ơnbb"]oii)(wڠ (M/133 J7nhzx ?t}{K}^6j @>hY]]=R?3? R_uց-,,ę3gbbbO#i&O<܏Z-^x+++6bLOOǹsbll# 0"jZ,..bdO>כ5~iX^^n8aaa!"KArܼy/ϟǧ~RJR^,ܹsɿϝ;bmb~i~:] bMoV]*kii)fffRc/-..9  s\   s4@2GȜ% $:2IENDB`PythonQwt-0.5.5/qwt/tests/LogCurveDemo.py0000666000000000000000000000232612632324736017075 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import numpy as np np.seterr(all='raise') from qwt.qt.QtGui import QApplication, QPen from qwt.qt.QtCore import Qt from qwt import QwtPlot, QwtPlotCurve, QwtLogScaleEngine def create_log_plot(): plot = QwtPlot('LogCurveDemo.py (or how to handle -inf values)') plot.enableAxis(QwtPlot.xBottom) plot.setAxisScaleEngine(QwtPlot.yLeft, QwtLogScaleEngine()) curve = QwtPlotCurve() curve.setRenderHint(QwtPlotCurve.RenderAntialiased) pen = QPen(Qt.magenta) pen.setWidth(1.5) curve.setPen(pen) curve.attach(plot) x = np.arange(0.0, 10.0, 0.1) y = 10*np.cos(x)**2-.1 print("y<=0:", y<=0) curve.setData(x, y) plot.replot() return plot if __name__ == '__main__': app = QApplication([]) plot = create_log_plot() plot.resize(800, 500) plot.show() app.exec_() PythonQwt-0.5.5/qwt/tests/EventFilterDemo.py0000666000000000000000000004075712613414512017577 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import (QApplication, QPen, QBrush, QColor, QWidget, QMainWindow, QPainter, QPixmap, QToolBar, QWhatsThis) from qwt.qt.QtCore import QSize, QEvent, Signal, QRect, QObject, Qt, QPoint from qwt.qt import PYQT5 from qwt import (QwtPlot, QwtScaleDraw, QwtSymbol, QwtPlotGrid, QwtPlotCurve, QwtPlotCanvas, QwtScaleDiv) class ColorBar(QWidget): colorSelected = Signal(QColor) def __init__(self, orientation, *args): QWidget.__init__(self, *args) self.__orientation = orientation self.__light = QColor(Qt.white) self.__dark = QColor(Qt.black) self.setCursor(Qt.PointingHandCursor) def setOrientation(self, orientation): self.__orientation = orientation self.update() def orientation(self): return self.__orientation def setRange(self, light, dark): self.__light = light self.__dark = dark self.update() def setLight(self, color): self.__light = color self.update() def setDark(self, color): self.__dark = color self.update() def light(self): return self.__light def dark(self): return self.__dark def mousePressEvent(self, event): if event.button() == Qt.LeftButton: if PYQT5: pm = self.grab() else: pm = QPixmap.grabWidget(self) color = QColor() color.setRgb(pm.toImage().pixel(event.x(), event.y())) self.colorSelected.emit(color) event.accept() def paintEvent(self, _): painter = QPainter(self) self.drawColorBar(painter, self.rect()) def drawColorBar(self, painter, rect): h1, s1, v1, _ = self.__light.getHsv() h2, s2, v2, _ = self.__dark.getHsv() painter.save() painter.setClipRect(rect) painter.setClipping(True) painter.fillRect(rect, QBrush(self.__dark)) sectionSize = 2 if (self.__orientation == Qt.Horizontal): numIntervals = rect.width()/sectionSize else: numIntervals = rect.height()/sectionSize section = QRect() for i in range(int(numIntervals)): if self.__orientation == Qt.Horizontal: section.setRect(rect.x() + i*sectionSize, rect.y(), sectionSize, rect.heigh()) else: section.setRect(rect.x(), rect.y() + i*sectionSize, rect.width(), sectionSize) ratio = float(i)/float(numIntervals) color = QColor() color.setHsv(h1 + int(ratio*(h2-h1) + 0.5), s1 + int(ratio*(s2-s1) + 0.5), v1 + int(ratio*(v2-v1) + 0.5)) painter.fillRect(section, color) painter.restore() class Plot(QwtPlot): def __init__(self, *args): QwtPlot.__init__(self, *args) self.setTitle("Interactive Plot") self.setCanvasColor(Qt.darkCyan) grid = QwtPlotGrid() grid.attach(self) grid.setMajorPen(QPen(Qt.white, 0, Qt.DotLine)) self.setAxisScale(QwtPlot.xBottom, 0.0, 100.0) self.setAxisScale(QwtPlot.yLeft, 0.0, 100.0) # Avoid jumping when label with 3 digits # appear/disappear when scrolling vertically scaleDraw = self.axisScaleDraw(QwtPlot.yLeft) scaleDraw.setMinimumExtent(scaleDraw.extent( self.axisWidget(QwtPlot.yLeft).font())) self.plotLayout().setAlignCanvasToScales(True) self.__insertCurve(Qt.Vertical, Qt.blue, 30.0) self.__insertCurve(Qt.Vertical, Qt.magenta, 70.0) self.__insertCurve(Qt.Horizontal, Qt.yellow, 30.0) self.__insertCurve(Qt.Horizontal, Qt.white, 70.0) self.replot() scaleWidget = self.axisWidget(QwtPlot.yLeft) scaleWidget.setMargin(10) self.__colorBar = ColorBar(Qt.Vertical, scaleWidget) self.__colorBar.setRange( QColor(Qt.red), QColor(Qt.darkBlue)) self.__colorBar.setFocusPolicy(Qt.TabFocus) self.__colorBar.colorSelected.connect(self.setCanvasColor) # we need the resize events, to lay out the color bar scaleWidget.installEventFilter(self) # we need the resize events, to lay out the wheel self.canvas().installEventFilter(self) scaleWidget.setWhatsThis( 'Selecting a value at the scale will insert a new curve.') self.__colorBar.setWhatsThis( 'Selecting a color will change the background of the plot.') self.axisWidget(QwtPlot.xBottom).setWhatsThis( 'Selecting a value at the scale will insert a new curve.') def setCanvasColor(self, color): self.setCanvasBackground(color) self.replot() def scrollLeftAxis(self, value): self.setAxisScale(QwtPlot.yLeft, value, value + 100) self.replot() def eventFilter(self, object, event): if event.type() == QEvent.Resize: size = event.size() if object == self.axisWidget(QwtPlot.yLeft): margin = 2 x = size.width() - object.margin() + margin w = object.margin() - 2 * margin y = object.startBorderDist() h = (size.height() - object.startBorderDist() - object.endBorderDist()) self.__colorBar.setGeometry(x, y, w, h) return QwtPlot.eventFilter(self, object, event) def insertCurve(self, axis, base): if axis == QwtPlot.yLeft or axis == QwtPlot.yRight: o = Qt.Horizontal else: o = Qt.Vertical self.__insertCurve(o, QColor(Qt.red), base) self.replot() def __insertCurve(self, orientation, color, base): curve = QwtPlotCurve() curve.attach(self) curve.setPen(QPen(color)) curve.setSymbol(QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.gray), QPen(color), QSize(8, 8))) fixed = base*np.ones(10, np.float) changing = np.arange(0, 95.0, 10.0, np.float) + 5.0 if orientation == Qt.Horizontal: curve.setData(changing, fixed) else: curve.setData(fixed, changing) class CanvasPicker(QObject): def __init__(self, plot): QObject.__init__(self, plot) self.__selectedCurve = None self.__selectedPoint = -1 self.__plot = plot canvas = plot.canvas() canvas.installEventFilter(self) # We want the focus, but no focus rect. # The selected point will be highlighted instead. canvas.setFocusPolicy(Qt.StrongFocus) canvas.setCursor(Qt.PointingHandCursor) canvas.setFocusIndicator(QwtPlotCanvas.ItemFocusIndicator) canvas.setFocus() canvas.setWhatsThis( 'All points can be moved using the left mouse button ' 'or with these keys:\n\n' '- Up: Select next curve\n' '- Down: Select previous curve\n' '- Left, "-": Select next point\n' '- Right, "+": Select previous point\n' '- 7, 8, 9, 4, 6, 1, 2, 3: Move selected point' ) self.__shiftCurveCursor(True) def event(self, event): if event.type() == QEvent.User: self.__showCursor(True) return True return QObject.event(self, event) def eventFilter(self, object, event): if event.type() == QEvent.FocusIn: self.__showCursor(True) if event.type() == QEvent.FocusOut: self.__showCursor(False) if event.type() == QEvent.Paint: QApplication.postEvent(self, QEvent(QEvent.User)) elif event.type() == QEvent.MouseButtonPress: self.__select(event.pos()) return True elif event.type() == QEvent.MouseMove: self.__move(event.pos()) return True if event.type() == QEvent.KeyPress: delta = 5 key = event.key() if key == Qt.Key_Up: self.__shiftCurveCursor(True) return True elif key == Qt.Key_Down: self.__shiftCurveCursor(False) return True elif key == Qt.Key_Right or key == Qt.Key_Plus: if self.__selectedCurve: self.__shiftPointCursor(True) else: self.__shiftCurveCursor(True) return True elif key == Qt.Key_Left or key == Qt.Key_Minus: if self.__selectedCurve: self.__shiftPointCursor(False) else: self.__shiftCurveCursor(True) return True if key == Qt.Key_1: self.__moveBy(-delta, delta) elif key == Qt.Key_2: self.__moveBy(0, delta) elif key == Qt.Key_3: self.__moveBy(delta, delta) elif key == Qt.Key_4: self.__moveBy(-delta, 0) elif key == Qt.Key_6: self.__moveBy(delta, 0) elif key == Qt.Key_7: self.__moveBy(-delta, -delta) elif key == Qt.Key_8: self.__moveBy(0, -delta) elif key == Qt.Key_9: self.__moveBy(delta, -delta) return QwtPlot.eventFilter(self.__plot, object, event) def __select(self, pos): found, distance, point = None, 1e100, -1 for curve in self.__plot.itemList(): if isinstance(curve, QwtPlotCurve): i, d = curve.closestPoint(pos) if d < distance: found = curve point = i distance = d self.__showCursor(False) self.__selectedCurve = None self.__selectedPoint = -1 if found and distance < 10: self.__selectedCurve = found self.__selectedPoint = point self.__showCursor(True) def __moveBy(self, dx, dy): if dx == 0 and dy == 0: return curve = self.__selectedCurve if not curve: return s = curve.sample(self.__selectedPoint) x = self.__plot.transform(curve.xAxis(), s.x()) + dx y = self.__plot.transform(curve.yAxis(), s.y()) + dy self.__move(QPoint(x, y)) def __move(self, pos): curve = self.__selectedCurve if not curve: return xData = np.zeros(curve.dataSize(), np.float) yData = np.zeros(curve.dataSize(), np.float) for i in range(curve.dataSize()): if i == self.__selectedPoint: xData[i] = self.__plot.invTransform(curve.xAxis(), pos.x()) yData[i] = self.__plot.invTransform(curve.yAxis(), pos.y()) else: s = curve.sample(i) xData[i] = s.x() yData[i] = s.y() curve.setData(xData, yData) self.__showCursor(True) self.__plot.replot() def __showCursor(self, showIt): curve = self.__selectedCurve if not curve: return symbol = curve.symbol() brush = symbol.brush() if showIt: symbol.setBrush(symbol.brush().color().darker(180)) curve.directPaint(self.__selectedPoint, self.__selectedPoint) if showIt: symbol.setBrush(brush) def __shiftCurveCursor(self, up): curves = [curve for curve in self.__plot.itemList() if isinstance(curve, QwtPlotCurve)] if not curves: return if self.__selectedCurve in curves: index = curves.index(self.__selectedCurve) if up: index += 1 else: index -= 1 # keep index within [0, len(curves)) index += len(curves) index %= len(curves) else: index = 0 self.__showCursor(False) self.__selectedPoint = 0 self.__selectedCurve = curves[index] self.__showCursor(True) def __shiftPointCursor(self, up): curve = self.__selectedCurve if not curve: return if up: index = self.__selectedPoint + 1 else: index = self.__selectedPoint - 1 # keep index within [0, curve.dataSize()) index += curve.dataSize() index %= curve.dataSize() if index != self.__selectedPoint: self.__showCursor(False) self.__selectedPoint = index self.__showCursor(True) class ScalePicker(QObject): clicked = Signal(int, float) def __init__(self, plot): QObject.__init__(self, plot) for i in range(QwtPlot.axisCnt): scaleWidget = plot.axisWidget(i) if scaleWidget: scaleWidget.installEventFilter(self) def eventFilter(self, object, event): if (event.type() == QEvent.MouseButtonPress): self.__mouseClicked(object, event.pos()) return True return QObject.eventFilter(self, object, event) def __mouseClicked(self, scale, pos): rect = self.__scaleRect(scale) margin = 10 rect.setRect(rect.x() - margin, rect.y() - margin, rect.width() + 2 * margin, rect.height() + 2 * margin) if rect.contains(pos): value = 0.0 axis = -1 sd = scale.scaleDraw() if scale.alignment() == QwtScaleDraw.LeftScale: value = sd.scaleMap().invTransform(pos.y()) axis = QwtPlot.yLeft elif scale.alignment() == QwtScaleDraw.RightScale: value = sd.scaleMap().invTransform(pos.y()) axis = QwtPlot.yRight elif scale.alignment() == QwtScaleDraw.BottomScale: value = sd.scaleMap().invTransform(pos.x()) axis = QwtPlot.xBottom elif scale.alignment() == QwtScaleDraw.TopScale: value = sd.scaleMap().invTransform(pos.x()) axis = QwtPlot.xBottom self.clicked.emit(axis, value) def __scaleRect(self, scale): bld = scale.margin() mjt = scale.scaleDraw().tickLength(QwtScaleDiv.MajorTick) sbd = scale.startBorderDist() ebd = scale.endBorderDist() if scale.alignment() == QwtScaleDraw.LeftScale: return QRect(scale.width() - bld - mjt, sbd, mjt, scale.height() - sbd - ebd) elif scale.alignment() == QwtScaleDraw.RightScale: return QRect(bld, sbd,mjt, scale.height() - sbd - ebd) elif scale.alignment() == QwtScaleDraw.BottomScale: return QRect(sbd, bld, scale.width() - sbd - ebd, mjt) elif scale.alignment() == QwtScaleDraw.TopScale: return QRect(sbd, scale.height() - bld - mjt, scale.width() - sbd - ebd, mjt) else: return QRect() def make(): demo = QMainWindow() toolBar = QToolBar(demo) toolBar.addAction(QWhatsThis.createAction(toolBar)) demo.addToolBar(toolBar) plot = Plot(demo) demo.setCentralWidget(plot) plot.setWhatsThis( 'An useless plot to demonstrate how to use event filtering.\n\n' 'You can click on the color bar, the scales or move the slider.\n' 'All points can be moved using the mouse or the keyboard.' ) CanvasPicker(plot) scalePicker = ScalePicker(plot) scalePicker.clicked.connect(plot.insertCurve) demo.resize(540, 400) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/CurveDemo2.py0000666000000000000000000001106512617623052016511 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import (QApplication, QPen, QBrush, QFrame, QColor, QPainter, QPalette) from qwt.qt.QtCore import QSize from qwt.qt.QtCore import Qt from qwt import QwtScaleMap, QwtSymbol, QwtPlotCurve Size = 15 USize = 13 class CurveDemo(QFrame): def __init__(self, *args): QFrame.__init__(self, *args) self.setFrameStyle(QFrame.Box | QFrame.Raised) self.setLineWidth(2) self.setMidLineWidth(3) p = QPalette() p.setColor(self.backgroundRole(), QColor(30, 30, 50)) self.setPalette(p) # make curves and maps self.tuples = [] # curve 1 curve = QwtPlotCurve() curve.setPen(QPen(QColor(150, 150, 200), 2)) curve.setStyle(QwtPlotCurve.Lines) curve.setSymbol(QwtSymbol(QwtSymbol.XCross, QBrush(), QPen(Qt.yellow, 2), QSize(7, 7))) self.tuples.append((curve, QwtScaleMap(0, 100, -1.5, 1.5), QwtScaleMap(0, 100, 0.0, 2*np.pi))) # curve 2 curve = QwtPlotCurve() curve.setPen(QPen(QColor(200, 150, 50), 1, Qt.DashDotDotLine)) curve.setStyle(QwtPlotCurve.Sticks) curve.setSymbol(QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.blue), QPen(Qt.yellow), QSize(5, 5))) self.tuples.append((curve, QwtScaleMap(0, 100, 0.0, 2*np.pi), QwtScaleMap(0, 100, -3.0, 1.1))) # curve 3 curve = QwtPlotCurve() curve.setPen(QPen(QColor(100, 200, 150))) curve.setStyle(QwtPlotCurve.Lines) self.tuples.append((curve, QwtScaleMap(0, 100, -1.1, 3.0), QwtScaleMap(0, 100, -1.1, 3.0))) # curve 4 curve = QwtPlotCurve() curve.setPen(QPen(Qt.red)) curve.setStyle(QwtPlotCurve.Lines) self.tuples.append((curve, QwtScaleMap(0, 100, -5.0, 1.1), QwtScaleMap(0, 100, -1.1, 5.0))) # data self.phase = 0.0 self.base = np.arange(0.0, 2.01*np.pi, 2*np.pi/(USize-1)) self.uval = np.cos(self.base) self.vval = np.sin(self.base) self.uval[1::2] *= 0.5 self.vval[1::2] *= 0.5 self.newValues() # start timer self.tid = self.startTimer(250) def paintEvent(self, event): QFrame.paintEvent(self,event) painter = QPainter(self) painter.setClipRect(self.contentsRect()) self.drawContents(painter) def drawContents(self, painter): r = self.contentsRect() for curve, xMap, yMap in self.tuples: xMap.setPaintInterval(r.left(), r.right()) yMap.setPaintInterval(r.top(), r.bottom()) curve.draw(painter, xMap, yMap, r) def timerEvent(self, event): self.newValues() self.repaint() def newValues(self): phase = self.phase self.xval = np.arange(0, 2.01*np.pi, 2*np.pi/(Size-1)) self.yval = np.sin(self.xval - phase) self.zval = np.cos(3*(self.xval + phase)) s = 0.25 * np.sin(phase) c = np.sqrt(1.0 - s*s) u = self.uval self.uval = c*self.uval-s*self.vval self.vval = c*self.vval+s*u self.tuples[0][0].setData(self.yval, self.xval) self.tuples[1][0].setData(self.xval, self.zval) self.tuples[2][0].setData(self.yval, self.zval) self.tuples[3][0].setData(self.uval, self.vval) self.phase += 2*np.pi/100 if self.phase>2*np.pi: self.phase = 0.0 def make(): demo = CurveDemo() demo.resize(600, 600) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_())PythonQwt-0.5.5/qwt/tests/CartesianDemo.py0000666000000000000000000000754312605720032017253 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import QApplication, QPen from qwt.qt.QtCore import Qt from qwt import QwtPlot, QwtScaleDraw, QwtPlotGrid, QwtPlotCurve, QwtPlotItem class CartesianAxis(QwtPlotItem): """Supports a coordinate system similar to http://en.wikipedia.org/wiki/Image:Cartesian-coordinate-system.svg""" def __init__(self, masterAxis, slaveAxis): """Valid input values for masterAxis and slaveAxis are QwtPlot.yLeft, QwtPlot.yRight, QwtPlot.xBottom, and QwtPlot.xTop. When masterAxis is an x-axis, slaveAxis must be an y-axis; and vice versa.""" QwtPlotItem.__init__(self) self.__axis = masterAxis if masterAxis in (QwtPlot.yLeft, QwtPlot.yRight): self.setAxes(slaveAxis, masterAxis) else: self.setAxes(masterAxis, slaveAxis) self.scaleDraw = QwtScaleDraw() self.scaleDraw.setAlignment((QwtScaleDraw.LeftScale, QwtScaleDraw.RightScale, QwtScaleDraw.BottomScale, QwtScaleDraw.TopScale)[masterAxis]) def draw(self, painter, xMap, yMap, rect): """Draw an axis on the plot canvas""" xtr = xMap.transform ytr = yMap.transform if self.__axis in (QwtPlot.yLeft, QwtPlot.yRight): self.scaleDraw.move(round(xtr(0.0)), yMap.p2()) self.scaleDraw.setLength(yMap.p1()-yMap.p2()) elif self.__axis in (QwtPlot.xBottom, QwtPlot.xTop): self.scaleDraw.move(xMap.p1(), round(ytr(0.0))) self.scaleDraw.setLength(xMap.p2()-xMap.p1()) self.scaleDraw.setScaleDiv(self.plot().axisScaleDiv(self.__axis)) self.scaleDraw.draw(painter, self.plot().palette()) class CartesianPlot(QwtPlot): """Creates a coordinate system similar system http://en.wikipedia.org/wiki/Image:Cartesian-coordinate-system.svg""" def __init__(self, *args): QwtPlot.__init__(self, *args) self.setTitle('Cartesian Coordinate System Demo') # create a plot with a white canvas self.setCanvasBackground(Qt.white) # set plot layout self.plotLayout().setCanvasMargin(0) self.plotLayout().setAlignCanvasToScales(True) # attach a grid grid = QwtPlotGrid() grid.attach(self) grid.setPen(QPen(Qt.black, 0, Qt.DotLine)) # attach a x-axis xaxis = CartesianAxis(QwtPlot.xBottom, QwtPlot.yLeft) xaxis.attach(self) self.enableAxis(QwtPlot.xBottom, False) # attach a y-axis yaxis = CartesianAxis(QwtPlot.yLeft, QwtPlot.xBottom) yaxis.attach(self) self.enableAxis(QwtPlot.yLeft, False) # calculate 3 NumPy arrays x = np.arange(-2*np.pi, 2*np.pi, 0.01) y = np.pi*np.sin(x) z = 4*np.pi*np.cos(x)*np.cos(x)*np.sin(x) # attach a curve curve = QwtPlotCurve('y = pi*sin(x)') curve.attach(self) curve.setPen(QPen(Qt.green, 2)) curve.setData(x, y) # attach another curve curve = QwtPlotCurve('y = 4*pi*sin(x)*cos(x)**2') curve.attach(self) curve.setPen(QPen(Qt.black, 2)) curve.setData(x, z) self.replot() def make(): demo = CartesianPlot() demo.resize(400, 300) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/tests/__init__.py0000666000000000000000000000102212605720032016256 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the MIT License # Copyright (c) 2015 Pierre Raybaut # (see LICENSE file for more details) """ PythonQwt test package ====================== """ def run(): """Run PythonQwt test launcher (requires `guidata`)""" import qwt try: from guidata.guitest import run_testlauncher except ImportError: raise ImportError("This feature requires `guidata` 1.7+.") run_testlauncher(qwt) if __name__ == '__main__': run() PythonQwt-0.5.5/qwt/tests/ErrorBarDemo.py0000666000000000000000000002447612642011620017061 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the PyQwt License # Copyright (C) 2003-2009 Gerard Vermeulen, for the original PyQwt example # Copyright (c) 2015 Pierre Raybaut, for the PyQt5/PySide port and further # developments (e.g. ported to PythonQwt API) # (see LICENSE file for more details) SHOW = True # Show test in GUI-based test launcher import sys import numpy as np from qwt.qt.QtGui import QApplication, QPen, QBrush from qwt.qt.QtCore import QSize, QRectF, QLineF from qwt.qt.QtCore import Qt from qwt import QwtPlot, QwtSymbol, QwtPlotGrid, QwtPlotCurve, QwtText class ErrorBarPlotCurve(QwtPlotCurve): def __init__(self, x=[], y=[], dx=None, dy=None, curvePen=None, curveStyle=None, curveSymbol=None, errorPen=None, errorCap=0, errorOnTop=False): """A curve of x versus y data with error bars in dx and dy. Horizontal error bars are plotted if dx is not None. Vertical error bars are plotted if dy is not None. x and y must be sequences with a shape (N,) and dx and dy must be sequences (if not None) with a shape (), (N,), or (2, N): - if dx or dy has a shape () or (N,), the error bars are given by (x-dx, x+dx) or (y-dy, y+dy), - if dx or dy has a shape (2, N), the error bars are given by (x-dx[0], x+dx[1]) or (y-dy[0], y+dy[1]). curvePen is the pen used to plot the curve curveStyle is the style used to plot the curve curveSymbol is the symbol used to plot the symbols errorPen is the pen used to plot the error bars errorCap is the size of the error bar caps errorOnTop is a boolean: - if True, plot the error bars on top of the curve, - if False, plot the curve on top of the error bars. """ QwtPlotCurve.__init__(self) if curvePen is None: curvePen = QPen(Qt.NoPen) if curveStyle is None: curveStyle = QwtPlotCurve.Lines if curveSymbol is None: curveSymbol = QwtSymbol() if errorPen is None: errorPen = QPen(Qt.NoPen) self.setData(x, y, dx, dy) self.setPen(curvePen) self.setStyle(curveStyle) self.setSymbol(curveSymbol) self.errorPen = errorPen self.errorCap = errorCap self.errorOnTop = errorOnTop def setData(self, *args): """Set x versus y data with error bars in dx and dy. Horizontal error bars are plotted if dx is not None. Vertical error bars are plotted if dy is not None. x and y must be sequences with a shape (N,) and dx and dy must be sequences (if not None) with a shape (), (N,), or (2, N): - if dx or dy has a shape () or (N,), the error bars are given by (x-dx, x+dx) or (y-dy, y+dy), - if dx or dy has a shape (2, N), the error bars are given by (x-dx[0], x+dx[1]) or (y-dy[0], y+dy[1]). """ if len(args) == 1: QwtPlotCurve.setData(self, *args) return dx = None dy = None x, y = args[:2] if len(args) > 2: dx = args[2] if len(args) > 3: dy = args[3] self.__x = np.asarray(x, np.float) if len(self.__x.shape) != 1: raise RuntimeError('len(asarray(x).shape) != 1') self.__y = np.asarray(y, np.float) if len(self.__y.shape) != 1: raise RuntimeError('len(asarray(y).shape) != 1') if len(self.__x) != len(self.__y): raise RuntimeError('len(asarray(x)) != len(asarray(y))') if dx is None: self.__dx = None else: self.__dx = np.asarray(dx, np.float) if len(self.__dx.shape) not in [0, 1, 2]: raise RuntimeError('len(asarray(dx).shape) not in [0, 1, 2]') if dy is None: self.__dy = dy else: self.__dy = np.asarray(dy, np.float) if len(self.__dy.shape) not in [0, 1, 2]: raise RuntimeError('len(asarray(dy).shape) not in [0, 1, 2]') QwtPlotCurve.setData(self, self.__x, self.__y) def boundingRect(self): """Return the bounding rectangle of the data, error bars included. """ if self.__dx is None: xmin = min(self.__x) xmax = max(self.__x) elif len(self.__dx.shape) in [0, 1]: xmin = min(self.__x - self.__dx) xmax = max(self.__x + self.__dx) else: xmin = min(self.__x - self.__dx[0]) xmax = max(self.__x + self.__dx[1]) if self.__dy is None: ymin = min(self.__y) ymax = max(self.__y) elif len(self.__dy.shape) in [0, 1]: ymin = min(self.__y - self.__dy) ymax = max(self.__y + self.__dy) else: ymin = min(self.__y - self.__dy[0]) ymax = max(self.__y + self.__dy[1]) return QRectF(xmin, ymin, xmax-xmin, ymax-ymin) def drawSeries(self, painter, xMap, yMap, canvasRect, first, last = -1): """Draw an interval of the curve, including the error bars painter is the QPainter used to draw the curve xMap is the QwtDiMap used to map x-values to pixels yMap is the QwtDiMap used to map y-values to pixels first is the index of the first data point to draw last is the index of the last data point to draw. If last < 0, last is transformed to index the last data point """ if last < 0: last = self.dataSize() - 1 if self.errorOnTop: QwtPlotCurve.drawSeries(self, painter, xMap, yMap, canvasRect, first, last) # draw the error bars painter.save() painter.setPen(self.errorPen) # draw the error bars with caps in the x direction if self.__dx is not None: # draw the bars if len(self.__dx.shape) in [0, 1]: xmin = (self.__x - self.__dx) xmax = (self.__x + self.__dx) else: xmin = (self.__x - self.__dx[0]) xmax = (self.__x + self.__dx[1]) y = self.__y n, i = len(y), 0 lines = [] while i < n: yi = yMap.transform(y[i]) lines.append(QLineF(xMap.transform(xmin[i]), yi, xMap.transform(xmax[i]), yi)) i += 1 painter.drawLines(lines) if self.errorCap > 0: # draw the caps cap = self.errorCap/2 n, i, = len(y), 0 lines = [] while i < n: yi = yMap.transform(y[i]) lines.append( QLineF(xMap.transform(xmin[i]), yi - cap, xMap.transform(xmin[i]), yi + cap)) lines.append( QLineF(xMap.transform(xmax[i]), yi - cap, xMap.transform(xmax[i]), yi + cap)) i += 1 painter.drawLines(lines) # draw the error bars with caps in the y direction if self.__dy is not None: # draw the bars if len(self.__dy.shape) in [0, 1]: ymin = (self.__y - self.__dy) ymax = (self.__y + self.__dy) else: ymin = (self.__y - self.__dy[0]) ymax = (self.__y + self.__dy[1]) x = self.__x n, i, = len(x), 0 lines = [] while i < n: xi = xMap.transform(x[i]) lines.append( QLineF(xi, yMap.transform(ymin[i]), xi, yMap.transform(ymax[i]))) i += 1 painter.drawLines(lines) # draw the caps if self.errorCap > 0: cap = self.errorCap/2 n, i, j = len(x), 0, 0 lines = [] while i < n: xi = xMap.transform(x[i]) lines.append( QLineF(xi - cap, yMap.transform(ymin[i]), xi + cap, yMap.transform(ymin[i]))) lines.append( QLineF(xi - cap, yMap.transform(ymax[i]), xi + cap, yMap.transform(ymax[i]))) i += 1 painter.drawLines(lines) painter.restore() if not self.errorOnTop: QwtPlotCurve.drawSeries(self, painter, xMap, yMap, canvasRect, first, last) def make(): # create a plot with a white canvas demo = QwtPlot(QwtText("Errorbar Demonstation")) demo.setCanvasBackground(Qt.white) demo.plotLayout().setAlignCanvasToScales(True) grid = QwtPlotGrid() grid.attach(demo) grid.setPen(QPen(Qt.black, 0, Qt.DotLine)) # calculate data and errors for a curve with error bars x = np.arange(0, 10.1, 0.5, np.float) y = np.sin(x) dy = 0.2 * abs(y) # dy = (0.15 * abs(y), 0.25 * abs(y)) # uncomment for asymmetric error bars dx = 0.2 # all error bars the same size errorOnTop = False # uncomment to draw the curve on top of the error bars # errorOnTop = True # uncomment to draw the error bars on top of the curve curve = ErrorBarPlotCurve( x = x, y = y, dx = dx, dy = dy, curvePen = QPen(Qt.black, 2), curveSymbol = QwtSymbol(QwtSymbol.Ellipse, QBrush(Qt.red), QPen(Qt.black, 2), QSize(9, 9)), errorPen = QPen(Qt.blue, 2), errorCap = 10, errorOnTop = errorOnTop, ) curve.attach(demo) demo.resize(640, 480) demo.show() return demo if __name__ == '__main__': app = QApplication(sys.argv) demo = make() sys.exit(app.exec_()) PythonQwt-0.5.5/qwt/plot_series.py0000666000000000000000000002353612632324736015736 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ Plotting series item -------------------- QwtPlotSeriesItem ~~~~~~~~~~~~~~~~~ .. autoclass:: QwtPlotSeriesItem :members: QwtSeriesData ~~~~~~~~~~~~~ .. autoclass:: QwtSeriesData :members: QwtPointArrayData ~~~~~~~~~~~~~~~~~ .. autoclass:: QwtPointArrayData :members: QwtSeriesStore ~~~~~~~~~~~~~~ .. autoclass:: QwtSeriesStore :members: """ import numpy as np from qwt.plot import QwtPlotItem, QwtPlotItem_PrivateData from qwt.text import QwtText from qwt.qt.QtCore import Qt, QRectF, QPointF class QwtPlotSeriesItem_PrivateData(QwtPlotItem_PrivateData): def __init__(self): QwtPlotItem_PrivateData.__init__(self) self.orientation = Qt.Horizontal class QwtPlotSeriesItem(QwtPlotItem): """ Base class for plot items representing a series of samples """ def __init__(self, title): if not isinstance(title, QwtText): title = QwtText(title) QwtPlotItem.__init__(self, title) self.__data = QwtPlotSeriesItem_PrivateData() def setOrientation(self, orientation): """ Set the orientation of the item. Default is `Qt.Horizontal`. The `orientation()` might be used in specific way by a plot item. F.e. a QwtPlotCurve uses it to identify how to display the curve int `QwtPlotCurve.Steps` or `QwtPlotCurve.Sticks` style. .. seealso:: :py:meth`orientation()` """ if self.__data.orientation != orientation: self.__data.orientation = orientation self.legendChanged() self.itemChanged() def orientation(self): """ :return: Orientation of the plot item .. seealso:: :py:meth`setOrientation()` """ return self.__data.orientation def draw(self, painter, xMap, yMap, canvasRect): """ Draw the complete series :param QPainter painter: Painter :param qwt.scale_map.QwtScaleMap xMap: Maps x-values into pixel coordinates. :param qwt.scale_map.QwtScaleMap yMap: Maps y-values into pixel coordinates. :param QRectF canvasRect: Contents rectangle of the canvas """ self.drawSeries(painter, xMap, yMap, canvasRect, 0, -1) def boundingRect(self): return self.dataRect() def updateScaleDiv(self, xScaleDiv, yScaleDiv): rect = QRectF(xScaleDiv.lowerBound(), yScaleDiv.lowerBound(), xScaleDiv.range(), yScaleDiv.range()) self.setRectOfInterest(rect) def dataChanged(self): self.itemChanged() class QwtSeriesData(object): """ Abstract interface for iterating over samples `PythonQwt` offers several implementations of the QwtSeriesData API, but in situations, where data of an application specific format needs to be displayed, without having to copy it, it is recommended to implement an individual data access. A subclass of `QwtSeriesData` must implement: - size(): Should return number of data points. - sample() Should return values x and y values of the sample at specific position as QPointF object. - boundingRect() Should return the bounding rectangle of the data series. It is used for autoscaling and might help certain algorithms for displaying the data. The member `_boundingRect` is intended for caching the calculated rectangle. """ def __init__(self): self._boundingRect = QRectF(0.0, 0.0, -1.0, -1.0) def setRectOfInterest(self, rect): """ Set a the "rect of interest" QwtPlotSeriesItem defines the current area of the plot canvas as "rectangle of interest" ( QwtPlotSeriesItem::updateScaleDiv() ). It can be used to implement different levels of details. The default implementation does nothing. :param QRectF rect: Rectangle of interest """ pass def size(self): """ :return: Number of samples """ pass def sample(self, i): """ Return a sample :param int i: Index :return: Sample at position i """ pass def boundingRect(self): """ Calculate the bounding rect of all samples The bounding rect is necessary for autoscaling and can be used for a couple of painting optimizations. :return: Bounding rectangle """ pass class QwtPointArrayData(QwtSeriesData): """ Interface for iterating over two array objects .. py:class:: QwtPointArrayData(x, y, [size=None], [finite=True]) :param x: Array of x values :type x: list or tuple or numpy.array :param y: Array of y values :type y: list or tuple or numpy.array :param int size: Size of the x and y arrays :param bool finite: if True, keep only finite array elements (remove all infinity and not a number values), otherwise do not filter array elements """ def __init__(self, x=None, y=None, size=None, finite=None): QwtSeriesData.__init__(self) if x is None and y is not None: x = np.arange(len(y)) elif y is None and x is not None: y = x x = np.arange(len(y)) elif x is None and y is None: x = np.array([]) y = np.array([]) if isinstance(x, (tuple, list)): x = np.array(x) if isinstance(y, (tuple, list)): y = np.array(y) if size is not None: x = np.resize(x, (size, )) y = np.resize(y, (size, )) if finite if finite is not None else True: indexes = np.logical_and(np.isfinite(x), np.isfinite(y)) self.__x = x[indexes] self.__y = y[indexes] else: self.__x = x self.__y = y def boundingRect(self): """ Calculate the bounding rectangle The bounding rectangle is calculated once by iterating over all points and is stored for all following requests. :return: Bounding rectangle """ xmin = self.__x.min() xmax = self.__x.max() ymin = self.__y.min() ymax = self.__y.max() return QRectF(xmin, ymin, xmax-xmin, ymax-ymin) def size(self): """ :return: Size of the data set """ return min([self.__x.size, self.__y.size]) def sample(self, index): """ :param int index: Index :return: Sample at position `index` """ return QPointF(self.__x[index], self.__y[index]) def xData(self): """ :return: Array of the x-values """ return self.__x def yData(self): """ :return: Array of the y-values """ return self.__y class QwtSeriesStore(object): """ Class storing a `QwtSeriesData` object `QwtSeriesStore` and `QwtPlotSeriesItem` are intended as base classes for all plot items iterating over a series of samples. """ def __init__(self): self.__series = None def setData(self, series): """ Assign a series of samples :param qwt.plot_series.QwtSeriesData series: Data .. warning:: The item takes ownership of the data object, deleting it when its not used anymore. """ if self.__series != series: self.__series = series self.dataChanged() def data(self): """ :return: the series data """ return self.__series def sample(self, index): """ :param int index: Index :return: Sample at position index """ if self.__series: return self.__series.sample(index) else: return def dataSize(self): """ :return: Number of samples of the series .. seealso:: :py:meth:`setData()`, :py:meth:`qwt.plot_series.QwtSeriesData.size()` """ if self.__series is None: return 0 return self.__series.size() def dataRect(self): """ :return: Bounding rectangle of the series or an invalid rectangle, when no series is stored .. seealso:: :py:meth:`qwt.plot_series.QwtSeriesData.boundingRect()` """ if self.__series is None or self.dataSize() == 0: return QRectF(1.0, 1.0, -2.0, -2.0) return self.__series.boundingRect() def setRectOfInterest(self, rect): """ Set a the "rect of interest" for the series :param QRectF rect: Rectangle of interest .. seealso:: :py:meth:`qwt.plot_series.QwtSeriesData.setRectOfInterest()` """ if self.__series: self.__series.setRectOfInterest(rect) def swapData(self, series): """ Replace a series without deleting the previous one :param qwt.plot_series.QwtSeriesData series: New series :return: Previously assigned series """ swappedSeries = self.__series self.__series = series return swappedSeries PythonQwt-0.5.5/qwt/scale_div.py0000666000000000000000000002176112605040216015321 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtScaleDiv ----------- .. autoclass:: QwtScaleDiv :members: """ from qwt.interval import QwtInterval import copy class QwtScaleDiv(object): """ A class representing a scale division A Qwt scale is defined by its boundaries and 3 list for the positions of the major, medium and minor ticks. The `upperLimit()` might be smaller than the `lowerLimit()` to indicate inverted scales. Scale divisions can be calculated from a `QwtScaleEngine`. .. seealso:: :py:meth:`qwt.scale_engine.QwtScaleEngine.divideScale()`, :py:meth:`qwt.plot.QwtPlot.setAxisScaleDiv()` Scale tick types: * `QwtScaleDiv.NoTick`: No ticks * `QwtScaleDiv.MinorTick`: Minor ticks * `QwtScaleDiv.MediumTick`: Medium ticks * `QwtScaleDiv.MajorTick`: Major ticks * `QwtScaleDiv.NTickTypes`: Number of valid tick types .. py:class:: QwtScaleDiv() Basic constructor. Lower bound = Upper bound = 0. .. py:class:: QwtScaleDiv(interval, ticks) :param qwt.interval.QwtInterval interval: Interval :param list ticks: list of major, medium and minor ticks .. py:class:: QwtScaleDiv(lowerBound, upperBound) :param float lowerBound: First boundary :param float upperBound: Second boundary .. py:class:: QwtScaleDiv(lowerBound, upperBound, ticks) :param float lowerBound: First boundary :param float upperBound: Second boundary :param list ticks: list of major, medium and minor ticks .. py:class:: QwtScaleDiv(lowerBound, upperBound, minorTicks, mediumTicks, majorTicks) :param float lowerBound: First boundary :param float upperBound: Second boundary :param list minorTicks: list of minor ticks :param list mediumTicks: list of medium ticks :param list majorTicks: list of major ticks .. note:: lowerBound might be greater than upperBound for inverted scales """ # enum TickType NoTick = -1 MinorTick, MediumTick, MajorTick, NTickTypes = list(range(4)) def __init__(self, *args): self.__ticks = None if len(args) == 2 and isinstance(args[1], (tuple, list)): interval, ticks = args self.__lowerBound = interval.minValue() self.__upperBound = interval.maxValue() self.__ticks = ticks[:] elif len(args) == 2: self.__lowerBound, self.__upperBound = args elif len(args) == 3: self.__lowerBound, self.__upperBound, ticks = args self.__ticks = ticks[:] elif len(args) == 5: (self.__lowerBound, self.__upperBound, minorTicks, mediumTicks, majorTicks) = args self.__ticks[self.MinorTick] = minorTicks self.__ticks[self.MediumTick] = mediumTicks self.__ticks[self.MajorTick] = majorTicks elif len(args) == 0: self.__lowerBound, self.__upperBound = 0., 0. else: raise TypeError("%s() takes 0, 2, 3 or 5 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def setInterval(self, *args): """ Change the interval .. py:method:: setInterval(lowerBound, upperBound) :param float lowerBound: First boundary :param float upperBound: Second boundary .. py:method:: setInterval(interval) :param qwt.interval.QwtInterval interval: Interval .. note:: lowerBound might be greater than upperBound for inverted scales """ if len(args) == 2: self.__lowerBound, self.__upperBound = args elif len(args) == 1: interval, = args self.__lowerBound = interval.minValue() self.__upperBound = interval.maxValue() else: raise TypeError("%s().setInterval() takes 1 or 2 argument(s) (%s "\ "given)" % (self.__class__.__name__, len(args))) def interval(self): """ :return: Interval """ return QwtInterval(self.__lowerBound, self.__upperBound) def setLowerBound(self, lowerBound): """ Set the first boundary :param float lowerBound: First boundary .. seealso:: :py:meth:`lowerBound()`, :py:meth:`setUpperBound()` """ self.__lowerBound = lowerBound def lowerBound(self): """ :return: the first boundary .. seealso:: :py:meth:`upperBound()` """ return self.__lowerBound def setUpperBound(self, upperBound): """ Set the second boundary :param float lowerBound: Second boundary .. seealso:: :py:meth:`upperBound()`, :py:meth:`setLowerBound()` """ self.__upperBound = upperBound def upperBound(self): """ :return: the second boundary .. seealso:: :py:meth:`lowerBound()` """ return self.__upperBound def range(self): """ :return: upperBound() - lowerBound() """ return self.__upperBound - self.__lowerBound def __eq__(self, other): if self.__ticks is None: return False if self.__lowerBound != other.__lowerBound or\ self.__upperBound != other.__upperBound: return False return self.__ticks == other.__ticks def __ne__(self, other): return not self.__eq__(other) def isEmpty(self): """ Check if the scale division is empty( lowerBound() == upperBound() ) """ return self.__lowerBound == self.__upperBound def isIncreasing(self): """ Check if the scale division is increasing( lowerBound() <= upperBound() ) """ return self.__lowerBound <= self.__upperBound def contains(self, value): """ Return if a value is between lowerBound() and upperBound() :param float value: Value :return: True/False """ min_ = min([self.__lowerBound, self.__upperBound]) max_ = max([self.__lowerBound, self.__upperBound]) return value >= min_ and value <= max_ def invert(self): """ Invert the scale division .. seealso:: :py:meth:`inverted()` """ (self.__lowerBound, self.__upperBound) = self.__upperBound, self.__lowerBound for index in range(self.NTickTypes): self.__ticks[index].reverse() def inverted(self): """ :return: A scale division with inverted boundaries and ticks .. seealso:: :py:meth:`invert()` """ other = copy.deepcopy(self) other.invert() return other def bounded(self, lowerBound, upperBound): """ Return a scale division with an interval [lowerBound, upperBound] where all ticks outside this interval are removed :param float lowerBound: First boundary :param float lowerBound: Second boundary :return: Scale division with all ticks inside of the given interval .. note:: lowerBound might be greater than upperBound for inverted scales """ min_ = min([self.__lowerBound, self.__upperBound]) max_ = max([self.__lowerBound, self.__upperBound]) sd = QwtScaleDiv() sd.setInterval(lowerBound, upperBound) for tickType in range(self.NTickTypes): sd.setTicks(tickType, [tick for tick in self.__ticks[tickType] if tick >= min_ and tick <= max_]) return sd def setTicks(self, tickType, ticks): """ Assign ticks :param int type: MinorTick, MediumTick or MajorTick :param list ticks: Values of the tick positions """ if tickType in range(self.NTickTypes): self.__ticks[tickType] = ticks def ticks(self, tickType): """ Return a list of ticks :param int type: MinorTick, MediumTick or MajorTick :return: Tick list """ if self.__ticks is not None and tickType in range(self.NTickTypes): return self.__ticks[tickType] else: return [] PythonQwt-0.5.5/qwt/transform.py0000666000000000000000000001410012632324736015404 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ Coordinate tranformations ------------------------- QwtTransform ~~~~~~~~~~~~ .. autoclass:: QwtTransform :members: QwtNullTransform ~~~~~~~~~~~~~~~~ .. autoclass:: QwtNullTransform :members: QwtLogTransform ~~~~~~~~~~~~~~~ .. autoclass:: QwtLogTransform :members: QwtPowerTransform ~~~~~~~~~~~~~~~~~ .. autoclass:: QwtPowerTransform :members: """ import numpy as np class QwtTransform(object): """ A transformation between coordinate systems QwtTransform manipulates values, when being mapped between the scale and the paint device coordinate system. A transformation consists of 2 methods: - transform - invTransform where one is is the inverse function of the other. When p1, p2 are the boundaries of the paint device coordinates and s1, s2 the boundaries of the scale, QwtScaleMap uses the following calculations:: p = p1 + (p2 - p1) * ( T(s) - T(s1) / (T(s2) - T(s1)) ) s = invT( T(s1) + ( T(s2) - T(s1) ) * (p - p1) / (p2 - p1) ) """ def __init__(self): pass def bounded(self, value): """ Modify value to be a valid value for the transformation. The default implementation does nothing. """ return value def transform(self, value): """ Transformation function :param float value: Value :return: Modified value .. seealso:: :py:meth:`invTransform()` """ raise NotImplementedError def invTransform(self, value): """ Inverse transformation function :param float value: Value :return: Modified value .. seealso:: :py:meth:`transform()` """ raise NotImplementedError def copy(self): """ :return: Clone of the transformation The default implementation does nothing. """ raise NotImplementedError class QwtNullTransform(QwtTransform): def transform(self, value): """ Transformation function :param float value: Value :return: Modified value .. seealso:: :py:meth:`invTransform()` """ return value def invTransform(self, value): """ Inverse transformation function :param float value: Value :return: Modified value .. seealso:: :py:meth:`transform()` """ return value def copy(self): """ :return: Clone of the transformation """ return QwtNullTransform() class QwtLogTransform(QwtTransform): """ Logarithmic transformation `QwtLogTransform` modifies the values using `numpy.log()` and `numpy.exp()`. .. note:: In the calculations of `QwtScaleMap` the base of the log function has no effect on the mapping. So `QwtLogTransform` can be used for logarithmic scale in base 2 or base 10 or any other base. Extremum values: * `QwtLogTransform.LogMin`: Smallest allowed value for logarithmic scales: 1.0e-150 * `QwtLogTransform.LogMax`: Largest allowed value for logarithmic scales: 1.0e150 """ LogMin = 1.0e-150 LogMax = 1.0e150 def bounded(self, value): """ Modify value to be a valid value for the transformation. :param float value: Value to be bounded :return: Value modified """ return np.clip(value, self.LogMin, self.LogMax) def transform(self, value): """ Transformation function :param float value: Value :return: Modified value .. seealso:: :py:meth:`invTransform()` """ return np.log(self.bounded(value)) def invTransform(self, value): """ Inverse transformation function :param float value: Value :return: Modified value .. seealso:: :py:meth:`transform()` """ return np.exp(value) def copy(self): """ :return: Clone of the transformation """ return QwtLogTransform() class QwtPowerTransform(QwtTransform): """ A transformation using `numpy.pow()` `QwtPowerTransform` preserves the sign of a value. F.e. a transformation with a factor of 2 transforms a value of -3 to -9 and v.v. Thus `QwtPowerTransform` can be used for scales including negative values. """ def __init__(self, exponent): self.__exponent = exponent super(QwtPowerTransform, self).__init__() def transform(self, value): """ Transformation function :param float value: Value :return: Modified value .. seealso:: :py:meth:`invTransform()` """ if value < 0.: return -np.pow(-value, 1./self.__exponent) else: return np.pow(value, 1./self.__exponent) def invTransform(self, value): """ Inverse transformation function :param float value: Value :return: Modified value .. seealso:: :py:meth:`transform()` """ if value < 0.: return -np.pow(-value, self.__exponent) else: return np.pow(value, self.__exponent) def copy(self): """ :return: Clone of the transformation """ return QwtPowerTransform(self.__exponent) PythonQwt-0.5.5/qwt/scale_draw.py0000666000000000000000000011257112632324736015510 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtAbstractScaleDraw -------------------- .. autoclass:: QwtAbstractScaleDraw :members: QwtScaleDraw ------------ .. autoclass:: QwtScaleDraw :members: """ from qwt.scale_div import QwtScaleDiv from qwt.scale_map import QwtScaleMap from qwt.text import QwtText from qwt.math import qwtRadians from qwt.qt.QtGui import QPalette, QFontMetrics, QTransform from qwt.qt.QtCore import (Qt, qFuzzyCompare, QLocale, QRectF, QPointF, QRect, QPoint) import numpy as np class QwtAbstractScaleDraw_PrivateData(object): def __init__(self): self.spacing = 4 self.penWidth = 0 self.minExtent = 0. self.components = QwtAbstractScaleDraw.Backbone|\ QwtAbstractScaleDraw.Ticks|\ QwtAbstractScaleDraw.Labels self.tickLength = [None] * 3 self.tickLength[QwtScaleDiv.MinorTick] = 4.0 self.tickLength[QwtScaleDiv.MediumTick] = 6.0 self.tickLength[QwtScaleDiv.MajorTick] = 8.0 self.map = QwtScaleMap() self.scaleDiv = QwtScaleDiv() self.labelCache = {} class QwtAbstractScaleDraw(object): """ A abstract base class for drawing scales `QwtAbstractScaleDraw` can be used to draw linear or logarithmic scales. After a scale division has been specified as a `QwtScaleDiv` object using `setScaleDiv()`, the scale can be drawn with the `draw()` member. Scale components: * `QwtAbstractScaleDraw.Backbone`: Backbone = the line where the ticks are located * `QwtAbstractScaleDraw.Ticks`: Ticks * `QwtAbstractScaleDraw.Labels`: Labels .. py:class:: QwtAbstractScaleDraw() The range of the scale is initialized to [0, 100], The spacing (distance between ticks and labels) is set to 4, the tick lengths are set to 4,6 and 8 pixels """ # enum ScaleComponent Backbone = 0x01 Ticks = 0x02 Labels = 0x04 def __init__(self): self.__data = QwtAbstractScaleDraw_PrivateData() def extent(self, font): """ Calculate the extent The extent is the distance from the baseline to the outermost pixel of the scale draw in opposite to its orientation. It is at least minimumExtent() pixels. :param QFont font: Font used for drawing the tick labels :return: Number of pixels .. seealso:: :py:meth:`setMinimumExtent()`, :py:meth:`minimumExtent()` """ return 0. def drawTick(self, painter, value, len_): """ Draw a tick :param QPainter painter: Painter :param float value: Value of the tick :param float len: Length of the tick .. seealso:: :py:meth:`drawBackbone()`, :py:meth:`drawLabel()` """ pass def drawBackbone(self, painter): """ Draws the baseline of the scale :param QPainter painter: Painter .. seealso:: :py:meth:`drawTick()`, :py:meth:`drawLabel()` """ pass def drawLabel(self, painter, value): """ Draws the label for a major scale tick :param QPainter painter: Painter :param float value: Value .. seealso:: :py:meth:`drawTick()`, :py:meth:`drawBackbone()` """ pass def enableComponent(self, component, enable): """ En/Disable a component of the scale :param int component: Scale component :param bool enable: On/Off .. seealso:: :py:meth:`hasComponent()` """ if enable: self.__data.components |= component else: self.__data.components &= ~component def hasComponent(self, component): """ Check if a component is enabled :param int component: Component type :return: True, when component is enabled .. seealso:: :py:meth:`enableComponent()` """ return self.__data.components & component def setScaleDiv(self, scaleDiv): """ Change the scale division :param qwt.scale_div.QwtScaleDiv scaleDiv: New scale division """ self.__data.scaleDiv = scaleDiv self.__data.map.setScaleInterval(scaleDiv.lowerBound(), scaleDiv.upperBound()) self.__data.labelCache.clear() def setTransformation(self, transformation): """ Change the transformation of the scale :param qwt.transform.QwtTransform transformation: New scale transformation """ self.__data.map.setTransformation(transformation) def scaleMap(self): """ :return: Map how to translate between scale and pixel values """ return self.__data.map def scaleDiv(self): """ :return: scale division """ return self.__data.scaleDiv def setPenWidth(self, width): """ Specify the width of the scale pen :param int width: Pen width .. seealso:: :py:meth:`penWidth()` """ if width < 0: width = 0 if width != self.__data.penWidth: self.__data.penWidth = width def penWidth(self): """ :return: Scale pen width .. seealso:: :py:meth:`setPenWidth()` """ return self.__data.penWidth def draw(self, painter, palette): """ Draw the scale :param QPainter painter: The painter :param QPalette palette: Palette, text color is used for the labels, foreground color for ticks and backbone """ painter.save() pen = painter.pen() pen.setWidth(self.__data.penWidth) pen.setCosmetic(False) painter.setPen(pen) if self.hasComponent(QwtAbstractScaleDraw.Labels): painter.save() painter.setPen(palette.color(QPalette.Text)) majorTicks = self.__data.scaleDiv.ticks(QwtScaleDiv.MajorTick) for v in majorTicks: if self.__data.scaleDiv.contains(v): self.drawLabel(painter, v) painter.restore() if self.hasComponent(QwtAbstractScaleDraw.Ticks): painter.save() pen = painter.pen() pen.setColor(palette.color(QPalette.WindowText)) pen.setCapStyle(Qt.FlatCap) painter.setPen(pen) for tickType in range(QwtScaleDiv.NTickTypes): tickLen = self.__data.tickLength[tickType] if tickLen <= 0.: continue ticks = self.__data.scaleDiv.ticks(tickType) for v in ticks: if self.__data.scaleDiv.contains(v): self.drawTick(painter, v, tickLen) painter.restore() if self.hasComponent(QwtAbstractScaleDraw.Backbone): painter.save() pen = painter.pen() pen.setColor(palette.color(QPalette.WindowText)) pen.setCapStyle(Qt.FlatCap) painter.setPen(pen) self.drawBackbone(painter) painter.restore() painter.restore() def setSpacing(self, spacing): """ Set the spacing between tick and labels The spacing is the distance between ticks and labels. The default spacing is 4 pixels. :param float spacing: Spacing .. seealso:: :py:meth:`spacing()` """ if spacing < 0: spacing = 0 self.__data.spacing = spacing def spacing(self): """ Get the spacing The spacing is the distance between ticks and labels. The default spacing is 4 pixels. :return: Spacing .. seealso:: :py:meth:`setSpacing()` """ return self.__data.spacing def setMinimumExtent(self, minExtent): """ Set a minimum for the extent The extent is calculated from the components of the scale draw. In situations, where the labels are changing and the layout depends on the extent (f.e scrolling a scale), setting an upper limit as minimum extent will avoid jumps of the layout. :param float minExtent: Minimum extent .. seealso:: :py:meth:`extent()`, :py:meth:`minimumExtent()` """ if minExtent < 0.: minExtent = 0. self.__data.minExtent = minExtent def minimumExtent(self): """ Get the minimum extent :return: Minimum extent .. seealso:: :py:meth:`extent()`, :py:meth:`setMinimumExtent()` """ return self.__data.minExtent def setTickLength(self, tickType, length): """ Set the length of the ticks :param int tickType: Tick type :param float length: New length .. warning:: the length is limited to [0..1000] """ if tickType < QwtScaleDiv.MinorTick or\ tickType > QwtScaleDiv.MajorTick: return if length < 0.: length = 0. maxTickLen = 1000. if length > maxTickLen: length = maxTickLen self.__data.tickLength[tickType] = length def tickLength(self, tickType): """ :return: Length of the ticks .. seealso:: :py:meth:`setTickLength()`, :py:meth:`maxTickLength()` """ if tickType < QwtScaleDiv.MinorTick or\ tickType > QwtScaleDiv.MajorTick: return 0 return self.__data.tickLength[tickType] def maxTickLength(self): """ :return: Length of the longest tick Useful for layout calculations .. seealso:: :py:meth:`tickLength()`, :py:meth:`setTickLength()` """ length = 0. for tickType in range(QwtScaleDiv.NTickTypes): length = max([length, self.__data.tickLength[tickType]]) return length def label(self, value): """ Convert a value into its representing label The value is converted to a plain text using `QLocale().toString(value)`. This method is often overloaded by applications to have individual labels. :param float value: Value :return: Label string """ return QLocale().toString(value) def tickLabel(self, font, value): """ Convert a value into its representing label and cache it. The conversion between value and label is called very often in the layout and painting code. Unfortunately the calculation of the label sizes might be slow (really slow for rich text in Qt4), so it's necessary to cache the labels. :param QFont font: Font :param float value: Value :return: Tick label """ lbl = self.__data.labelCache.get(value) if lbl is None: lbl = QwtText(self.label(value)) lbl.setRenderFlags(0) lbl.setLayoutAttribute(QwtText.MinimumLayout) lbl.textSize(font) self.__data.labelCache[value] = lbl return lbl def invalidateCache(self): """ Invalidate the cache used by `tickLabel()` The cache is invalidated, when a new `QwtScaleDiv` is set. If the labels need to be changed. while the same `QwtScaleDiv` is set, `invalidateCache()` needs to be called manually. """ self.__data.labelCache.clear() class QwtScaleDraw_PrivateData(object): def __init__(self): self.len = 0 self.alignment = QwtScaleDraw.BottomScale self.labelAlignment = 0 self.labelRotation = 0. self.labelAutoSize = True self.pos = QPointF() class QwtScaleDraw(QwtAbstractScaleDraw): """ A class for drawing scales QwtScaleDraw can be used to draw linear or logarithmic scales. A scale has a position, an alignment and a length, which can be specified . The labels can be rotated and aligned to the ticks using `setLabelRotation()` and `setLabelAlignment()`. After a scale division has been specified as a QwtScaleDiv object using `QwtAbstractScaleDraw.setScaleDiv(scaleDiv)`, the scale can be drawn with the `QwtAbstractScaleDraw.draw()` member. Alignment of the scale draw: * `QwtScaleDraw.BottomScale`: The scale is below * `QwtScaleDraw.TopScale`: The scale is above * `QwtScaleDraw.LeftScale`: The scale is left * `QwtScaleDraw.RightScale`: The scale is right .. py:class:: QwtAbstractScaleDraw() The range of the scale is initialized to [0, 100], The position is at (0, 0) with a length of 100. The orientation is `QwtAbstractScaleDraw.Bottom`. """ # enum Alignment BottomScale, TopScale, LeftScale, RightScale = list(range(4)) Flags = ( Qt.AlignHCenter|Qt.AlignBottom, # BottomScale Qt.AlignHCenter|Qt.AlignTop, # TopScale Qt.AlignLeft|Qt.AlignVCenter, # LeftScale Qt.AlignRight|Qt.AlignVCenter, # RightScale ) def __init__(self): QwtAbstractScaleDraw.__init__(self) self.__data = QwtScaleDraw_PrivateData() self.setLength(100) self._max_label_sizes = {} def alignment(self): """ :return: Alignment of the scale .. seealso:: :py:meth:`setAlignment()` """ return self.__data.alignment def setAlignment(self, align): """ Set the alignment of the scale :param int align: Alignment of the scale Alignment of the scale draw: * `QwtScaleDraw.BottomScale`: The scale is below * `QwtScaleDraw.TopScale`: The scale is above * `QwtScaleDraw.LeftScale`: The scale is left * `QwtScaleDraw.RightScale`: The scale is right The default alignment is `QwtScaleDraw.BottomScale` .. seealso:: :py:meth:`alignment()` """ self.__data.alignment = align def orientation(self): """ Return the orientation TopScale, BottomScale are horizontal (`Qt.Horizontal`) scales, LeftScale, RightScale are vertical (`Qt.Vertical`) scales. :return: Orientation of the scale .. seealso:: :py:meth:`alignment()` """ if self.__data.alignment in (self.TopScale, self.BottomScale): return Qt.Horizontal elif self.__data.alignment in (self.LeftScale, self.RightScale): return Qt.Vertical def getBorderDistHint(self, font): """ Determine the minimum border distance This member function returns the minimum space needed to draw the mark labels at the scale's endpoints. :param QFont font: Font :return: tuple `(start, end)` Returned tuple: * start: Start border distance * end: End border distance """ start, end = 0, 1. if not self.hasComponent(QwtAbstractScaleDraw.Labels): return start, end ticks = self.scaleDiv().ticks(QwtScaleDiv.MajorTick) if len(ticks) == 0: return start, end minTick = ticks[0] minPos = self.scaleMap().transform(minTick) maxTick = minTick maxPos = minPos for tick in ticks: tickPos = self.scaleMap().transform(tick) if tickPos < minPos: minTick = tick minPos = tickPos if tickPos > self.scaleMap().transform(maxTick): maxTick = tick maxPos = tickPos s = 0. e = 0. if self.orientation() == Qt.Vertical: s = -self.labelRect(font, minTick).top() s -= abs(minPos - round(self.scaleMap().p2())) e = self.labelRect(font, maxTick).bottom() e -= abs(maxPos - self.scaleMap().p1()) else: s = -self.labelRect(font, minTick).left() s -= abs(minPos - self.scaleMap().p1()) e = self.labelRect(font, maxTick).right() e -= abs(maxPos - self.scaleMap().p2()) start, end = np.ceil(np.nan_to_num(np.array([s, e])).clip(0, None)) return start, end def minLabelDist(self, font): """ Determine the minimum distance between two labels, that is necessary that the texts don't overlap. :param QFont font: Font :return: The maximum width of a label .. seealso:: :py:meth:`getBorderDistHint()` """ if not self.hasComponent(QwtAbstractScaleDraw.Labels): return 0 ticks = self.scaleDiv().ticks(QwtScaleDiv.MajorTick) if not ticks: return 0 fm = QFontMetrics(font) vertical = self.orientation() == Qt.Vertical bRect1 = QRectF() bRect2 = self.labelRect(font, ticks[0]) if vertical: bRect2.setRect(-bRect2.bottom(), 0., bRect2.height(), bRect2.width()) maxDist = 0. for tick in ticks: bRect1 = bRect2 bRect2 = self.labelRect(font, tick) if vertical: bRect2.setRect(-bRect2.bottom(), 0., bRect2.height(), bRect2.width()) dist = fm.leading() if bRect1.right() > 0: dist += bRect1.right() if bRect2.left() < 0: dist += -bRect2.left() if dist > maxDist: maxDist = dist angle = qwtRadians(self.labelRotation()) if vertical: angle += np.pi/2 sinA = np.sin(angle) if qFuzzyCompare(sinA+1., 1.): return np.ceil(maxDist) fmHeight = fm.ascent()-2 labelDist = fmHeight/np.sin(angle)*np.cos(angle) if labelDist < 0: labelDist = -labelDist if labelDist > maxDist: labelDist = maxDist if labelDist < fmHeight: labelDist = fmHeight return np.ceil(labelDist) def extent(self, font): """ Calculate the width/height that is needed for a vertical/horizontal scale. The extent is calculated from the pen width of the backbone, the major tick length, the spacing and the maximum width/height of the labels. :param QFont font: Font used for painting the labels :return: Extent .. seealso:: :py:meth:`minLength()` """ d = 0. if self.hasComponent(QwtAbstractScaleDraw.Labels): if self.orientation() == Qt.Vertical: d = self.maxLabelWidth(font) else: d = self.maxLabelHeight(font) if d > 0: d += self.spacing() if self.hasComponent(QwtAbstractScaleDraw.Ticks): d += self.maxTickLength() if self.hasComponent(QwtAbstractScaleDraw.Backbone): pw = max([1, self.penWidth()]) d += pw return max([d, self.minimumExtent()]) def minLength(self, font): """ Calculate the minimum length that is needed to draw the scale :param QFont font: Font used for painting the labels :return: Minimum length that is needed to draw the scale .. seealso:: :py:meth:`extent()` """ startDist, endDist = self.getBorderDistHint(font) sd = self.scaleDiv() minorCount = len(sd.ticks(QwtScaleDiv.MinorTick))+\ len(sd.ticks(QwtScaleDiv.MediumTick)) majorCount = len(sd.ticks(QwtScaleDiv.MajorTick)) lengthForLabels = 0 if self.hasComponent(QwtAbstractScaleDraw.Labels): lengthForLabels = self.minLabelDist(font)*majorCount lengthForTicks = 0 if self.hasComponent(QwtAbstractScaleDraw.Ticks): pw = max([1, self.penWidth()]) lengthForTicks = np.ceil((majorCount+minorCount)*(pw+1.)) return startDist + endDist + max([lengthForLabels, lengthForTicks]) def labelPosition(self, value): """ Find the position, where to paint a label The position has a distance that depends on the length of the ticks in direction of the `alignment()`. :param float value: Value :return: Position, where to paint a label """ tval = self.scaleMap().transform(value) dist = self.spacing() if self.hasComponent(QwtAbstractScaleDraw.Backbone): dist += max([1, self.penWidth()]) if self.hasComponent(QwtAbstractScaleDraw.Ticks): dist += self.tickLength(QwtScaleDiv.MajorTick) px = 0 py = 0 if self.alignment() == self.RightScale: px = self.__data.pos.x() + dist py = tval elif self.alignment() == self.LeftScale: px = self.__data.pos.x() - dist py = tval elif self.alignment() == self.BottomScale: px = tval py = self.__data.pos.y() + dist elif self.alignment() == self.TopScale: px = tval py = self.__data.pos.y() - dist return QPointF(px, py) def drawTick(self, painter, value, len_): """ Draw a tick :param QPainter painter: Painter :param float value: Value of the tick :param float len: Length of the tick .. seealso:: :py:meth:`drawBackbone()`, :py:meth:`drawLabel()` """ if len_ <= 0: return pos = self.__data.pos tval = self.scaleMap().transform(value) pw = self.penWidth() a = 0 if self.alignment() == self.LeftScale: x1 = pos.x() + a x2 = pos.x() + a - pw - len_ painter.drawLine(x1, tval, x2, tval) elif self.alignment() == self.RightScale: x1 = pos.x() x2 = pos.x() + pw + len_ painter.drawLine(x1, tval, x2, tval) elif self.alignment() == self.BottomScale: y1 = pos.y() y2 = pos.y() + pw + len_ painter.drawLine(tval, y1, tval, y2) elif self.alignment() == self.TopScale: y1 = pos.y() + a y2 = pos.y() - pw - len_ + a painter.drawLine(tval, y1, tval, y2) def drawBackbone(self, painter): """ Draws the baseline of the scale :param QPainter painter: Painter .. seealso:: :py:meth:`drawTick()`, :py:meth:`drawLabel()` """ pos = self.__data.pos len_ = self.__data.len off = .5*self.penWidth() if self.alignment() == self.LeftScale: x = pos.x() - off painter.drawLine(x, pos.y(), x, pos.y()+len_) elif self.alignment() == self.RightScale: x = pos.x() + off painter.drawLine(x, pos.y(), x, pos.y()+len_) elif self.alignment() == self.TopScale: y = pos.y() - off painter.drawLine(pos.x(), y, pos.x()+len_, y) elif self.alignment() == self.BottomScale: y = pos.y() + off painter.drawLine(pos.x(), y, pos.x()+len_, y) def move(self, *args): """ Move the position of the scale The meaning of the parameter pos depends on the alignment: * `QwtScaleDraw.LeftScale`: The origin is the topmost point of the backbone. The backbone is a vertical line. Scale marks and labels are drawn at the left of the backbone. * `QwtScaleDraw.RightScale`: The origin is the topmost point of the backbone. The backbone is a vertical line. Scale marks and labels are drawn at the right of the backbone. * `QwtScaleDraw.TopScale`: The origin is the leftmost point of the backbone. The backbone is a horizontal line. Scale marks and labels are drawn above the backbone. * `QwtScaleDraw.BottomScale`: The origin is the leftmost point of the backbone. The backbone is a horizontal line Scale marks and labels are drawn below the backbone. .. py:method:: move(x, y) :param float x: X coordinate :param float y: Y coordinate .. py:method:: move(pos) :param QPointF pos: position .. seealso:: :py:meth:`pos()`, :py:meth:`setLength()` """ if len(args) == 2: x, y = args self.move(QPointF(x, y)) elif len(args) == 1: pos, = args self.__data.pos = pos self.updateMap() else: raise TypeError("%s().move() takes 1 or 2 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def pos(self): """ :return: Origin of the scale .. seealso:: :py:meth:`pos()`, :py:meth:`setLength()` """ return self.__data.pos def setLength(self, length): """ Set the length of the backbone. The length doesn't include the space needed for overlapping labels. :param float length: Length of the backbone .. seealso:: :py:meth:`move()`, :py:meth:`minLabelDist()` """ if length >= 0 and length < 10: length = 10 if length < 0 and length > -10: length = -10 self.__data.len = length self.updateMap() def length(self): """ :return: the length of the backbone .. seealso:: :py:meth:`setLength()`, :py:meth:`pos()` """ return self.__data.len def drawLabel(self, painter, value): """ Draws the label for a major scale tick :param QPainter painter: Painter :param float value: Value .. seealso:: :py:meth:`drawTick()`, :py:meth:`drawBackbone()`, :py:meth:`boundingLabelRect()` """ lbl = self.tickLabel(painter.font(), value) if lbl is None or lbl.isEmpty(): return pos = self.labelPosition(value) labelSize = lbl.textSize(painter.font()) transform = self.labelTransformation(pos, labelSize) painter.save() painter.setWorldTransform(transform, True) lbl.draw(painter, QRect(QPoint(0, 0), labelSize.toSize())) painter.restore() def boundingLabelRect(self, font, value): """ Find the bounding rectangle for the label. The coordinates of the rectangle are absolute (calculated from `pos()`) in direction of the tick. :param QFont font: Font used for painting :param float value: Value :return: Bounding rectangle .. seealso:: :py:meth:`labelRect()` """ lbl = self.tickLabel(font, value) if lbl.isEmpty(): return QRect() pos = self.labelPosition(value) labelSize = lbl.textSize(font) transform = self.labelTransformation(pos, labelSize) return transform.mapRect(QRect(QPoint(0, 0), labelSize.toSize())) def labelTransformation(self, pos, size): """ Calculate the transformation that is needed to paint a label depending on its alignment and rotation. :param QPointF pos: Position where to paint the label :param QSizeF size: Size of the label :return: Transformation matrix .. seealso:: :py:meth:`setLabelAlignment()`, :py:meth:`setLabelRotation()` """ transform = QTransform() transform.translate(pos.x(), pos.y()) transform.rotate(self.labelRotation()) flags = self.labelAlignment() if flags == 0: flags = self.Flags[self.alignment()] if flags & Qt.AlignLeft: x = -size.width() elif flags & Qt.AlignRight: x = 0. else: x = -(.5*size.width()) if flags & Qt.AlignTop: y = -size.height() elif flags & Qt.AlignBottom: y = 0 else: y = -(.5*size.height()) transform.translate(x, y) return transform def labelRect(self, font, value): """ Find the bounding rectangle for the label. The coordinates of the rectangle are relative to spacing + tick length from the backbone in direction of the tick. :param QFont font: Font used for painting :param float value: Value :return: Bounding rectangle that is needed to draw a label """ lbl = self.tickLabel(font, value) if not lbl or lbl.isEmpty(): return QRectF(0., 0., 0., 0.) pos = self.labelPosition(value) labelSize = lbl.textSize(font) transform = self.labelTransformation(pos, labelSize) br = transform.mapRect(QRectF(QPointF(0, 0), labelSize)) br.translate(-pos.x(), -pos.y()) return br def labelSize(self, font, value): """ Calculate the size that is needed to draw a label :param QFont font: Label font :param float value: Value :return: Size that is needed to draw a label """ return self.labelRect(font, value).size() def setLabelRotation(self, rotation): """ Rotate all labels. When changing the rotation, it might be necessary to adjust the label flags too. Finding a useful combination is often the result of try and error. :param float rotation: Angle in degrees. When changing the label rotation, the label flags often needs to be adjusted too. .. seealso:: :py:meth:`setLabelAlignment()`, :py:meth:`labelRotation()`, :py:meth:`labelAlignment()` """ self.__data.labelRotation = rotation def labelRotation(self): """ :return: the label rotation .. seealso:: :py:meth:`setLabelRotation()`, :py:meth:`labelAlignment()` """ return self.__data.labelRotation def setLabelAlignment(self, alignment): """ Change the label flags Labels are aligned to the point tick length + spacing away from the backbone. The alignment is relative to the orientation of the label text. In case of an flags of 0 the label will be aligned depending on the orientation of the scale: * `QwtScaleDraw.TopScale`: `Qt.AlignHCenter | Qt.AlignTop` * `QwtScaleDraw.BottomScale`: `Qt.AlignHCenter | Qt.AlignBottom` * `QwtScaleDraw.LeftScale`: `Qt.AlignLeft | Qt.AlignVCenter` * `QwtScaleDraw.RightScale`: `Qt.AlignRight | Qt.AlignVCenter` Changing the alignment is often necessary for rotated labels. :param Qt.Alignment alignment Or'd `Qt.AlignmentFlags` .. seealso:: :py:meth:`setLabelRotation()`, :py:meth:`labelRotation()`, :py:meth:`labelAlignment()` .. warning:: The various alignments might be confusing. The alignment of the label is not the alignment of the scale and is not the alignment of the flags (`QwtText.flags()`) returned from `QwtAbstractScaleDraw.label()`. """ self.__data.labelAlignment = alignment def labelAlignment(self): """ :return: the label flags .. seealso:: :py:meth:`setLabelAlignment()`, :py:meth:`labelRotation()` """ return self.__data.labelAlignment def setLabelAutoSize(self, state): """ Set label automatic size option state When drawing text labels, if automatic size mode is enabled (default behavior), the axes are drawn in order to optimize layout space and depends on text label individual sizes. Otherwise, width and height won't change when axis range is changing. This option is not implemented in Qwt C++ library: this may be used either as an optimization (updating plot layout is faster when this option is enabled) or as an appearance preference (with Qwt default behavior, the size of axes may change when zooming and/or panning plot canvas which in some cases may not be desired). :param bool state: On/off .. seealso:: :py:meth:`labelAutoSize()` """ self.__data.labelAutoSize = state def labelAutoSize(self): """ :return: True if automatic size option is enabled for labels .. seealso:: :py:meth:`setLabelAutoSize()` """ return self.__data.labelAutoSize def _get_max_label_size(self, font): key = (font.toString(), self.labelRotation()) size = self._max_label_sizes.get(key) if size is None: size = self.labelSize(font, -999999) # -999999 is the biggest label size.setWidth(np.ceil(size.width())) size.setHeight(np.ceil(size.height())) return self._max_label_sizes.setdefault(key, size) else: return size def maxLabelWidth(self, font): """ :param QFont font: Font :return: the maximum width of a label """ ticks = self.scaleDiv().ticks(QwtScaleDiv.MajorTick) if not ticks: return 0 if self.labelAutoSize(): vmax = sorted([v for v in ticks if self.scaleDiv().contains(v)], key=lambda obj: len(QLocale().toString(obj)))[-1] return np.ceil(self.labelSize(font, vmax).width()) ## Original implementation (closer to Qwt's C++ code, but slower): #return np.ceil(max([self.labelSize(font, v).width() # for v in ticks if self.scaleDiv().contains(v)])) else: return self._get_max_label_size(font).width() def maxLabelHeight(self, font): """ :param QFont font: Font :return: the maximum height of a label """ ticks = self.scaleDiv().ticks(QwtScaleDiv.MajorTick) if not ticks: return 0 if self.labelAutoSize(): vmax = sorted([v for v in ticks if self.scaleDiv().contains(v)], key=lambda obj: len(QLocale().toString(obj)))[-1] return np.ceil(self.labelSize(font, vmax).height()) ## Original implementation (closer to Qwt's C++ code, but slower): #return np.ceil(max([self.labelSize(font, v).height() # for v in ticks if self.scaleDiv().contains(v)])) else: return self._get_max_label_size(font).height() def updateMap(self): pos = self.__data.pos len_ = self.__data.len sm = self.scaleMap() if self.orientation() == Qt.Vertical: sm.setPaintInterval(pos.y()+len_, pos.y()) else: sm.setPaintInterval(pos.x(), pos.x()+len_) PythonQwt-0.5.5/qwt/plot_canvas.py0000666000000000000000000007372212605040216015705 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPlotCanvas ------------- .. autoclass:: QwtPlotCanvas :members: """ from qwt.null_paintdevice import QwtNullPaintDevice from qwt.painter import QwtPainter from qwt.qt import PYQT5 from qwt.qt.QtGui import (QFrame, QPaintEngine, QPen, QBrush, QRegion, QImage, QPainterPath, QPixmap, QGradient, QPainter, qAlpha, QPolygonF, QStyleOption, QStyle, QStyleOptionFrame) from qwt.qt.QtCore import Qt, QSizeF, QT_VERSION, QEvent, QPointF, QRectF class Border(object): def __init__(self): self.pathlist = [] self.rectList = [] self.clipRegion = QRegion() class Background(object): def __init__(self): self.path = QPainterPath() self.brush = QBrush() self.origin = QPointF() class QwtStyleSheetRecorder(QwtNullPaintDevice): def __init__(self, size): super(QwtStyleSheetRecorder, self).__init__() self.__size = size self.__pen = QPen() self.__brush = QBrush() self.__origin = QPointF() self.clipRects = [] self.border = Border() self.background = Background() def updateState(self, state): if state.state() & QPaintEngine.DirtyPen: self.__pen = state.pen() if state.state() & QPaintEngine.DirtyBrush: self.__brush = state.brush() if state.state() & QPaintEngine.DirtyBrushOrigin: self.__origin = state.brushOrigin() def drawRects(self, rects, count): for i in range(count): self.border.rectList += rects[i] def drawPath(self, path): rect = QRectF(QPointF(0., 0.), self.__size) if path.controlPointRect().contains(rect.center()): self.setCornerRects(path) self.alignCornerRects(rect) self.background.path = path self.background.brush = self.__brush self.background.origin = self.__origin else: self.border.pathlist += [path] def setCornerRects(self, path): pos = QPointF(0., 0.) for i in range(path.elementCount()): el = path.elementAt(i) if el.type in (QPainterPath.MoveToElement, QPainterPath.LineToElement): pos.setX(el.x) pos.setY(el.y) elif el.type == QPainterPath.CurveToElement: r = QRectF(pos, QPointF(el.x, el.y)) self.clipRects += [r.normalized()] pos.setX(el.x) pos.setY(el.y) elif el.type == QPainterPath.CurveToDataElement: if self.clipRects: r = self.clipRects[-1] r.setCoords(min([r.left(), el.x]), min([r.top(), el.y]), max([r.right(), el.x]), max([r.bottom(), el.y])) self.clipRects[-1] = r.normalized() def sizeMetrics(self): return self.__size def alignCornerRects(self, rect): for r in self.clipRects: if r.center().x() < rect.center().x(): r.setLeft(rect.left()) else: r.setRight(rect.right()) if r.center().y() < rect.center().y(): r.setTop(rect.top()) else: r.setBottom(rect.bottom()) def _rects_conv_PyQt5(rects): # PyQt5 compatibility: the conversion from QRect to QRectF should not # be necessary but it seems to be anyway... PyQt5 bug? if PYQT5: return [QRectF(rect) for rect in rects] else: return rects def qwtDrawBackground(painter, canvas): painter.save() borderClip = canvas.borderPath(canvas.rect()) if not borderClip.isEmpty(): painter.setClipPath(borderClip, Qt.IntersectClip) brush = canvas.palette().brush(canvas.backgroundRole()) if brush.style() == Qt.TexturePattern: pm = QPixmap(canvas.size()) QwtPainter.fillPixmap(canvas, pm) painter.drawPixmap(0, 0, pm) elif brush.gradient(): rects = [] if brush.gradient().coordinateMode() == QGradient.ObjectBoundingMode: rects += [canvas.rect()] else: rects += [painter.clipRegion().rects()] useRaster = False if painter.paintEngine().type() == QPaintEngine.X11: useRaster = True if useRaster: format_ = QImage.Format_RGB32 stops = brush.gradient().stops() for stop in stops: if stop.second.alpha() != 255: format_ = QImage.Format_ARGB32 break image = QImage(canvas.size(), format_) p = QPainter(image) p.setPen(Qt.NoPen) p.setBrush(brush) p.drawRects(_rects_conv_PyQt5(rects)) p.end() painter.drawImage(0, 0, image) else: painter.setPen(Qt.NoPen) painter.setBrush(brush) painter.drawRects(_rects_conv_PyQt5(rects)) else: painter.setPen(Qt.NoPen) painter.setBrush(brush) painter.drawRects(_rects_conv_PyQt5(painter.clipRegion().rects())) painter.restore() def qwtRevertPath(path): if path.elementCount() == 4: el0 = path.elementAt(0) el3 = path.elementAt(3) path.setElementPositionAt(0, el3.x, el3.y) path.setElementPositionAt(3, el0.x, el0.y) def qwtCombinePathList(rect, pathList): if not pathList: return QPainterPath() ordered = [None] * 8 for subPath in pathList: index = -1 br = subPath.controlPointRect() if br.center().x() < rect.center().x(): if br.center().y() < rect.center().y(): if abs(br.top()-rect.top()) < abs(br.left()-rect.left()): index = 1 else: index = 0 else: if abs(br.bottom()-rect.bottom) < abs(br.left()-rect.left()): index = 6 else: index = 7 if subPath.currentPosition().y() > br.center().y(): qwtRevertPath(subPath) else: if br.center().y() < rect.center().y(): if abs(br.top()-rect.top()) < abs(br.right()-rect.right()): index = 2 else: index = 3 else: if abs(br.bottom()-rect.bottom()) < abs(br.right()-rect.right()): index = 5 else: index = 4 if subPath.currentPosition().y() < br.center().y(): qwtRevertPath(subPath) ordered[index] = subPath for i in range(4): if ordered[2*i].isEmpty() != ordered[2*i+1].isEmpty(): return QPainterPath() corners = QPolygonF(rect) path = QPainterPath() for i in range(4): if ordered[2*i].isEmpty(): path.lineTo(corners[i]) else: path.connectPath(ordered[2*i]) path.connectPath(ordered[2*i+1]) path.closeSubpath() return path def qwtDrawStyledBackground(w, painter): opt = QStyleOption() opt.initFrom(w) w.style().drawPrimitive(QStyle.PE_Widget, opt, painter, w) def qwtBackgroundWidget(w): if w.parentWidget() is None: return w if w.autoFillBackground(): brush = w.palette().brush(w.backgroundRole()) if brush.color().alpha() > 0: return w if w.testAttribute(Qt.WA_StyledBackground): image = QImage(1, 1, QImage.Format_ARGB32) image.fill(Qt.transparent) painter = QPainter(image) painter.translate(-w.rect().center()) qwtDrawStyledBackground(w, painter) painter.end() if qAlpha(image.pixel(0, 0)) != 0: return w return qwtBackgroundWidget(w.parentWidget()) def qwtFillBackground(*args): if len(args) == 2: painter, canvas = args rects = [] if canvas.testAttribute(Qt.WA_StyledBackground): recorder = QwtStyleSheetRecorder(canvas.size()) p = QPainter(recorder) qwtDrawStyledBackground(canvas, p) p.end() if recorder.background.brush.isOpaque(): rects = recorder.clipRects else: rects += [canvas.rect()] else: r = canvas.rect() radius = canvas.borderRadius() if radius > 0.: sz = QSizeF(radius, radius) rects += [QRectF(r.topLeft(), sz), QRectF(r.topRight()-QPointF(radius, 0), sz), QRectF(r.bottomRight()-QPointF(radius, radius), sz), QRectF(r.bottomLeft()-QPointF(0, radius), sz)] qwtFillBackground(painter, canvas, rects) elif len(args) == 3: painter, widget, fillRects = args if not fillRects: return if painter.hasClipping(): clipRegion = painter.transform().map(painter.clipRegion()) else: clipRegion = widget.contentsRect() bgWidget = qwtBackgroundWidget(widget.parentWidget()) for fillRect in fillRects: rect = fillRect.toAlignedRect() if clipRegion.intersects(rect): pm = QPixmap(rect.size()) QwtPainter.fillPixmap(bgWidget, pm, widget.mapTo(bgWidget, rect.topLeft())) painter.drawPixmap(rect, pm) else: raise TypeError("%s() takes 2 or 3 argument(s) (%s given)"\ % ("qwtFillBackground", len(args))) class StyleSheetBackground(object): def __init__(self): self.brush = QBrush() self.origin = QPointF() class StyleSheet(object): def __init__(self): self.hasBorder = False self.borderPath = QPainterPath() self.cornerRects = [] self.background = StyleSheetBackground() class QwtPlotCanvas_PrivateData(object): def __init__(self): self.focusIndicator = QwtPlotCanvas.NoFocusIndicator self.borderRadius = 0 self.paintAttributes = 0 self.backingStore = None self.styleSheet = StyleSheet() self.styleSheet.hasBorder = False class QwtPlotCanvas(QFrame): """ Canvas of a QwtPlot. Canvas is the widget where all plot items are displayed .. seealso:: :py:meth:`qwt.plot.QwtPlot.setCanvas()` Paint attributes: * `QwtPlotCanvas.BackingStore`: Paint double buffered reusing the content of the pixmap buffer when possible. Using a backing store might improve the performance significantly, when working with widget overlays (like rubber bands). Disabling the cache might improve the performance for incremental paints (using :py:class:`qwt.plot_directpainter.QwtPlotDirectPainter`). * `QwtPlotCanvas.Opaque`: Try to fill the complete contents rectangle of the plot canvas When using styled backgrounds Qt assumes, that the canvas doesn't fill its area completely (f.e because of rounded borders) and fills the area below the canvas. When this is done with gradients it might result in a serious performance bottleneck - depending on the size. When the Opaque attribute is enabled the canvas tries to identify the gaps with some heuristics and to fill those only. .. warning:: Will not work for semitransparent backgrounds * `QwtPlotCanvas.HackStyledBackground`: Try to improve painting of styled backgrounds `QwtPlotCanvas` supports the box model attributes for customizing the layout with style sheets. Unfortunately the design of Qt style sheets has no concept how to handle backgrounds with rounded corners - beside of padding. When HackStyledBackground is enabled the plot canvas tries to separate the background from the background border by reverse engineering to paint the background before and the border after the plot items. In this order the border gets perfectly antialiased and you can avoid some pixel artifacts in the corners. * `QwtPlotCanvas.ImmediatePaint`: When ImmediatePaint is set replot() calls repaint() instead of update(). .. seealso:: :py:meth:`replot()`, :py:meth:`QWidget.repaint()`, :py:meth:`QWidget.update()` Focus indicators: * `QwtPlotCanvas.NoFocusIndicator`: Don't paint a focus indicator * `QwtPlotCanvas.CanvasFocusIndicator`: The focus is related to the complete canvas. Paint the focus indicator using paintFocus() * `QwtPlotCanvas.ItemFocusIndicator`: The focus is related to an item (curve, point, ...) on the canvas. It is up to the application to display a focus indication using f.e. highlighting. .. py:class:: QwtPlotCanvas([plot=None]) Constructor :param qwt.plot.QwtPlot plot: Parent plot widget .. seealso:: :py:meth:`qwt.plot.QwtPlot.setCanvas()` """ # enum PaintAttribute BackingStore = 1 Opaque = 2 HackStyledBackground = 4 ImmediatePaint = 8 # enum FocusIndicator NoFocusIndicator, CanvasFocusIndicator, ItemFocusIndicator = list(range(3)) def __init__(self, plot=None): super(QwtPlotCanvas, self).__init__(plot) self.__plot = plot self.setFrameStyle(QFrame.Panel|QFrame.Sunken) self.setLineWidth(2) self.__data = QwtPlotCanvas_PrivateData() self.setCursor(Qt.CrossCursor) self.setAutoFillBackground(True) self.setPaintAttribute(QwtPlotCanvas.BackingStore, False) self.setPaintAttribute(QwtPlotCanvas.Opaque, True) self.setPaintAttribute(QwtPlotCanvas.HackStyledBackground, True) def plot(self): """ :return: Parent plot widget """ return self.__plot def setPaintAttribute(self, attribute, on=True): """ Changing the paint attributes Paint attributes: * `QwtPlotCanvas.BackingStore` * `QwtPlotCanvas.Opaque` * `QwtPlotCanvas.HackStyledBackground` * `QwtPlotCanvas.ImmediatePaint` :param int attribute: Paint attribute :param bool on: On/Off .. seealso:: :py:meth:`testPaintAttribute()`, :py:meth:`backingStore()` """ if bool(self.__data.paintAttributes & attribute) == on: return if on: self.__data.paintAttributes |= attribute else: self.__data.paintAttributes &= ~attribute if attribute == self.BackingStore: if on: if self.__data.backingStore is None: self.__data.backingStore = QPixmap() if self.isVisible(): if QT_VERSION >= 0x050000: self.__data.backingStore = self.grab(self.rect()) else: if PYQT5: pm = QPixmap.grabWidget(self, self.rect()) else: pm = self.grab(self.rect()) self.__data.backingStore = pm else: self.__data.backingStore = None elif attribute == self.Opaque: if on: self.setAttribute(Qt.WA_OpaquePaintEvent, True) elif attribute in (self.HackStyledBackground, self.ImmediatePaint): pass def testPaintAttribute(self, attribute): """ Test whether a paint attribute is enabled :param int attribute: Paint attribute :return: True, when attribute is enabled .. seealso:: :py:meth:`setPaintAttribute()` """ return self.__data.paintAttributes & attribute def backingStore(self): """ :return: Backing store, might be None """ return self.__data.backingStore def invalidateBackingStore(self): """Invalidate the internal backing store""" if self.__data.backingStore: self.__data.backingStore = QPixmap() def setFocusIndicator(self, focusIndicator): """ Set the focus indicator Focus indicators: * `QwtPlotCanvas.NoFocusIndicator` * `QwtPlotCanvas.CanvasFocusIndicator` * `QwtPlotCanvas.ItemFocusIndicator` :param int focusIndicator: Focus indicator .. seealso:: :py:meth:`focusIndicator()` """ self.__data.focusIndicator = focusIndicator def focusIndicator(self): """ :return: Focus indicator .. seealso:: :py:meth:`setFocusIndicator()` """ return self.__data.focusIndicator def setBorderRadius(self, radius): """ Set the radius for the corners of the border frame :param float radius: Radius of a rounded corner .. seealso:: :py:meth:`borderRadius()` """ self.__data.borderRadius = max([0., radius]) def borderRadius(self): """ :return: Radius for the corners of the border frame .. seealso:: :py:meth:`setBorderRadius()` """ return self.__data.borderRadius def event(self, event): if event.type() == QEvent.PolishRequest: if self.testPaintAttribute(self.Opaque): self.setAttribute(Qt.WA_OpaquePaintEvent, True) if event.type() in (QEvent.PolishRequest, QEvent.StyleChange): self.updateStyleSheetInfo() return QFrame.event(self, event) def paintEvent(self, event): painter = QPainter(self) painter.setClipRegion(event.region()) if self.testPaintAttribute(self.BackingStore) and\ self.__data.backingStore is not None: bs = self.__data.backingStore if bs.size() != self.size(): bs = QwtPainter.backingStore(self, self.size()) if self.testAttribute(Qt.WA_StyledBackground): p = QPainter(bs) qwtFillBackground(p, self) self.drawCanvas(p, True) else: p = QPainter() if self.__data.borderRadius <= 0.: # print('**DEBUG: QwtPlotCanvas.paintEvent') QwtPainter.fillPixmap(self, bs) p.begin(bs) self.drawCanvas(p, False) else: p.begin(bs) qwtFillBackground(p, self) self.drawCanvas(p, True) if self.frameWidth() > 0: self.drawBorder(p) p.end() painter.drawPixmap(0, 0, self.__data.backingStore) else: if self.testAttribute(Qt.WA_StyledBackground): if self.testAttribute(Qt.WA_OpaquePaintEvent): qwtFillBackground(painter, self) self.drawCanvas(painter, True) else: self.drawCanvas(painter, False) else: if self.testAttribute(Qt.WA_OpaquePaintEvent): if self.autoFillBackground(): qwtFillBackground(painter, self) qwtDrawBackground(painter, self) else: if self.borderRadius() > 0.: clipPath = QPainterPath() clipPath.addRect(self.rect()) clipPath = clipPath.subtracted(self.borderPath(self.rect())) painter.save() painter.setClipPath(clipPath, Qt.IntersectClip) qwtFillBackground(painter, self) qwtDrawBackground(painter, self) painter.restore() self.drawCanvas(painter, False) if self.frameWidth() > 0: self.drawBorder(painter) if self.hasFocus() and self.focusIndicator() == self.CanvasFocusIndicator: self.drawFocusIndicator(painter) def drawCanvas(self, painter, withBackground): hackStyledBackground = False if withBackground and self.testAttribute(Qt.WA_StyledBackground) and\ self.testPaintAttribute(self.HackStyledBackground): # Antialiasing rounded borders is done by # inserting pixels with colors between the # border color and the color on the canvas, # When the border is painted before the plot items # these colors are interpolated for the canvas # and the plot items need to be clipped excluding # the anialiased pixels. In situations, where # the plot items fill the area at the rounded # borders this is noticeable. # The only way to avoid these annoying "artefacts" # is to paint the border on top of the plot items. if self.__data.styleSheet.hasBorder and\ not self.__data.styleSheet.borderPath.isEmpty(): # We have a border with at least one rounded corner hackStyledBackground = True if withBackground: painter.save() if self.testAttribute(Qt.WA_StyledBackground): if hackStyledBackground: # paint background without border painter.setPen(Qt.NoPen) painter.setBrush(self.__data.styleSheet.background.brush) painter.setBrushOrigin(self.__data.styleSheet.background.origin) painter.setClipPath(self.__data.styleSheet.borderPath) painter.drawRect(self.contentsRect()) else: qwtDrawStyledBackground(self, painter) elif self.autoFillBackground(): painter.setPen(Qt.NoPen) painter.setBrush(self.palette().brush(self.backgroundRole())) if self.__data.borderRadius > 0. and self.rect() == self.frameRect(): if self.frameWidth() > 0: painter.setClipPath(self.borderPath(self.rect())) painter.drawRect(self.rect()) else: painter.setRenderHint(QPainter.Antialiasing, True) painter.drawPath(self.borderPath(self.rect())) else: painter.drawRect(self.rect()) painter.restore() painter.save() if not self.__data.styleSheet.borderPath.isEmpty(): painter.setClipPath(self.__data.styleSheet.borderPath, Qt.IntersectClip) else: if self.__data.borderRadius > 0.: painter.setClipPath(self.borderPath(self.frameRect()), Qt.IntersectClip) else: # print('**DEBUG: QwtPlotCanvas.drawCanvas') painter.setClipRect(self.contentsRect(), Qt.IntersectClip) self.plot().drawCanvas(painter) painter.restore() if withBackground and hackStyledBackground: # Now paint the border on top opt = QStyleOptionFrame() opt.initFrom(self) self.style().drawPrimitive(QStyle.PE_Frame, opt, painter, self) def drawBorder(self, painter): """ Draw the border of the plot canvas :param QPainter painter: Painter .. seealso:: :py:meth:`setBorderRadius()` """ if self.__data.borderRadius > 0: if self.frameWidth() > 0: QwtPainter.drawRoundedFrame(painter, QRectF(self.frameRect()), self.__data.borderRadius, self.__data.borderRadius, self.palette(), self.frameWidth(), self.frameStyle()) else: if QT_VERSION >= 0x040500: if PYQT5: from qwt.qt.QtGui import QStyleOptionFrame else: from qwt.qt.QtGui import QStyleOptionFrameV3 as\ QStyleOptionFrame opt = QStyleOptionFrame() opt.initFrom(self) frameShape = self.frameStyle() & QFrame.Shape_Mask frameShadow = self.frameStyle() & QFrame.Shadow_Mask opt.frameShape = QFrame.Shape(int(opt.frameShape)|frameShape) if frameShape in (QFrame.Box, QFrame.HLine, QFrame.VLine, QFrame.StyledPanel, QFrame.Panel): opt.lineWidth = self.lineWidth() opt.midLineWidth = self.midLineWidth() else: opt.lineWidth = self.frameWidth() if frameShadow == self.Sunken: opt.state |= QStyle.State_Sunken elif frameShadow == self.Raised: opt.state |= QStyle.State_Raised self.style().drawControl(QStyle.CE_ShapedFrame, opt, painter, self) else: self.drawFrame(painter) def resizeEvent(self, event): QFrame.resizeEvent(self, event) self.updateStyleSheetInfo() def drawFocusIndicator(self, painter): """ Draw the focus indication :param QPainter painter: Painter """ margin = 1 focusRect = self.contentsRect() focusRect.setRect(focusRect.x()+margin, focusRect.y()+margin, focusRect.width()-2*margin, focusRect.height()-2*margin) QwtPainter.drawFocusRect(painter, self, focusRect) def replot(self): """ Invalidate the paint cache and repaint the canvas """ self.invalidateBackingStore() if self.testPaintAttribute(self.ImmediatePaint): self.repaint(self.contentsRect()) else: self.update(self.contentsRect()) def invalidatePaintCache(self): import warnings warnings.warn("`invalidatePaintCache` has been removed: "\ "please use `replot` instead", RuntimeWarning) self.replot() def updateStyleSheetInfo(self): """ Update the cached information about the current style sheet """ if not self.testAttribute(Qt.WA_StyledBackground): return recorder = QwtStyleSheetRecorder(self.size()) painter = QPainter(recorder) opt = QStyleOption() opt.initFrom(self) self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) painter.end() self.__data.styleSheet.hasBorder = not recorder.border.rectList.isEmpty() self.__data.styleSheet.cornerRects = recorder.clipRects if recorder.background.path.isEmpty(): if not recorder.border.rectList.isEmpty(): self.__data.styleSheet.borderPath =\ qwtCombinePathList(self.rect(), recorder.border.pathlist) else: self.__data.styleSheet.borderPath = recorder.background.path self.__data.styleSheet.background.brush = recorder.background.brush self.__data.styleSheet.background.origin = recorder.background.origin def borderPath(self, rect): """ Calculate the painter path for a styled or rounded border When the canvas has no styled background or rounded borders the painter path is empty. :param QRect rect: Bounding rectangle of the canvas :return: Painter path, that can be used for clipping """ if self.testAttribute(Qt.WA_StyledBackground): recorder = QwtStyleSheetRecorder(rect.size()) painter = QPainter(recorder) opt = QStyleOption() opt.initFrom(self) opt.rect = rect self.style().drawPrimitive(QStyle.PE_Widget, opt, painter, self) painter.end() if not recorder.background.path.isEmpty(): return recorder.background.path if not recorder.border.rectList.isEmpty(): return qwtCombinePathList(rect, recorder.border.pathlist) elif self.__data.borderRadius > 0.: fw2 = self.frameWidth()*.5 r = QRectF(rect).adjusted(fw2, fw2, -fw2, -fw2) path = QPainterPath() path.addRoundedRect(r, self.__data.borderRadius, self.__data.borderRadius) return path return QPainterPath() PythonQwt-0.5.5/qwt/null_paintdevice.py0000666000000000000000000002167512605040216016721 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtNullPaintDevice ------------------ .. autoclass:: QwtNullPaintDevice :members: """ from qwt.qt.QtGui import QPaintEngine, QPainterPath, QPaintDevice class QwtNullPaintDevice_PrivateData(object): def __init__(self): self.mode = QwtNullPaintDevice.NormalMode class QwtNullPaintDevice_PaintEngine(QPaintEngine): def __init__(self, paintdevice): super(QwtNullPaintDevice_PaintEngine, self ).__init__(QPaintEngine.AllFeatures) self.__paintdevice = paintdevice def begin(self, paintdevice): self.setActive(True) return True def end(self): self.setActive(False) return True def type(self): return QPaintEngine.User def drawRects(self, rects, rectCount=None): if rectCount is None: rectCount = len(rects) device = self.nullDevice() if device is None: return if device.mode() != QwtNullPaintDevice.NormalMode: try: QPaintEngine.drawRects(self, rects, rectCount) except TypeError: # PyQt <=4.9 QPaintEngine.drawRects(self, rects) return device.drawRects(rects, rectCount) def drawLines(self, lines, lineCount=None): if lineCount is None: lineCount = len(lines) device = self.nullDevice() if device is None: return if device.mode() != QwtNullPaintDevice.NormalMode: try: QPaintEngine.drawLines(lines, lineCount) except TypeError: # PyQt <=4.9 QPaintEngine.drawLines(self, lines) return device.drawLines(lines, lineCount) def drawEllipse(self, rect): device = self.nullDevice() if device is None: return if device.mode() != QwtNullPaintDevice.NormalMode: QPaintEngine.drawEllipse(rect) return device.drawEllipse(rect) def drawPath(self, path): device = self.nullDevice() if device is None: return device.drawPath(path) def drawPoints(self, points, pointCount=None): if pointCount is None: pointCount = len(points) device = self.nullDevice() if device is None: return if device.mode() != QwtNullPaintDevice.NormalMode: try: QPaintEngine.drawPoints(points, pointCount) except TypeError: # PyQt <=4.9 QPaintEngine.drawPoints(self, points) return device.drawPoints(points, pointCount) def drawPolygon(self, *args): if len(args) == 3: points, pointCount, mode = args elif len(args) == 2: points, mode = args pointCount = len(points) else: raise TypeError("Unexpected arguments") device = self.nullDevice() if device is None: return if device.mode() == QwtNullPaintDevice.PathMode: path = QPainterPath() if pointCount > 0: path.moveTo(points[0]) for i in range(1, pointCount): path.lineTo(points[i]) if mode != QPaintEngine.PolylineMode: path.closeSubpath() device.drawPath(path) return device.drawPolygon(points, pointCount, mode) def drawPixmap(self, rect, pm, subRect): device = self.nullDevice() if device is None: return device.drawPixmap(rect, pm, subRect) def drawTextItem(self, pos, textItem): device = self.nullDevice() if device is None: return if device.mode() != QwtNullPaintDevice.NormalMode: QPaintEngine.drawTextItem(pos, textItem) return device.drawTextItem(pos, textItem) def drawTiledPixmap(self, rect, pixmap, subRect): device = self.nullDevice() if device is None: return if device.mode() != QwtNullPaintDevice.NormalMode: QPaintEngine.drawTiledPixmap(rect, pixmap, subRect) return device.drawTiledPixmap(rect, pixmap, subRect) def drawImage(self, rect, image, subRect, flags): device = self.nullDevice() if device is None: return device.drawImage(rect, image, subRect, flags) def updateState(self, state): device = self.nullDevice() if device is None: return device.updateState(state) def nullDevice(self): if not self.isActive(): return return self.__paintdevice class QwtNullPaintDevice(QPaintDevice): """ A null paint device doing nothing Sometimes important layout/rendering geometries are not available or changeable from the public Qt class interface. ( f.e hidden in the style implementation ). `QwtNullPaintDevice` can be used to manipulate or filter out this information by analyzing the stream of paint primitives. F.e. `QwtNullPaintDevice` is used by `QwtPlotCanvas` to identify styled backgrounds with rounded corners. Modes: * `NormalMode`: All vector graphic primitives are painted by the corresponding draw methods * `PolygonPathMode`: Vector graphic primitives ( beside polygons ) are mapped to a `QPainterPath` and are painted by `drawPath`. In `PolygonPathMode` mode only a few draw methods are called: - `drawPath()` - `drawPixmap()` - `drawImage()` - `drawPolygon()` * `PathMode`: Vector graphic primitives are mapped to a `QPainterPath` and are painted by `drawPath`. In `PathMode` mode only a few draw methods are called: - `drawPath()` - `drawPixmap()` - `drawImage()` """ # enum Mode NormalMode, PolygonPathMode, PathMode = list(range(3)) def __init__(self): super(QwtNullPaintDevice, self).__init__() self.__engine = None self.__data = QwtNullPaintDevice_PrivateData() def setMode(self, mode): """ Set the render mode :param int mode: New mode .. seealso:: :py:meth:`mode()` """ self.__data.mode = mode def mode(self): """ :return: Render mode .. seealso:: :py:meth:`setMode()` """ return self.__data.mode def paintEngine(self): if self.__engine is None: self.__engine = QwtNullPaintDevice_PaintEngine(self) return self.__engine def metric(self, deviceMetric): if deviceMetric == QPaintDevice.PdmWidth: value = self.sizeMetrics().width() elif deviceMetric == QPaintDevice.PdmHeight: value = self.sizeMetrics().height() elif deviceMetric == QPaintDevice.PdmNumColors: value = 0xffffffff elif deviceMetric == QPaintDevice.PdmDepth: value = 32 elif deviceMetric in (QPaintDevice.PdmPhysicalDpiX, QPaintDevice.PdmPhysicalDpiY, QPaintDevice.PdmDpiY, QPaintDevice.PdmDpiX): value = 72 elif deviceMetric == QPaintDevice.PdmWidthMM: value = round(self.metric(QPaintDevice.PdmWidth)*25.4/self.metric(QPaintDevice.PdmDpiX)) elif deviceMetric == QPaintDevice.PdmHeightMM: value = round(self.metric(QPaintDevice.PdmHeight)*25.4/self.metric(QPaintDevice.PdmDpiY)) else: value = 0 return value def drawRects(self, rects, rectCount): pass def drawLines(self, lines, lineCount): pass def drawEllipse(self, rect): pass def drawPath(self, path): pass def drawPoints(self, points, pointCount): pass def drawPolygon(self, points, pointCount, mode): pass def drawPixmap(self, rect, pm, subRect): pass def drawTextItem(self, pos, textItem): pass def drawTiledPixmap(self, rect, pm, subRect): pass def drawImage(self, rect, image, subRect, flags): pass def updateState(self, state): pass PythonQwt-0.5.5/qwt/py3compat.py0000666000000000000000000001406412605040216015305 0ustar rootroot# -*- coding: utf-8 -*- # # Copyright © 2012-2013 Pierre Raybaut # Licensed under the terms of the MIT License # (see LICENSE file for details) """ guidata.py3compat (exact copy of spyderlib.py3compat) ----------------------------------------------------- Transitional module providing compatibility functions intended to help migrating from Python 2 to Python 3. This module should be fully compatible with: * Python >=v2.6 * Python 3 """ from __future__ import print_function import sys import os PY2 = sys.version[0] == '2' PY3 = sys.version[0] == '3' #============================================================================== # Data types #============================================================================== if PY2: # Python 2 TEXT_TYPES = (str, unicode) INT_TYPES = (int, long) else: # Python 3 TEXT_TYPES = (str,) INT_TYPES = (int,) NUMERIC_TYPES = tuple(list(INT_TYPES) + [float, complex]) #============================================================================== # Renamed/Reorganized modules #============================================================================== if PY2: # Python 2 import __builtin__ as builtins import ConfigParser as configparser try: import _winreg as winreg except ImportError: pass from sys import maxint as maxsize try: import CStringIO as io except ImportError: import StringIO as io try: import cPickle as pickle except ImportError: import pickle from UserDict import DictMixin as MutableMapping else: # Python 3 import builtins import configparser try: import winreg except ImportError: pass from sys import maxsize import io import pickle from collections import MutableMapping #============================================================================== # Strings #============================================================================== def is_text_string(obj): """Return True if `obj` is a text string, False if it is anything else, like binary data (Python 3) or QString (Python 2, PyQt API #1)""" if PY2: # Python 2 return isinstance(obj, basestring) else: # Python 3 return isinstance(obj, str) def is_binary_string(obj): """Return True if `obj` is a binary string, False if it is anything else""" if PY2: # Python 2 return isinstance(obj, str) else: # Python 3 return isinstance(obj, bytes) def is_string(obj): """Return True if `obj` is a text or binary Python string object, False if it is anything else, like a QString (Python 2, PyQt API #1)""" return is_text_string(obj) or is_binary_string(obj) def is_unicode(obj): """Return True if `obj` is unicode""" if PY2: # Python 2 return isinstance(obj, unicode) else: # Python 3 return isinstance(obj, str) def to_text_string(obj, encoding=None): """Convert `obj` to (unicode) text string""" if PY2: # Python 2 if encoding is None: return unicode(obj) else: return unicode(obj, encoding) else: # Python 3 if encoding is None: return str(obj) elif isinstance(obj, str): # In case this function is not used properly, this could happen return obj else: return str(obj, encoding) def to_binary_string(obj, encoding=None): """Convert `obj` to binary string (bytes in Python 3, str in Python 2)""" if PY2: # Python 2 if encoding is None: return str(obj) else: return obj.encode(encoding) else: # Python 3 return bytes(obj, 'utf-8' if encoding is None else encoding) #============================================================================== # Function attributes #============================================================================== def get_func_code(func): """Return function code object""" if PY2: # Python 2 return func.func_code else: # Python 3 return func.__code__ def get_func_name(func): """Return function name""" if PY2: # Python 2 return func.func_name else: # Python 3 return func.__name__ def get_func_defaults(func): """Return function default argument values""" if PY2: # Python 2 return func.func_defaults else: # Python 3 return func.__defaults__ #============================================================================== # Special method attributes #============================================================================== def get_meth_func(obj): """Return method function object""" if PY2: # Python 2 return obj.im_func else: # Python 3 return obj.__func__ def get_meth_class_inst(obj): """Return method class instance""" if PY2: # Python 2 return obj.im_self else: # Python 3 return obj.__self__ def get_meth_class(obj): """Return method class""" if PY2: # Python 2 return obj.im_class else: # Python 3 return obj.__self__.__class__ #============================================================================== # Misc. #============================================================================== if PY2: # Python 2 input = raw_input getcwd = os.getcwdu cmp = cmp import string str_lower = string.lower else: # Python 3 input = input getcwd = os.getcwd def cmp(a, b): return (a > b) - (a < b) str_lower = str.lower def qbytearray_to_str(qba): """Convert QByteArray object to str in a way compatible with Python 2/3""" return str(bytes(qba.toHex()).decode()) if __name__ == '__main__': pass PythonQwt-0.5.5/qwt/scale_engine.py0000666000000000000000000007334712646231674016032 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtScaleEngine -------------- .. autoclass:: QwtScaleEngine :members: QwtLinearScaleEngine -------------------- .. autoclass:: QwtLinearScaleEngine :members: QwtLogScaleEngine ----------------- .. autoclass:: QwtLogScaleEngine :members: """ from __future__ import division from qwt.interval import QwtInterval from qwt.scale_div import QwtScaleDiv from qwt.transform import QwtLogTransform from qwt.math import qwtFuzzyCompare from qwt.transform import QwtTransform from qwt.qt.QtCore import qFuzzyCompare import numpy as np DBL_MAX = np.finfo(float).max LOG_MIN = 1.0e-100 LOG_MAX = 1.0e100 def qwtLog(base, value): return np.log(value)/np.log(base) def qwtLogInterval(base, interval): return QwtInterval(qwtLog(base, interval.minValue()), qwtLog(base, interval.maxValue())) def qwtPowInterval(base, interval): return QwtInterval(np.power(base, interval.minValue()), np.power(base, interval.maxValue())) def qwtStepSize(intervalSize, maxSteps, base): """this version often doesn't find the best ticks: f.e for 15: 5, 10""" minStep = divideInterval(intervalSize, maxSteps, base) if minStep != 0.: # # ticks per interval numTicks = np.ceil(abs(intervalSize/minStep))-1 # Do the minor steps fit into the interval? if qwtFuzzyCompare((numTicks+1)*abs(minStep), abs(intervalSize), intervalSize) > 0: # The minor steps doesn't fit into the interval return .5*intervalSize return minStep EPS = 1.0e-6 def ceilEps(value, intervalSize): """ Ceil a value, relative to an interval :param float value: Value to be ceiled :param float intervalSize: Interval size :return: Rounded value .. seealso:: :py:func:`qwt.scale_engine.floorEps()` """ eps = EPS*intervalSize value = (value-eps)/intervalSize return np.ceil(value)*intervalSize def floorEps(value, intervalSize): """ Floor a value, relative to an interval :param float value: Value to be floored :param float intervalSize: Interval size :return: Rounded value .. seealso:: :py:func:`qwt.scale_engine.ceilEps()` """ eps = EPS*intervalSize value = (value+eps)/intervalSize return np.floor(value)*intervalSize def divideEps(intervalSize, numSteps): """ Divide an interval into steps `stepSize = (intervalSize - intervalSize * 10**-6) / numSteps` :param float intervalSize: Interval size :param float numSteps: Number of steps :return: Step size """ if numSteps == 0. or intervalSize == 0.: return 0. return (intervalSize-(EPS*intervalSize))/numSteps def divideInterval(intervalSize, numSteps, base): """ Calculate a step size for a given interval :param float intervalSize: Interval size :param float numSteps: Number of steps :param int base: Base for the division (usually 10) :return: Calculated step size """ if numSteps <= 0: return 0. v = divideEps(intervalSize, numSteps) if v == 0.: return 0. lx = qwtLog(base, abs(v)) p = np.floor(lx) fraction = np.power(base, lx-p) n = base while n > 1 and fraction <= n/2: n /= 2 stepSize = n*np.power(base, p) if v < 0: stepSize = -stepSize return stepSize class QwtScaleEngine_PrivateData(object): def __init__(self): self.attributes = QwtScaleEngine.NoAttribute self.lowerMargin = 0. self.upperMargin = 0. self.referenceValue = 0. self.base = 10 self.transform = None # QwtTransform class QwtScaleEngine(object): """ Base class for scale engines. A scale engine tries to find "reasonable" ranges and step sizes for scales. The layout of the scale can be varied with `setAttribute()`. `PythonQwt` offers implementations for logarithmic and linear scales. Layout attributes: * `QwtScaleEngine.NoAttribute`: No attributes * `QwtScaleEngine.IncludeReference`: Build a scale which includes the `reference()` value * `QwtScaleEngine.Symmetric`: Build a scale which is symmetric to the `reference()` value * `QwtScaleEngine.Floating`: The endpoints of the scale are supposed to be equal the outmost included values plus the specified margins (see `setMargins()`). If this attribute is *not* set, the endpoints of the scale will be integer multiples of the step size. * `QwtScaleEngine.Inverted`: Turn the scale upside down """ # enum Attribute NoAttribute = 0x00 IncludeReference = 0x01 Symmetric = 0x02 Floating = 0x04 Inverted = 0x08 def __init__(self, base=10): self.__data = QwtScaleEngine_PrivateData() self.setBase(base) def autoScale(self, maxNumSteps, x1, x2, stepSize): """ Align and divide an interval :param int maxNumSteps: Max. number of steps :param float x1: First limit of the interval (In/Out) :param float x2: Second limit of the interval (In/Out) :param float stepSize: Step size :return: tuple (x1, x2, stepSize) """ pass def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.): """ Calculate a scale division :param float x1: First interval limit :param float x2: Second interval limit :param int maxMajorSteps: Maximum for the number of major steps :param int maxMinorSteps: Maximum number of minor steps :param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one :return: Calculated scale division """ pass def setTransformation(self, transform): """ Assign a transformation :param qwt.transform.QwtTransform transform: Transformation The transformation object is used as factory for clones that are returned by `transformation()` The scale engine takes ownership of the transformation. .. seealso:: :py:meth:`QwtTransform.copy()`, :py:meth:`transformation()` """ assert transform is None or isinstance(transform, QwtTransform) if transform != self.__data.transform: self.__data.transform = transform def transformation(self): """ Create and return a clone of the transformation of the engine. When the engine has no special transformation None is returned, indicating no transformation. :return: A clone of the transfomation .. seealso:: :py:meth:`setTransformation()` """ if self.__data.transform: return self.__data.transform.copy() def lowerMargin(self): """ :return: the margin at the lower end of the scale The default margin is 0. .. seealso:: :py:meth:`setMargins()` """ return self.__data.lowerMargin def upperMargin(self): """ :return: the margin at the upper end of the scale The default margin is 0. .. seealso:: :py:meth:`setMargins()` """ return self.__data.upperMargin def setMargins(self, lower, upper): """ Specify margins at the scale's endpoints :param float lower: minimum distance between the scale's lower boundary and the smallest enclosed value :param float upper: minimum distance between the scale's upper boundary and the greatest enclosed value :return: A clone of the transfomation Margins can be used to leave a minimum amount of space between the enclosed intervals and the boundaries of the scale. .. warning:: `QwtLogScaleEngine` measures the margins in decades. .. seealso:: :py:meth:`upperMargin()`, :py:meth:`lowerMargin()` """ self.__data.lowerMargin = max([lower, 0.]) self.__data.upperMargin = max([upper, 0.]) def divideInterval(self, intervalSize, numSteps): """ Calculate a step size for a given interval :param float intervalSize: Interval size :param float numSteps: Number of steps :return: Step size """ return divideInterval(intervalSize, numSteps, self.__data.base) def contains(self, interval, value): """ Check if an interval "contains" a value :param float intervalSize: Interval size :param float value: Value :return: True, when the value is inside the interval """ if not interval.isValid(): return False elif qwtFuzzyCompare(value, interval.minValue(), interval.width()) < 0: return False elif qwtFuzzyCompare(value, interval.maxValue(), interval.width()) > 0: return False else: return True def strip(self, ticks, interval): """ Remove ticks from a list, that are not inside an interval :param list ticks: Tick list :param qwt.interval.QwtInterval interval: Interval :return: Stripped tick list """ if not interval.isValid() or not ticks: return [] if self.contains(interval, ticks[0]) and\ self.contains(interval, ticks[-1]): return ticks return [tick for tick in ticks if self.contains(interval, tick)] def buildInterval(self, value): """ Build an interval around a value In case of v == 0.0 the interval is [-0.5, 0.5], otherwide it is [0.5 * v, 1.5 * v] :param float value: Initial value :return: Calculated interval """ if value == 0.: delta = .5 else: delta = abs(.5*value) if DBL_MAX-delta < value: return QwtInterval(DBL_MAX-delta, DBL_MAX) if -DBL_MAX+delta > value: return QwtInterval(-DBL_MAX, -DBL_MAX+delta) return QwtInterval(value-delta, value+delta) def setAttribute(self, attribute, on=True): """ Change a scale attribute :param int attribute: Attribute to change :param bool on: On/Off :return: Calculated interval .. seealso:: :py:meth:`testAttribute()` """ if on: self.__data.attributes |= attribute else: self.__data.attributes &= ~attribute def testAttribute(self, attribute): """ :param int attribute: Attribute to be tested :return: True, if attribute is enabled .. seealso:: :py:meth:`setAttribute()` """ return self.__data.attributes & attribute def setAttributes(self, attributes): """ Change the scale attribute :param attributes: Set scale attributes .. seealso:: :py:meth:`attributes()` """ self.__data.attributes = attributes def attributes(self): """ :return: Scale attributes .. seealso:: :py:meth:`setAttributes()`, :py:meth:`testAttribute()` """ return self.__data.attributes def setReference(self, r): """ Specify a reference point :param float r: new reference value The reference point is needed if options `IncludeReference` or `Symmetric` are active. Its default value is 0.0. """ self.__data.referenceValue = r def reference(self): """ :return: the reference value .. seealso:: :py:meth:`setReference()`, :py:meth:`setAttribute()` """ return self.__data.referenceValue def setBase(self, base): """ Set the base of the scale engine While a base of 10 is what 99.9% of all applications need certain scales might need a different base: f.e 2 The default setting is 10 :param int base: Base of the engine .. seealso:: :py:meth:`base()` """ self.__data.base = max([base, 2]) def base(self): """ :return: Base of the scale engine .. seealso:: :py:meth:`setBase()` """ return self.__data.base class QwtLinearScaleEngine(QwtScaleEngine): """ A scale engine for linear scales The step size will fit into the pattern \f$\left\{ 1,2,5\right\} \cdot 10^{n}\f$, where n is an integer. """ def __init__(self, base=10): super(QwtLinearScaleEngine, self).__init__(base) def autoScale(self, maxNumSteps, x1, x2, stepSize): """ Align and divide an interval :param int maxNumSteps: Max. number of steps :param float x1: First limit of the interval (In/Out) :param float x2: Second limit of the interval (In/Out) :param float stepSize: Step size :return: tuple (x1, x2, stepSize) .. seealso:: :py:meth:`setAttribute()` """ interval = QwtInterval(x1, x2) interval = interval.normalized() interval.setMinValue(interval.minValue()-self.lowerMargin()) interval.setMaxValue(interval.maxValue()+self.upperMargin()) if self.testAttribute(QwtScaleEngine.Symmetric): interval = interval.symmetrize(self.reference()) if self.testAttribute(QwtScaleEngine.IncludeReference): interval = interval.extend(self.reference()) if interval.width() == 0.: interval = self.buildInterval(interval.minValue()) stepSize = divideInterval(interval.width(), max([maxNumSteps, 1]), self.base()) if not self.testAttribute(QwtScaleEngine.Floating): interval = self.align(interval, stepSize) x1 = interval.minValue() x2 = interval.maxValue() if self.testAttribute(QwtScaleEngine.Inverted): x1, x2 = x2, x1 stepSize = -stepSize return x1, x2, stepSize def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.): """ Calculate a scale division for an interval :param float x1: First interval limit :param float x2: Second interval limit :param int maxMajorSteps: Maximum for the number of major steps :param int maxMinorSteps: Maximum number of minor steps :param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one :return: Calculated scale division """ interval = QwtInterval(x1, x2).normalized() if interval.width() <= 0: return QwtScaleDiv() stepSize = abs(stepSize) if stepSize == 0.: if maxMajorSteps < 1: maxMajorSteps = 1 stepSize = divideInterval(interval.width(), maxMajorSteps, self.base()) scaleDiv = QwtScaleDiv() if stepSize != 0.: ticks = self.buildTicks(interval, stepSize, maxMinorSteps) scaleDiv = QwtScaleDiv(interval, ticks) if x1 > x2: scaleDiv.invert() return scaleDiv def buildTicks(self, interval, stepSize, maxMinorSteps): """ Calculate ticks for an interval :param qwt.interval.QwtInterval interval: Interval :param float stepSize: Step size :param int maxMinorSteps: Maximum number of minor steps :return: Calculated ticks """ ticks = [[] for _i in range(QwtScaleDiv.NTickTypes)] boundingInterval = self.align(interval, stepSize) ticks[QwtScaleDiv.MajorTick] = self.buildMajorTicks(boundingInterval, stepSize) if maxMinorSteps > 0: self.buildMinorTicks(ticks, maxMinorSteps, stepSize) for i in range(QwtScaleDiv.NTickTypes): ticks[i] = self.strip(ticks[i], interval) for j in range(len(ticks[i])): if qwtFuzzyCompare(ticks[i][j], 0., stepSize) == 0: ticks[i][j] = 0. return ticks def buildMajorTicks(self, interval, stepSize): """ Calculate major ticks for an interval :param qwt.interval.QwtInterval interval: Interval :param float stepSize: Step size :return: Calculated ticks """ numTicks = min([round(interval.width()/stepSize)+1, 10000]) if np.isnan(numTicks): numTicks = 0 ticks = [interval.minValue()] for i in range(1, int(numTicks-1)): ticks += [interval.minValue()+i*stepSize] ticks += [interval.maxValue()] return ticks def buildMinorTicks(self, ticks, maxMinorSteps, stepSize): """ Calculate minor ticks for an interval :param list ticks: Major ticks (returned) :param int maxMinorSteps: Maximum number of minor steps :param float stepSize: Step size """ minStep = qwtStepSize(stepSize, maxMinorSteps, self.base()) if minStep == 0.: return numTicks = int(np.ceil(abs(stepSize/minStep))-1) medIndex = -1 if numTicks % 2: medIndex = numTicks/2 for val in ticks[QwtScaleDiv.MajorTick]: for k in range(numTicks): val += minStep alignedValue = val if qwtFuzzyCompare(val, 0., stepSize) == 0: alignedValue = 0. if k == medIndex: ticks[QwtScaleDiv.MediumTick] += [alignedValue] else: ticks[QwtScaleDiv.MinorTick] += [alignedValue] def align(self, interval, stepSize): """ Align an interval to a step size The limits of an interval are aligned that both are integer multiples of the step size. :param qwt.interval.QwtInterval interval: Interval :param float stepSize: Step size :return: Aligned interval """ x1 = interval.minValue() x2 = interval.maxValue() if -DBL_MAX+stepSize <= x1: x = floorEps(x1, stepSize) if qwtFuzzyCompare(x1, x, stepSize) != 0: x1 = x if DBL_MAX-stepSize >= x2: x = ceilEps(x2, stepSize) if qwtFuzzyCompare(x2, x, stepSize) != 0: x2 = x return QwtInterval(x1, x2) class QwtLogScaleEngine(QwtScaleEngine): """ A scale engine for logarithmic scales The step size is measured in *decades* and the major step size will be adjusted to fit the pattern {1,2,3,5}.10**n, where n is a natural number including zero. .. warning:: The step size as well as the margins are measured in *decades*. """ def __init__(self, base=10): super(QwtLogScaleEngine, self).__init__(base) self.setTransformation(QwtLogTransform()) def autoScale(self, maxNumSteps, x1, x2, stepSize): """ Align and divide an interval :param int maxNumSteps: Max. number of steps :param float x1: First limit of the interval (In/Out) :param float x2: Second limit of the interval (In/Out) :param float stepSize: Step size :return: tuple (x1, x2, stepSize) .. seealso:: :py:meth:`setAttribute()` """ if x1 > x2: x1, x2 = x2, x1 logBase = self.base() interval = QwtInterval(x1/np.power(logBase, self.lowerMargin()), x2*np.power(logBase, self.upperMargin())) interval = interval.limited(LOG_MIN, LOG_MAX) if interval.maxValue()/interval.minValue() < logBase: linearScaler = QwtLinearScaleEngine() linearScaler.setAttributes(self.attributes()) linearScaler.setReference(self.reference()) linearScaler.setMargins(self.lowerMargin(), self.upperMargin()) x1, x2, stepSize = linearScaler.autoScale(maxNumSteps, x1, x2, stepSize) linearInterval = QwtInterval(x1, x2).normalized() linearInterval = linearInterval.limited(LOG_MIN, LOG_MAX) if linearInterval.maxValue()/linearInterval.minValue() < logBase: if stepSize < 0.: stepSize = -qwtLog(logBase, abs(stepSize)) else: stepSize = qwtLog(logBase, stepSize) return x1, x2, stepSize logRef = 1. if self.reference() > LOG_MIN/2: logRef = min([self.reference(), LOG_MAX/2]) if self.testAttribute(QwtScaleEngine.Symmetric): delta = max([interval.maxValue()/logRef, logRef/interval.minValue()]) interval.setInterval(logRef/delta, logRef*delta) if self.testAttribute(QwtScaleEngine.IncludeReference): interval = interval.extend(logRef) interval = interval.limited(LOG_MIN, LOG_MAX) if interval.width() == 0.: interval = self.buildInterval(interval.minValue()) stepSize = self.divideInterval( qwtLogInterval(logBase, interval).width(), max([maxNumSteps, 1])) if stepSize < 1.: stepSize = 1. if not self.testAttribute(QwtScaleEngine.Floating): interval = self.align(interval, stepSize) x1 = interval.minValue() x2 = interval.maxValue() if self.testAttribute(QwtScaleEngine.Inverted): x1, x2 = x2, x1 stepSize = -stepSize return x1, x2, stepSize def divideScale(self, x1, x2, maxMajorSteps, maxMinorSteps, stepSize=0.): """ Calculate a scale division for an interval :param float x1: First interval limit :param float x2: Second interval limit :param int maxMajorSteps: Maximum for the number of major steps :param int maxMinorSteps: Maximum number of minor steps :param float stepSize: Step size. If stepSize == 0.0, the scaleEngine calculates one :return: Calculated scale division """ interval = QwtInterval(x1, x2).normalized() interval = interval.limited(LOG_MIN, LOG_MAX) if interval.width() <= 0: return QwtScaleDiv() logBase = self.base() if interval.maxValue()/interval.minValue() < logBase: linearScaler = QwtLinearScaleEngine() linearScaler.setAttributes(self.attributes()) linearScaler.setReference(self.reference()) linearScaler.setMargins(self.lowerMargin(), self.upperMargin()) if stepSize != 0.: if stepSize < 0.: stepSize = -np.power(logBase, -stepSize) else: stepSize = np.power(logBase, stepSize) return linearScaler.divideScale(x1, x2, maxMajorSteps, maxMinorSteps, stepSize) stepSize = abs(stepSize) if stepSize == 0.: if maxMajorSteps < 1: maxMajorSteps = 1 stepSize = self.divideInterval( qwtLogInterval(logBase, interval).width(), maxMajorSteps) if stepSize < 1.: stepSize = 1. scaleDiv = QwtScaleDiv() if stepSize != 0.: ticks = self.buildTicks(interval, stepSize, maxMinorSteps) scaleDiv = QwtScaleDiv(interval, ticks) if x1 > x2: scaleDiv.invert() return scaleDiv def buildTicks(self, interval, stepSize, maxMinorSteps): """ Calculate ticks for an interval :param qwt.interval.QwtInterval interval: Interval :param float stepSize: Step size :param int maxMinorSteps: Maximum number of minor steps :return: Calculated ticks """ ticks = [[] for _i in range(QwtScaleDiv.NTickTypes)] boundingInterval = self.align(interval, stepSize) ticks[QwtScaleDiv.MajorTick] = self.buildMajorTicks(boundingInterval, stepSize) if maxMinorSteps > 0: self.buildMinorTicks(ticks, maxMinorSteps, stepSize) for i in range(QwtScaleDiv.NTickTypes): ticks[i] = self.strip(ticks[i], interval) return ticks def buildMajorTicks(self, interval, stepSize): """ Calculate major ticks for an interval :param qwt.interval.QwtInterval interval: Interval :param float stepSize: Step size :return: Calculated ticks """ width = qwtLogInterval(self.base(), interval).width() numTicks = min([int(round(width/stepSize))+1, 10000]) lxmin = np.log(interval.minValue()) lxmax = np.log(interval.maxValue()) lstep = (lxmax-lxmin)/float(numTicks-1) ticks = [interval.minValue()] for i in range(1, numTicks-1): ticks += [np.exp(lxmin+float(i)*lstep)] ticks += [interval.maxValue()] return ticks def buildMinorTicks(self, ticks, maxMinorSteps, stepSize): """ Calculate minor ticks for an interval :param list ticks: Major ticks (returned) :param int maxMinorSteps: Maximum number of minor steps :param float stepSize: Step size """ logBase = self.base() if stepSize < 1.1: minStep = self.divideInterval(stepSize, maxMinorSteps+1) if minStep == 0.: return numSteps = int(round(stepSize/minStep)) mediumTickIndex = -1 if numSteps > 2 and numSteps % 2 == 0: mediumTickIndex = numSteps/2 for v in ticks[QwtScaleDiv.MajorTick]: s = logBase/numSteps if s >= 1.: if not qFuzzyCompare(s, 1.): ticks[QwtScaleDiv.MinorTick] += [v*s] for j in range(2, numSteps): ticks[QwtScaleDiv.MinorTick] += [v*j*s] else: for j in range(1, numSteps): tick = v + j*v*(logBase-1)/numSteps if j == mediumTickIndex: ticks[QwtScaleDiv.MediumTick] += [tick] else: ticks[QwtScaleDiv.MinorTick] += [tick] else: minStep = self.divideInterval(stepSize, maxMinorSteps) if minStep == 0.: return if minStep < 1.: minStep = 1. numTicks = int(round(stepSize/minStep))-1 if qwtFuzzyCompare((numTicks+1)*minStep, stepSize, stepSize) > 0: numTicks = 0 if numTicks < 1: return mediumTickIndex = -1 if numTicks > 2 and numTicks % 2: mediumTickIndex = numTicks/2 minFactor = max([np.power(logBase, minStep), float(logBase)]) for tick in ticks[QwtScaleDiv.MajorTick]: for j in range(numTicks): tick *= minFactor if j == mediumTickIndex: ticks[QwtScaleDiv.MediumTick] += [tick] else: ticks[QwtScaleDiv.MinorTick] += [tick] def align(self, interval, stepSize): """ Align an interval to a step size The limits of an interval are aligned that both are integer multiples of the step size. :param qwt.interval.QwtInterval interval: Interval :param float stepSize: Step size :return: Aligned interval """ intv = qwtLogInterval(self.base(), interval) x1 = floorEps(intv.minValue(), stepSize) if qwtFuzzyCompare(interval.minValue(), x1, stepSize) == 0: x1 = interval.minValue() x2 = ceilEps(intv.maxValue(), stepSize) if qwtFuzzyCompare(interval.maxValue(), x2, stepSize) == 0: x2 = interval.maxValue() return qwtPowInterval(self.base(), QwtInterval(x1, x2)) PythonQwt-0.5.5/qwt/interval.py0000666000000000000000000003072012617477042015225 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtInterval ----------- .. autoclass:: QwtInterval :members: """ class QwtInterval(object): """ A class representing an interval The interval is represented by 2 doubles, the lower and the upper limit. .. py:class:: QwtInterval(minValue=0., maxValue=-1., borderFlags=None) Build an interval with from min/max values :param float minValue: Minimum value :param float maxValue: Maximum value :param int borderFlags: Include/Exclude borders """ # enum BorderFlag IncludeBorders = 0x00 ExcludeMinimum = 0x01 ExcludeMaximum = 0x02 ExcludeBorders = ExcludeMinimum | ExcludeMaximum def __init__(self, minValue=0., maxValue=-1., borderFlags=None): assert not isinstance(minValue, QwtInterval) assert not isinstance(maxValue, QwtInterval) self.__minValue = float(minValue) # avoid overflows with NumPy scalars self.__maxValue = float(maxValue) # avoid overflows with NumPy scalars if borderFlags is None: self.__borderFlags = self.IncludeBorders else: self.__borderFlags = borderFlags def setInterval(self, minValue, maxValue, borderFlags): """ Assign the limits of the interval :param float minValue: Minimum value :param float maxValue: Maximum value :param int borderFlags: Include/Exclude borders """ self.__minValue = float(minValue) # avoid overflows with NumPy scalars self.__maxValue = float(maxValue) # avoid overflows with NumPy scalars self.__borderFlags = borderFlags def setBorderFlags(self, borderFlags): """ Change the border flags :param int borderFlags: Include/Exclude borders .. seealso:: :py:meth:`borderFlags()` """ self.__borderFlags = borderFlags def borderFlags(self): """ :return: Border flags .. seealso:: :py:meth:`setBorderFlags()` """ return self.__borderFlags def setMinValue(self, minValue): """ Assign the lower limit of the interval :param float minValue: Minimum value """ self.__minValue = float(minValue) # avoid overflows with NumPy scalars def setMaxValue(self, maxValue): """ Assign the upper limit of the interval :param float maxValue: Maximum value """ self.__maxValue = float(maxValue) # avoid overflows with NumPy scalars def minValue(self): """ :return: Lower limit of the interval """ return self.__minValue def maxValue(self): """ :return: Upper limit of the interval """ return self.__maxValue def isValid(self): """ A interval is valid when minValue() <= maxValue(). In case of `QwtInterval.ExcludeBorders` it is true when minValue() < maxValue() :return: True, when the interval is valid """ if (self.__borderFlags & self.ExcludeBorders) == 0: return self.__minValue <= self.__maxValue else: return self.__minValue < self.__maxValue def width(self): """ The width of invalid intervals is 0.0, otherwise the result is maxValue() - minValue(). :return: the width of an interval """ if self.isValid(): return self.__maxValue - self.__minValue else: return 0. def __and__(self, other): return self.intersect(other) def __iand__(self, other): self = self & other return self def __or__(self, other): if isinstance(other, QwtInterval): return self.unite(other) else: return self.extend(other) def __ior__(self, other): self = self | other return self def __eq__(self, other): return self.__minValue == other.__minValue and\ self.__maxValue == other.__maxValue and\ self.__borderFlags == other.__borderFlags def __ne__(self, other): return not self.__eq__(other) def isNull(self): """ :return: true, if isValid() && (minValue() >= maxValue()) """ return self.isValid() and self.__minValue >= self.__maxValue def invalidate(self): """ The limits are set to interval [0.0, -1.0] .. seealso:: :py:meth:`isValid()` """ self.__minValue = 0. self.__maxValue = -1. def normalized(self): """ Normalize the limits of the interval If maxValue() < minValue() the limits will be inverted. :return: Normalized interval .. seealso:: :py:meth:`isValid()`, :py:meth:`inverted()` """ if self.__minValue > self.__maxValue: return self.inverted() elif self.__minValue == self.__maxValue and\ self.__borderFlags == self.ExcludeMinimum: return self.inverted() else: return self def inverted(self): """ Invert the limits of the interval :return: Inverted interval .. seealso:: :py:meth:`normalized()` """ borderFlags = self.IncludeBorders if self.__borderFlags & self.ExcludeMinimum: borderFlags |= self.ExcludeMaximum if self.__borderFlags & self.ExcludeMaximum: borderFlags |= self.ExcludeMinimum return QwtInterval(self.__maxValue, self.__minValue, borderFlags) def contains(self, value): """ Test if a value is inside an interval :param float value: Value :return: true, if value >= minValue() && value <= maxValue() """ if not self.isValid(): return False elif value < self.__minValue or value > self.__maxValue: return False elif value == self.__minValue and\ self.__borderFlags & self.ExcludeMinimum: return False elif value == self.__maxValue and\ self.__borderFlags & self.ExcludeMaximum: return False else: return True def unite(self, other): """ Unite two intervals :param qwt.interval.QwtInterval other: other interval to united with :return: united interval """ if not self.isValid(): if not other.isValid(): return QwtInterval() else: return other elif not other.isValid(): return self united = QwtInterval() flags = self.IncludeBorders # minimum if self.__minValue < other.minValue(): united.setMinValue(self.__minValue) flags &= self.__borderFlags & self.ExcludeMinimum elif other.minValue() < self.__minValue: united.setMinValue(other.minValue()) flags &= other.borderFlags() & self.ExcludeMinimum else: united.setMinValue(self.__minValue) flags &= (self.__borderFlags & other.borderFlags())\ & self.ExcludeMinimum # maximum if self.__maxValue > other.maxValue(): united.setMaxValue(self.__maxValue) flags &= self.__borderFlags & self.ExcludeMaximum elif other.maxValue() > self.__maxValue: united.setMaxValue(other.maxValue()) flags &= other.borderFlags() & self.ExcludeMaximum else: united.setMaxValue(self.__maxValue) flags &= self.__borderFlags & other.borderFlags()\ & self.ExcludeMaximum united.setBorderFlags(flags) return united def intersect(self, other): """ Intersect two intervals :param qwt.interval.QwtInterval other: other interval to intersect with :return: intersected interval """ if not other.isValid() or not self.isValid(): return QwtInterval() i1 = self i2 = other if i1.minValue() > i2.minValue(): i1, i2 = i2, i1 elif i1.minValue() == i2.minValue(): if i1.borderFlags() & self.ExcludeMinimum: i1, i2 = i2, i1 if i1.maxValue() < i2.maxValue(): return QwtInterval() if i1.maxValue() == i2.minValue(): if i1.borderFlags() & self.ExcludeMaximum or\ i2.borderFlags() & self.ExcludeMinimum: return QwtInterval() intersected = QwtInterval() flags = self.IncludeBorders intersected.setMinValue(i2.minValue()) flags |= i2.borderFlags() & self.ExcludeMinimum if i1.maxValue() < i2.maxValue(): intersected.setMaxValue(i1.maxValue()) flags |= i1.borderFlags() & self.ExcludeMaximum elif i2.maxValue() < i1.maxValue(): intersected.setMaxValue(i2.maxValue()) flags |= i2.borderFlags() & self.ExcludeMaximum else: # i1.maxValue() == i2.maxValue() intersected.setMaxValue(i1.maxValue()) flags |= i1.borderFlags() & i2.borderFlags() & self.ExcludeMaximum intersected.setBorderFlags(flags) return intersected def intersects(self, other): """ Test if two intervals overlap :param qwt.interval.QwtInterval other: other interval :return: True, when the intervals are intersecting """ if not other.isValid() or not self.isValid(): return False i1 = self i2 = other if i1.minValue() > i2.minValue(): i1, i2 = i2, i1 elif i1.minValue() == i2.minValue() and\ i1.borderFlags() & self.ExcludeMinimum: i1, i2 = i2, i1 if i1.maxValue() > i2.minValue(): return True elif i1.maxValue() == i2.minValue(): return i1.borderFlags() & self.ExcludeMaximum and\ i2.borderFlags() & self.ExcludeMinimum return False def symmetrize(self, value): """ Adjust the limit that is closer to value, so that value becomes the center of the interval. :param float value: Center :return: Interval with value as center """ if not self.isValid(): return self delta = max([abs(value-self.__maxValue), abs(value-self.__minValue)]) return QwtInterval(value-delta, value+delta) def limited(self, lowerBound, upperBound): """ Limit the interval, keeping the border modes :param float lowerBound: Lower limit :param float upperBound: Upper limit :return: Limited interval """ if not self.isValid() or lowerBound > upperBound: return QwtInterval() minValue = max([self.__minValue, lowerBound]) minValue = min([minValue, upperBound]) maxValue = max([self.__maxValue, lowerBound]) maxValue = min([maxValue, upperBound]) return QwtInterval(minValue, maxValue, self.__borderFlags) def extend(self, value): """ Extend the interval If value is below minValue(), value becomes the lower limit. If value is above maxValue(), value becomes the upper limit. extend() has no effect for invalid intervals :param float value: Value :return: extended interval """ if not self.isValid(): return self return QwtInterval(min([value, self.__minValue]), max([value, self.__maxValue])) PythonQwt-0.5.5/qwt/color_map.py0000666000000000000000000002710112605040216015335 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ Color maps ---------- QwtColorMap ~~~~~~~~~~~ .. autoclass:: QwtColorMap :members: QwtLinearColorMap ~~~~~~~~~~~~~~~~~ .. autoclass:: QwtLinearColorMap :members: QwtAlphaColorMap ~~~~~~~~~~~~~~~~ .. autoclass:: QwtAlphaColorMap :members: """ from qwt.qt.QtGui import QColor, qRed, qGreen, qBlue, qRgb, qRgba, qAlpha from qwt.qt.QtCore import Qt, qIsNaN class ColorStop(object): def __init__(self, pos=0., color=None): self.pos = pos if color is None: self.rgb = 0 else: self.rgb = color.rgba() self.r = qRed(self.rgb) self.g = qGreen(self.rgb) self.b = qBlue(self.rgb) self.a = qAlpha(self.rgb) # when mapping a value to rgb we will have to calcualate: # - const int v = int( ( s1.v0 + ratio * s1.vStep ) + 0.5 ); # Thus adding 0.5 ( for rounding ) can be done in advance self.r0 = self.r + 0.5 self.g0 = self.g + 0.5 self.b0 = self.b + 0.5 self.a0 = self.a + 0.5 self.rStep = self.gStep = self.bStep = self.aStep = 0. self.posStep = 0. def updateSteps(self, nextStop): self.rStep = nextStop.r - self.r self.gStep = nextStop.g - self.g self.bStep = nextStop.b - self.b self.aStep = nextStop.a - self.a self.posStep = nextStop.pos - self.pos class ColorStops(object): def __init__(self): self.__doAlpha = False self.__stops = [] self.stops = [] def insert(self, pos, color): if pos < 0. or pos > 1.: return if len(self.__stops) == 0: index = 0 self.__stops = [None] else: index = self.findUpper(pos) if index == len(self.__stops) or\ abs(self.__stops[index].pos-pos) >= .001: self.__stops.append(None) for i in range(len(self.__stops)-1, index, -1): self.__stops[i] = self.__stops[i-1] self.__stops[index] = ColorStop(pos, color) if color.alpha() != 255: self.__doAlpha = True if index > 0: self.__stops[index-1].updateSteps(self.__stops[index]) if index < len(self.__stops)-1: self.__stops[index].updateSteps(self.__stops[index+1]) def stops(self): return [stop.pos for stop in self.__stops] def findUpper(self, pos): index = 0 n = len(self.__stops) stops = self.__stops while n > 0: half = n >> 1 middle = index + half if stops[middle].pos <= pos: index = middle + 1 n -= half + 1 else: n = half return index def rgb(self, mode, pos): if pos <= 0.: return self.__stops[0].rgb if pos >= 1.0: return self.__stops[-1].rgb index = self.findUpper(pos) if mode == QwtLinearColorMap.FixedColors: return self.__stops[index-1].rgb else: s1 = self.__stops[index-1] ratio = (pos-s1.pos)/s1.posStep r = int(s1.r0 + ratio*s1.rStep) g = int(s1.g0 + ratio*s1.gStep) b = int(s1.b0 + ratio*s1.bStep) if self.__doAlpha: if s1.aStep: a = int(s1.a0 + ratio*s1.aStep) return qRgba(r, g, b, a) else: return qRgba(r, g, b, s1.a) else: return qRgb(r, g, b) class QwtColorMap(object): """ QwtColorMap is used to map values into colors. For displaying 3D data on a 2D plane the 3rd dimension is often displayed using colors, like f.e in a spectrogram. Each color map is optimized to return colors for only one of the following image formats: * `QImage.Format_Indexed8` * `QImage.Format_ARGB32` .. py:class:: QwtColorMap(format_) :param int format_: Preferred format of the color map (:py:data:`QwtColorMap.RGB` or :py:data:`QwtColorMap.Indexed`) .. seealso :: :py:data:`qwt.QwtScaleWidget` """ # enum Format RGB, Indexed = list(range(2)) def __init__(self, format_=None): if format_ is None: format_ = self.RGB self.__format = format_ def color(self, interval, value): """ Map a value into a color :param qwt.interval.QwtInterval interval: valid interval for value :param float value: value :return: the color corresponding to value .. warning :: This method is slow for Indexed color maps. If it is necessary to map many values, its better to get the color table once and find the color using `colorIndex()`. """ if self.__format == self.RGB: return QColor.fromRgba(self.rgb(interval, value)) else: index = self.colorIndex(interval, value) return self.colorTable(interval)[index] def format(self): return self.__format def colorTable(self, interval): """ Build and return a color map of 256 colors :param qwt.interval.QwtInterval interval: range for the values :return: a color table, that can be used for a `QImage` The color table is needed for rendering indexed images in combination with using `colorIndex()`. """ table = [0] * 256 if interval.isValid(): step = interval.width()/(len(table)-1) for i in range(len(table)): table[i] = self.rgb(interval, interval.minValue()+step*i) return table def colorIndex(self, interval, value): raise NotImplementedError class QwtLinearColorMap_PrivateData(object): def __init__(self): self.colorStops = None self.mode = None class QwtLinearColorMap(QwtColorMap): """ Build a linear color map with two stops. .. py:class:: QwtLinearColorMap(format_) Build a color map with two stops at 0.0 and 1.0. The color at 0.0 is `Qt.blue`, at 1.0 it is `Qt.yellow`. :param int format_: Preferred format of the color map (:py:data:`QwtColorMap.RGB` or :py:data:`QwtColorMap.Indexed`) .. py:class:: QwtLinearColorMap(color1, color2, [format_=QwtColorMap.RGB]): Build a color map with two stops at 0.0 and 1.0. :param QColor color1: color at 0. :param QColor color2: color at 1. :param int format_: Preferred format of the color map (:py:data:`QwtColorMap.RGB` or :py:data:`QwtColorMap.Indexed`) """ # enum Mode FixedColors, ScaledColors = list(range(2)) def __init__(self, *args): color1, color2 = QColor(Qt.blue), QColor(Qt.yellow) format_ = QwtColorMap.RGB if len(args) == 1: format_, = args elif len(args) == 2: color1, color2 = args elif len(args) == 3: color1, color2, format_ = args elif len(args) != 0: raise TypeError("%s() takes 0, 1, 2 or 3 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) super(QwtLinearColorMap, self).__init__(format_) self.__data = QwtLinearColorMap_PrivateData() self.__data.mode = self.ScaledColors self.setColorInterval(color1, color2) def setMode(self, mode): """ Set the mode of the color map :param int mode: :py:data:`QwtLinearColorMap.FixedColors` or :py:data:`QwtLinearColorMap.ScaledColors` `FixedColors` means the color is calculated from the next lower color stop. `ScaledColors` means the color is calculated by interpolating the colors of the adjacent stops. """ self.__data.mode = mode def mode(self): """ :return: the mode of the color map .. seealso :: :py:meth:`QwtLinearColorMap.setMode` """ return self.__data.mode def setColorInterval(self, color1, color2): self.__data.colorStops = ColorStops() self.__data.colorStops.insert(0., QColor(color1)) self.__data.colorStops.insert(1., QColor(color2)) def addColorStop(self, value, color): if value >= 0. and value <= 1.: self.__data.colorStops.insert(value, QColor(color)) def colorStops(self): return self.__data.colorStops.stops() def color1(self): return QColor(self.__data.colorStops.rgb(self.__data.mode, 0.)) def color2(self): return QColor(self.__data.colorStops.rgb(self.__data.mode, 1.)) def rgb(self, interval, value): if qIsNaN(value): return 0 width = interval.width() if width <= 0.: return 0 ratio = (value-interval.minValue())/width return self.__data.colorStops.rgb(self.__data.mode, ratio) def colorIndex(self, interval, value): width = interval.width() if qIsNaN(value) or width <= 0. or value <= interval.minValue(): return 0 if value >= interval.maxValue(): return 255 ratio = (value-interval.minValue())/width if self.__data.mode == self.FixedColors: return int(ratio*255) else: return int(ratio*255+.5) class QwtAlphaColorMap_PrivateData(object): def __init__(self): self.color = None self.rgb = None self.rgbMax = None class QwtAlphaColorMap(QwtColorMap): """ QwtAlphaColorMap varies the alpha value of a color .. py:class:: QwtAlphaColorMap(color) Build a color map varying the alpha value of a color. :param QColor color: color of the map """ def __init__(self, color): super(QwtAlphaColorMap, self).__init__(QwtColorMap.RGB) self.__data = QwtAlphaColorMap_PrivateData() self.setColor(color) def setColor(self, color): """ Set the color of the map :param QColor color: color of the map """ self.__data.color = color self.__data.rgb = color.rgb() & qRgba(255, 255, 255, 0) self.__data.rgbMax = self.__data.rgb | ( 255 << 24 ) def color(self): """ :return: the color of the map .. seealso :: :py:meth:`QwtAlphaColorMap.setColor` """ return self.__data.color() def rgb(self, interval, value): if qIsNaN(value): return 0 width = interval.width() if width <= 0.: return 0 if value <= interval.minValue(): return self.__data.rgb if value >= interval.maxValue(): return self.__data.rgbMax ratio = (value-interval.minValue())/width return self.__data.rgb | (int(round(255*ratio)) << 24) def colorIndex(self, interval, value): return 0 PythonQwt-0.5.5/qwt/scale_map.py0000666000000000000000000002307112646231674015327 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtScaleMap ----------- .. autoclass:: QwtScaleMap :members: """ from qwt.math import qwtFuzzyCompare from qwt.qt.QtCore import QRectF, QPointF class QwtScaleMap(object): """ A scale map `QwtScaleMap` offers transformations from the coordinate system of a scale into the linear coordinate system of a paint device and vice versa. The scale and paint device intervals are both set to [0,1]. .. py:class:: QwtScaleMap([other=None]) Constructor (eventually, copy constructor) :param qwt.scale_map.QwtScaleMap other: Other scale map .. py:class:: QwtScaleMap(p1, p2, s1, s2) Constructor (was provided by `PyQwt` but not by `Qwt`) :param int p1: First border of the paint interval :param int p2: Second border of the paint interval :param float s1: First border of the scale interval :param float s2: Second border of the scale interval """ def __init__(self, *args): self.__transform = None # QwtTransform self.__s1 = 0. self.__s2 = 1. self.__p1 = 0. self.__p2 = 1. other = None if len(args) == 1: other, = args elif len(args) == 4: p1, p2, s1, s2 = args self.__s1 = s1 self.__s2 = s2 self.__p1 = p1 self.__p2 = p2 elif len(args) != 0: raise TypeError("%s() takes 0, 1, or 4 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) if other is None: self.__cnv = 1. self.__ts1 = 0. else: self.__s1 = other.__s1 self.__s2 = other.__s2 self.__p1 = other.__p1 self.__p2 = other.__p2 self.__cnv = other.__cnv self.__ts1 = other.__ts1 if other.__transform: self.__transform = other.__transform.copy() def __eq__(self, other): return self.__s1 == other.__s1 and\ self.__s2 == other.__s2 and\ self.__p1 == other.__p1 and\ self.__p2 == other.__p2 and\ self.__cnv == other.__cnv and\ self.__ts1 == other.__ts1 def s1(self): """ :return: First border of the scale interval """ return self.__s1 def s2(self): """ :return: Second border of the scale interval """ return self.__s2 def p1(self): """ :return: First border of the paint interval """ return self.__p1 def p2(self): """ :return: Second border of the paint interval """ return self.__p2 def pDist(self): """ :return: `abs(p2() - p1())` """ return abs(self.__p2 - self.__p1) def sDist(self): """ :return: `abs(s2() - s1())` """ return abs(self.__s2 - self.__s1) def transform_scalar(self, s): """ Transform a point related to the scale interval into an point related to the interval of the paint device :param float s: Value relative to the coordinates of the scale :return: Transformed value .. seealso:: :py:meth:`invTransform_scalar()` """ if self.__transform: s = self.__transform.transform(s) return self.__p1 + (s - self.__ts1)*self.__cnv def invTransform_scalar(self, p): """ Transform an paint device value into a value in the interval of the scale. :param float p: Value relative to the coordinates of the paint device :return: Transformed value .. seealso:: :py:meth:`transform_scalar()` """ if self.__cnv == 0: s = self.__ts1 # avoid divide by zero else: s = self.__ts1 + ( p - self.__p1 ) / self.__cnv if self.__transform: s = self.__transform.invTransform(s) return s def isInverting(self): """ :return: True, when ( p1() < p2() ) != ( s1() < s2() ) """ return ( self.__p1 < self.__p2 ) != ( self.__s1 < self.__s2 ) def setTransformation(self, transform): """ Initialize the map with a transformation :param qwt.transform.QwtTransform transform: Transformation """ if self.__transform != transform: self.__transform = transform self.setScaleInterval(self.__s1, self.__s2) def transformation(self): """ :return: the transformation """ return self.__transform def setScaleInterval(self, s1, s2): """ Specify the borders of the scale interval :param float s1: first border :param float s2: second border .. warning:: Scales might be aligned to transformation depending boundaries """ self.__s1 = s1 self.__s2 = s2 if self.__transform: self.__s1 = self.__transform.bounded(self.__s1) self.__s2 = self.__transform.bounded(self.__s2) self.updateFactor() def setPaintInterval(self, p1, p2): """ Specify the borders of the paint device interval :param float p1: first border :param float p2: second border """ self.__p1 = p1 self.__p2 = p2 self.updateFactor() def updateFactor(self): self.__ts1 = self.__s1 ts2 = self.__s2 if self.__transform: self.__ts1 = self.__transform.transform(self.__ts1) ts2 = self.__transform.transform(ts2) self.__cnv = 1. if self.__ts1 != ts2: self.__cnv = (self.__p2 - self.__p1)/(ts2 - self.__ts1) def transform(self, *args): """ Transform a rectangle from scale to paint coordinates .. py:method:: transform(scalar) :param float scalar: Scalar .. py:method:: transform(xMap, yMap, rect) Transform a rectangle from scale to paint coordinates :param qwt.scale_map.QwtScaleMap xMap: X map :param qwt.scale_map.QwtScaleMap yMap: Y map :param QRectF rect: Rectangle in paint coordinates .. py:method:: transform(xMap, yMap, pos) Transform a point from scale to paint coordinates :param qwt.scale_map.QwtScaleMap xMap: X map :param qwt.scale_map.QwtScaleMap yMap: Y map :param QPointF pos: Position in scale coordinates Scalar: scalemap.transform(scalar) Point (QPointF): scalemap.transform(xMap, yMap, pos) Rectangle (QRectF): scalemap.transform(xMap, yMap, rect) .. seealso:: :py:meth:`invTransform()` """ if len(args) == 1: # Scalar transform return self.transform_scalar(args[0]) elif len(args) == 3 and isinstance(args[2], QPointF): xMap, yMap, pos = args return QPointF(xMap.transform(pos.x()), yMap.transform(pos.y())) elif len(args) == 3 and isinstance(args[2], QRectF): xMap, yMap, rect = args x1 = xMap.transform(rect.left()) x2 = xMap.transform(rect.right()) y1 = yMap.transform(rect.top()) y2 = yMap.transform(rect.bottom()) if x2 < x1: x1, x2 = x2, x1 if y2 < y1: y1, y2 = y2, y1 if qwtFuzzyCompare(x1, 0., x2-x1) == 0: x1 = 0. if qwtFuzzyCompare(x2, 0., x2-x1) == 0: x2 = 0. if qwtFuzzyCompare(y1, 0., y2-y1) == 0: y1 = 0. if qwtFuzzyCompare(y2, 0., y2-y1) == 0: y2 = 0. return QRectF(x1, y1, x2-x1+1, y2-y1+1) else: raise TypeError("%s().transform() takes 1 or 3 argument(s) (%s "\ "given)" % (self.__class__.__name__, len(args))) def invTransform(self, *args): """Transform from paint to scale coordinates Scalar: scalemap.invTransform(scalar) Point (QPointF): scalemap.invTransform(xMap, yMap, pos) Rectangle (QRectF): scalemap.invTransform(xMap, yMap, rect) """ if len(args) == 1: # Scalar transform return self.invTransform_scalar(args[0]) elif isinstance(args[2], QPointF): xMap, yMap, pos = args return QPointF(xMap.invTransform(pos.x()), yMap.invTransform(pos.y())) elif isinstance(args[2], QRectF): xMap, yMap, rect = args x1 = xMap.invTransform(rect.left()) x2 = xMap.invTransform(rect.right()-1) y1 = yMap.invTransform(rect.top()) y2 = yMap.invTransform(rect.bottom()-1) r = QRectF(x1, y1, x2-x1, y2-y1) return r.normalized() PythonQwt-0.5.5/qwt/legend.py0000666000000000000000000007630112615225450014634 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtLegend --------- .. autoclass:: QwtLegendData :members: .. autoclass:: QwtLegendLabel :members: .. autoclass:: QwtLegend :members: """ import numpy as np from qwt.qt.QtGui import (QFrame, QScrollArea, QWidget, QVBoxLayout, QPalette, QApplication, QStyleOption, QStyle, QPixmap, QPainter, qDrawWinButton) from qwt.qt.QtCore import Signal, QEvent, QSize, Qt, QRect, QRectF, QPoint from qwt.text import QwtText, QwtTextLabel from qwt.dyngrid_layout import QwtDynGridLayout from qwt.painter import QwtPainter class QwtLegendData(object): """ Attributes of an entry on a legend `QwtLegendData` is an abstract container ( like `QAbstractModel` ) to exchange attributes, that are only known between to the plot item and the legend. By overloading `QwtPlotItem.legendData()` any other set of attributes could be used, that can be handled by a modified ( or completely different ) implementation of a legend. .. seealso:: :py:class:`qwt.legend.QwtLegend` .. note:: The stockchart example implements a legend as a tree with checkable items """ # enum Mode ReadOnly, Clickable, Checkable = list(range(3)) # enum Role ModeRole, TitleRole, IconRole = list(range(3)) UserRole = 32 def __init__(self): self.__map = {} def setValues(self, map_): """ Set the legend attributes :param dict map\_: Values .. seealso:: :py:meth:`values()` """ self.__map = map_ def values(self): """ :return: Legend attributes .. seealso:: :py:meth:`setValues()` """ return self.__map def hasRole(self, role): """ :param int role: Attribute role :return: True, when the internal map has an entry for role """ return role in self.__map def setValue(self, role, data): """ Set an attribute value :param int role: Attribute role :param QVariant data: Attribute value .. seealso:: :py:meth:`value()` """ self.__map[role] = data def value(self, role): """ :param int role: Attribute role :return: Attribute value for a specific role .. seealso:: :py:meth:`setValue()` """ return self.__map.get(role) def isValid(self): """ :return: True, when the internal map is empty """ return len(self.__map) != 0 def title(self): """ :return: Value of the TitleRole attribute """ titleValue = self.value(QwtLegendData.TitleRole) if isinstance(titleValue, QwtText): text = titleValue else: text.setText(titleValue) return text def icon(self): """ :return: Value of the IconRole attribute """ return self.value(QwtLegendData.IconRole) def mode(self): """ :return: Value of the ModeRole attribute """ modeValue = self.value(QwtLegendData.ModeRole) if isinstance(modeValue, int): return modeValue return QwtLegendData.ReadOnly BUTTONFRAME = 2 MARGIN = 2 def buttonShift(w): option = QStyleOption() option.initFrom(w) ph = w.style().pixelMetric(QStyle.PM_ButtonShiftHorizontal, option, w) pv = w.style().pixelMetric(QStyle.PM_ButtonShiftVertical, option, w) return QSize(ph, pv) class QwtLegendLabel_PrivateData(object): def __init__(self): self.itemMode = QwtLegendData.ReadOnly self.isDown = False self.spacing = MARGIN self.legendData = QwtLegendData() self.icon = QPixmap() class QwtLegendLabel(QwtTextLabel): """A widget representing something on a QwtLegend.""" clicked = Signal() pressed = Signal() released = Signal() checked = Signal(bool) def __init__(self, parent=None): QwtTextLabel.__init__(self, parent) self.__data = QwtLegendLabel_PrivateData() self.setMargin(MARGIN) self.setIndent(MARGIN) def setData(self, legendData): """ Set the attributes of the legend label :param QwtLegendData legendData: Attributes of the label .. seealso:: :py:meth:`data()` """ self.__data.legendData = legendData doUpdate = self.updatesEnabled() self.setUpdatesEnabled(False) self.setText(legendData.title()) icon = legendData.icon() if icon is not None: self.setIcon(icon.toPixmap()) if legendData.hasRole(QwtLegendData.ModeRole): self.setItemMode(legendData.mode()) if doUpdate: self.setUpdatesEnabled(True) self.update() def data(self): """ :return: Attributes of the label .. seealso:: :py:meth:`setData()`, :py:meth:`qwt.plot.QwtPlotItem.legendData()` """ return self.__data.legendData def setText(self, text): """ Set the text to the legend item :param qwt.text.QwtText text: Text label .. seealso:: :py:meth:`text()` """ flags = Qt.AlignLeft|Qt.AlignVCenter|Qt.TextExpandTabs|Qt.TextWordWrap txt = text #TODO: WTF? txt.setRenderFlags(flags) QwtTextLabel.setText(self, text) def setItemMode(self, mode): """ Set the item mode. The default is `QwtLegendData.ReadOnly`. :param int mode: Item mode .. seealso:: :py:meth:`itemMode()` """ if mode != self.__data.itemMode: self.__data.itemMode = mode self.__data.isDown = False self.setFocusPolicy(Qt.TabFocus if mode != QwtLegendData.ReadOnly else Qt.NoFocus) self.setMargin(BUTTONFRAME+MARGIN) self.updateGeometry() def itemMode(self): """ :return: Item mode .. seealso:: :py:meth:`setItemMode()` """ return self.__data.itemMode def setIcon(self, icon): """ Assign the icon :param QPixmap icon: Pixmap representing a plot item .. seealso:: :py:meth:`icon()`, :py:meth:`qwt.plot.QwtPlotItem.legendIcon()` """ self.__data.icon = icon indent = self.margin()+self.__data.spacing if icon.width() > 0: indent += icon.width()+self.__data.spacing self.setIndent(indent) def icon(self): """ :return: Pixmap representing a plot item .. seealso:: :py:meth:`setIcon()` """ return self.__data.icon def setSpacing(self, spacing): """ Change the spacing between icon and text :param int spacing: Spacing .. seealso:: :py:meth:`spacing()`, :py:meth:`qwt.text.QwtTextLabel.margin()` """ spacing = max([spacing, 0]) if spacing != self.__data.spacing: self.__data.spacing = spacing indent = self.margin()+self.__data.spacing if self.__data.icon.width() > 0: indent += self.__data.icon.width()+self.__data.spacing self.setIndent(indent) def spacing(self): """ :return: Spacing between icon and text .. seealso:: :py:meth:`setSpacing()` """ return self.__data.spacing def setChecked(self, on): """ Check/Uncheck a the item :param bool on: check/uncheck .. seealso:: :py:meth:`isChecked()`, :py:meth:`setItemMode()` """ if self.__data.itemMode == QwtLegendData.Checkable: isBlocked = self.signalsBlocked() self.blockSignals(True) self.setDown(on) self.blockSignals(isBlocked) def isChecked(self): """ :return: true, if the item is checked .. seealso:: :py:meth:`setChecked()` """ return self.__data.itemMode == QwtLegendData.Checkable and self.isDown() def setDown(self, down): """ Set the item being down :param bool on: true, if the item is down .. seealso:: :py:meth:`isDown()` """ if down == self.__data.isDown: return self.__data.isDown = down self.update() if self.__data.itemMode == QwtLegendData.Clickable: if self.__data.isDown: self.pressed.emit() else: self.released.emit() self.clicked.emit() if self.__data.itemMode == QwtLegendData.Checkable: self.checked.emit(self.__data.isDown) def isDown(self): """ :return: true, if the item is down .. seealso:: :py:meth:`setDown()` """ return self.__data.isDown def sizeHint(self): """ :return: a size hint """ sz = QwtTextLabel.sizeHint(self) sz.setHeight(max([sz.height(), self.__data.icon.height()+4])) if self.__data.itemMode != QwtLegendData.ReadOnly: sz += buttonShift(self) sz = sz.expandedTo(QApplication.globalStrut()) return sz def paintEvent(self, e): cr = self.contentsRect() painter = QPainter(self) painter.setClipRegion(e.region()) if self.__data.isDown: qDrawWinButton(painter, 0, 0, self.width(), self.height(), self.palette(), True) painter.save() if self.__data.isDown: shiftSize = buttonShift(self) painter.translate(shiftSize.width(), shiftSize.height()) painter.setClipRect(cr) self.drawContents(painter) if not self.__data.icon.isNull(): iconRect = QRect(cr) iconRect.setX(iconRect.x()+self.margin()) if self.__data.itemMode != QwtLegendData.ReadOnly: iconRect.setX(iconRect.x()+BUTTONFRAME) iconRect.setSize(self.__data.icon.size()) iconRect.moveCenter(QPoint(iconRect.center().x(), cr.center().y())) painter.drawPixmap(iconRect, self.__data.icon) painter.restore() def mousePressEvent(self, e): if e.button() == Qt.LeftButton: if self.__data.itemMode == QwtLegendData.Clickable: self.setDown(True) return elif self.__data.itemMode == QwtLegendData.Checkable: self.setDown(not self.isDown()) return QwtTextLabel.mousePressEvent(self, e) def mouseReleaseEvent(self, e): if e.button() == Qt.LeftButton: if self.__data.itemMode == QwtLegendData.Clickable: self.setDown(False) return elif self.__data.itemMode == QwtLegendData.Checkable: return QwtTextLabel.mouseReleaseEvent(self, e) def keyPressEvent(self, e): if e.key() == Qt.Key_Space: if self.__data.itemMode == QwtLegendData.Clickable: if not e.isAutoRepeat(): self.setDown(True) return elif self.__data.itemMode == QwtLegendData.Checkable: if not e.isAutoRepeat(): self.setDown(not self.isDown()) return QwtTextLabel.keyPressEvent(self, e) def keyReleaseEvent(self, e): if e.key() == Qt.Key_Space: if self.__data.itemMode == QwtLegendData.Clickable: if not e.isAutoRepeat(): self.setDown(False) return elif self.__data.itemMode == QwtLegendData.Checkable: return QwtTextLabel.keyReleaseEvent(self, e) class QwtAbstractLegend(QFrame): def __init__(self, parent): QFrame.__init__(self, parent) def renderLegend(self, painter, rect, fillBackground): raise NotImplementedError def isEmpty(self): return 0 def scrollExtent(self, orientation): return 0 def updateLegend(self, itemInfo, data): raise NotImplementedError class Entry(object): def __init__(self): self.itemInfo = None self.widgets = [] class QwtLegendMap(object): def __init__(self): self.__entries = [] def isEmpty(self): return len(self.__entries) == 0 def insert(self, itemInfo, widgets): for entry in self.__entries: if entry.itemInfo == itemInfo: entry.widgets = widgets return newEntry = Entry() newEntry.itemInfo = itemInfo newEntry.widgets = widgets self.__entries += [newEntry] def remove(self, itemInfo): for entry in self.__entries[:]: if entry.itemInfo == itemInfo: self.__entries.remove(entry) return def removeWidget(self, widget): for entry in self.__entries: while widget in entry.widgets: entry.widgets.remove(widget) def itemInfo(self, widget): if widget is not None: for entry in self.__entries: if widget in entry.widgets: return entry.itemInfo def legendWidgets(self, itemInfo): if itemInfo is not None: for entry in self.__entries: if entry.itemInfo == itemInfo: return entry.widgets return [] class LegendView(QScrollArea): def __init__(self, parent): QScrollArea.__init__(self, parent) self.gridLayout = None self.contentsWidget = QWidget(self) self.contentsWidget.setObjectName("QwtLegendViewContents") self.setWidget(self.contentsWidget) self.setWidgetResizable(False) self.viewport().setObjectName("QwtLegendViewport") self.contentsWidget.setAutoFillBackground(False) self.viewport().setAutoFillBackground(False) def event(self, event): if event.type() == QEvent.PolishRequest: self.setFocusPolicy(Qt.NoFocus) if event.type() == QEvent.Resize: cr = self.contentsRect() w = cr.width() h = self.contentsWidget.heightForWidth(cr.width()) if h > w: w -= self.verticalScrollBar().sizeHint().width() h = self.contentsWidget.heightForWidth(w) self.contentsWidget.resize(w, h) return QScrollArea.event(self, event) def viewportEvent(self, event): ok = QScrollArea.viewportEvent(self, event) if event.type() == QEvent.Resize: self.layoutContents() return ok def viewportSize(self, w, h): sbHeight = self.horizontalScrollBar().sizeHint().height() sbWidth = self.verticalScrollBar().sizeHint().width() cw = self.contentsRect().width() ch = self.contentsRect().height() vw = cw vh = ch if w > vw: vh -= sbHeight if h > vh: vw -= sbWidth if w > vw and vh == ch: vh -= sbHeight return QSize(vw, vh) def layoutContents(self): tl = self.gridLayout if tl is None: return visibleSize = self.viewport().contentsRect().size() margins = tl.contentsMargins() margin_w = margins.left() + margins.right() minW = int(tl.maxItemWidth()+margin_w) w = max([visibleSize.width(), minW]) h = max([tl.heightForWidth(w), visibleSize.height()]) vpWidth = self.viewportSize(w, h).width() if w > vpWidth: w = max([vpWidth, minW]) h = max([tl.heightForWidth(w), visibleSize.height()]) self.contentsWidget.resize(w, h) class QwtLegend_PrivateData(object): def __init__(self): self.itemMode = QwtLegendData.ReadOnly self.view = None self.itemMap = QwtLegendMap() class QwtLegend(QwtAbstractLegend): """ The legend widget The QwtLegend widget is a tabular arrangement of legend items. Legend items might be any type of widget, but in general they will be a QwtLegendLabel. .. seealso :: :py:class`qwt.legend.QwtLegendLabel`, :py:class`qwt.plot.QwtPlotItem`, :py:class`qwt.plot.QwtPlot` .. py:class:: QwtLegend([parent=None]) Constructor :param QWidget parent: Parent widget .. py:data:: clicked A signal which is emitted when the user has clicked on a legend label, which is in `QwtLegendData.Clickable` mode. :param itemInfo: Info for the item item of the selected legend item :param index: Index of the legend label in the list of widgets that are associated with the plot item .. note:: Clicks are disabled as default .. py:data:: checked A signal which is emitted when the user has clicked on a legend label, which is in `QwtLegendData.Checkable` mode :param itemInfo: Info for the item of the selected legend label :param index: Index of the legend label in the list of widgets that are associated with the plot item :param on: True when the legend label is checked .. note:: Clicks are disabled as default """ clicked = Signal("PyQt_PyObject", int) checked = Signal("PyQt_PyObject", bool, int) def __init__(self, parent=None): QwtAbstractLegend.__init__(self, parent) self.setFrameStyle(QFrame.NoFrame) self.__data = QwtLegend_PrivateData() self.__data.view = LegendView(self) self.__data.view.setObjectName("QwtLegendView") self.__data.view.setFrameStyle(QFrame.NoFrame) gridLayout = QwtDynGridLayout(self.__data.view.contentsWidget) gridLayout.setAlignment(Qt.AlignHCenter|Qt.AlignTop) self.__data.view.gridLayout = gridLayout self.__data.view.contentsWidget.installEventFilter(self) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self.__data.view) def setMaxColumns(self, numColumns): """ Set the maximum number of entries in a row F.e when the maximum is set to 1 all items are aligned vertically. 0 means unlimited :param int numColumns: Maximum number of entries in a row .. seealso:: :py:meth:`maxColumns()`, :py:meth:`QwtDynGridLayout.setMaxColumns()` """ tl = self.__data.view.gridLayout if tl is not None: tl.setMaxColumns(numColumns) def maxColumns(self): """ :return: Maximum number of entries in a row .. seealso:: :py:meth:`setMaxColumns()`, :py:meth:`QwtDynGridLayout.maxColumns()` """ tl = self.__data.view.gridLayout if tl is not None: return tl.maxColumns() return 0 def setDefaultItemMode(self, mode): """ Set the default mode for legend labels Legend labels will be constructed according to the attributes in a `QwtLegendData` object. When it doesn't contain a value for the `QwtLegendData.ModeRole` the label will be initialized with the default mode of the legend. :param int mode: Default item mode .. seealso:: :py:meth:`itemMode()`, :py:meth:`QwtLegendData.value()`, :py:meth:`QwtPlotItem::legendData()` ... note:: Changing the mode doesn't have any effect on existing labels. """ self.__data.itemMode = mode def defaultItemMode(self): """ :return: Default item mode .. seealso:: :py:meth:`setDefaultItemMode()` """ return self.__data.itemMode def contentsWidget(self): """ The contents widget is the only child of the viewport of the internal `QScrollArea` and the parent widget of all legend items. :return: Container widget of the legend items """ return self.__data.view.contentsWidget def horizontalScrollBar(self): """ :return: Horizontal scrollbar .. seealso:: :py:meth:`verticalScrollBar()` """ return self.__data.view.horizontalScrollBar() def verticalScrollBar(self): """ :return: Vertical scrollbar .. seealso:: :py:meth:`horizontalScrollBar()` """ return self.__data.view.verticalScrollBar() def updateLegend(self, itemInfo, data): """ Update the entries for an item :param QVariant itemInfo: Info for an item :param list data: Default item mode """ widgetList = self.legendWidgets(itemInfo) if len(widgetList) != len(data): contentsLayout = self.__data.view.gridLayout while len(widgetList) > len(data): w = widgetList.pop(-1) contentsLayout.removeWidget(w) w.hide() w.deleteLater() for i in range(len(widgetList), len(data)): widget = self.createWidget(data[i]) if contentsLayout is not None: contentsLayout.addWidget(widget) if self.isVisible(): widget.setVisible(True) widgetList.append(widget) if not widgetList: self.__data.itemMap.remove(itemInfo) else: self.__data.itemMap.insert(itemInfo, widgetList) self.updateTabOrder() for i in range(len(data)): self.updateWidget(widgetList[i], data[i]) def createWidget(self, data): """ Create a widget to be inserted into the legend The default implementation returns a `QwtLegendLabel`. :param QwtLegendData data: Attributes of the legend entry :return: Widget representing data on the legend ... note:: updateWidget() will called soon after createWidget() with the same attributes. """ label = QwtLegendLabel() label.setItemMode(self.defaultItemMode()) label.clicked.connect(lambda: self.itemClicked(label)) label.checked.connect(lambda state: self.itemChecked(state, label)) return label def updateWidget(self, widget, data): """ Update the widget :param QWidget widget: Usually a QwtLegendLabel :param QwtLegendData data: Attributes to be displayed .. seealso:: :py:meth:`createWidget()` ... note:: When widget is no QwtLegendLabel updateWidget() does nothing. """ label = widget #TODO: cast to QwtLegendLabel! if label is not None: label.setData(data) if data.value(QwtLegendData.ModeRole) is None: label.setItemMode(self.defaultItemMode()) def updateTabOrder(self): contentsLayout = self.__data.view.gridLayout if contentsLayout is not None: w = None for i in range(contentsLayout.count()): item = contentsLayout.itemAt(i) if w is not None and item.widget(): QWidget.setTabOrder(w, item.widget()) w = item.widget() def sizeHint(self): """Return a size hint""" hint = self.__data.view.contentsWidget.sizeHint() hint += QSize(2*self.frameWidth(), 2*self.frameWidth()) return hint def heightForWidth(self, width): """ :param int width: Width :return: The preferred height, for a width. """ width -= 2*self.frameWidth() h = self.__data.view.contentsWidget.heightForWidth(width) if h >= 0: h += 2*self.frameWidth() return h def eventFilter(self, object_, event): """ Handle QEvent.ChildRemoved andQEvent.LayoutRequest events for the contentsWidget(). :param QObject object: Object to be filtered :param QEvent event: Event :return: Forwarded to QwtAbstractLegend.eventFilter() """ if object_ is self.__data.view.contentsWidget: if event.type() == QEvent.ChildRemoved: ce = event #TODO: cast to QChildEvent if ce.child().isWidgetType(): w = ce.child() #TODO: cast to QWidget self.__data.itemMap.removeWidget(w) elif event.type() == QEvent.LayoutRequest: self.__data.view.layoutContents() if self.parentWidget() and self.parentWidget().layout() is None: QApplication.postEvent(self.parentWidget(), QEvent(QEvent.LayoutRequest)) return QwtAbstractLegend.eventFilter(self, object_, event) def itemClicked(self, widget): # w = self.sender() #TODO: cast to QWidget w = widget if w is not None: itemInfo = self.__data.itemMap.itemInfo(w) if itemInfo is not None: widgetList = self.__data.itemMap.legendWidgets(itemInfo) if w in widgetList: index = widgetList.index(w) self.clicked.emit(itemInfo, index) def itemChecked(self, on, widget): # w = self.sender() #TODO: cast to QWidget w = widget if w is not None: itemInfo = self.__data.itemMap.itemInfo(w) if itemInfo is not None: widgetList = self.__data.itemMap.legendWidgets(itemInfo) if w in widgetList: index = widgetList.index(w) self.checked.emit(itemInfo, on, index) def renderLegend(self, painter, rect, fillBackground): """ Render the legend into a given rectangle. :param QPainter painter: Painter :param QRectF rect: Bounding rectangle :param bool fillBackground: When true, fill rect with the widget background """ if self.__data.itemMap.isEmpty(): return if fillBackground: if self.autoFillBackground() or\ self.testAttribute(Qt.WA_StyledBackground): QwtPainter.drawBackground(painter, rect, self) # const QwtDynGridLayout *legendLayout = # qobject_cast( contentsWidget()->layout() ); #TODO: not the exact same implementation legendLayout = self.__data.view.contentsWidget.layout() if legendLayout is None: return left, right, top, bottom = self.getContentsMargins() layoutRect = QRect() layoutRect.setLeft(np.ceil(rect.left())+left) layoutRect.setTop(np.ceil(rect.top())+top) layoutRect.setRight(np.ceil(rect.right())-right) layoutRect.setBottom(np.ceil(rect.bottom())-bottom) numCols = legendLayout.columnsForWidth(layoutRect.width()) itemRects = legendLayout.layoutItems(layoutRect, numCols) index = 0 for i in range(legendLayout.count()): item = legendLayout.itemAt(i) w = item.widget() if w is not None: painter.save() painter.setClipRect(itemRects[index], Qt.IntersectClip) self.renderItem(painter, w, itemRects[index], fillBackground) index += 1 painter.restore() def renderItem(self, painter, widget, rect, fillBackground): """ Render a legend entry into a given rectangle. :param QPainter painter: Painter :param QWidget widget: Widget representing a legend entry :param QRectF rect: Bounding rectangle :param bool fillBackground: When true, fill rect with the widget background """ if fillBackground: if widget.autoFillBackground() or\ widget.testAttribute(Qt.WA_StyledBackground): QwtPainter.drawBackground(painter, rect, widget) label = widget #TODO: cast to QwtLegendLabel if label is not None: icon = label.data().icon() sz = icon.defaultSize() iconRect = QRectF(rect.x()+label.margin(), rect.center().y()-.5*sz.height(), sz.width(), sz.height()) icon.render(painter, iconRect, Qt.KeepAspectRatio) titleRect = QRectF(rect) titleRect.setX(iconRect.right()+2*label.spacing()) painter.setFont(label.font()) painter.setPen(label.palette().color(QPalette.Text)) label.drawText(painter, titleRect) #TODO: cast label to QwtLegendLabel def legendWidgets(self, itemInfo): """ List of widgets associated to a item :param QVariant itemInfo: Info about an item """ return self.__data.itemMap.legendWidgets(itemInfo) def legendWidget(self, itemInfo): """ First widget in the list of widgets associated to an item :param QVariant itemInfo: Info about an item """ list_ = self.__data.itemMap.legendWidgets(itemInfo) if list_: return list_[0] def itemInfo(self, widget): """ Find the item that is associated to a widget :param QWidget widget: Widget on the legend :return: Associated item info """ return self.__data.itemMap.itemInfo(widget) def isEmpty(self): return self.__data.itemMap.isEmpty() PythonQwt-0.5.5/qwt/symbol.py0000666000000000000000000011544412615225450014705 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtSymbol --------- .. autoclass:: QwtSymbol :members: """ from qwt.graphic import QwtGraphic from qwt.painter import QwtPainter from qwt.qt.QtGui import (QPainter, QTransform, QPixmap, QPen, QPolygonF, QPainterPath, QBrush) from qwt.qt.QtCore import QSize, QRect, QPointF, QRectF, QSizeF, Qt, QPoint from qwt.qt.QtSvg import QSvgRenderer import numpy as np class QwtTriangle(object): # enum Type Left, Right, Up, Down = list(range(4)) def qwtPathGraphic(path, pen, brush): graphic = QwtGraphic() graphic.setRenderHint(QwtGraphic.RenderPensUnscaled) painter = QPainter(graphic) painter.setPen(pen) painter.setBrush(brush) painter.drawPath(path) painter.end() return graphic def qwtScaleBoundingRect(graphic, size): scaledSize = QSize(size) if scaledSize.isEmpty(): scaledSize = graphic.defaultSize() sz = graphic.controlPointRect().size() sx = 1. if sz.width() > 0.: sx = scaledSize.width()/sz.width() sy = 1. if sz.height() > 0.: sy = scaledSize.height()/sz.height() return graphic.scaledBoundingRect(sx, sy) def qwtDrawPixmapSymbols(painter, points, numPoints, symbol): size = symbol.size() if size.isEmpty(): size = symbol.pixmap().size() transform = QTransform(painter.transform()) if transform.isScaling(): r = QRect(0, 0, size.width(), size.height()) size = transform.mapRect(r).size() pm = QPixmap(symbol.pixmap()) if pm.size() != size: pm = pm.scaled(size) pinPoint = QPointF(.5*size.width(), .5*size.height()) if symbol.isPinPointEnabled(): pinPoint = symbol.pinPoint() painter.resetTransform() for pos in points: pos = QPointF(transform.map(pos))-pinPoint QwtPainter.drawPixmap(painter, QRect(pos.toPoint(), pm.size(), pm)) def qwtDrawSvgSymbols(painter, points, numPoints, renderer, symbol): if renderer is None or not renderer.isValid(): return viewBox = QRectF(renderer.viewBoxF()) if viewBox.isEmpty(): return sz = QSizeF(symbol.size()) if not sz.isValid(): sz = viewBox.size() sx = sz.width()/viewBox.width() sy = sz.height()/viewBox.height() pinPoint = QPointF(viewBox.center()) if symbol.isPinPointEnabled(): pinPoint = symbol.pinPoint() dx = sx*(pinPoint.x()-viewBox.left()) dy = sy*(pinPoint.y()-viewBox.top()) for pos in points: x = pos.x()-dx y = pos.y()-dy renderer.render(painter, QRectF(x, y, sz.width(), sz.height())) def qwtDrawGraphicSymbols(painter, points, numPoint, graphic, symbol): pointRect = QRectF(graphic.controlPointRect()) if pointRect.isEmpty(): return sx = 1. sy = 1. sz = symbol.size() if sz.isValid(): sx = sz.width()/pointRect.width() sy = sz.height()/pointRect.height() pinPoint = QPointF(pointRect.center()) if symbol.isPinPointEnabled(): pinPoint = symbol.pinPoint() transform = QTransform(painter.transform()) for pos in points: tr = QTransform(transform) tr.translate(pos.x(), pos.y()) tr.scale(sx, sy) tr.translate(-pinPoint.x(), -pinPoint.y()) painter.setTransform(tr) graphic.render(painter) painter.setTransform(transform) def qwtDrawEllipseSymbols(painter, points, numPoints, symbol): painter.setBrush(symbol.brush()) painter.setPen(symbol.pen()) size =symbol.size() sw = size.width() sh = size.height() sw2 = .5*size.width() sh2 = .5*size.height() for pos in points: x = pos.x() y = pos.y() r = QRectF(x-sw2, y-sh2, sw, sh) painter.drawEllipse(r) def qwtDrawRectSymbols(painter, points, numPoints, symbol): size = symbol.size() pen = QPen(symbol.pen()) pen.setJoinStyle(Qt.MiterJoin) painter.setPen(pen) painter.setBrush(symbol.brush()) painter.setRenderHint(QPainter.Antialiasing, False) sw = size.width() sh = size.height() sw2 = .5*size.width() sh2 = .5*size.height() for pos in points: x = pos.x() y = pos.y() r = QRectF(x-sw2, y-sh2, sw, sh) painter.drawRect(r) def qwtDrawDiamondSymbols(painter, points, numPoints, symbol): size =symbol.size() pen = QPen(symbol.pen()) pen.setJoinStyle(Qt.MiterJoin) painter.setPen(pen) painter.setBrush(symbol.brush()) for pos in points: x1 = pos.x()-.5*size.width() y1 = pos.y()-.5*size.height() x2 = x1+size.width() y2 = y1+size.height() polygon = QPolygonF() polygon += QPointF(pos.x(), y1) polygon += QPointF(x1, pos.y()) polygon += QPointF(pos.x(), y2) polygon += QPointF(x2, pos.y()) painter.drawPolygon(polygon) def qwtDrawTriangleSymbols(painter, type, points, numPoint, symbol): size =symbol.size() pen = QPen(symbol.pen()) pen.setJoinStyle(Qt.MiterJoin) painter.setPen(pen) painter.setBrush(symbol.brush()) sw2 = .5*size.width() sh2 = .5*size.height() for pos in points: x = pos.x() y = pos.y() x1 = x-sw2 x2 = x1+size.width() y1 = y-sh2 y2 = y1+size.height() if type == QwtTriangle.Left: triangle = [QPointF(x2, y1), QPointF(x1, y), QPointF(x2, y2)] elif type == QwtTriangle.Right: triangle = [QPointF(x1, y1), QPointF(x2, y), QPointF(x1, y2)] elif type == QwtTriangle.Up: triangle = [QPointF(x1, y2), QPointF(x, y1), QPointF(x2, y2)] elif type == QwtTriangle.Down: triangle = [QPointF(x1, y1), QPointF(x, y2), QPointF(x2, y1)] painter.drawPolygon(QPolygonF(triangle)) def qwtDrawLineSymbols(painter, orientations, points, numPoints, symbol): size =symbol.size() pen = QPen(symbol.pen()) if pen.width() > 1: pen.setCapStyle(Qt.FlatCap) painter.setPen(pen) painter.setRenderHint(QPainter.Antialiasing, False) sw = size.width() sh = size.height() sw2 = .5*size.width() sh2 = .5*size.height() for pos in points: if orientations & Qt.Horizontal: x = round(pos.x())-sw2 y = round(pos.y()) painter.drawLine(x, y, x+sw, y) if orientations & Qt.Vertical: x = round(pos.x()) y = round(pos.y())-sh2 painter.drawLine(x, y, x, y+sh) def qwtDrawXCrossSymbols(painter, points, numPoints, symbol): size =symbol.size() pen = QPen(symbol.pen()) if pen.width() > 1: pen.setCapStyle(Qt.FlatCap) painter.setPen(pen) sw = size.width() sh = size.height() sw2 = .5*size.width() sh2 = .5*size.height() for pos in points: x1 = pos.x()-sw2 x2 = x1+sw y1 = pos.y()-sh2 y2 = y1+sh painter.drawLine(x1, y1, x2, y2) painter.drawLine(x2, y1, x1, y2) def qwtDrawStar1Symbols(painter, points, numPoints, symbol): size =symbol.size() painter.setPen(symbol.pen()) sqrt1_2 = np.sqrt(.5) r = QRectF(0, 0, size.width(), size.height()) for pos in points: r.moveCenter(pos.toPoint()) c = QPointF(r.center()) d1 = r.width()/2.*(1.-sqrt1_2) painter.drawLine(r.left()+d1, r.top()+d1, r.right()-d1, r.bottom()-d1) painter.drawLine(r.left()+d1, r.bottom()-d1, r.right()-d1, r.top()+d1) painter.drawLine(c.x(), r.top(), c.x(), r.bottom()) painter.drawLine(r.left(), c.y(), r.right(), c.y()) def qwtDrawStar2Symbols(painter, points, numPoints, symbol): pen = QPen(symbol.pen()) if pen.width() > 1: pen.setCapStyle(Qt.FlatCap) pen.setJoinStyle(Qt.MiterJoin) painter.setPen(pen) painter.setBrush(symbol.brush()) cos30 = np.cos(30*np.pi/180.) dy = .25*symbol.size().height() dx = .5*symbol.size().width()*cos30/3. for pos in points: x = pos.x() y = pos.y() x1 = x-3*dx y1 = y-2*dy x2 = x1+1*dx x3 = x1+2*dx x4 = x1+3*dx x5 = x1+4*dx x6 = x1+5*dx x7 = x1+6*dx y2 = y1+1*dy y3 = y1+2*dy y4 = y1+3*dy y5 = y1+4*dy star = [QPointF(x4, y1), QPointF(x5, y2), QPointF(x7, y2), QPointF(x6, y3), QPointF(x7, y4), QPointF(x5, y4), QPointF(x4, y5), QPointF(x3, y4), QPointF(x1, y4), QPointF(x2, y3), QPointF(x1, y2), QPointF(x3, y2)] painter.drawPolygon(QPolygonF(star)) def qwtDrawHexagonSymbols(painter, points, numPoints, symbol): painter.setBrush(symbol.brush()) painter.setPen(symbol.pen()) cos30 = np.cos(30*np.pi/180.) dx = .5*(symbol.size().width()-cos30) dy = .25*symbol.size().height() for pos in points: x = pos.x() y = pos.y() x1 = x-dx y1 = y-2*dy x2 = x1+1*dx x3 = x1+2*dx y2 = y1+1*dy y3 = y1+3*dy y4 = y1+4*dy hexa = [QPointF(x2, y1), QPointF(x3, y2), QPointF(x3, y3), QPointF(x2, y4), QPointF(x1, y3), QPointF(x1, y2)] painter.drawPolygon(QPolygonF(hexa)) class QwtSymbol_PrivateData(object): def __init__(self, st, br, pn ,sz): self.style = st self.size = sz self.brush = br self.pen = pn self.isPinPointEnabled = False self.pinPoint = QPointF() class Path(object): def __init__(self): self.path = QPainterPath() self.graphic = QwtGraphic() self.path = Path() class Pixmap(object): def __init__(self): self.pixmap = QPixmap() self.pixmap = None #Pixmap() class Graphic(object): def __init__(self): self.graphic = QwtGraphic() self.graphic = Graphic() class SVG(object): def __init__(self): self.renderer = QSvgRenderer() self.svg = SVG() class PaintCache(object): def __init__(self): self.policy = 0 self.pixmap = None #QPixmap() self.cache = PaintCache() class QwtSymbol(object): """ A class for drawing symbols Symbol styles: * `QwtSymbol.NoSymbol`: No Style. The symbol cannot be drawn. * `QwtSymbol.Ellipse`: Ellipse or circle * `QwtSymbol.Rect`: Rectangle * `QwtSymbol.Diamond`: Diamond * `QwtSymbol.Triangle`: Triangle pointing upwards * `QwtSymbol.DTriangle`: Triangle pointing downwards * `QwtSymbol.UTriangle`: Triangle pointing upwards * `QwtSymbol.LTriangle`: Triangle pointing left * `QwtSymbol.RTriangle`: Triangle pointing right * `QwtSymbol.Cross`: Cross (+) * `QwtSymbol.XCross`: Diagonal cross (X) * `QwtSymbol.HLine`: Horizontal line * `QwtSymbol.VLine`: Vertical line * `QwtSymbol.Star1`: X combined with + * `QwtSymbol.Star2`: Six-pointed star * `QwtSymbol.Hexagon`: Hexagon * `QwtSymbol.Path`: The symbol is represented by a painter path, where the origin (0, 0) of the path coordinate system is mapped to the position of the symbol ..seealso:: :py:meth:`setPath()`, :py:meth:`path()` * `QwtSymbol.Pixmap`: The symbol is represented by a pixmap. The pixmap is centered or aligned to its pin point. ..seealso:: :py:meth:`setPinPoint()` * `QwtSymbol.Graphic`: The symbol is represented by a graphic. The graphic is centered or aligned to its pin point. ..seealso:: :py:meth:`setPinPoint()` * `QwtSymbol.SvgDocument`: The symbol is represented by a SVG graphic. The graphic is centered or aligned to its pin point. ..seealso:: :py:meth:`setPinPoint()` * `QwtSymbol.UserStyle`: Styles >= `QwtSymbol.UserStyle` are reserved for derived classes of `QwtSymbol` that overload `drawSymbols()` with additional application specific symbol types. Cache policies: Depending on the render engine and the complexity of the symbol shape it might be faster to render the symbol to a pixmap and to paint this pixmap. F.e. the raster paint engine is a pure software renderer where in cache mode a draw operation usually ends in raster operation with the the backing store, that are usually faster, than the algorithms for rendering polygons. But the opposite can be expected for graphic pipelines that can make use of hardware acceleration. The default setting is AutoCache ..seealso:: :py:meth:`setCachePolicy()`, :py:meth:`cachePolicy()` .. note:: The policy has no effect, when the symbol is painted to a vector graphics format (PDF, SVG). .. warning:: Since Qt 4.8 raster is the default backend on X11 Valid cache policies: * `QwtSymbol.NoCache`: Don't use a pixmap cache * `QwtSymbol.Cache`: Always use a pixmap cache * `QwtSymbol.AutoCache`: Use a cache when the symbol is rendered with the software renderer (`QPaintEngine.Raster`) .. py:class:: QwtSymbol([style=QwtSymbol.NoSymbol]) The symbol is constructed with gray interior, black outline with zero width, no size and style 'NoSymbol'. :param int style: Symbol Style .. py:class:: QwtSymbol(style, brush, pen, size) :param int style: Symbol Style :param QBrush brush: Brush to fill the interior :param QPen pen: Outline pen :param QSize size: Size .. py:class:: QwtSymbol(path, brush, pen) :param QPainterPath path: Painter path :param QBrush brush: Brush to fill the interior :param QPen pen: Outline pen .. seealso:: :py:meth:`setPath()`, :py:meth:`setBrush()`, :py:meth:`setPen()`, :py:meth:`setSize()` """ # enum Style Style = int NoSymbol = -1 (Ellipse, Rect, Diamond, Triangle, DTriangle, UTriangle, LTriangle, RTriangle, Cross, XCross, HLine, VLine, Star1, Star2, Hexagon, Path, Pixmap, Graphic, SvgDocument) = list(range(19)) UserStyle = 1000 # enum CachePolicy NoCache, Cache, AutoCache = list(range(3)) def __init__(self, *args): if len(args) in (0, 1): if args: style, = args else: style = QwtSymbol.NoSymbol self.__data = QwtSymbol_PrivateData(style, QBrush(Qt.gray), QPen(Qt.black, 0), QSize()) elif len(args) == 4: style, brush, pen, size = args self.__data = QwtSymbol_PrivateData(style, brush, pen, size) elif len(args) == 3: path, brush, pen = args self.__data = QwtSymbol_PrivateData(QwtSymbol.Path, brush, pen, QSize()) self.setPath(path) else: raise TypeError("%s() takes 1, 3, or 4 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def setCachePolicy(self, policy): """ Change the cache policy The default policy is AutoCache :param int policy: Cache policy .. seealso:: :py:meth:`cachePolicy()` """ if self.__data.cache.policy != policy: self.__data.cache.policy = policy self.invalidateCache() def cachePolicy(self): """ :return: Cache policy .. seealso:: :py:meth:`setCachePolicy()` """ return self.__data.cache.policy def setPath(self, path): """ Set a painter path as symbol The symbol is represented by a painter path, where the origin (0, 0) of the path coordinate system is mapped to the position of the symbol. When the symbol has valid size the painter path gets scaled to fit into the size. Otherwise the symbol size depends on the bounding rectangle of the path. The following code defines a symbol drawing an arrow:: from qwt.qt.QtGui import QApplication, QPen, QPainterPath, QTransform from qwt.qt.QtCore import Qt, QPointF from qwt import QwtPlot, QwtPlotCurve, QwtSymbol import numpy as np app = QApplication([]) # --- Construct custom symbol --- path = QPainterPath() path.moveTo(0, 8) path.lineTo(0, 5) path.lineTo(-3, 5) path.lineTo(0, 0) path.lineTo(3, 5) path.lineTo(0, 5) transform = QTransform() transform.rotate(-30.0) path = transform.map(path) pen = QPen(Qt.black, 2 ); pen.setJoinStyle(Qt.MiterJoin) symbol = QwtSymbol() symbol.setPen(pen) symbol.setBrush(Qt.red) symbol.setPath(path) symbol.setPinPoint(QPointF(0., 0.)) symbol.setSize(10, 14) # --- Test it within a simple plot --- curve = QwtPlotCurve() curve_pen = QPen(Qt.blue) curve_pen.setStyle(Qt.DotLine) curve.setPen(curve_pen) curve.setSymbol(symbol) x = np.linspace(0, 10, 10) curve.setData(x, np.sin(x)) plot = QwtPlot() curve.attach(plot) plot.resize(600, 300) plot.replot() plot.show() app.exec_() .. image:: /images/symbol_path_example.png :param QPainterPath path: Painter path .. seealso:: :py:meth:`path()`, :py:meth:`setSize()` """ self.__data.style = QwtSymbol.Path self.__data.path.path = path self.__data.path.graphic.reset() def path(self): """ :return: Painter path for displaying the symbol .. seealso:: :py:meth:`setPath()` """ return self.__data.path.path def setPixmap(self, pixmap): """ Set a pixmap as symbol :param QPixmap pixmap: Pixmap .. seealso:: :py:meth:`pixmap()`, :py:meth:`setGraphic()` .. note:: The `style()` is set to `QwtSymbol.Pixmap` .. note:: `brush()` and `pen()` have no effect """ self.__data.style = QwtSymbol.Pixmap self.__data.pixmap.pixmap = pixmap def pixmap(self): """ :return: Assigned pixmap .. seealso:: :py:meth:`setPixmap()` """ return self.__data.pixmap.pixmap def setGraphic(self, graphic): """ Set a graphic as symbol :param qwt.graphic.QwtGraphic graphic: Graphic .. seealso:: :py:meth:`graphic()`, :py:meth:`setPixmap()` .. note:: The `style()` is set to `QwtSymbol.Graphic` .. note:: `brush()` and `pen()` have no effect """ self.__data.style = QwtSymbol.Graphic self.__data.graphic.graphic = graphic def graphic(self): """ :return: Assigned graphic .. seealso:: :py:meth:`setGraphic()` """ return self.__data.graphic.graphic def setSvgDocument(self, svgDocument): """ Set a SVG icon as symbol :param svgDocument: SVG icon .. seealso:: :py:meth:`setGraphic()`, :py:meth:`setPixmap()` .. note:: The `style()` is set to `QwtSymbol.SvgDocument` .. note:: `brush()` and `pen()` have no effect """ self.__data.style = QwtSymbol.SvgDocument if self.__data.svg.renderer is None: self.__data.svg.renderer = QSvgRenderer() self.__data.svg.renderer.load(svgDocument) def setSize(self, *args): """ Specify the symbol's size .. py:method:: setSize(width, [height=-1]) :param int width: Width :param int height: Height .. py:method:: setSize(size) :param QSize size: Size .. seealso:: :py:meth:`size()` """ if len(args) == 2: width, height = args if width >= 0 and height < 0: height = width self.setSize(QSize(width, height)) elif len(args) == 1: if isinstance(args[0], QSize): size, = args if size.isValid() and size != self.__data.size: self.__data.size = size self.invalidateCache() else: width, = args self.setSize(width, -1) else: raise TypeError("%s().setSize() takes 1 or 2 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def size(self): """ :return: Size .. seealso:: :py:meth:`setSize()` """ return self.__data.size def setBrush(self, brush): """ Assign a brush The brush is used to draw the interior of the symbol. :param QBrush brush: Brush .. seealso:: :py:meth:`brush()` """ if brush != self.__data.brush: self.__data.brush = brush self.invalidateCache() if self.__data.style == QwtSymbol.Path: self.__data.path.graphic.reset() def brush(self): """ :return: Brush .. seealso:: :py:meth:`setBrush()` """ return self.__data.brush def setPen(self, *args): """ Build and/or assign a pen, depending on the arguments. .. py:method:: setPen(color, width, style) Build and assign a pen In Qt5 the default pen width is 1.0 ( 0.0 in Qt4 ) what makes it non cosmetic (see `QPen.isCosmetic()`). This method signature has been introduced to hide this incompatibility. :param QColor color: Pen color :param float width: Pen width :param Qt.PenStyle style: Pen style .. py:method:: setPen(pen) Assign a pen :param QPen pen: New pen .. seealso:: :py:meth:`pen()`, :py:meth:`brush()` """ if len(args) == 3: color, width, style = args self.setPen(QPen(color, width, style)) elif len(args) == 1: pen, = args if pen != self.__data.pen: self.__data.pen = pen self.invalidateCache() if self.__data.style == QwtSymbol.Path: self.__data.path.graphic.reset() else: raise TypeError("%s().setPen() takes 1 or 3 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def pen(self): """ :return: Pen .. seealso:: :py:meth:`setPen()`, :py:meth:`brush()` """ return self.__data.pen def setColor(self, color): """ Set the color of the symbol Change the color of the brush for symbol types with a filled area. For all other symbol types the color will be assigned to the pen. :param QColor color: Color .. seealso:: :py:meth:`setPen()`, :py:meth:`setBrush()`, :py:meth:`brush()`, :py:meth:`pen()` """ if self.__data.style in (QwtSymbol.Ellipse, QwtSymbol.Rect, QwtSymbol.Diamond, QwtSymbol.Triangle, QwtSymbol.UTriangle, QwtSymbol.DTriangle, QwtSymbol.RTriangle, QwtSymbol.LTriangle, QwtSymbol.Star2, QwtSymbol.Hexagon): if self.__data.brush.color() != color: self.__data.brush.setColor(color) self.invalidateCache() elif self.__data.style in (QwtSymbol.Cross, QwtSymbol.XCross, QwtSymbol.HLine, QwtSymbol.VLine, QwtSymbol.Star1): if self.__data.pen.color() != color: self.__data.pen.setColor(color) self.invalidateCache() else: if self.__data.brush.color() != color or\ self.__data.pen.color() != color: self.invalidateCache() self.__data.brush.setColor(color) self.__data.pen.setColor(color) def setPinPoint(self, pos, enable=True): """ Set and enable a pin point The position of a complex symbol is not always aligned to its center ( f.e an arrow, where the peak points to a position ). The pin point defines the position inside of a Pixmap, Graphic, SvgDocument or PainterPath symbol where the represented point has to be aligned to. :param QPointF pos: Position :enable bool enable: En/Disable the pin point alignment .. seealso:: :py:meth:`pinPoint()`, :py:meth:`setPinPointEnabled()` """ if self.__data.pinPoint != pos: self.__data.pinPoint = pos if self.__data.isPinPointEnabled: self.invalidateCache() self.setPinPointEnabled(enable) def pinPoint(self): """ :return: Pin point .. seealso:: :py:meth:`setPinPoint()`, :py:meth:`setPinPointEnabled()` """ return self.__data.pinPoint def setPinPointEnabled(self, on): """ En/Disable the pin point alignment :param bool on: Enabled, when on is true .. seealso:: :py:meth:`setPinPoint()`, :py:meth:`isPinPointEnabled()` """ if self.__data.isPinPointEnabled != on: self.__data.isPinPointEnabled = on self.invalidateCache() def isPinPointEnabled(self): """ :return: True, when the pin point translation is enabled .. seealso:: :py:meth:`setPinPoint()`, :py:meth:`setPinPointEnabled()` """ return self.__data.isPinPointEnabled def drawSymbols(self, painter, points, numPoints=None): """ Render an array of symbols Painting several symbols is more effective than drawing symbols one by one, as a couple of layout calculations and setting of pen/brush can be done once for the complete array. :param QPainter painter: Painter :param QPolygonF points: Positions of the symbols in screen coordinates """ #TODO: remove argument numPoints (not necessary in `PythonQwt`) if numPoints is not None and numPoints <= 0: return painter.save() self.renderSymbols(painter, points, numPoints) painter.restore() def drawSymbol(self, painter, point_or_rect): """ Draw the symbol into a rectangle The symbol is painted centered and scaled into the target rectangle. It is always painted uncached and the pin point is ignored. This method is primarily intended for drawing a symbol to the legend. :param QPainter painter: Painter :param point_or_rect: Position or target rectangle of the symbol in screen coordinates :type point_or_rect: QPointF or QPoint or QRectF """ if isinstance(point_or_rect, (QPointF, QPoint)): # drawSymbol( QPainter *, const QPointF & ) self.drawSymbols(painter, [point_or_rect]) return # drawSymbol( QPainter *, const QRectF & ) rect = point_or_rect assert isinstance(rect, QRectF) if self.__data.style == QwtSymbol.NoSymbol: return if self.__data.style == QwtSymbol.Graphic: self.__data.graphic.graphic.render(painter, rect, Qt.KeepAspectRatio) elif self.__data.style == QwtSymbol.Path: if self.__data.path.graphic.isNull(): self.__data.path.graphic = qwtPathGraphic( self.__data.path.path, self.__data.pen, self.__data.brush) self.__data.path.graphic.render(painter, rect, Qt.KeepAspectRatio) return elif self.__data.style == QwtSymbol.SvgDocument: if self.__data.svg.renderer is not None: scaledRect = QRectF() sz = QSizeF(self.__data.svg.renderer.viewBoxF().size()) if not sz.isEmpty(): sz.scale(rect.size(), Qt.KeepAspectRatio) scaledRect.setSize(sz) scaledRect.moveCenter(rect.center()) else: scaledRect = rect self.__data.svg.renderer.render(painter, scaledRect) else: br = QRect(self.boundingRect()) ratio = min([rect.width()/br.width(), rect.height()/br.height()]) painter.save() painter.translate(rect.center()) painter.scale(ratio, ratio) isPinPointEnabled = self.__data.isPinPointEnabled self.__data.isPinPointEnabled = False pos = QPointF() self.renderSymbols(painter, pos, 1) self.__data.isPinPointEnabled = isPinPointEnabled painter.restore() def renderSymbols(self, painter, points, numPoints=None): """ Render the symbol to series of points :param QPainter painter: Painter :param point_or_rect: Positions of the symbols """ #TODO: remove argument numPoints (not necessary in `PythonQwt`) try: assert numPoints is None except AssertionError: raise RuntimeError("argument numPoints is not implemented "\ "in `PythonQwt`") if self.__data.style == QwtSymbol.Ellipse: qwtDrawEllipseSymbols(painter, points, numPoints, self) elif self.__data.style == QwtSymbol.Rect: qwtDrawRectSymbols(painter, points, numPoints, self) elif self.__data.style == QwtSymbol.Diamond: qwtDrawDiamondSymbols(painter, points, numPoints, self) elif self.__data.style == QwtSymbol.Cross: qwtDrawLineSymbols(painter, Qt.Horizontal|Qt.Vertical, points, numPoints, self) elif self.__data.style == QwtSymbol.XCross: qwtDrawXCrossSymbols(painter, points, numPoints, self) elif self.__data.style in (QwtSymbol.Triangle, QwtSymbol.UTriangle): qwtDrawTriangleSymbols(painter, QwtTriangle.Up, points, numPoints, self) elif self.__data.style == QwtSymbol.DTriangle: qwtDrawTriangleSymbols(painter, QwtTriangle.Down, points, numPoints, self) elif self.__data.style == QwtSymbol.RTriangle: qwtDrawTriangleSymbols(painter, QwtTriangle.Right, points, numPoints, self) elif self.__data.style == QwtSymbol.LTriangle: qwtDrawTriangleSymbols(painter, QwtTriangle.Left, points, numPoints, self) elif self.__data.style == QwtSymbol.HLine: qwtDrawLineSymbols(painter, Qt.Horizontal, points, numPoints, self) elif self.__data.style == QwtSymbol.VLine: qwtDrawLineSymbols(painter, Qt.Vertical, points, numPoints, self) elif self.__data.style == QwtSymbol.Star1: qwtDrawStar1Symbols(painter, points, numPoints, self) elif self.__data.style == QwtSymbol.Star2: qwtDrawStar2Symbols(painter, points, numPoints, self) elif self.__data.style == QwtSymbol.Hexagon: qwtDrawHexagonSymbols(painter, points, numPoints, self) elif self.__data.style == QwtSymbol.Path: if self.__data.path.graphic.isNull(): self.__data.path.graphic = qwtPathGraphic( self.__data.path.path, self.__data.pen, self.__data.brush) qwtDrawGraphicSymbols(painter, points, numPoints, self.__data.path.graphic, self) elif self.__data.style == QwtSymbol.Pixmap: qwtDrawPixmapSymbols(painter, points, numPoints, self) elif self.__data.style == QwtSymbol.Graphic: qwtDrawGraphicSymbols(painter, points, numPoints, self.__data.graphic.graphic, self) elif self.__data.style == QwtSymbol.SvgDocument: qwtDrawSvgSymbols(painter, points, numPoints, self.__data.svg.renderer, self) def boundingRect(self): """ Calculate the bounding rectangle for a symbol at position (0,0). :return: Bounding rectangle """ rect = QRectF() pinPointTranslation = False if self.__data.style in (QwtSymbol.Ellipse, QwtSymbol.Rect, QwtSymbol.Hexagon): pw = 0. if self.__data.pen.style() != Qt.NoPen: pw = max([self.__data.pen.widthF(), 1.]) rect.setSize(self.__data.size+QSizeF(pw, pw)) rect.moveCenter(QPointF(0., 0.)) elif self.__data.style in (QwtSymbol.XCross, QwtSymbol.Diamond, QwtSymbol.Triangle, QwtSymbol.UTriangle, QwtSymbol.DTriangle, QwtSymbol.RTriangle, QwtSymbol.LTriangle, QwtSymbol.Star1, QwtSymbol.Star2): pw = 0. if self.__data.pen.style() != Qt.NoPen: pw = max([self.__data.pen.widthF(), 1.]) rect.setSize(QSizeF(self.__data.size)+QSizeF(2*pw, 2*pw)) rect.moveCenter(QPointF(0., 0.)) elif self.__data.style == QwtSymbol.Path: if self.__data.path.graphic.isNull(): self.__data.path.graphic = qwtPathGraphic( self.__data.path.path, self.__data.pen, self.__data.brush) rect = qwtScaleBoundingRect(self.__data.path.graphic, self.__data.size) pinPointTranslation = True elif self.__data.style == QwtSymbol.Pixmap: if self.__data.size.isEmpty(): rect.setSize(self.__data.pixmap.pixmap.size()) else: rect.setSize(self.__data.size) pinPointTranslation = True elif self.__data.style == QwtSymbol.Graphic: rect = qwtScaleBoundingRect(self.__data.graphic.graphic, self.__data.size) pinPointTranslation = True elif self.__data.style == QwtSymbol.SvgDocument: if self.__data.svg.renderer is not None: rect = self.__data.svg.renderer.viewBoxF() if self.__data.size.isValid() and not rect.isEmpty(): sz = QSizeF(rect.size()) sx = self.__data.size.width()/sz.width() sy = self.__data.size.height()/sz.height() transform = QTransform() transform.scale(sx, sy) rect = transform.mapRect(rect) pinPointTranslation = True else: rect.setSize(self.__data.size) rect.moveCenter(QPointF(0., 0.)) if pinPointTranslation: pinPoint = QPointF(0., 0.) if self.__data.isPinPointEnabled: pinPoint = rect.center()-self.__data.pinPoint rect.moveCenter(pinPoint) r = QRect() r.setLeft(np.floor(rect.left())) r.setTop(np.floor(rect.top())) r.setRight(np.floor(rect.right())) r.setBottom(np.floor(rect.bottom())) if self.__data.style != QwtSymbol.Pixmap: r.adjust(-1, -1, 1, 1) return r def invalidateCache(self): """ Invalidate the cached symbol pixmap The symbol invalidates its cache, whenever an attribute is changed that has an effect ob how to display a symbol. In case of derived classes with individual styles (>= `QwtSymbol.UserStyle`) it might be necessary to call invalidateCache() for attributes that are relevant for this style. .. seealso:: :py:meth:`setCachePolicy()`, :py:meth:`drawSymbols()` """ if self.__data.cache.pixmap is not None: self.__data.cache.pixmap = QPixmap() def setStyle(self, style): """ Specify the symbol style :param int style: Style .. seealso:: :py:meth:`style()` """ if self.__data.style != style: self.__data.style = style self.invalidateCache() def style(self): """ :return: Current symbol style .. seealso:: :py:meth:`setStyle()` """ return self.__data.style PythonQwt-0.5.5/qwt/__init__.py0000666000000000000000000001337512646710122015136 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ PythonQwt ========= The ``PythonQwt`` package is a 2D-data plotting library using Qt graphical user interfaces for the Python programming language. It is compatible with both ``PyQt4`` and ``PyQt5`` (``PySide`` is currently not supported but it could be in the near future as it would "only" requires testing to support it as a stable alternative to PyQt). It consists of a single Python package named `qwt` which is a pure Python implementation of Qwt C++ library with some limitations. .. image:: images/panorama.png External resources: * Python Package Index: `PyPI`_ * Project page on GitHub: `GitHubPage`_ * Bug reports and feature requests: `GitHub`_ .. _PyPI: https://pypi.python.org/pypi/PythonQwt .. _GitHubPage: http://pierreraybaut.github.io/PythonQwt .. _GitHub: https://github.com/PierreRaybaut/PythonQwt """ __version__ = '0.5.5' QWT_VERSION_STR = '6.1.2' import warnings from qwt.plot import QwtPlot from qwt.symbol import QwtSymbol as QSbl # see deprecated section from qwt.scale_engine import QwtLinearScaleEngine, QwtLogScaleEngine from qwt.text import QwtText from qwt.plot_canvas import QwtPlotCanvas from qwt.plot_curve import QwtPlotCurve as QPC # see deprecated section from qwt.plot_curve import QwtPlotItem from qwt.scale_map import QwtScaleMap from qwt.interval import QwtInterval from qwt.legend import QwtLegend, QwtLegendData, QwtLegendLabel from qwt.plot_marker import QwtPlotMarker from qwt.plot_grid import QwtPlotGrid as QPG # see deprecated section from qwt.color_map import QwtLinearColorMap from qwt.toqimage import array_to_qimage as toQImage from qwt.scale_div import QwtScaleDiv from qwt.scale_draw import QwtScaleDraw from qwt.scale_draw import QwtAbstractScaleDraw from qwt.painter import QwtPainter from qwt.plot_series import (QwtSeriesData, QwtPointArrayData, QwtSeriesStore, QwtPlotSeriesItem) from qwt.plot_renderer import QwtPlotRenderer from qwt.plot_directpainter import QwtPlotDirectPainter ## ============================================================================ ## Deprecated classes and attributes (to be removed in next major release) ## ============================================================================ # Remove deprecated QwtPlotItem.setAxis (replaced by setAxes) # Remove deprecated QwtPlotCanvas.invalidatePaintCache (replaced by replot) ## ============================================================================ class QwtDoubleInterval(QwtInterval): def __init__(self, minValue=0., maxValue=-1., borderFlags=None): warnings.warn("`QwtDoubleInterval` has been removed in Qwt6: "\ "please use `QwtInterval` instead", RuntimeWarning) super(QwtDoubleInterval, self).__init__(minValue, maxValue, borderFlags) ## ============================================================================ class QwtLog10ScaleEngine(QwtLogScaleEngine): def __init__(self): warnings.warn("`QwtLog10ScaleEngine` has been removed in Qwt6: "\ "please use `QwtLogScaleEngine` instead", RuntimeWarning) super(QwtLog10ScaleEngine, self).__init__(10) ## ============================================================================ class QwtPlotPrintFilter(object): def __init__(self): raise NotImplementedError("`QwtPlotPrintFilter` has been removed in Qwt6: "\ "please rely on `QwtPlotRenderer` instead") ## ============================================================================ class QwtPlotCurve(QPC): @property def Yfx(self): raise NotImplementedError("`Yfx` attribute has been removed "\ "(curve types are no longer implemented in Qwt6)") @property def Xfy(self): raise NotImplementedError("`Yfx` attribute has been removed "\ "(curve types are no longer implemented in Qwt6)") ## ============================================================================ class QwtSymbol(QSbl): def draw(self, painter, *args): warnings.warn("`draw` has been removed in Qwt6: "\ "please rely on `drawSymbol` and `drawSymbols` instead", RuntimeWarning) from qwt.qt.QtCore import QPointF if len(args) == 2: self.drawSymbols(painter, [QPointF(*args)]) else: self.drawSymbol(painter, *args) ## ============================================================================ class QwtPlotGrid(QPG): def majPen(self): warnings.warn("`majPen` has been removed in Qwt6: "\ "please use `majorPen` instead", RuntimeWarning) return self.majorPen() def minPen(self): warnings.warn("`minPen` has been removed in Qwt6: "\ "please use `minorPen` instead", RuntimeWarning) return self.minorPen() def setMajPen(self, *args): warnings.warn("`setMajPen` has been removed in Qwt6: "\ "please use `setMajorPen` instead", RuntimeWarning) return self.setMajorPen(*args) def setMinPen(self, *args): warnings.warn("`setMinPen` has been removed in Qwt6: "\ "please use `setMinorPen` instead", RuntimeWarning) return self.setMinorPen(*args) ## ============================================================================ PythonQwt-0.5.5/qwt/column_symbol.py0000666000000000000000000001304112615225450016250 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) from qwt.interval import QwtInterval from qwt.qt.QtGui import QPolygonF, QPalette from qwt.qt.QtCore import QRectF, Qt def qwtDrawBox(p, rect, pal, lw): if lw > 0.: if rect.width() == 0.: p.setPen(pal.dark().color()) p.drawLine(rect.topLeft(), rect.bottomLeft()) return if rect.height() == 0.: p.setPen(pal.dark().color()) p.drawLine(rect.topLeft(), rect.topRight()) return lw = min([lw, rect.height()/2.-1.]) lw = min([lw, rect.width()/2.-1.]) outerRect = rect.adjusted(0, 0, 1, 1) polygon = QPolygonF(outerRect) if outerRect.width() > 2*lw and outerRect.height() > 2*lw: innerRect = outerRect.adjusted(lw, lw, -lw, -lw) polygon = polygon.subtracted(innerRect) p.setPen(Qt.NoPen) p.setBrush(pal.dark()) p.drawPolygon(polygon) windowRect = rect.adjusted(lw, lw, -lw+1, -lw+1) if windowRect.isValid(): p.fillRect(windowRect, pal.window()) def qwtDrawPanel(painter, rect, pal, lw): if lw > 0.: if rect.width() == 0.: painter.setPen(pal.window().color()) painter.drawLine(rect.topLeft(), rect.bottomLeft()) return if rect.height() == 0.: painter.setPen(pal.window().color()) painter.drawLine(rect.topLeft(), rect.topRight()) return lw = min([lw, rect.height()/2.-1.]) lw = min([lw, rect.width()/2.-1.]) outerRect = rect.adjusted(0, 0, 1, 1) innerRect = outerRect.adjusted(lw, lw, -lw, -lw) lines = [QPolygonF(), QPolygonF()] lines[0] += outerRect.bottomLeft() lines[0] += outerRect.topLeft() lines[0] += outerRect.topRight() lines[0] += innerRect.topRight() lines[0] += innerRect.topLeft() lines[0] += innerRect.bottomLeft() lines[1] += outerRect.topRight() lines[1] += outerRect.bottomRight() lines[1] += outerRect.bottomLeft() lines[1] += innerRect.bottomLeft() lines[1] += innerRect.bottomRight() lines[1] += innerRect.topRight() painter.setPen(Qt.NoPen) painter.setBrush(pal.light()) painter.drawPolygon(lines[0]) painter.setBrush(pal.dark()) painter.drawPolygon(lines[1]) painter.fillRect(rect.adjusted(lw, lw, -lw+1, -lw+1), pal.window()) class QwtColumnSymbol_PrivateData(object): def __init__(self): self.style = QwtColumnSymbol.Box self.frameStyle = QwtColumnSymbol.Raised self.lineWidth = 2 self.palette = QPalette(Qt.gray) class QwtColumnSymbol(object): # enum Style NoStyle = -1 Box = 0 UserStyle = 1000 # enum FrameStyle NoFrame, Plain, Raised = list(range(3)) def __init__(self, style): self.__data = QwtColumnSymbol_PrivateData() self.__data.style = style def setStyle(self, style): self.__data.style = style def style(self): return self.__data.style def setPalette(self, palette): self.__data.palette = palette def palette(self): return self.__data.palette def setFrameStyle(self, frameStyle): self.__data.frameStyle = frameStyle def frameStyle(self): return self.__data.frameStyle def setLineWidth(self, width): self.__data.lineWidth = width def lineWidth(self): return self.__data.lineWidth def draw(self, painter, rect): painter.save() if self.__data.style == QwtColumnSymbol.Box: self.drawBox(painter, rect) painter.restore() def drawBox(self, painter, rect): r = rect.toRect() if self.__data.frameStyle == QwtColumnSymbol.Raised: qwtDrawPanel(painter, r, self.__data.palette, self.__data.lineWidth) elif self.__data.frameStyle == QwtColumnSymbol.Plain: qwtDrawBox(painter, r, self.__data.palette, self.__data.lineWidth) else: painter.fillRect(r, self.__data.palette.window()) class QwtColumnRect(object): # enum Direction LeftToRight, RightToLeft, BottomToTop, TopToBottom = list(range(4)) def __init__(self): self.hInterval = QwtInterval() self.vInterval = QwtInterval() self.direction = 0 def toRect(self): r = QRectF(self.hInterval.minValue(), self.vInterval.minValue(), self.hInterval.maxValue()-self.hInterval.minValue(), self.vInterval.maxValue()-self.vInterval.minValue()) r = r.normalized() if self.hInterval.borderFlags() & QwtInterval.ExcludeMinimum: r.adjust(1, 0, 0, 0) if self.hInterval.borderFlags() & QwtInterval.ExcludeMaximum: r.adjust(0, 0, -1, 0) if self.vInterval.borderFlags() & QwtInterval.ExcludeMinimum: r.adjust(0, 1, 0, 0) if self.vInterval.borderFlags() & QwtInterval.ExcludeMaximum: r.adjust(0, 0, 0, -1) return r def orientation(self): if self.direction in (self.LeftToRight, self.RightToLeft): return Qt.Horizontal return Qt.Vertical PythonQwt-0.5.5/qwt/graphic.py0000666000000000000000000007001212605040216014776 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtGraphic ---------- .. autoclass:: QwtGraphic :members: """ from qwt.null_paintdevice import QwtNullPaintDevice from qwt.painter_command import QwtPainterCommand from qwt.qt.QtGui import (QPainter, QPainterPathStroker, QPaintEngine, QPixmap, QTransform, QImage) from qwt.qt.QtCore import Qt, QRectF, QSizeF, QSize, QPointF, QRect import numpy as np def qwtHasScalablePen(painter): pen = painter.pen() scalablePen = False if pen.style() != Qt.NoPen and pen.brush().style() != Qt.NoBrush: scalablePen = not pen.isCosmetic() if not scalablePen and pen.widthF() == 0.: hints = painter.renderHints() if hints & QPainter.NonCosmeticDefaultPen: scalablePen = True return scalablePen def qwtStrokedPathRect(painter, path): stroker = QPainterPathStroker() stroker.setWidth(painter.pen().widthF()) stroker.setCapStyle(painter.pen().capStyle()) stroker.setJoinStyle(painter.pen().joinStyle()) stroker.setMiterLimit(painter.pen().miterLimit()) rect = QRectF() if qwtHasScalablePen(painter): stroke = stroker.createStroke(path) rect = painter.transform().map(stroke).boundingRect() else: mappedPath = painter.transform().map(path) mappedPath = stroker.createStroke(mappedPath) rect = mappedPath.boundingRect() return rect def qwtExecCommand(painter, cmd, renderHints, transform, initialTransform): if cmd.type() == QwtPainterCommand.Path: doMap = False if bool(renderHints & QwtGraphic.RenderPensUnscaled)\ and painter.transform().isScaling(): isCosmetic = painter.pen().isCosmetic() if isCosmetic and painter.pen().widthF() == 0.: hints = painter.renderHints() if hints & QPainter.NonCosmeticDefaultPen: isCosmetic = False doMap = not isCosmetic if doMap: tr = painter.transform() painter.resetTransform() path = tr.map(cmd.path()) if initialTransform: painter.setTransform(initialTransform) invt, _ok = initialTransform.inverted() path = invt.map(path) painter.drawPath(path) painter.setTransform(tr) else: painter.drawPath(cmd.path()) elif cmd.type() == QwtPainterCommand.Pixmap: data = cmd.pixmapData() painter.drawPixmap(data.rect, data.pixmap, data.subRect) elif cmd.type() == QwtPainterCommand.Image: data = cmd.imageData() painter.drawImage(data.rect, data.image, data.subRect, data.flags) elif cmd.type() == QwtPainterCommand.State: data = cmd.stateData() if data.flags & QPaintEngine.DirtyPen: painter.setPen(data.pen) if data.flags & QPaintEngine.DirtyBrush: painter.setBrush(data.brush) if data.flags & QPaintEngine.DirtyBrushOrigin: painter.setBrushOrigin(data.brushOrigin) if data.flags & QPaintEngine.DirtyFont: painter.setFont(data.font) if data.flags & QPaintEngine.DirtyBackground: painter.setBackgroundMode(data.backgroundMode) painter.setBackground(data.backgroundBrush) if data.flags & QPaintEngine.DirtyTransform: painter.setTransform(data.transform) if data.flags & QPaintEngine.DirtyClipEnabled: painter.setClipping(data.isClipEnabled) if data.flags & QPaintEngine.DirtyClipRegion: painter.setClipRegion(data.clipRegion, data.clipOperation) if data.flags & QPaintEngine.DirtyClipPath: painter.setClipPath(data.clipPath, data.clipOperation) if data.flags & QPaintEngine.DirtyHints: for hint in (QPainter.Antialiasing, QPainter.TextAntialiasing, QPainter.SmoothPixmapTransform, QPainter.HighQualityAntialiasing, QPainter.NonCosmeticDefaultPen): painter.setRenderHint(hint, bool(data.renderHints & hint)) if data.flags & QPaintEngine.DirtyCompositionMode: painter.setCompositionMode(data.compositionMode) if data.flags & QPaintEngine.DirtyOpacity: painter.setOpacity(data.opacity) class PathInfo(object): def __init__(self, *args): if len(args) == 0: self.__scalablePen = False elif len(args) == 3: pointRect, boundingRect, scalablePen = args self.__pointRect = pointRect self.__boundingRect = boundingRect self.__scalablePen = scalablePen else: raise TypeError("%s() takes 0 or 3 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def scaledBoundingRect(self, sx, sy, scalePens): if sx == 1. and sy == 1.: return self.__boundingRect transform = QTransform() transform.scale(sx, sy) if scalePens and self.__scalablePen: rect = transform.mapRect(self.__boundingRect) else: rect = transform.mapRect(self.__pointRect) l = abs(self.__pointRect.left()-self.__boundingRect.left()) r = abs(self.__pointRect.right()-self.__boundingRect.right()) t = abs(self.__pointRect.top()-self.__boundingRect.top()) b = abs(self.__pointRect.bottom()-self.__boundingRect.bottom()) rect.adjust(-l, -t, r, b) return rect def scaleFactorX(self, pathRect, targetRect, scalePens): if pathRect.width() <= 0.0: return 0. p0 = self.__pointRect.center() l = abs(pathRect.left()-p0.x()) r = abs(pathRect.right()-p0.x()) w = 2.*min([l, r])*targetRect.width()/pathRect.width() if scalePens and self.__scalablePen: sx = w/self.__boundingRect.width() else: pw = max([abs(self.__boundingRect.left()-self.__pointRect.left()), abs(self.__boundingRect.right()-self.__pointRect.right())]) sx = (w-2*pw)/self.__pointRect.width() return sx def scaleFactorY(self, pathRect, targetRect, scalePens): if pathRect.height() <= 0.0: return 0. p0 = self.__pointRect.center() t = abs(pathRect.top()-p0.y()) b = abs(pathRect.bottom()-p0.y()) h = 2.*min([t, b])*targetRect.height()/pathRect.height() if scalePens and self.__scalablePen: sy = h/self.__boundingRect.height() else: pw = max([abs(self.__boundingRect.top()-self.__pointRect.top()), abs(self.__boundingRect.bottom()-self.__pointRect.bottom())]) sy = (h-2*pw)/self.__pointRect.height() return sy class QwtGraphic_PrivateData(object): def __init__(self): self.boundingRect = QRectF(0.0, 0.0, -1.0, -1.0) self.pointRect = QRectF(0.0, 0.0, -1.0, -1.0) self.initialTransform = None self.defaultSize = QSizeF() self.commands = [] self.pathInfos = [] self.renderHints = 0 class QwtGraphic(QwtNullPaintDevice): """ A paint device for scalable graphics `QwtGraphic` is the representation of a graphic that is tailored for scalability. Like `QPicture` it will be initialized by `QPainter` operations and can be replayed later to any target paint device. While the usual image representations `QImage` and `QPixmap` are not scalable `Qt` offers two paint devices, that might be candidates for representing a vector graphic: - `QPicture`: Unfortunately `QPicture` had been forgotten, when Qt4 introduced floating point based render engines. Its API is still on integers, what make it unusable for proper scaling. - `QSvgRenderer`, `QSvgGenerator`: Unfortunately `QSvgRenderer` hides to much information about its nodes in internal APIs, that are necessary for proper layout calculations. Also it is derived from `QObject` and can't be copied like `QImage`/`QPixmap`. `QwtGraphic` maps all scalable drawing primitives to a `QPainterPath` and stores them together with the painter state changes ( pen, brush, transformation ... ) in a list of `QwtPaintCommands`. For being a complete `QPaintDevice` it also stores pixmaps or images, what is somehow against the idea of the class, because these objects can't be scaled without a loss in quality. The main issue about scaling a `QwtGraphic` object are the pens used for drawing the outlines of the painter paths. While non cosmetic pens ( `QPen.isCosmetic()` ) are scaled with the same ratio as the path, cosmetic pens have a fixed width. A graphic might have paths with different pens - cosmetic and non-cosmetic. `QwtGraphic` caches 2 different rectangles: - control point rectangle: The control point rectangle is the bounding rectangle of all control point rectangles of the painter paths, or the target rectangle of the pixmaps/images. - bounding rectangle: The bounding rectangle extends the control point rectangle by what is needed for rendering the outline with an unscaled pen. Because the offset for drawing the outline depends on the shape of the painter path ( the peak of a triangle is different than the flat side ) scaling with a fixed aspect ratio always needs to be calculated from the control point rectangle. .. py:class:: QwtGraphic() Initializes a null graphic .. py:class:: QwtGraphic(other) Copy constructor :param qwt.graphic.QwtGraphic other: Source """ # enum RenderHint RenderPensUnscaled = 0x1 def __init__(self, *args): QwtNullPaintDevice.__init__(self) if len(args) == 0: self.setMode(QwtNullPaintDevice.PathMode) self.__data = QwtGraphic_PrivateData() elif len(args) == 1: other, = args self.setMode(other.mode()) self.__data = other.__data else: raise TypeError("%s() takes 0 or 1 argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def reset(self): """Clear all stored commands""" self.__data.commands = [] self.__data.pathInfos = [] self.__data.boundingRect = QRectF(0.0, 0.0, -1.0, -1.0) self.__data.pointRect = QRectF(0.0, 0.0, -1.0, -1.0) self.__data.defaultSize = QSizeF() def isNull(self): """Return True, when no painter commands have been stored""" return len(self.__data.commands) == 0 def isEmpty(self): """Return True, when the bounding rectangle is empty""" return self.__data.boundingRect.isEmpty() def setRenderHint(self, hint, on=True): """Toggle an render hint""" if on: self.__data.renderHints |= hint else: self.__data.renderHints &= ~hint def testRenderHint(self, hint): """Test a render hint""" return bool(self.__data.renderHints & hint) def boundingRect(self): """ The bounding rectangle is the :py:meth:`controlPointRect` extended by the areas needed for rendering the outlines with unscaled pens. :return: Bounding rectangle of the graphic .. seealso:: :py:meth:`controlPointRect`, :py:meth:`scaledBoundingRect` """ if self.__data.boundingRect.width() < 0: return QRectF() return self.__data.boundingRect def controlPointRect(self): """ The control point rectangle is the bounding rectangle of all control points of the paths and the target rectangles of the images/pixmaps. :return: Control point rectangle .. seealso:: :py:meth:`boundingRect()`, :py:meth:`scaledBoundingRect()` """ if self.__data.pointRect.width() < 0: return QRectF() return self.__data.pointRect def scaledBoundingRect(self, sx, sy): """ Calculate the target rectangle for scaling the graphic :param float sx: Horizontal scaling factor :param float sy: Vertical scaling factor :return: Scaled bounding rectangle .. note:: In case of paths that are painted with a cosmetic pen (see :py:meth:`QPen.isCosmetic()`) the target rectangle is different to multiplying the bounding rectangle. .. seealso:: :py:meth:`boundingRect()`, :py:meth:`controlPointRect()` """ if sx == 1. and sy == 1.: return self.__data.boundingRect transform = QTransform() transform.scale(sx, sy) rect = transform.mapRect(self.__data.pointRect) for pathInfo in self.__data.pathInfos: rect |= pathInfo.scaledBoundingRect(sx, sy, not bool(self.__data.renderHints & self.RenderPensUnscaled)) return rect def sizeMetrics(self): """Return Ceiled :py:meth:`defaultSize()`""" sz = self.defaultSize() return QSize(np.ceil(sz.width()), np.ceil(sz.height())) def setDefaultSize(self, size): """ The default size is used in all methods rendering the graphic, where no size is explicitly specified. Assigning an empty size means, that the default size will be calculated from the bounding rectangle. :param QSizeF size: Default size .. seealso:: :py:meth:`defaultSize()`, :py:meth:`boundingRect()` """ w = max([0., size.width()]) h = max([0., size.height()]) self.__data.defaultSize = QSizeF(w, h) def defaultSize(self): """ When a non empty size has been assigned by setDefaultSize() this size will be returned. Otherwise the default size is the size of the bounding rectangle. The default size is used in all methods rendering the graphic, where no size is explicitly specified. :return: Default size .. seealso:: :py:meth:`setDefaultSize()`, :py:meth:`boundingRect()` """ if not self.__data.defaultSize.isEmpty(): return self.__data.defaultSize return self.boundingRect().size() def render(self, *args): """ .. py:method:: render(painter) Replay all recorded painter commands :param QPainter painter: Qt painter .. py:method:: render(painter, size, aspectRatioMode) Replay all recorded painter commands The graphic is scaled to fit into the rectangle of the given size starting at ( 0, 0 ). :param QPainter painter: Qt painter :param QSizeF size: Size for the scaled graphic :param Qt.AspectRatioMode aspectRatioMode: Mode how to scale .. py:method:: render(painter, rect, aspectRatioMode) Replay all recorded painter commands The graphic is scaled to fit into the given rectangle :param QPainter painter: Qt painter :param QRectF rect: Rectangle for the scaled graphic :param Qt.AspectRatioMode aspectRatioMode: Mode how to scale .. py:method:: render(painter, pos, aspectRatioMode) Replay all recorded painter commands The graphic is scaled to the :py:meth:`defaultSize()` and aligned to a position. :param QPainter painter: Qt painter :param QPointF pos: Reference point, where to render :param Qt.AspectRatioMode aspectRatioMode: Mode how to scale """ if len(args) == 1: painter, = args if self.isNull(): return transform = painter.transform() painter.save() for command in self.__data.commands: qwtExecCommand(painter, command, self.__data.renderHints, transform, self.__data.initialTransform) painter.restore() elif len(args) in (2, 3) and isinstance(args[1], QSizeF): painter, size = args[:2] aspectRatioMode = Qt.IgnoreAspectRatio if len(args) == 3: aspectRatioMode = args[-1] r = QRectF(0., 0., size.width(), size.height()) self.render(painter, r, aspectRatioMode) elif len(args) in (2, 3) and isinstance(args[1], QRectF): painter, rect = args[:2] aspectRatioMode = Qt.IgnoreAspectRatio if len(args) == 3: aspectRatioMode = args[-1] if self.isEmpty() or rect.isEmpty(): return sx = 1. sy = 1. if self.__data.pointRect.width() > 0.: sx = rect.width()/self.__data.pointRect.width() if self.__data.pointRect.height() > 0.: sy = rect.height()/self.__data.pointRect.height() scalePens = not bool(self.__data.renderHints & self.RenderPensUnscaled) for info in self.__data.pathInfos: ssx = info.scaleFactorX(self.__data.pointRect, rect, scalePens) if ssx > 0.: sx = min([sx, ssx]) ssy = info.scaleFactorY(self.__data.pointRect, rect, scalePens) if ssy > 0.: sy = min([sy, ssy]) if aspectRatioMode == Qt.KeepAspectRatio: s = min([sx, sy]) sx = s sy = s elif aspectRatioMode == Qt.KeepAspectRatioByExpanding: s = max([sx, sy]) sx = s sy = s tr = QTransform() tr.translate(rect.center().x()-.5*sx*self.__data.pointRect.width(), rect.center().y()-.5*sy*self.__data.pointRect.height()) tr.scale(sx, sy) tr.translate(-self.__data.pointRect.x(), -self.__data.pointRect.y()) transform = painter.transform() if not scalePens and transform.isScaling(): # we don't want to scale pens according to sx/sy, # but we want to apply the scaling from the # painter transformation later self.__data.initialTransform = QTransform() self.__data.initialTransform.scale(transform.m11(), transform.m22()) painter.setTransform(tr, True) self.render(painter) painter.setTransform(transform) self.__data.initialTransform = None elif len(args) in (2, 3) and isinstance(args[1], QPointF): painter, pos = args[:2] alignment = Qt.AlignTop|Qt.AlignLeft if len(args) == 3: alignment = args[-1] r = QRectF(pos, self.defaultSize()) if alignment & Qt.AlignLeft: r.moveLeft(pos.x()) elif alignment & Qt.AlignHCenter: r.moveCenter(QPointF(pos.x(), r.center().y())) elif alignment & Qt.AlignRight: r.moveRight(pos.x()) if alignment & Qt.AlignTop: r.moveTop(pos.y()) elif alignment & Qt.AlignVCenter: r.moveCenter(QPointF(r.center().x(), pos.y())) elif alignment & Qt.AlignBottom: r.moveBottom(pos.y()) self.render(painter, r) else: raise TypeError("%s().render() takes 1, 2 or 3 argument(s) (%s "\ "given)" % (self.__class__.__name__, len(args))) def toPixmap(self, *args): """ Convert the graphic to a `QPixmap` All pixels of the pixmap get initialized by `Qt.transparent` before the graphic is scaled and rendered on it. The size of the pixmap is the default size ( ceiled to integers ) of the graphic. :return: The graphic as pixmap in default size .. seealso:: :py:meth:`defaultSize()`, :py:meth:`toImage()`, :py:meth:`render()` """ if len(args) == 0: if self.isNull(): return QPixmap() sz = self.defaultSize() w = np.ceil(sz.width()) h = np.ceil(sz.height()) pixmap = QPixmap(w, h) pixmap.fill(Qt.transparent) r = QRectF(0., 0., sz.width(), sz.height()) painter = QPainter(pixmap) self.render(painter, r, Qt.KeepAspectRatio) painter.end() return pixmap elif len(args) in (1, 2): size = args[0] aspectRatioMode = Qt.IgnoreAspectRatio if len(args) == 2: aspectRatioMode = args[-1] pixmap = QPixmap(size) pixmap.fill(Qt.transparent) r = QRect(0, 0, size.width(), size.height()) painter = QPainter(pixmap) self.render(painter, r, aspectRatioMode) painter.end() return pixmap def toImage(self, *args): """ .. py:method:: toImage() Convert the graphic to a `QImage` All pixels of the image get initialized by 0 ( transparent ) before the graphic is scaled and rendered on it. The format of the image is `QImage.Format_ARGB32_Premultiplied`. The size of the image is the default size ( ceiled to integers ) of the graphic. :return: The graphic as image in default size .. py:method:: toImage(size, [aspectRatioMode=Qt.IgnoreAspectRatio]) Convert the graphic to a `QImage` All pixels of the image get initialized by 0 ( transparent ) before the graphic is scaled and rendered on it. The format of the image is `QImage.Format_ARGB32_Premultiplied`. :param QSize size: Size of the image :param `Qt.AspectRatioMode` aspectRatioMode: Aspect ratio how to scale the graphic :return: The graphic as image .. seealso:: :py:meth:`toPixmap()`, :py:meth:`render()` """ if len(args) == 0: if self.isNull(): return QImage() sz = self.defaultSize() w = np.ceil(sz.width()) h = np.ceil(sz.height()) image = QImage(w, h, QImage.Format_ARGB32) image.fill(0) r = QRect(0, 0, sz.width(), sz.height()) painter = QPainter(image) self.render(painter, r, Qt.KeepAspectRatio) painter.end() return image elif len(args) in (1, 2): size = args[0] aspectRatioMode = Qt.IgnoreAspectRatio if len(args) == 2: aspectRatioMode = args[-1] image = QImage(size, QImage.Format_ARGB32_Premultiplied) image.fill(0) r = QRect(0, 0, size.width(), size.height()) painter = QPainter(image) self.render(painter, r, aspectRatioMode) return image def drawPath(self, path): """ Store a path command in the command list :param QPainterPath path: Painter path .. seealso:: :py:meth:`QPaintEngine.drawPath()` """ painter = self.paintEngine().painter() if painter is None: return self.__data.commands += [QwtPainterCommand(path)] if not path.isEmpty(): scaledPath = painter.transform().map(path) pointRect = scaledPath.boundingRect() boundingRect = QRectF(pointRect) if painter.pen().style() != Qt.NoPen\ and painter.pen().brush().style() != Qt.NoBrush: boundingRect = qwtStrokedPathRect(painter, path) self.updateControlPointRect(pointRect) self.updateBoundingRect(boundingRect) self.__data.pathInfos += [PathInfo(pointRect, boundingRect, qwtHasScalablePen(painter))] def drawPixmap(self, rect, pixmap, subRect): """ Store a pixmap command in the command list :param QRectF rect: target rectangle :param QPixmap pixmap: Pixmap to be painted :param QRectF subRect: Reactangle of the pixmap to be painted .. seealso:: :py:meth:`QPaintEngine.drawPixmap()` """ painter = self.paintEngine().painter() if painter is None: return self.__data.commands += [QwtPainterCommand(rect, pixmap, subRect)] r = painter.transform().mapRect(rect) self.updateControlPointRect(r) self.updateBoundingRect(r) def drawImage(self, rect, image, subRect, flags): """ Store a image command in the command list :param QRectF rect: target rectangle :param QImage image: Pixmap to be painted :param QRectF subRect: Reactangle of the pixmap to be painted :param Qt.ImageConversionFlags flags: Pixmap to be painted .. seealso:: :py:meth:`QPaintEngine.drawImage()` """ painter = self.paintEngine().painter() if painter is None: return self.__data.commands += [QwtPainterCommand(rect, image, subRect, flags)] r = painter.transform().mapRect(rect) self.updateControlPointRect(r) self.updateBoundingRect(r) def updateState(self, state): """ Store a state command in the command list :param QPaintEngineState state: State to be stored .. seealso:: :py:meth:`QPaintEngine.updateState()` """ #XXX: shall we call the parent's implementation of updateState? self.__data.commands += [QwtPainterCommand(state)] def updateBoundingRect(self, rect): br = QRectF(rect) painter = self.paintEngine().painter() if painter and painter.hasClipping(): #XXX: there's something fishy about the following lines... cr = painter.clipRegion().boundingRect() cr = painter.transform().mapRect(br) br &= cr if self.__data.boundingRect.width() < 0: self.__data.boundingRect = br else: self.__data.boundingRect |= br def updateControlPointRect(self, rect): if self.__data.pointRect.width() < 0.: self.__data.pointRect = rect else: self.__data.pointRect |= rect def commands(self): return self.__data.commands def setCommands(self, commands): self.reset() painter = QPainter(self) for cmd in commands: qwtExecCommand(painter, cmd, 0, QTransform(), None) painter.end() PythonQwt-0.5.5/qwt/plot_layout.py0000666000000000000000000012602412613444650015752 0ustar rootroot# -*- coding: utf-8 -*- # # Licensed under the terms of the Qwt License # Copyright (c) 2002 Uwe Rathmann, for the original C++ code # Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization # (see LICENSE file for more details) """ QwtPlotLayout ------------- .. autoclass:: QwtPlotLayout :members: """ from qwt.text import QwtText from qwt.scale_widget import QwtScaleWidget from qwt.plot import QwtPlot from qwt.scale_draw import QwtAbstractScaleDraw from qwt.qt.QtGui import QFont, QRegion from qwt.qt.QtCore import QSize, Qt, QRectF import numpy as np QWIDGETSIZE_MAX = (1<<24)-1 class LegendData(object): def __init__(self): self.frameWidth = None self.hScrollExtent = None self.vScrollExtent = None self.hint = QSize() class TitleData(object): def __init__(self): self.text = QwtText() self.frameWidth = None class FooterData(object): def __init__(self): self.text = QwtText() self.frameWidth = None class ScaleData(object): def __init__(self): self.isEnabled = None self.scaleWidget = QwtScaleWidget() self.scaleFont = QFont() self.start = None self.end = None self.baseLineOffset = None self.tickOffset = None self.dimWithoutTitle = None class CanvasData(object): def __init__(self): self.contentsMargins = [0 for _i in QwtPlot.validAxes] class QwtPlotLayout_LayoutData(object): def __init__(self): self.legend = LegendData() self.title = TitleData() self.footer = FooterData() self.scale = [ScaleData() for _i in QwtPlot.validAxes] self.canvas = CanvasData() def init(self, plot, rect): """Extract all layout relevant data from the plot components""" # legend if plot.legend(): self.legend.frameWidth = plot.legend().frameWidth() self.legend.hScrollExtent = plot.legend().scrollExtent(Qt.Horizontal) self.legend.vScrollExtent = plot.legend().scrollExtent(Qt.Vertical) hint = plot.legend().sizeHint() w = min([hint.width(), np.floor(rect.width())]) h = plot.legend().heightForWidth(w) if h <= 0: h = hint.height() if h > rect.height(): w += self.legend.hScrollExtent self.legend.hint = QSize(w, h) # title self.title.frameWidth = 0 self.title.text = QwtText() if plot.titleLabel(): label = plot.titleLabel() self.title.text = label.text() if not self.title.text.testPaintAttribute(QwtText.PaintUsingTextFont): self.title.text.setFont(label.font()) self.title.frameWidth = plot.titleLabel().frameWidth() # footer self.footer.frameWidth = 0 self.footer.text = QwtText() if plot.footerLabel(): label = plot.footerLabel() self.footer.text = label.text() if not self.footer.text.testPaintAttribute(QwtText.PaintUsingTextFont): self.footer.text.setFont(label.font()) self.footer.frameWidth = plot.footerLabel().frameWidth() # scales for axis in QwtPlot.validAxes: if plot.axisEnabled(axis): scaleWidget = plot.axisWidget(axis) self.scale[axis].isEnabled = True self.scale[axis].scaleWidget = scaleWidget self.scale[axis].scaleFont = scaleWidget.font() self.scale[axis].start = scaleWidget.startBorderDist() self.scale[axis].end = scaleWidget.endBorderDist() self.scale[axis].baseLineOffset = scaleWidget.margin() self.scale[axis].tickOffset = scaleWidget.margin() if scaleWidget.scaleDraw().hasComponent(QwtAbstractScaleDraw.Ticks): self.scale[axis].tickOffset += scaleWidget.scaleDraw().maxTickLength() self.scale[axis].dimWithoutTitle = scaleWidget.dimForLength( QWIDGETSIZE_MAX, self.scale[axis].scaleFont) if not scaleWidget.title().isEmpty(): self.scale[axis].dimWithoutTitle -= \ scaleWidget.titleHeightForWidth(QWIDGETSIZE_MAX) else: self.scale[axis].isEnabled = False self.scale[axis].start = 0 self.scale[axis].end = 0 self.scale[axis].baseLineOffset = 0 self.scale[axis].tickOffset = 0. self.scale[axis].dimWithoutTitle = 0 self.canvas.contentsMargins = plot.canvas().getContentsMargins() class QwtPlotLayout_PrivateData(object): def __init__(self): self.spacing = 5 self.titleRect = QRectF() self.footerRect = QRectF() self.legendRect = QRectF() self.scaleRect = [QRectF() for _i in QwtPlot.validAxes] self.canvasRect = QRectF() self.layoutData = QwtPlotLayout_LayoutData() self.legendPos = None self.legendRatio = None self.canvasMargin = [0] * QwtPlot.axisCnt self.alignCanvasToScales = [False] * QwtPlot.axisCnt class QwtPlotLayout(object): """ Layout engine for QwtPlot. It is used by the `QwtPlot` widget to organize its internal widgets or by `QwtPlot.print()` to render its content to a QPaintDevice like a QPrinter, QPixmap/QImage or QSvgRenderer. .. seealso:: :py:meth:`qwt.plot.QwtPlot.setPlotLayout()` Valid options: * `QwtPlotLayout.AlignScales`: Unused * `QwtPlotLayout.IgnoreScrollbars`: Ignore the dimension of the scrollbars. There are no scrollbars, when the plot is not rendered to widgets. * `QwtPlotLayout.IgnoreFrames`: Ignore all frames. * `QwtPlotLayout.IgnoreLegend`: Ignore the legend. * `QwtPlotLayout.IgnoreTitle`: Ignore the title. * `QwtPlotLayout.IgnoreFooter`: Ignore the footer. """ # enum Option AlignScales = 0x01 IgnoreScrollbars = 0x02 IgnoreFrames = 0x04 IgnoreLegend = 0x08 IgnoreTitle = 0x10 IgnoreFooter = 0x20 def __init__(self): self.__data = QwtPlotLayout_PrivateData() self.setLegendPosition(QwtPlot.BottomLegend) self.setCanvasMargin(4) self.setAlignCanvasToScales(False) self.invalidate() def setCanvasMargin(self, margin, axis=-1): """ Change a margin of the canvas. The margin is the space above/below the scale ticks. A negative margin will be set to -1, excluding the borders of the scales. :param int margin: New margin :param int axisId: Axis index .. seealso:: :py:meth:`canvasMargin()` .. warning:: The margin will have no effect when `alignCanvasToScale()` is True """ if margin < 1: margin = -1 if axis == -1: for axis in QwtPlot.validAxes: self.__data.canvasMargin[axis] = margin elif axis in QwtPlot.validAxes: self.__data.canvasMargin[axis] = margin def canvasMargin(self, axisId): """ :param int axisId: Axis index :return: Margin around the scale tick borders .. seealso:: :py:meth:`setCanvasMargin()` """ if axisId not in QwtPlot.validAxes: return 0 return self.__data.canvasMargin[axisId] def setAlignCanvasToScales(self, *args): """ Change the align-canvas-to-axis-scales setting. .. py:method:: setAlignCanvasToScales(on): Set the align-canvas-to-axis-scales flag for all axes :param bool on: True/False .. py:method:: setAlignCanvasToScales(axisId, on): Change the align-canvas-to-axis-scales setting. The canvas may: - extend beyond the axis scale ends to maximize its size, - align with the axis scale ends to control its size. The axisId parameter is somehow confusing as it identifies a border of the plot and not the axes, that are aligned. F.e when `QwtPlot.yLeft` is set, the left end of the the x-axes (`QwtPlot.xTop`, `QwtPlot.xBottom`) is aligned. :param int axisId: Axis index :param bool on: True/False .. seealso:: :py:meth:`setAlignCanvasToScale()`, :py:meth:`alignCanvasToScale()` """ if len(args) == 1: on, = args for axis in QwtPlot.validAxes: self.__data.alignCanvasToScales[axis] = on elif len(args) == 2: axisId, on = args if axis in QwtPlot.validAxes: self.__data.alignCanvasToScales[axisId] = on else: raise TypeError("%s().setAlignCanvasToScales() takes 1 or 2 "\ "argument(s) (%s given)"\ % (self.__class__.__name__, len(args))) def alignCanvasToScale(self, axisId): """ Return the align-canvas-to-axis-scales setting. The canvas may: - extend beyond the axis scale ends to maximize its size - align with the axis scale ends to control its size. :param int axisId: Axis index :return: align-canvas-to-axis-scales setting .. seealso:: :py:meth:`setAlignCanvasToScale()`, :py:meth:`setCanvasMargin()` """ if axisId not in QwtPlot.validAxes: return False return self.__data.alignCanvasToScales[axisId] def setSpacing(self, spacing): """ Change the spacing of the plot. The spacing is the distance between the plot components. :param int spacing: New spacing .. seealso:: :py:meth:`setCanvasMargin()`, :py:meth:`spacing()` """ self.__data.spacing = max([0, spacing]) def spacing(self): """ :return: Spacing .. seealso:: :py:meth:`margin()`, :py:meth:`setSpacing()` """ return self.__data.spacing def setLegendPosition(self, *args): """ Specify the position of the legend .. py:method:: setLegendPosition(pos, [ratio=0.]): Specify the position of the legend :param QwtPlot.LegendPosition pos: Legend position :param float ratio: Ratio between legend and the bounding rectangle of title, footer, canvas and axes The legend will be shrunk if it would need more space than the given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of <= 0.0 it will be reset to the default ratio. The default vertical/horizontal ratio is 0.33/0.5. Valid position values: * `QwtPlot.LeftLegend`, * `QwtPlot.RightLegend`, * `QwtPlot.TopLegend`, * `QwtPlot.BottomLegend` .. seealso:: :py:meth:`setLegendPosition()` """ if len(args) == 2: pos, ratio = args if ratio > 1.: ratio = 1. if pos in (QwtPlot.TopLegend, QwtPlot.BottomLegend): if ratio <= 0.: ratio = .33 self.__data.legendRatio = ratio self.__data.legendPos = pos elif pos in (QwtPlot.LeftLegend, QwtPlot.RightLegend): if ratio <= 0.: ratio = .5 self.__data.legendRatio = ratio self.__data.legendPos = pos elif len(args) == 1: pos, = args self.setLegendPosition(pos, 0.) else: raise TypeError("%s().setLegendPosition() takes 1 or 2 argument(s)"\ "(%s given)" % (self.__class__.__name__, len(args))) def legendPosition(self): """ :return: Position of the legend .. seealso:: :py:meth:`legendPosition()` """ return self.__data.legendPos def setLegendRatio(self, ratio): """ Specify the relative size of the legend in the plot :param float ratio: Ratio between legend and the bounding rectangle of title, footer, canvas and axes The legend will be shrunk if it would need more space than the given ratio. The ratio is limited to ]0.0 .. 1.0]. In case of <= 0.0 it will be reset to the default ratio. The default vertical/horizontal ratio is 0.33/0.5. .. seealso:: :py:meth:`legendRatio()` """ self.setLegendPosition(self.legendPosition(), ratio) def legendRatio(self): """ :return: The relative size of the legend in the plot. .. seealso:: :py:meth:`setLegendRatio()` """ return self.__data.legendRatio def setTitleRect(self, rect): """ Set the geometry for the title This method is intended to be used from derived layouts overloading `activate()` :param QRectF rect: Rectangle .. seealso:: :py:meth:`titleRect()`, :py:meth:`activate()` """ self.__data.titleRect = rect def titleRect(self): """ :return: Geometry for the title .. seealso:: :py:meth:`invalidate()`, :py:meth:`activate()` """ return self.__data.titleRect def setFooterRect(self, rect): """ Set the geometry for the footer This method is intended to be used from derived layouts overloading `activate()` :param QRectF rect: Rectangle .. seealso:: :py:meth:`footerRect()`, :py:meth:`activate()` """ self.__data.footerRect = rect def footerRect(self): """ :return: Geometry for the footer .. seealso:: :py:meth:`invalidate()`, :py:meth:`activate()` """ return self.__data.footerRect def setLegendRect(self, rect): """ Set the geometry for the legend This method is intended to be used from derived layouts overloading `activate()` :param QRectF rect: Rectangle for the legend .. seealso:: :py:meth:`footerRect()`, :py:meth:`activate()` """ self.__data.legendRect = rect def legendRect(self): """ :return: Geometry for the legend .. seealso:: :py:meth:`invalidate()`, :py:meth:`activate()` """ return self.__data.legendRect def setScaleRect(self, axis, rect): """ Set the geometry for an axis This method is intended to be used from derived layouts overloading `activate()` :param int axisId: Axis index :param QRectF rect: Rectangle for the scale .. seealso:: :py:meth:`scaleRect()`, :py:meth:`activate()` """ if axis in QwtPlot.validAxes: self.__data.scaleRect[axis] = rect def scaleRect(self, axis): """ :param int axisId: Axis index :return: Geometry for the scale .. seealso:: :py:meth:`invalidate()`, :py:meth:`activate()` """ if axis not in QwtPlot.validAxes: return QRectF() return self.__data.scaleRect[axis] def setCanvasRect(self, rect): """ Set the geometry for the canvas This method is intended to be used from derived layouts overloading `activate()` :param QRectF rect: Rectangle .. seealso:: :py:meth:`canvasRect()`, :py:meth:`activate()` """ self.__data.canvasRect = rect def canvasRect(self): """ :return: Geometry for the canvas .. seealso:: :py:meth:`invalidate()`, :py:meth:`activate()` """ return self.__data.canvasRect def invalidate(self): """ Invalidate the geometry of all components. .. seealso:: :py:meth:`activate()` """ self.__data.titleRect = QRectF() self.__data.footerRect = QRectF() self.__data.legendRect = QRectF() self.__data.canvasRect = QRectF() for axis in QwtPlot.validAxes: self.__data.scaleRect[axis] = QRectF() def minimumSizeHint(self, plot): """ :param qwt.plot.QwtPlot plot: Plot widget :return: Minimum size hint .. seealso:: :py:meth:`qwt.plot.QwtPlot.minimumSizeHint()` """ class _ScaleData(object): def __init__(self): self.w = 0 self.h = 0 self.minLeft = 0 self.minRight = 0 self.tickOffset = 0 scaleData = [_ScaleData() for _i in QwtPlot.validAxes] canvasBorder = [0 for _i in QwtPlot.validAxes] fw, _, _, _ = plot.canvas().getContentsMargins() for axis in QwtPlot.validAxes: if plot.axisEnabled(axis): scl = plot.axisWidget(axis) sd = scaleData[axis] hint = scl.minimumSizeHint() sd.w = hint.width() sd.h = hint.height() sd.minLeft, sd.minLeft = scl.getBorderDistHint() sd.tickOffset = scl.margin() if scl.scaleDraw().hasComponent(QwtAbstractScaleDraw.Ticks): sd.tickOffset += np.ceil(scl.scaleDraw().maxTickLength()) canvasBorder[axis] = fw + self.__data.canvasMargin[axis] + 1 for axis in QwtPlot.validAxes: sd = scaleData[axis] if sd.w and axis in (QwtPlot.xBottom, QwtPlot.xTop): if sd.minLeft > canvasBorder[QwtPlot.yLeft]\ and scaleData[QwtPlot.yLeft].w: shiftLeft = sd.minLeft - canvasBorder[QwtPlot.yLeft] if shiftLeft > scaleData[QwtPlot.yLeft].w: shiftLeft = scaleData[QwtPlot.yLeft].w sd.w -= shiftLeft if sd.minRight > canvasBorder[QwtPlot.yRight]\ and scaleData[QwtPlot.yRight].w: shiftRight = sd.minRight - canvasBorder[QwtPlot.yRight] if shiftRight > scaleData[QwtPlot.yRight].w: shiftRight = scaleData[QwtPlot.yRight].w sd.w -= shiftRight if sd.h and axis in (QwtPlot.yLeft, QwtPlot.yRight): if sd.minLeft > canvasBorder[QwtPlot.xBottom]\ and scaleData[QwtPlot.xBottom].h: shiftBottom = sd.minLeft - canvasBorder[QwtPlot.xBottom] if shiftBottom > scaleData[QwtPlot.xBottom].tickOffset: shiftBottom = scaleData[QwtPlot.xBottom].tickOffset sd.h -= shiftBottom if sd.minLeft > canvasBorder[QwtPlot.xTop]\ and scaleData[QwtPlot.xTop].h: shiftTop = sd.minRight - canvasBorder[QwtPlot.xTop] if shiftTop > scaleData[QwtPlot.xTop].tickOffset: shiftTop = scaleData[QwtPlot.xTop].tickOffset sd.h -= shiftTop canvas = plot.canvas() left, top, right, bottom = canvas.getContentsMargins() minCanvasSize = canvas.minimumSize() w = scaleData[QwtPlot.yLeft].w + scaleData[QwtPlot.yRight].w cw = max([scaleData[QwtPlot.xBottom].w, scaleData[QwtPlot.xTop].w]) + left + 1 + right + 1 w += max([cw, minCanvasSize.width()]) h = scaleData[QwtPlot.xBottom].h + scaleData[QwtPlot.xTop].h ch = max([scaleData[QwtPlot.yLeft].h, scaleData[QwtPlot.yRight].h]) + top + 1 + bottom + 1 h += max([ch, minCanvasSize.height()]) for label in [plot.titleLabel(), plot.footerLabel()]: if label and not label.text().isEmpty(): centerOnCanvas = not plot.axisEnabled(QwtPlot.yLeft)\ and plot.axisEnabled(QwtPlot.yRight) labelW = w if centerOnCanvas: labelW -= scaleData[QwtPlot.yLeft].w +\ scaleData[QwtPlot.yRight].w labelH = label.heightForWidth(labelW) if labelH > labelW: w = labelW = labelH if centerOnCanvas: w += scaleData[QwtPlot.yLeft].w +\ scaleData[QwtPlot.yRight].w labelH = label.heightForWidth(labelW) h += labelH + self.__data.spacing legend = plot.legend() if legend and not legend.isEmpty(): if self.__data.legendPos in (QwtPlot.LeftLegend, QwtPlot.RightLegend): legendW = legend.sizeHint().width() legendH = legend.heightForWidth(legendW) if legend.frameWidth() > 0: w += self.__data.spacing if legendH > h: legendW += legend.scrollExtent(Qt.Horizontal) if self.__data.legendRatio < 1.: legendW = min([legendW, int(w/(1.-self.__data.legendRatio))]) w += legendW + self.__data.spacing else: legendW = min([legend.sizeHint().width(), w]) legendH = legend.heightForWidth(legendW) if legend.frameWidth() > 0: h += self.__data.spacing if self.__data.legendRatio < 1.: legendH = min([legendH, int(h/(1.-self.__data.legendRatio))]) h += legendH + self.__data.spacing return QSize(w, h) def layoutLegend(self, options, rect): """ Find the geometry for the legend :param options: Options how to layout the legend :param QRectF rect: Rectangle where to place the legend :return: Geometry for the legend """ hint = self.__data.layoutData.legend.hint if self.__data.legendPos in (QwtPlot.LeftLegend, QwtPlot.RightLegend): dim = min([hint.width(), int(rect.width()*self.__data.legendRatio)]) if not (options & self.IgnoreScrollbars): if hint.height() > rect.height(): dim += self.__data.layoutData.legend.hScrollExtent else: dim = min([hint.height(), int(rect.height()*self.__data.legendRatio)]) dim = max([dim, self.__data.layoutData.legend.vScrollExtent]) legendRect = QRectF(rect) if self.__data.legendPos == QwtPlot.LeftLegend: legendRect.setWidth(dim) elif self.__data.legendPos == QwtPlot.RightLegend: legendRect.setX(rect.right()-dim) legendRect.setWidth(dim) elif self.__data.legendPos == QwtPlot.TopLegend: legendRect.setHeight(dim) elif self.__data.legendPos == QwtPlot.BottomLegend: legendRect.setY(rect.bottom()-dim) legendRect.setHeight(dim) return legendRect def alignLegend(self, canvasRect, legendRect): """ Align the legend to the canvas :param QRectF canvasRect: Geometry of the canvas :param QRectF legendRect: Maximum geometry for the legend :return: Geometry for the aligned legend """ alignedRect = legendRect if self.__data.legendPos in (QwtPlot.BottomLegend, QwtPlot.TopLegend): if self.__data.layoutData.legend.hint.width() < canvasRect.width(): alignedRect.setX(canvasRect.x()) alignedRect.setWidth(canvasRect.width()) else: if self.__data.layoutData.legend.hint.height() < canvasRect.height(): alignedRect.setY(canvasRect.y()) alignedRect.setHeight(canvasRect.height()) return alignedRect def expandLineBreaks(self, options, rect): """ Expand all line breaks in text labels, and calculate the height of their widgets in orientation of the text. :param options: Options how to layout the legend :param QRectF rect: Bounding rectangle for title, footer, axes and canvas. :return: tuple `(dimTitle, dimFooter, dimAxis)` Returns: * `dimTitle`: Expanded height of the title widget * `dimFooter`: Expanded height of the footer widget * `dimAxis`: Expanded heights of the axis in axis orientation. """ dimTitle = dimFooter = 0 dimAxis = [0 for axis in QwtPlot.validAxes] backboneOffset = [0 for _i in QwtPlot.validAxes] for axis in QwtPlot.validAxes: if not (options & self.IgnoreFrames): backboneOffset[axis] += self.__data.layoutData.canvas.contentsMargins[axis] if not self.__data.alignCanvasToScales[axis]: backboneOffset[axis] += self.__data.canvasMargin[axis] done = False while not done: done = True # the size for the 4 axis depend on each other. Expanding # the height of a horizontal axis will shrink the height # for the vertical axis, shrinking the height of a vertical # axis will result in a line break what will expand the # width and results in shrinking the width of a horizontal # axis what might result in a line break of a horizontal # axis ... . So we loop as long until no size changes. if not ((options & self.IgnoreTitle) or \ self.__data.layoutData.title.text.isEmpty()): w = rect.width() if self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled !=\ self.__data.layoutData.scale[QwtPlot.yRight].isEnabled: w -= dimAxis[QwtPlot.yLeft]+dimAxis[QwtPlot.yRight] d = np.ceil(self.__data.layoutData.title.text.heightForWidth(w)) if not (options & self.IgnoreFrames): d += 2*self.__data.layoutData.title.frameWidth if d > dimTitle: dimTitle = d done = False if not ((options & self.IgnoreFooter) or \ self.__data.layoutData.footer.text.isEmpty()): w = rect.width() if self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled !=\ self.__data.layoutData.scale[QwtPlot.yRight].isEnabled: w -= dimAxis[QwtPlot.yLeft]+dimAxis[QwtPlot.yRight] d = np.ceil(self.__data.layoutData.footer.text.heightForWidth(w)) if not (options & self.IgnoreFrames): d += 2*self.__data.layoutData.footer.frameWidth if d > dimFooter: dimFooter = d done = False for axis in QwtPlot.validAxes: scaleData = self.__data.layoutData.scale[axis] if scaleData.isEnabled: if axis in (QwtPlot.xTop, QwtPlot.xBottom): length = rect.width()-dimAxis[QwtPlot.yLeft]-dimAxis[QwtPlot.yRight] length -= scaleData.start + scaleData.end if dimAxis[QwtPlot.yRight] > 0: length -= 1 length += min([dimAxis[QwtPlot.yLeft], scaleData.start-backboneOffset[QwtPlot.yLeft]]) length += min([dimAxis[QwtPlot.yRight], scaleData.end-backboneOffset[QwtPlot.yRight]]) else: length = rect.height()-dimAxis[QwtPlot.xTop]-dimAxis[QwtPlot.xBottom] length -= scaleData.start + scaleData.end length -= 1 if dimAxis[QwtPlot.xBottom] <= 0: length -= 1 if dimAxis[QwtPlot.xTop] <= 0: length -= 1 if dimAxis[QwtPlot.xBottom] > 0: length += min([self.__data.layoutData.scale[QwtPlot.xBottom].tickOffset, float(scaleData.start-backboneOffset[QwtPlot.xBottom])]) if dimAxis[QwtPlot.xTop] > 0: length += min([self.__data.layoutData.scale[QwtPlot.xTop].tickOffset, float(scaleData.end-backboneOffset[QwtPlot.xTop])]) if dimTitle > 0: length -= dimTitle + self.__data.spacing d = scaleData.dimWithoutTitle if not scaleData.scaleWidget.title().isEmpty(): d += scaleData.scaleWidget.titleHeightForWidth(np.floor(length)) if d > dimAxis[axis]: dimAxis[axis] = d done = False return dimTitle, dimFooter, dimAxis def alignScales(self, options, canvasRect, scaleRect): """ Align the ticks of the axis to the canvas borders using the empty corners. :param options: Options how to layout the legend :param QRectF canvasRect: Geometry of the canvas ( IN/OUT ) :param QRectF scaleRect: Geometry of the scales ( IN/OUT ) """ backboneOffset = [0 for _i in QwtPlot.validAxes] for axis in QwtPlot.validAxes: backboneOffset[axis] = 0 if not self.__data.alignCanvasToScales[axis]: backboneOffset[axis] += self.__data.canvasMargin[axis] if not options & self.IgnoreFrames: backboneOffset[axis] += self.__data.layoutData.canvas.contentsMargins[axis] for axis in QwtPlot.validAxes: if not scaleRect[axis].isValid(): continue startDist = self.__data.layoutData.scale[axis].start endDist = self.__data.layoutData.scale[axis].end axisRect = scaleRect[axis] if axis in (QwtPlot.xTop, QwtPlot.xBottom): leftScaleRect = scaleRect[QwtPlot.yLeft] leftOffset = backboneOffset[QwtPlot.yLeft]-startDist if leftScaleRect.isValid(): dx = leftOffset + leftScaleRect.width() if self.__data.alignCanvasToScales[QwtPlot.yLeft] and dx < 0.: cLeft = canvasRect.left() canvasRect.setLeft(max([cLeft, axisRect.left()-dx])) else: minLeft = leftScaleRect.left() left = axisRect.left()+leftOffset axisRect.setLeft(max([left, minLeft])) else: if self.__data.alignCanvasToScales[QwtPlot.yLeft] and leftOffset < 0: canvasRect.setLeft(max([canvasRect.left(), axisRect.left()-leftOffset])) else: if leftOffset > 0: axisRect.setLeft(axisRect.left()+leftOffset) rightScaleRect = scaleRect[QwtPlot.yRight] rightOffset = backboneOffset[QwtPlot.yRight]-endDist+1 if rightScaleRect.isValid(): dx = rightOffset+rightScaleRect.width() if self.__data.alignCanvasToScales[QwtPlot.yRight] and dx < 0: cRight = canvasRect.right() canvasRect.setRight(min([cRight, axisRect.right()+dx])) maxRight = rightScaleRect.right() right = axisRect.right()-rightOffset axisRect.setRight(min([right, maxRight])) else: if self.__data.alignCanvasToScales[QwtPlot.yRight] and rightOffset < 0: canvasRect.setRight(min([canvasRect.right(), axisRect.right()+rightOffset])) else: if rightOffset > 0: axisRect.setRight(axisRect.right()-rightOffset) else: bottomScaleRect = scaleRect[QwtPlot.xBottom] bottomOffset = backboneOffset[QwtPlot.xBottom]-endDist+1 if bottomScaleRect.isValid(): dy = bottomOffset+bottomScaleRect.height() if self.__data.alignCanvasToScales[QwtPlot.xBottom] and dy < 0: cBottom = canvasRect.bottom() canvasRect.setBottom(min([cBottom, axisRect.bottom()+dy])) else: maxBottom = bottomScaleRect.top()+\ self.__data.layoutData.scale[QwtPlot.xBottom].tickOffset bottom = axisRect.bottom()-bottomOffset axisRect.setBottom(min([bottom, maxBottom])) else: if self.__data.alignCanvasToScales[QwtPlot.xBottom] and bottomOffset < 0: canvasRect.setBottom(min([canvasRect.bottom(), axisRect.bottom()+bottomOffset])) else: if bottomOffset > 0: axisRect.setBottom(axisRect.bottom()-bottomOffset) topScaleRect = scaleRect[QwtPlot.xTop] topOffset = backboneOffset[QwtPlot.xTop]-startDist if topScaleRect.isValid(): dy = topOffset+topScaleRect.height() if self.__data.alignCanvasToScales[QwtPlot.xTop] and dy < 0: cTop = canvasRect.top() canvasRect.setTop(max([cTop, axisRect.top()-dy])) else: minTop = topScaleRect.bottom()-\ self.__data.layoutData.scale[QwtPlot.xTop].tickOffset top = axisRect.top()+topOffset axisRect.setTop(max([top, minTop])) else: if self.__data.alignCanvasToScales[QwtPlot.xTop] and topOffset < 0: canvasRect.setTop(max([canvasRect.top(), axisRect.top()-topOffset])) else: if topOffset > 0: axisRect.setTop(axisRect.top()+topOffset) for axis in QwtPlot.validAxes: sRect = scaleRect[axis] if not sRect.isValid(): continue if axis in (QwtPlot.xBottom, QwtPlot.xTop): if self.__data.alignCanvasToScales[QwtPlot.yLeft]: y = canvasRect.left()-self.__data.layoutData.scale[axis].start if not options & self.IgnoreFrames: y += self.__data.layoutData.canvas.contentsMargins[QwtPlot.yLeft] sRect.setLeft(y) if self.__data.alignCanvasToScales[QwtPlot.yRight]: y = canvasRect.right()-1+self.__data.layoutData.scale[axis].end if not options & self.IgnoreFrames: y -= self.__data.layoutData.canvas.contentsMargins[QwtPlot.yRight] sRect.setRight(y) if self.__data.alignCanvasToScales[axis]: if axis == QwtPlot.xTop: sRect.setBottom(canvasRect.top()) else: sRect.setTop(canvasRect.bottom()) else: if self.__data.alignCanvasToScales[QwtPlot.xTop]: x = canvasRect.top()-self.__data.layoutData.scale[axis].start if not options & self.IgnoreFrames: x += self.__data.layoutData.canvas.contentsMargins[QwtPlot.xTop] sRect.setTop(x) if self.__data.alignCanvasToScales[QwtPlot.xBottom]: x = canvasRect.bottom()-1+self.__data.layoutData.scale[axis].end if not options & self.IgnoreFrames: x -= self.__data.layoutData.canvas.contentsMargins[QwtPlot.xBottom] sRect.setBottom(x) if self.__data.alignCanvasToScales[axis]: if axis == QwtPlot.yLeft: sRect.setRight(canvasRect.left()) else: sRect.setLeft(canvasRect.right()) def activate(self, plot, plotRect, options=0x00): """ Recalculate the geometry of all components. :param qwt.plot.QwtPlot plot: Plot to be layout :param QRectF plotRect: Rectangle where to place the components :param options: Layout options """ self.invalidate() rect = QRectF(plotRect) self.__data.layoutData.init(plot, rect) if not (options & self.IgnoreLegend) and plot.legend() and\ not plot.legend().isEmpty(): self.__data.legendRect = self.layoutLegend(options, rect) region = QRegion(rect.toRect()) rect = region.subtracted(QRegion(self.__data.legendRect.toRect()) ).boundingRect() if self.__data.legendPos == QwtPlot.LeftLegend: rect.setLeft(rect.left()+self.__data.spacing) elif self.__data.legendPos == QwtPlot.RightLegend: rect.setRight(rect.right()-self.__data.spacing) elif self.__data.legendPos == QwtPlot.TopLegend: rect.setTop(rect.top()+self.__data.spacing) elif self.__data.legendPos == QwtPlot.BottomLegend: rect.setBottom(rect.bottom()-self.__data.spacing) # +---+-----------+---+ # | Title | # +---+-----------+---+ # | | Axis | | # +---+-----------+---+ # | A | | A | # | x | Canvas | x | # | i | | i | # | s | | s | # +---+-----------+---+ # | | Axis | | # +---+-----------+---+ # | Footer | # +---+-----------+---+ # title, footer and axes include text labels. The height of each # label depends on its line breaks, that depend on the width # for the label. A line break in a horizontal text will reduce # the available width for vertical texts and vice versa. # expandLineBreaks finds the height/width for title, footer and axes # including all line breaks. dimTitle, dimFooter, dimAxes = self.expandLineBreaks(options, rect) if dimTitle > 0: self.__data.titleRect.setRect(rect.left(), rect.top(), rect.width(), dimTitle) rect.setTop(self.__data.titleRect.bottom()+self.__data.spacing) if self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled !=\ self.__data.layoutData.scale[QwtPlot.yRight].isEnabled: self.__data.titleRect.setX(rect.left()+dimAxes[QwtPlot.yLeft]) self.__data.titleRect.setWidth(rect.width()\ -dimAxes[QwtPlot.yLeft]-dimAxes[QwtPlot.yRight]) if dimFooter > 0: self.__data.footerRect.setRect(rect.left(), rect.bottom()-dimFooter, rect.width(), dimFooter) rect.setBottom(self.__data.footerRect.top()-self.__data.spacing) if self.__data.layoutData.scale[QwtPlot.yLeft].isEnabled !=\ self.__data.layoutData.scale[QwtPlot.yRight].isEnabled: self.__data.footerRect.setX(rect.left()+dimAxes[QwtPlot.yLeft]) self.__data.footerRect.setWidth(rect.width()\ -dimAxes[QwtPlot.yLeft]-dimAxes[QwtPlot.yRight]) self.__data.canvasRect.setRect( rect.x()+dimAxes[QwtPlot.yLeft], rect.y()+dimAxes[QwtPlot.xTop], rect.width()-dimAxes[QwtPlot.yRight]-dimAxes[QwtPlot.yLeft], rect.height()-dimAxes[QwtPlot.xBottom]-dimAxes[QwtPlot.xTop]) for axis in QwtPlot.validAxes: if dimAxes[axis]: dim = dimAxes[axis] scaleRect = self.__data.scaleRect[axis] scaleRect.setRect(*self.__data.canvasRect.getRect()) if axis == QwtPlot.yLeft: scaleRect.setX(self.__data.canvasRect.left()-dim) scaleRect.setWidth(dim) elif axis == QwtPlot.yRight: scaleRect.setX(self.__data.canvasRect.right()) scaleRect.setWidth(dim) elif axis == QwtPlot.xBottom: scaleRect.setY(self.__data.canvasRect.bottom()) scaleRect.setHeight(dim) elif axis == QwtPlot.xTop: scaleRect.setY(self.__data.canvasRect.top()-dim) scaleRect.setHeight(dim) scaleRect = scaleRect.normalized() # +---+-----------+---+ # | <- Axis -> | # +-^-+-----------+-^-+ # | | | | | | # | | | | # | A | | A | # | x | Canvas | x | # | i | | i | # | s | | s | # | | | | # | | | | | | # +-V-+-----------+-V-+ # | <- Axis -> | # +---+-----------+---+ # The ticks of the axes - not the labels above - should # be aligned to the canvas. So we try to use the empty # corners to extend the axes, so that the label texts # left/right of the min/max ticks are moved into them. self.alignScales(options, self.__data.canvasRect, self.__data.scaleRect) if not self.__data.legendRect.isEmpty(): self.__data.legendRect = self.alignLegend(self.__data.canvasRect, self.__data.legendRect) PythonQwt-0.5.5/CHANGELOG.md0000666000000000000000000000544612646710232014025 0ustar rootroot# PythonQwt Releases # ### Version 0.5.5 ### - `QwtScaleMap.invTransform_scalar`: avoid divide by 0 - Avoid error when computing ticks: when the axis was so small that no tick could be drawn, an exception used to be raised ### Version 0.5.4 ### Fixed an annoying bug which caused scale widget (axis ticks in particular) to be misaligned with canvas grid: the user was forced to resize the plot widget as a workaround ### Version 0.5.3 ### - Better handling of infinity and `NaN` values in scales (removed `NumPy` warnings) - Now handling infinity and `NaN` values in series data: removing points that can't be drawn - Fixed logarithmic scale engine: presence of values <= 0 was slowing down series data plotting ### Version 0.5.2 ### - Added CHM documentation to wheel package - Fixed `QwtPlotRenderer.setDiscardFlag`/`setLayoutFlag` args - Fixed `QwtPlotItem.setItemInterest` args - Fixed `QwtPlot.setAxisAutoScale`/`setAutoReplot` args ### Version 0.5.1 ### - Fixed Issue #22: fixed scale issues in [CurveDemo2.py](qwt/tests/CurveDemo2.py) and [ImagePlotDemo.py](qwt/tests/ImagePlotDemo.py) - `QwtPlotCurve`: sticks were not drawn correctly depending on orientation - `QwtInterval`: avoid overflows with `NumPy` scalars - Fixed Issue #28: curve shading was broken since v0.5.0 - setup.py: using setuptools "entry_points" instead of distutils "scripts" - Showing curves/plots number in benchmarks to avoid any misinterpretation (see Issue #26) - Added Python2/Python3 scripts for running tests ### Version 0.5.0 ### - Various optimizations - Major API simplification, taking into account the feature that won't be implemented (fitting, rounding, weeding out points, clipping, etc.) - Added `QwtScaleDraw.setLabelAutoSize`/`labelAutoSize` methods to set the new auto size option (see [documentation](http://pythonhosted.org/PythonQwt/)) ### Version 0.4.0 ### - Color bar: fixed axis ticks shaking when color bar is enabled - Fixed `QwtPainter.drawColorBar` for horizontal color bars (typo) - Restored compatibility with original Qwt signals (`QwtPlot`, ...) ### Version 0.3.0 ### Renamed the project (python-qwt --> PythonQwt), for various reasons. ### Version 0.2.1 ### Fixed Issue #23: "argument numPoints is not implemented" error was showing up when calling `QwtSymbol.drawSymbol(symbol, QPoint(x, y))`. ### Version 0.2.0 ### Added docstrings in all Python modules and a complete documentation based on Sphinx. See the Overview section for API limitations when comparing to Qwt. ### Version 0.1.1 ### Fixed Issue #21 (blocking issue *only* on non-Windows platforms when building the package): typo in "PythonQwt-tests" script name (in [setup script](setup.py)) ### Version 0.1.0 ### First alpha public release. PythonQwt-0.5.5/PythonQwt.egg-info/0000755000000000000000000000000012651077706015656 5ustar rootrootPythonQwt-0.5.5/PythonQwt.egg-info/SOURCES.txt0000666000000000000000000000515112646715536017554 0ustar rootrootCHANGELOG.md LICENSE MANIFEST.in README.md setup.py PythonQwt.egg-info/PKG-INFO PythonQwt.egg-info/SOURCES.txt PythonQwt.egg-info/dependency_links.txt PythonQwt.egg-info/entry_points.txt PythonQwt.egg-info/requires.txt PythonQwt.egg-info/top_level.txt doc/conf.py doc/index.rst doc/installation.rst doc/overview.rst doc/examples/bode_demo.rst doc/examples/cartesian_demo.rst doc/examples/cpu_plot.rst doc/examples/curve_benchmark.rst doc/examples/curve_demo1.rst doc/examples/curve_styles.rst doc/examples/data_demo.rst doc/examples/error_bar.rst doc/examples/event_filter_demo.rst doc/examples/image_plot_demo.rst doc/examples/index.rst doc/examples/map_demo.rst doc/examples/really_simple_demo.rst doc/images/QwtPlot_example.png doc/images/panorama.png doc/images/symbol_path_example.png doc/images/tests/BodeDemo.png doc/images/tests/CPUplot.png doc/images/tests/CartesianDemo.png doc/images/tests/CurveBenchmark.png doc/images/tests/CurveDemo1.png doc/images/tests/CurveStyles.png doc/images/tests/DataDemo.png doc/images/tests/ErrorBarDemo.png doc/images/tests/EventFilterDemo.png doc/images/tests/HistogramDemo.png doc/images/tests/ImagePlotDemo.png doc/images/tests/MapDemo.png doc/images/tests/ReallySimpleDemo.png doc/images/tests/__init__.png doc/reference/graphic.rst doc/reference/index.rst doc/reference/interval.rst doc/reference/plot.rst doc/reference/plot_directpainter.rst doc/reference/plot_layout.rst doc/reference/plot_series.rst doc/reference/scale.rst doc/reference/symbol.rst doc/reference/text.rst doc/reference/toqimage.rst doc/reference/transform.rst qwt/__init__.py qwt/color_map.py qwt/column_symbol.py qwt/dyngrid_layout.py qwt/graphic.py qwt/interval.py qwt/legend.py qwt/math.py qwt/null_paintdevice.py qwt/painter.py qwt/painter_command.py qwt/plot.py qwt/plot_canvas.py qwt/plot_curve.py qwt/plot_directpainter.py qwt/plot_grid.py qwt/plot_layout.py qwt/plot_marker.py qwt/plot_renderer.py qwt/plot_series.py qwt/py3compat.py qwt/scale_div.py qwt/scale_draw.py qwt/scale_engine.py qwt/scale_map.py qwt/scale_widget.py qwt/symbol.py qwt/text.py qwt/toqimage.py qwt/transform.py qwt/qt/QtCore.py qwt/qt/QtGui.py qwt/qt/QtSvg.py qwt/qt/QtWebKit.py qwt/qt/__init__.py qwt/qt/compat.py qwt/tests/BodeDemo.py qwt/tests/CPUplot.py qwt/tests/CartesianDemo.py qwt/tests/CurveBenchmark.py qwt/tests/CurveDemo1.py qwt/tests/CurveDemo2.py qwt/tests/CurveStyles.py qwt/tests/DataDemo.py qwt/tests/ErrorBarDemo.py qwt/tests/EventFilterDemo.py qwt/tests/ImagePlotDemo.py qwt/tests/LogCurveDemo.py qwt/tests/MapDemo.py qwt/tests/MultiDemo.py qwt/tests/ReallySimpleDemo.py qwt/tests/__init__.py qwt/tests/comparative_benchmarks.py qwt/tests/demo.pngPythonQwt-0.5.5/PythonQwt.egg-info/requires.txt0000666000000000000000000000006612646715534020266 0ustar rootrootNumPy>=1.3 [Doc] Sphinx>=1.1 [Tests] guidata>=1.7.0 PythonQwt-0.5.5/PythonQwt.egg-info/PKG-INFO0000666000000000000000000000374612646715534016773 0ustar rootrootMetadata-Version: 1.1 Name: PythonQwt Version: 0.5.5 Summary: Qt plotting widgets for Python Home-page: https://github.com/PierreRaybaut/PythonQwt Author: Pierre Raybaut Author-email: pierre.raybaut@gmail.com License: UNKNOWN Description: The ``PythonQwt`` package is a 2D-data plotting library using Qt graphical user interfaces for the Python programming language. It is compatible with both ``PyQt4`` and ``PyQt5`` (``PySide`` is currently not supported but it could be in the near future as it would "only" requires testing to support it as a stable alternative to PyQt). The ``PythonQwt`` project was initiated to solve -at least temporarily- the obsolescence issue of `PyQwt` (the Python-Qwt C++ bindings library) which is no longer maintained. The idea was to translate the original Qwt C++ code to Python and then to optimize some parts of the code by writing new modules based on NumPy and other libraries. The ``PythonQwt`` package consists of a single Python package named `qwt` which is a pure Python implementation of Qwt C++ library with some limitations: efforts were concentrated on basic plotting features, leaving higher level features to the `guiqwt` library. Platform: Any Classifier: Development Status :: 3 - Alpha Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2) Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Scientific/Engineering :: Visualization Classifier: Topic :: Software Development :: Widget Sets Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: OS Independent Classifier: Operating System :: POSIX Classifier: Operating System :: Unix Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 PythonQwt-0.5.5/PythonQwt.egg-info/entry_points.txt0000666000000000000000000000007312646715534021162 0ustar rootroot[gui_scripts] PythonQwt-tests-py2 = qwt.tests:run [tests] PythonQwt-0.5.5/PythonQwt.egg-info/dependency_links.txt0000666000000000000000000000000112646715534021732 0ustar rootroot PythonQwt-0.5.5/PythonQwt.egg-info/top_level.txt0000666000000000000000000000000412646715534020410 0ustar rootrootqwt