Compare commits
23 Commits
buster
...
master-slo
| Author | SHA1 | Date | |
|---|---|---|---|
| 949dd0bef2 | |||
| 58cb777935 | |||
| 6ffc4f8aa2 | |||
| 87bd420d31 | |||
| 3e1a43c10f | |||
| ed0b21a496 | |||
| b93358dc32 | |||
| 7f8e69fcf3 | |||
| 751095a5a1 | |||
| 665cf6050b | |||
| 8e7435ff8f | |||
| 08310e074d | |||
| 598352f526 | |||
| e88171b31c | |||
| a5b393a879 | |||
| 9b4e733209 | |||
| 4cf102946e | |||
| c72c0800fc | |||
| 095a95c1eb | |||
| fd5e482de9 | |||
| ccbeb20f70 | |||
| a4dca60892 | |||
| 95e22b5d1f |
@@ -1 +1,132 @@
|
|||||||
site/
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
.python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# Include frontend libraries though
|
||||||
|
!/frontend/lib
|
||||||
|
|||||||
@@ -1,23 +1,34 @@
|
|||||||
# TeraHz
|
|
||||||
|
|
||||||
[](https://terahz.readthedocs.io/en/latest/?badge=latest)
|
[](https://terahz.readthedocs.io/en/latest/?badge=latest)
|
||||||
|
# <img alt="TeraHz logo" src="docs/imgs/logo-sq.png" width="200px"> TeraHz
|
||||||
|
|
||||||
TeraHz is a low-cost spectrometer based on a Raspberry Pi 3 or 3 B+ and three sensors:
|
*Za slovensko različico se odpravite na <http://git.sckr-lab.tk/kristjank/TeraHz>*
|
||||||
+ [__AS7265x__](https://www.tindie.com/products/onehorse/compact-as7265x-spectrometer/)
|
|
||||||
is a 18 channel spectrometer chipset that provides the device with spectral data
|
|
||||||
+ [__VEML6075__](https://www.sparkfun.com/products/15089) is an
|
|
||||||
UVA/UVB sensor
|
|
||||||
+ [__APDS-9301__](https://www.sparkfun.com/products/14350) is a calibrated illuminance (lux) meter that provides the device with reliable readings
|
|
||||||
|
|
||||||
## Why?
|
TeraHz is a low-cost portable spectrometer based on Raspberry Pi and a few
|
||||||
Because people and institutions could use an affordable and accurate light-analysing device that is also portable, easy to use and simple to assemble. TeraHz was started as an answer to our high school not being able to afford a commercially available solution. One TeraHz spectrometer costs around 150$ in parts, which makes it a competitive alternative to other solutions on the market today.
|
commonly available sensor breakout boards. It's designed to bring low-cost
|
||||||
|
scientific exploration of the light spectrum to educational institutions that
|
||||||
|
cannot afford the options available on the current market. It costs less than
|
||||||
|
200€ with money to spare and uses only free, libre and open-source software
|
||||||
|
(FLOSS). It is free to use under the ISC license, a spiritual successor to the
|
||||||
|
classic 3-clause BSD license.
|
||||||
|
|
||||||
|
## How to install
|
||||||
|
Stable releases are available under the releases tab and can be installed either
|
||||||
|
by flashing a **preinstalled** ready to boot image with a stable version of
|
||||||
|
TeraHz preinstalled or by flashing a **preconfigured** image of DietPi and
|
||||||
|
installing TeraHz manually, which is useful if you want to test bleeding edge
|
||||||
|
releases. For more information, check out the [Build
|
||||||
|
guide](https://terahz.readthedocs.io/en/latest/build/).
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
TeraHz usually works out of the box. A wireless network named `TeraHz_XXXXXX`
|
||||||
|
(XXXXXX = the last half of the MAC address) will appear. Password is
|
||||||
|
`terahertz`. After connection, open a web browser and visit `terahz.site`.
|
||||||
|
The UI will appear. To fetch data from the sensors, press the 'Get Data' button.
|
||||||
|
The readings are then plotted and written into the tables below the graph.
|
||||||
|
|
||||||
|
More documentation is available at <https://terahz.readthedocs.io>
|
||||||
|
|
||||||
## Development team
|
## Development team
|
||||||
Copyright 2018, 2019
|
- Kristjan Komloši (cls-02) - Project leader and main programmer
|
||||||
|
- Jakob Kosec (D3m1j4ck) - Frontend designer
|
||||||
- Kristjan "cls-02" Komloši (electronics, sensor drivers, backend)
|
- Juš Dolžan (ANormalPerson) - Math double-checker
|
||||||
- Jakob "D3m1j4ck" Kosec (frontend)
|
|
||||||
|
|
||||||
|
|
||||||
I would also like to thank Juš "ANormalPerson" Dolžan, who decided to leave the
|
|
||||||
team, but helped me a lot with backend development.
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# run.sh - run the backend server
|
# run.sh - run the backend server
|
||||||
|
cd `dirname $0`
|
||||||
sudo gunicorn app:app -b 0.0.0.0:5000 &
|
sudo gunicorn app:app -b 0.0.0.0:5000 &
|
||||||
disown
|
|
||||||
|
|||||||
@@ -1,169 +1,39 @@
|
|||||||
# TeraHz build guide
|
# TeraHz build guide
|
||||||
This document describes the process of building/installing TeraHz from the Git
|
In its early development phase, TeraHz was hard and time-consuming to compile and install.
|
||||||
repository.
|
This is not case now, as the more optimized DietPi Linux distribution allows
|
||||||
|
better performance and simpler configuration than formerly used Raspbian.
|
||||||
|
|
||||||
## Warning
|
## Downloading the complete image
|
||||||
The recommended way of getting TeraHz is the official Raspberry Pi SD card image
|
The easiest way to get TeraHz is to download the premade complete image and
|
||||||
provided under the releases tab in the GitHub repository. Installing TeraHz from
|
write it to an SD card. It already has TeraHz installed and **will work out of
|
||||||
source is a time consuming and painful process, even more so if you don't know
|
the box** with the correct hardware. The image is designed to run from a 16 GB
|
||||||
what you're doing, and whatever you end up building __will not be officially
|
micro SD card, class 10 or higher is recommended for snappy performance. The
|
||||||
supported__ (unless you're a core developer).
|
recommended image writer is Etcher
|
||||||
|
|
||||||
With this warning out of the way, let's begin.
|
Please note that while this installation process is the easiest to perform, it
|
||||||
|
does not guarantee the most recent TeraHz software. Complete images take time to
|
||||||
|
prepare and despite the developer's best efforts aren't guaranteed to be always
|
||||||
|
up-to-date.
|
||||||
|
|
||||||
## Getting the latest sources
|
## Downloading the clean DietPi image and installing TeraHz manually
|
||||||
The most reliable way to get working source code is by cloning the official
|
This process is a bit more involved, but the version of TeraHz installed is
|
||||||
GitHub repository and checking out the `development-stable` tag. This tag marks
|
guaranteed to be the latest one available from the repository.
|
||||||
the latest confirmed working commit. Building from the master branch is somewhat
|
|
||||||
risky, and building from development branches is straight up stupid if you're
|
|
||||||
not a developer.
|
|
||||||
|
|
||||||
Make sure that the repository is cloned into `/home/pi/TeraHz`, as Lighttpd
|
The SD card image used in this case also contains some pre-configuration, but no
|
||||||
expects to find frontend files inside this directory.
|
TeraHz code is installed. To install TeraHz, you'll need a console access to the
|
||||||
|
Raspberry Pi, preferably an SSH console over a wired network. DietPi is configured
|
||||||
|
to get an IP over DHCP. A tool such as arp-scan on linux is very helpful at determining
|
||||||
|
the device's IP address.
|
||||||
|
|
||||||
After cloning and checking out, check the documentation for module dependencies
|
After connecting to the Raspberry Pi with username `root` and password `terahz`,
|
||||||
and the required version of python in the `docs/dependencies.md` file.
|
TeraHz can be installed by cloning the Git repository and running the `etcs/install.sh`
|
||||||
|
script.
|
||||||
## Installing Python
|
|
||||||
This step depends a lot on the platform you're using. TeraHz was developed with
|
|
||||||
Raspberry Pi and Raspbian in mind. If you're familiar with Raspbian enough,
|
|
||||||
you'll know that the latest version of Python available is `3.5`, which is too
|
|
||||||
obsolete to run TeraHz and the required modules consistently. This leaves us
|
|
||||||
with compiling Python from source. __This step is guaranteed to be slow,
|
|
||||||
overnight compiling with something like tmux is recommended.__
|
|
||||||
|
|
||||||
### Pre-requirements
|
|
||||||
Installing python without most C libraries will lead to a rather minimalistic
|
|
||||||
Python install, missing a lot of important modules. To prevent this, update
|
|
||||||
the system packages. After that, reboot.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt update
|
git clone https://github.com/cls-02/TeraHz.git
|
||||||
sudo apt full-upgrade
|
cd TeraHz/etcs
|
||||||
sudo reboot
|
./install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Install the required build tools, libraries and their headers.
|
When the installation completes, reboot the Raspberry Pi and it will
|
||||||
|
automatically boot into TeraHz.
|
||||||
```
|
|
||||||
sudo apt-get install build-essential tk-dev libncurses5-dev libncursesw5-dev \
|
|
||||||
libreadline6-dev libdb5.3-dev libgdbm-dev libsqlite3-dev libssl-dev libbz2-dev \
|
|
||||||
libexpat1-dev liblzma-dev zlib1g-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compiling
|
|
||||||
Compiling Python from source is, in fact pretty easy, just time-consuming. To combat
|
|
||||||
that, using tmux to detach and later reattach the session is advised.
|
|
||||||
|
|
||||||
Python is packaged in many forms, but you'll be using the most basic
|
|
||||||
of them all: a gzipped tarball. Download and decompress it, then cd into its
|
|
||||||
directory.
|
|
||||||
|
|
||||||
```
|
|
||||||
wget https://www.python.org/ftp/python/3.6.8/Python-3.6.8.tgz
|
|
||||||
tar -xzf Python-3.6.8.tgz
|
|
||||||
cd Python-3.6.8
|
|
||||||
```
|
|
||||||
|
|
||||||
Python's build process is pretty classic, a `.configure` script and a Makefile.
|
|
||||||
Using the `-j` option with Make can reduce the compile time significantly. Go
|
|
||||||
with as many threads as you have cores: `-j 4` works great on the Pi 3 B/B+.
|
|
||||||
|
|
||||||
```
|
|
||||||
./configure
|
|
||||||
make -j4
|
|
||||||
```
|
|
||||||
|
|
||||||
When the compilation ends, install your freshly built version of python.
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo make altinstall
|
|
||||||
```
|
|
||||||
|
|
||||||
"Altinstall" means that the new version of Python will be installed beside the
|
|
||||||
existing version, and all related commands will use the full naming scheme:
|
|
||||||
think `python3.6` or `pip3.6` instead of the shorter `python3` or `pip3`.
|
|
||||||
|
|
||||||
### Modules
|
|
||||||
Another painfully slow part is the installation of all the required modules
|
|
||||||
needed by TeraHz. Luckily, `pip3.6` takes care of the entire installation
|
|
||||||
process. As before, using tmux is advised.
|
|
||||||
|
|
||||||
```
|
|
||||||
pip3.6 install smbus pyserial flask pandas
|
|
||||||
```
|
|
||||||
|
|
||||||
## Raspi-config
|
|
||||||
For some law-obeying reason, Raspbian locks down the Wi-Fi interface card until
|
|
||||||
the Wi-Fi country is set. This means that we will need to set it manually through
|
|
||||||
the `raspi-config` program. It requires superuser privileges.
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo raspi-config
|
|
||||||
```
|
|
||||||

|
|
||||||
|
|
||||||
Configure the network hostname to something specific. If setting up multiple
|
|
||||||
TeraHz machines, make their hostnames unique so you can tell them apart.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Enable SSH and I2C interfaces.
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Expand the root filesystem along the entire SD card.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Set the Wi-Fi country to the country you'll be using TeraHz in.
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
Save and reboot to enable Wi-Fi
|
|
||||||
|
|
||||||
## Installing packages
|
|
||||||
In addition to what's already installed, TeraHz requires the following daemons
|
|
||||||
to run:
|
|
||||||
- Lighttpd - Frontend HTTP server
|
|
||||||
- Dnsmasq - DNS and DHCP server, used to redirect the `terahz.site` domain
|
|
||||||
- Hostapd - Wi-Fi access point
|
|
||||||
|
|
||||||
They are available from the Raspbian repository. Install it via `apt`.
|
|
||||||
|
|
||||||
```
|
|
||||||
apt install hostapd dnsmasq hostapd
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuring daemons
|
|
||||||
By default, the daemons we installed are disabled and start only manually. To
|
|
||||||
change that, enable them through systemctl. Hostapd conflicts with
|
|
||||||
wpa_supplicant, the solution is to disable wpa_supplicant (this will break your
|
|
||||||
wireless connections, so use wired ethernet).
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo systemctl unmask hostapd
|
|
||||||
sudo systemctl stop wpa_supplicant
|
|
||||||
sudo systemctl disable wpa_supplicant
|
|
||||||
sudo systemctl enable dnsmasq hostapd lighttpd
|
|
||||||
```
|
|
||||||
|
|
||||||
## Copying configuration files
|
|
||||||
To simplify the process of configuring Raspbian to run TeraHz, sample
|
|
||||||
configuration file are provided in the `etcs` subdirectory of the Git
|
|
||||||
repository.
|
|
||||||
|
|
||||||
These files have been verified to work, but it's not a brilliant idea to just
|
|
||||||
copy them into your `/etc` directory. Use them carefully and more as a template
|
|
||||||
for your own configuration rather than as a _de facto_ way of configuring
|
|
||||||
TeraHz.
|
|
||||||
|
|||||||
@@ -1,26 +1,24 @@
|
|||||||
# Development-stable dependencies
|
# 1.0.0-rc1 Dependencies
|
||||||
The current development version of TeraHz has been verified to work with:
|
The 1.0.0-rc1 version of TeraHz is confirmed to work with:
|
||||||
|
- DietPi 6.26.3
|
||||||
- Raspbian Stretch (9)
|
- Python 3.7.3
|
||||||
- Python 3.6.8 (built from source code and altinstall'd)
|
- Pip 18.1
|
||||||
- Module versions (direct `pip3.6 list` output):
|
- Following versions of pip3 packages:
|
||||||
|
|
||||||
```
|
```
|
||||||
Package Version
|
Click 7.0
|
||||||
--------------- ---------
|
Flask 1.1.1
|
||||||
Click 7.0
|
gunicorn 20.0.0
|
||||||
Flask 1.0.3
|
itsdangerous 1.1.0
|
||||||
itsdangerous 1.1.0
|
Jinja2 2.10.3
|
||||||
Jinja2 2.10.1
|
MarkupSafe 1.1.1
|
||||||
MarkupSafe 1.1.1
|
numpy 1.17.4
|
||||||
numpy 1.16.4
|
pandas 0.25.3
|
||||||
pandas 0.24.2
|
pip 18.1
|
||||||
pip 18.1
|
pyserial 3.4
|
||||||
pyserial 3.4
|
python-dateutil 2.8.1
|
||||||
python-dateutil 2.8.0
|
pytz 2019.3
|
||||||
pytz 2019.1
|
setuptools 41.6.0
|
||||||
setuptools 40.6.2
|
six 1.13.0
|
||||||
six 1.12.0
|
smbus2 0.3.0
|
||||||
smbus 1.1.post2
|
Werkzeug 0.16.0
|
||||||
Werkzeug 0.15.4
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,17 +7,12 @@ TeraHz was developed on and for the Raspberry Pi 3 Model B+. Compatibility with
|
|||||||
other Raspberries can probably be achieved by tweaking the device paths in the
|
other Raspberries can probably be achieved by tweaking the device paths in the
|
||||||
`app.py` file, but isn't confirmed at this point. Theoretically, 3 Model B and
|
`app.py` file, but isn't confirmed at this point. Theoretically, 3 Model B and
|
||||||
Zero W should work out of the box, but models without Wi-Fi will need an
|
Zero W should work out of the box, but models without Wi-Fi will need an
|
||||||
external Wi-Fi adapter if Wi-Fi functionality is desired. The practicality of
|
external Wi-Fi adapter if Wi-Fi functionality is desired.
|
||||||
compiling Python on the first generation of Raspberry Pis is also very
|
|
||||||
questionable.
|
|
||||||
|
|
||||||
Sensors required for operation are:
|
TeraHz depends on three separate sensor boards:
|
||||||
+ AS7265x
|
- AS7265x spectral chipset
|
||||||
+ VEML6075
|
- VEML6075 UV sensor
|
||||||
+ APDS-9301
|
- APDS-9301 lux-meter
|
||||||
|
|
||||||
They provide the spectrometry data, UV data and illuminance data, respectively.
|
|
||||||
They all support I2C, AS7265x supports UART in addition.
|
|
||||||
|
|
||||||
The sensors leech power from the GPIO connector, thus eliminating the need for a
|
The sensors leech power from the GPIO connector, thus eliminating the need for a
|
||||||
separate power supply. The necessary power for the whole system is delivered through
|
separate power supply. The necessary power for the whole system is delivered through
|
||||||
|
|||||||
@@ -21,13 +21,9 @@ project when basic testing will be done.
|
|||||||
|
|
||||||
GPIO can be routed to the PCB with a standard old IDE disk cable, and terminated
|
GPIO can be routed to the PCB with a standard old IDE disk cable, and terminated
|
||||||
with another 40-pin connector at the PCB. Sensor breakouts should be mounted
|
with another 40-pin connector at the PCB. Sensor breakouts should be mounted
|
||||||
<<<<<<< HEAD
|
|
||||||
through standard 0.1" connectors, male on the sensor breakout and female on the
|
through standard 0.1" connectors, male on the sensor breakout and female on the
|
||||||
PCB. A shitty add-on header and a shitty add-on header v1.69bis can't hurt, either.
|
PCB. A shitty add-on header and a shitty add-on header v1.69bis can't hurt, either.
|
||||||
=======
|
|
||||||
through standard 0.1" connectors, male on the sensor brakout and female on the
|
|
||||||
PCB. A shitty addon header and a shitty addon header v1.69bis can't hurt, either.
|
|
||||||
>>>>>>> fd1f07d40dace3e003e49377d4771de53f8bdeb8
|
|
||||||
|
|
||||||
## SMBus sensors
|
## SMBus sensors
|
||||||
SMBus is a well-defined version of the well-known I2C bus, widely used
|
SMBus is a well-defined version of the well-known I2C bus, widely used
|
||||||
@@ -39,11 +35,8 @@ Pins are familiarly marked as SDA and SCL, the same as with classic I2C. They
|
|||||||
connect to the SDA and SCL pins on the VEML6075 and APDS-9301 sensor.
|
connect to the SDA and SCL pins on the VEML6075 and APDS-9301 sensor.
|
||||||
|
|
||||||
## UART sensor
|
## UART sensor
|
||||||
<<<<<<< HEAD
|
|
||||||
Spectral sensor attaches through the UART port on the Raspberry pi (see picture).
|
Spectral sensor attaches through the UART port on the Raspberry pi (see picture).
|
||||||
=======
|
|
||||||
Spectrometry sensor attaches through the UART port on the Raspberry pi (see picture).
|
|
||||||
>>>>>>> fd1f07d40dace3e003e49377d4771de53f8bdeb8
|
|
||||||
|
|
||||||
The Tx and Rx lines must cross over, connecting the sensor's Tx line to the
|
The Tx and Rx lines must cross over, connecting the sensor's Tx line to the
|
||||||
computer's Rx line and vice versa.
|
computer's Rx line and vice versa.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# edit_ssid.sh - edits hostapd.conf and sets a MAC address-based SSID
|
# edit_ssid.sh - edits hostapd.conf and sets a MAC address-based SSID
|
||||||
|
cd `dirname $0`
|
||||||
ssid=`ip link | awk '/wlan0/ {getline; print $2}' | awk -v FS=':' '{printf("TeraHz_%s%s%s\n", $4, $5, $6)}'`
|
ssid=`ip link | awk '/wlan0/ {getline; print $2}' | awk -v FS=':' '{printf("TeraHz_%s%s%s\n", $4, $5, $6)}'`
|
||||||
sed "/ssid=.*/s/ssid=.*/ssid=$ssid/" hostapd.conf > newconf
|
sed "/ssid=.*/s/ssid=.*/ssid=$ssid/" hostapd.conf > newconf
|
||||||
mv newconf hostapd.conf
|
mv newconf hostapd.conf
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
# install.sh - install TeraHz onto a Raspbian or DietPi installation
|
# install.sh - install TeraHz onto a Raspbian or DietPi installation
|
||||||
|
cd `dirname $0`
|
||||||
|
|
||||||
apt -y update
|
apt -y update
|
||||||
apt -y full-upgrade
|
apt -y full-upgrade
|
||||||
apt install -y python3 python3-pip lighttpd dnsmasq hostapd libatlas-base-dev
|
apt install -y python3 python3-pip lighttpd dnsmasq hostapd libatlas-base-dev
|
||||||
pip3 install numpy pandas flask smbus2 pyserial
|
pip3 install numpy pandas flask smbus2 pyserial gunicorn
|
||||||
|
|
||||||
cp -R hostapd/ /etc
|
cp -R hostapd/ /etc
|
||||||
cp -R lighttpd/ /etc
|
chmod +rx /etc/hostapd/edit_ssid.sh
|
||||||
cp dnsmasq.conf /etc
|
cp dnsmasq.conf /etc
|
||||||
cp rc.local /etc
|
cp rc.local /etc
|
||||||
|
cp interfaces-terahz /etc/network/interfaces.d/
|
||||||
|
|
||||||
cp -R ../frontend/* /var/www/html
|
cp -R ../frontend/* /var/www/html
|
||||||
mkdir -p /usr/local/lib/terahz
|
mkdir -p /usr/local/lib/terahz
|
||||||
cp -R ../backend/* /usr/local/lib/terahz
|
cp -R ../backend/* /usr/local/lib/terahz
|
||||||
cd /etc/hostapd
|
|
||||||
chmod +x edit_ssid.sh
|
|
||||||
./edit_ssid.sh
|
|
||||||
|
|
||||||
systemctl unmask dnsmasq hostapd lighttpd
|
systemctl unmask dnsmasq hostapd lighttpd
|
||||||
systemctl enable dnsmasq hostapd lighttpd
|
systemctl enable dnsmasq hostapd lighttpd
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
iface wlan0 inet static
|
||||||
|
address 192.168.1.1
|
||||||
|
netmask 255.255.255.0
|
||||||
@@ -10,5 +10,7 @@
|
|||||||
# bits.
|
# bits.
|
||||||
#
|
#
|
||||||
# By default this script does nothing.
|
# By default this script does nothing.
|
||||||
|
/etc/hostapd/edit_ssid.sh &
|
||||||
/usr/local/lib/terahz/run.sh &
|
/usr/local/lib/terahz/run.sh &
|
||||||
disown
|
sleep 3
|
||||||
|
service hostapd restart # restart hostapd to prevent a weird startup race condition
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ nav:
|
|||||||
- Advanced guides:
|
- Advanced guides:
|
||||||
- Build guide: 'build.md'
|
- Build guide: 'build.md'
|
||||||
- Developer's guide: 'dev-guide.md'
|
- Developer's guide: 'dev-guide.md'
|
||||||
- Electrical connections: 'electrical.md'
|
- Electrical guide: 'electrical.md'
|
||||||
- Latest dependencies: 'dependencies.md'
|
- Latest dependencies: 'dependencies.md'
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:p="http://www.evolus.vn/Namespace/Pencil" xmlns:pencil="http://www.evolus.vn/Namespace/Pencil" xmlns:html="http://www.w3.org/1999/xhtml" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:chart="urn:oasis:names:tc:opendocument:xmlns:chart:1.0" xmlns:dr3d="urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0" xmlns:math="http://www.w3.org/1998/Math/MathML" xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0" xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0" xmlns:ooo="http://openoffice.org/2004/office" xmlns:ooow="http://openoffice.org/2004/writer" xmlns:oooc="http://openoffice.org/2004/calc" xmlns:dom="http://www.w3.org/2001/xml-events" xmlns:xforms="http://www.w3.org/2002/xforms" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rpt="http://openoffice.org/2005/report" xmlns:of="urn:oasis:names:tc:opendocument:xmlns:of:1.2" xmlns:rdfa="http://docs.oasis-open.org/opendocument/meta/rdfa#" xmlns:field="urn:openoffice:names:experimental:ooo-ms-interop:xmlns:field:1.0" xmlns:regexp="http://exslt.org/regular-expressions" xmlns:em="http://exslt.org/math" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="1385" height="852" id="exportedSVG" version="1.1" pencil:version="1.2.2" sodipodi:docname="demo"><g inkscape:label="Untitled Page" inkscape:groupmode="layer" id="layer_untitled_page"><g><rect x="0" y="0" width="1385" height="852" fill="none"/><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="316ea25d69264185809ddf96933f1b9d" transform="matrix(1,0,0,1,479,239)"><p:metadata><p:property name="box">379,359</p:property><p:property name="textPadding">18.95,23.933333333333334</p:property><p:property name="fillColor">#FFFFFFFF</p:property><p:property name="strokeColor">#000000FF</p:property><p:property name="strokeStyle">1|</p:property><p:property name="textContent"/><p:property name="textFont">'Comic Sans MS'|normal|normal|12px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="9f2cd04595654a2daa5c78cc57fc3ad1" transform="translate(0.5,0.5)" d="M 0 0 C 126 -1 253 -1 379 0 C 381 120 381 239 379 359 C 253 360 126 360 0 359 C -2 239 -2 120 0 0 z"/>
|
|
||||||
<text p:name="text" id="22d3cf61288a4e6f8aea91fd9f11d090" transform="translate(190,180)" style="font-family: "Comic Sans MS"; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;"/>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:Bitmap" id="f2992ac1faff43ef857d97b6097a3377" transform="matrix(1,0,0,1,545,295)"><p:metadata><p:property name="box">248,248</p:property><p:property name="imageData">1600,1600,ref://c549d0e3af32443d81f8de3a764ef050.png</p:property><p:property name="withBlur">false</p:property><p:property name="fillColor">#FFFFFF00</p:property><p:property name="strokeColor">#000000FF</p:property><p:property name="strokeStyle">0|</p:property></p:metadata>
|
|
||||||
<defs>
|
|
||||||
<filter height="1.2558399" y="-0.12792" width="1.06396" x="-0.03198" p:name="imageShading" id="47f2743a5502445daeea6763b1824d99">
|
|
||||||
<feGaussianBlur stdDeviation="1.3325" in="SourceAlpha"/>
|
|
||||||
</filter>
|
|
||||||
<g p:name="container" id="c807f25ef7cd4046b815700f4e8dd837">
|
|
||||||
<rect style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 0; fill-opacity: 0;" p:name="bgRect" id="442171111cd644568d10f9aaa8367c03" transform="translate(0,0)" width="248" height="248"/>
|
|
||||||
<g p:name="imageContainer" id="149b425645654fd99d93a63c39be92cd" transform="scale(0.155,0.155)">
|
|
||||||
<image x="0" y="0" p:name="image" id="6f19a34b96bf4773969f6d58610f56bd" xlink:href="file:///tmp/tmp-27399CzGZUH8OkSfx/refs/c549d0e3af32443d81f8de3a764ef050.png?token=1549194154000" width="1600" height="1600"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#c807f25ef7cd4046b815700f4e8dd837" transform="translate(1, 1)" p:filter="url(#47f2743a5502445daeea6763b1824d99)" style="opacity: 0.6; visibility: hidden; display: none;" p:name="bgCopy" id="f490b3b5a7e340a89dd6f4e992753bb7"/>
|
|
||||||
<use xlink:href="#c807f25ef7cd4046b815700f4e8dd837"/>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="d874a4977f474fdeb875d7e6bf5944ed" transform="matrix(1,0,0,1,596,289)"><p:metadata><p:property name="disabled">false</p:property><p:property name="width">100,0</p:property><p:property name="fixedWidth">false</p:property><p:property name="label">Raspberry Pi 3 B+</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textFont">'Comic Sans MS'|normal|normal|18px|none</p:property><p:property name="textAlign">0,0</p:property></p:metadata>
|
|
||||||
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="de7b105d7eec4d18b5eb971d7883baaa" width="147.640625" height="25.09375"/>
|
|
||||||
<text xml:space="preserve" p:name="text" id="949a08b7fd4044b794cc5b69b6f83422" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: "Comic Sans MS"; font-size: 18px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Raspberry Pi 3 B+</tspan></text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="4e6d928174bb44b2b165b10e439c8069" transform="matrix(1,0,0,1,215,144)"><p:metadata><p:property name="box">200,150</p:property><p:property name="textPadding">10,10</p:property><p:property name="fillColor">#FFFFFFFF</p:property><p:property name="strokeColor">#000000FF</p:property><p:property name="strokeStyle">1|</p:property><p:property name="textContent">AS7265X
|
|
||||||
Spektrometerski senzor za IR in vidno svetlobo</p:property><p:property name="textFont">'Comic Sans MS'|normal|normal|12px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="ba226d4a59e9445cb033b5f6cda2407c" transform="translate(0.5,0.5)" d="M 0 0 C 67 1 133 1 200 0 C 200 50 200 100 200 150 C 133 148 67 148 0 150 C -1 100 -1 50 0 0 z"/>
|
|
||||||
<text p:name="text" id="2f20051133e249f78ca5e893035376d7" transform="translate(10,64)" style="font-family: "Comic Sans MS"; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;"><tspan x="62.4609375" y="0">AS7265X</tspan><tspan x="5.109375" y="16">Spektrometerski senzor za IR</tspan><tspan x="43.7109375" y="32.390625">in vidno svetlobo</tspan></text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="0788d286529c4856817fe87c11c8c576" transform="matrix(1,0,0,1,113,331)"><p:metadata><p:property name="box">200,150</p:property><p:property name="textPadding">10,10</p:property><p:property name="fillColor">#FFFFFFFF</p:property><p:property name="strokeColor">#000000FF</p:property><p:property name="strokeStyle">1|</p:property><p:property name="textContent">VEML6075
|
|
||||||
Senzor UV svetlobe</p:property><p:property name="textFont">'Comic Sans MS'|normal|normal|12px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="fb336ddc2a0e4285ab16ca92def9a93f" transform="translate(0.5,0.5)" d="M 0 0 C 67 2 133 2 200 0 C 199 50 199 100 200 150 C 133 151 67 151 0 150 C -1 100 -1 50 0 0 z"/>
|
|
||||||
<text p:name="text" id="5a27b79034a142a182fbba178737c976" transform="translate(10,72)" style="font-family: "Comic Sans MS"; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;"><tspan x="59.109375" y="0">VEML6075</tspan><tspan x="34.640625" y="16">Senzor UV svetlobe</tspan></text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="fdd2989c419f44e49052af8a9433398b" transform="matrix(1,0,0,1,112,499)"><p:metadata><p:property name="box">200,150</p:property><p:property name="textPadding">10,10</p:property><p:property name="fillColor">#FFFFFFFF</p:property><p:property name="strokeColor">#000000FF</p:property><p:property name="strokeStyle">1|</p:property><p:property name="textContent">APDS-9301
|
|
||||||
Merilec osvetljenosti
|
|
||||||
(lux-meter)</p:property><p:property name="textFont">'Comic Sans MS'|normal|normal|12px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="fd47ea2f4fea4465b5145099e062a50f" transform="translate(0.5,0.5)" d="M 0 0 C 67 0 133 0 200 0 C 198 50 198 100 200 150 C 133 151 67 151 0 150 C 1 100 1 50 0 0 z"/>
|
|
||||||
<text p:name="text" id="3a1e6ef59bfe4e32a29f2d83e8a0eda4" transform="translate(10,64)" style="font-family: "Comic Sans MS"; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;"><tspan x="57.8125" y="0">APDS-9301</tspan><tspan x="30.8671875" y="16">Merilec osvetljenosti</tspan><tspan x="57.859375" y="32.484375">(lux-meter)</tspan></text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="6a62643b2a9e40ad92de2807a3b4d499" transform="matrix(1,0,0,1,898,448)"><p:metadata><p:property name="box">200,150</p:property><p:property name="textPadding">10,10</p:property><p:property name="fillColor">#FFFFFFFF</p:property><p:property name="strokeColor">#000000FF</p:property><p:property name="strokeStyle">1|</p:property><p:property name="textContent">2600 mAh Powerbank
|
|
||||||
Napaja spektrometer</p:property><p:property name="textFont">'Comic Sans MS'|normal|normal|12px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="0e72ea3ae7374eae96f6d39616ad398b" transform="translate(0.5,0.5)" d="M 0 0 C 67 -1 133 -1 200 0 C 200 50 200 100 200 150 C 133 150 67 150 0 150 C 0 100 0 50 0 0 z"/>
|
|
||||||
<text p:name="text" id="e73d9b50f41c4e3eb2efffdb7a5f58d9" transform="translate(10,72)" style="font-family: "Comic Sans MS"; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;"><tspan x="29.6875" y="0">2600 mAh Powerbank</tspan><tspan x="30.125" y="16">Napaja spektrometer</tspan></text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:arrow" id="bc03848225fa44a3a7c3339794d1aa4e" transform="matrix(1,0,0,1,479,127)"><p:metadata><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="startPin" ns9889235:viay="92" ns9889235:viax="-54" ns9889235:connectedOutletId="middle-right" ns9889235:connectedShapeId="4e6d928174bb44b2b165b10e439c8069">-64,92</p:property><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="endPin" ns9889235:viay="102" ns9889235:viax="189.5" ns9889235:connectedOutletId="top-center" ns9889235:connectedShapeId="316ea25d69264185809ddf96933f1b9d">189.5,112</p:property><p:property name="withStartArrow">true</p:property><p:property name="withEndArrow">true</p:property><p:property name="mode">curvy</p:property><p:property name="detached">false</p:property><p:property name="strokeColor">#1B3280FF</p:property><p:property name="strokeStyle">2|</p:property><p:property name="textContent"/><p:property name="textFont">"Liberation Sans",Arial,sans-serif|normal|normal|13px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<defs>
|
|
||||||
<path style="stroke-linejoin: round; fill: none;" p:name="path" id="590e362dc0a941558c8729fc4369cced" d="M -58 98 L -64 92 L -58 86 M -64 92 C -4 92 190 52 189.5 112 M 195 106 L 189.5 112 L 184 106"/>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#590e362dc0a941558c8729fc4369cced" stroke-width="10" stroke-opacity="0" stroke="#FF0000"/>
|
|
||||||
<use xlink:href="#590e362dc0a941558c8729fc4369cced" p:name="outArrow1" id="9c47d44f84ff4e8c9c60f2427e5649e4" style="stroke: rgb(27, 50, 128); stroke-opacity: 1; stroke-width: 2;"/>
|
|
||||||
<text p:name="text" id="2027c38e59a7415ea356985839c3cb6a" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: "Liberation Sans", Arial, sans-serif; font-size: 13px; font-weight: normal; font-style: normal; text-decoration: none;">
|
|
||||||
<textPath xlink:href="#590e362dc0a941558c8729fc4369cced" startOffset="50%" text-anchor="middle" alignment-baseline="middle">
|
|
||||||
<tspan dy="-4.333333333333333" p:name="textSpan" id="7e3a712874e84610942b4fa041b7a43b" dx="0"/>
|
|
||||||
</textPath>
|
|
||||||
</text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Sketchy.GUI:box" id="3f86fad121f34239a0d9a0381b8e5293" transform="matrix(1,0,0,1,339,331)"><p:metadata><p:property name="box">76,316</p:property><p:property name="textPadding">3.8,21.066666666666663</p:property><p:property name="fillColor">#FFFFFFFF</p:property><p:property name="strokeColor">#000000FF</p:property><p:property name="strokeStyle">1|</p:property><p:property name="textContent">I2C
|
|
||||||
Vodilo</p:property><p:property name="textFont">'Comic Sans MS'|normal|normal|12px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<path style="stroke-linejoin: round; fill: rgb(255, 255, 255); fill-opacity: 1; stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 1;" p:name="line1" id="38738b385aac4e4f882b7957ae3baf95" transform="translate(0.5,0.5)" d="M 0 0 C 25 -1 51 -1 76 0 C 76 105 76 211 76 316 C 51 315 25 315 0 316 C -1 211 -1 105 0 0 z"/>
|
|
||||||
<text p:name="text" id="d829667556ac42128e839eeb4432aec3" transform="translate(4,155)" style="font-family: "Comic Sans MS"; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none; fill: rgb(0, 0, 0); fill-opacity: 1;"><tspan x="23.645312500000003" y="0">I2C</tspan><tspan x="17.145312500000003" y="16">Vodilo</tspan></text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:arrow" id="e6e45b080a4640e2af58d063d5abe1ef" transform="matrix(1,0,0,1,562,696)"><p:metadata><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="startPin" ns9889235:connectedShapeId="0788d286529c4856817fe87c11c8c576" ns9889235:connectedOutletId="middle-right" ns9889235:viax="-239" ns9889235:viay="-290">-249,-290</p:property><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="endPin" ns9889235:connectedShapeId="3f86fad121f34239a0d9a0381b8e5293" ns9889235:connectedOutletId="middle-left" ns9889235:viax="-233" ns9889235:viay="-207">-223,-207</p:property><p:property name="withStartArrow">true</p:property><p:property name="withEndArrow">true</p:property><p:property name="mode">curvy</p:property><p:property name="detached">false</p:property><p:property name="strokeColor">#1B3280FF</p:property><p:property name="strokeStyle">2|</p:property><p:property name="textContent"/><p:property name="textFont">"Liberation Sans",Arial,sans-serif|normal|normal|13px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<defs>
|
|
||||||
<path style="stroke-linejoin: round; fill: none;" p:name="path" id="564ef2543bfd4d4f8bf59146b4632c7c" d="M -243 -284 L -249 -290 L -243 -296 M -249 -290 C -206 -290 -266 -207 -223 -207 M -229 -213 L -223 -207 L -229 -201"/>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#564ef2543bfd4d4f8bf59146b4632c7c" stroke-width="10" stroke-opacity="0" stroke="#FF0000"/>
|
|
||||||
<use xlink:href="#564ef2543bfd4d4f8bf59146b4632c7c" p:name="outArrow1" id="9fd757e4b8d042a09378b6031cfe99e2" style="stroke: rgb(27, 50, 128); stroke-opacity: 1; stroke-width: 2;"/>
|
|
||||||
<text p:name="text" id="c73cdd327696416eab5fa6971dba53ef" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: "Liberation Sans", Arial, sans-serif; font-size: 13px; font-weight: normal; font-style: normal; text-decoration: none;">
|
|
||||||
<textPath xlink:href="#564ef2543bfd4d4f8bf59146b4632c7c" startOffset="50%" text-anchor="middle" alignment-baseline="middle">
|
|
||||||
<tspan dy="-4.333333333333333" p:name="textSpan" id="221213c9b3fc487facaa478640fecf0f" dx="0"/>
|
|
||||||
</textPath>
|
|
||||||
</text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:arrow" id="540bab7bb3f3442f8429144581700e47" transform="matrix(1,0,0,1,324,705)"><p:metadata><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="startPin" ns9889235:viay="-131" ns9889235:viax="-2" ns9889235:connectedOutletId="middle-right" ns9889235:connectedShapeId="fdd2989c419f44e49052af8a9433398b">-12,-131</p:property><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="endPin" ns9889235:connectedShapeId="3f86fad121f34239a0d9a0381b8e5293" ns9889235:connectedOutletId="middle-left" ns9889235:viax="5" ns9889235:viay="-216">15,-216</p:property><p:property name="withStartArrow">true</p:property><p:property name="withEndArrow">true</p:property><p:property name="mode">curvy</p:property><p:property name="detached">false</p:property><p:property name="strokeColor">#1B3280FF</p:property><p:property name="strokeStyle">2|</p:property><p:property name="textContent"/><p:property name="textFont">"Liberation Sans",Arial,sans-serif|normal|normal|13px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<defs>
|
|
||||||
<path style="stroke-linejoin: round; fill: none;" p:name="path" id="b08f88ad83b84d5fad883f5b2d7152d9" d="M -6 -125 L -12 -131 L -6 -137 M -12 -131 C 33 -131 -30 -216 15 -216 M 9 -222 L 15 -216 L 9 -210"/>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#b08f88ad83b84d5fad883f5b2d7152d9" stroke-width="10" stroke-opacity="0" stroke="#FF0000"/>
|
|
||||||
<use xlink:href="#b08f88ad83b84d5fad883f5b2d7152d9" p:name="outArrow1" id="81869e15f13e41609130f9b556eef9d3" style="stroke: rgb(27, 50, 128); stroke-opacity: 1; stroke-width: 2;"/>
|
|
||||||
<text p:name="text" id="2fa0348639be49eda6fa12a65623e9b6" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: "Liberation Sans", Arial, sans-serif; font-size: 13px; font-weight: normal; font-style: normal; text-decoration: none;">
|
|
||||||
<textPath xlink:href="#b08f88ad83b84d5fad883f5b2d7152d9" startOffset="50%" text-anchor="middle" alignment-baseline="middle">
|
|
||||||
<tspan dy="-4.333333333333333" p:name="textSpan" id="c544ec23fdcd4df998935ca8bb683723" dx="0"/>
|
|
||||||
</textPath>
|
|
||||||
</text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:arrow" id="0bc4ad3e83614786a0f2348a47ec40f2" transform="matrix(1,0,0,1,463,627)"><p:metadata><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="startPin" ns9889235:connectedShapeId="3f86fad121f34239a0d9a0381b8e5293" ns9889235:connectedOutletId="middle-right" ns9889235:viax="-38" ns9889235:viay="-138">-48,-138</p:property><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="endPin" ns9889235:connectedShapeId="316ea25d69264185809ddf96933f1b9d" ns9889235:connectedOutletId="middle-left" ns9889235:viax="5.999999999999972" ns9889235:viay="-208.5">16,-208</p:property><p:property name="withStartArrow">true</p:property><p:property name="withEndArrow">true</p:property><p:property name="mode">curvy</p:property><p:property name="detached">false</p:property><p:property name="strokeColor">#1B3280FF</p:property><p:property name="strokeStyle">2|</p:property><p:property name="textContent"/><p:property name="textFont">"Liberation Sans",Arial,sans-serif|normal|normal|13px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<defs>
|
|
||||||
<path style="stroke-linejoin: round; fill: none;" p:name="path" id="6c567c02fe2642e19f2b63f417f770a9" d="M -42 -132 L -48 -138 L -42 -144 M -48 -138 C -1 -138 -31 -210 16 -208 M 11 -214 L 16 -208 L 10 -203"/>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#6c567c02fe2642e19f2b63f417f770a9" stroke-width="10" stroke-opacity="0" stroke="#FF0000"/>
|
|
||||||
<use xlink:href="#6c567c02fe2642e19f2b63f417f770a9" p:name="outArrow1" id="cad90b7ac097400b8a9febcd1059b193" style="stroke: rgb(27, 50, 128); stroke-opacity: 1; stroke-width: 2;"/>
|
|
||||||
<text p:name="text" id="7256ea09df6f4b7ebf33202f1ced446d" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: "Liberation Sans", Arial, sans-serif; font-size: 13px; font-weight: normal; font-style: normal; text-decoration: none;">
|
|
||||||
<textPath xlink:href="#6c567c02fe2642e19f2b63f417f770a9" startOffset="50%" text-anchor="middle" alignment-baseline="middle">
|
|
||||||
<tspan dy="-4.333333333333333" p:name="textSpan" id="a3e251b119324498b6079771d87cf26b" dx="0"/>
|
|
||||||
</textPath>
|
|
||||||
</text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:RichTextBoxV2" id="f9704a7a815d4cdba445660a10c7ddc8" transform="matrix(1,0,0,1,479,601)"><p:metadata><p:property name="width">200,0</p:property><p:property name="fixedWidth">false</p:property><p:property name="textContent"><span xmlns="http://www.w3.org/1999/xhtml" style="font-family: &quot;Comic Sans MS&quot;;">Računalnik na tiskanem vezju (SBC = single board computer)<br xmlns="http://www.w3.org/1999/xhtml" />Opravlja prevzem in obdelavo podatkov s senzorja, in jih preda telefonu, ki upravlja spektrmeter</span></p:property><p:property name="textFont">"Liberation Sans",Arial,sans-serif|normal|normal|13px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="customStyle">
|
|
||||||
</p:property></p:metadata>
|
|
||||||
|
|
||||||
<foreignObject x="0" y="0" width="585" height="38" p:name="htmlObject" id="54de4aab4c144b8ead414b2e22841369" style="color: rgb(0, 0, 0); font-family: "Liberation Sans", Arial, sans-serif; font-size: 13px; font-weight: normal; font-style: normal; text-decoration: none;">
|
|
||||||
<div xmlns="http://www.w3.org/1999/xhtml" p:name="textDiv" id="0cbc982da2c54d4aa9a65d4f6e9727c6" style="display: inline-block; white-space: nowrap; text-decoration: none;"><div><span style="font-family: "Comic Sans MS";">Računalnik na tiskanem vezju (SBC = single board computer)<br />Opravlja prevzem in obdelavo podatkov s senzorja, in jih preda telefonu, ki upravlja spektrmeter</span></div></div>
|
|
||||||
</foreignObject>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:arrow" id="c884a2093fdc477bb3a74e719a1c7c77" transform="matrix(1,0,0,1,951,527)"><p:metadata><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="startPin" ns9889235:connectedShapeId="316ea25d69264185809ddf96933f1b9d" ns9889235:connectedOutletId="middle-right" ns9889235:viax="-83" ns9889235:viay="-108.5">-93,-108</p:property><p:property xmlns:ns9889235="http://www.evolus.vn/Namespace/Pencil" name="endPin" ns9889235:viay="-4" ns9889235:viax="-63.000000000000014" ns9889235:connectedOutletId="middle-left" ns9889235:connectedShapeId="6a62643b2a9e40ad92de2807a3b4d499">-53,-4</p:property><p:property name="withStartArrow">true</p:property><p:property name="withEndArrow">true</p:property><p:property name="mode">curvy</p:property><p:property name="detached">false</p:property><p:property name="strokeColor">#1B3280FF</p:property><p:property name="strokeStyle">2|</p:property><p:property name="textContent"/><p:property name="textFont">"Liberation Sans",Arial,sans-serif|normal|normal|13px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<defs>
|
|
||||||
<path style="stroke-linejoin: round; fill: none;" p:name="path" id="384134701047435fac995fc6eaee5932" d="M -87 -102 L -93 -108 L -87 -114 M -93 -108 C -37 -108 -109 -4 -53 -4 M -59 -10 L -53 -4 L -59 2"/>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#384134701047435fac995fc6eaee5932" stroke-width="10" stroke-opacity="0" stroke="#FF0000"/>
|
|
||||||
<use xlink:href="#384134701047435fac995fc6eaee5932" p:name="outArrow1" id="14d921eb06d44f7d9acd062d2b01551a" style="stroke: rgb(27, 50, 128); stroke-opacity: 1; stroke-width: 2;"/>
|
|
||||||
<text p:name="text" id="5485420a3edb40669c089909c03dc1f8" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: "Liberation Sans", Arial, sans-serif; font-size: 13px; font-weight: normal; font-style: normal; text-decoration: none;">
|
|
||||||
<textPath xlink:href="#384134701047435fac995fc6eaee5932" startOffset="50%" text-anchor="middle" alignment-baseline="middle">
|
|
||||||
<tspan dy="-4.333333333333333" p:name="textSpan" id="428895865f0145dca4a5ff61843e80ef" dx="0"/>
|
|
||||||
</textPath>
|
|
||||||
</text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:Bitmap" id="b6ed13921fdd40f9a3eb0e1e3d0ecbe6" transform="matrix(1,0,0,1,1103,146)"><p:metadata><p:property name="box">195,276</p:property><p:property name="imageData">318,450,ref://7214557d80314d2ea0269f3a449d0000.png</p:property><p:property name="withBlur">false</p:property><p:property name="fillColor">#FFFFFF00</p:property><p:property name="strokeColor">#000000FF</p:property><p:property name="strokeStyle">0|</p:property></p:metadata>
|
|
||||||
<defs>
|
|
||||||
<filter height="1.2558399" y="-0.12792" width="1.06396" x="-0.03198" p:name="imageShading" id="45b9efdc21654623a49f9d3d92738b31">
|
|
||||||
<feGaussianBlur stdDeviation="1.3325" in="SourceAlpha"/>
|
|
||||||
</filter>
|
|
||||||
<g p:name="container" id="dc93c38161c8442faea0e63e5cbf5843">
|
|
||||||
<rect style="fill: rgb(255, 255, 255); stroke: rgb(0, 0, 0); stroke-opacity: 1; stroke-width: 0; fill-opacity: 0;" p:name="bgRect" id="62e29bf3081e4449ae9fc80c6c967b71" transform="translate(0,0)" width="195" height="276"/>
|
|
||||||
<g p:name="imageContainer" id="6f7191c994c644f183d02acf013570d0" transform="scale(0.6132075471698113,0.6133333333333333)">
|
|
||||||
<image x="0" y="0" p:name="image" id="a6b907023988404f8a2a4c4daeb1b81e" xlink:href="file:///tmp/tmp-27399CzGZUH8OkSfx/refs/7214557d80314d2ea0269f3a449d0000.png?token=1549194986000" width="318" height="450"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#dc93c38161c8442faea0e63e5cbf5843" transform="translate(1, 1)" p:filter="url(#45b9efdc21654623a49f9d3d92738b31)" style="opacity: 0.6; visibility: hidden; display: none;" p:name="bgCopy" id="02792f50e6104ccc99ad1f691c8d3a5e"/>
|
|
||||||
<use xlink:href="#dc93c38161c8442faea0e63e5cbf5843"/>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:arrow" id="7b94aab3e1cd494696d3b5cf838dfde6" transform="matrix(1,0,0,1,858,286)"><p:metadata><p:property name="startPin">0,0</p:property><p:property name="endPin">282,-4</p:property><p:property name="withStartArrow">true</p:property><p:property name="withEndArrow">true</p:property><p:property name="mode">curvy</p:property><p:property name="detached">false</p:property><p:property name="strokeColor">#1B3280FF</p:property><p:property name="strokeStyle">4|3,3</p:property><p:property name="textContent"/><p:property name="textFont">"Liberation Sans",Arial,sans-serif|normal|normal|13px|none</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textAlign">1,1</p:property></p:metadata>
|
|
||||||
<defs>
|
|
||||||
<path style="stroke-linejoin: round; fill: none;" p:name="path" id="b8c0a2b9697f46288c408540e8af57fa" d="M 11 11 L 0 0 L 11 -11 M 0 0 C 60 0 222 -3 282 -4 M 271 -15 L 282 -4 L 271 7"/>
|
|
||||||
</defs>
|
|
||||||
<use xlink:href="#b8c0a2b9697f46288c408540e8af57fa" stroke-width="10" stroke-opacity="0" stroke="#FF0000"/>
|
|
||||||
<use xlink:href="#b8c0a2b9697f46288c408540e8af57fa" p:name="outArrow1" id="97846f435e74416e996116a08ef664e4" style="stroke: rgb(27, 50, 128); stroke-opacity: 1; stroke-width: 4; stroke-dasharray: 3, 3;"/>
|
|
||||||
<text p:name="text" id="987b10b79cfa4658917b6bb36a6005fb" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: "Liberation Sans", Arial, sans-serif; font-size: 13px; font-weight: normal; font-style: normal; text-decoration: none;">
|
|
||||||
<textPath xlink:href="#b8c0a2b9697f46288c408540e8af57fa" startOffset="50%" text-anchor="middle" alignment-baseline="middle">
|
|
||||||
<tspan dy="-4.333333333333333" p:name="textSpan" id="667515745b224bb79dbd2e134284f6d2" dx="0"/>
|
|
||||||
</textPath>
|
|
||||||
</text>
|
|
||||||
</g><g p:type="Shape" p:def="Evolus.Common:PlainTextV2" p:sc="Label" id="bb91f7fb7faf445a834363edc2cb287a" transform="matrix(1,0,0,1,1034,157)"><p:metadata><p:property name="disabled">false</p:property><p:property name="width">100,0</p:property><p:property name="fixedWidth">false</p:property><p:property name="label">Mobilni telefon preko WiFi povezave upravlja spektrometer</p:property><p:property name="textColor">#000000FF</p:property><p:property name="textFont">'Comic Sans MS'|normal|normal|12px|none</p:property><p:property name="textAlign">0,0</p:property></p:metadata>
|
|
||||||
<rect x="0" y="0" style="fill: none; stroke: none; visibility: hidden; display: none;" p:name="bgRect" id="c2e343a9ad3246cdb047e5e01ee1fb42" width="332.390625" height="16.484375"/>
|
|
||||||
<text xml:space="preserve" p:name="text" id="357a8a3e3c4044bb8518698b7d607b60" style="fill: rgb(0, 0, 0); fill-opacity: 1; font-family: "Comic Sans MS"; font-size: 12px; font-weight: normal; font-style: normal; text-decoration: none;"><tspan x="0" y="0">Mobilni telefon preko WiFi povezave upravlja spektrometer</tspan></text>
|
|
||||||
</g></g></g></svg>
|
|
||||||
|
Before Width: | Height: | Size: 31 KiB |
@@ -1 +0,0 @@
|
|||||||
# sysprep - How to prepare a Raspberry Pi 3 B+ to run TeraHz
|
|
||||||
|
Before Width: | Height: | Size: 97 KiB |
@@ -1,76 +0,0 @@
|
|||||||
# This file must be used with "source bin/activate" *from bash*
|
|
||||||
# you cannot run it directly
|
|
||||||
|
|
||||||
deactivate () {
|
|
||||||
# reset old environment variables
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
|
|
||||||
PATH="${_OLD_VIRTUAL_PATH:-}"
|
|
||||||
export PATH
|
|
||||||
unset _OLD_VIRTUAL_PATH
|
|
||||||
fi
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
|
|
||||||
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
|
|
||||||
export PYTHONHOME
|
|
||||||
unset _OLD_VIRTUAL_PYTHONHOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This should detect bash and zsh, which have a hash command that must
|
|
||||||
# be called to get it to forget past commands. Without forgetting
|
|
||||||
# past commands the $PATH changes we made may not be respected
|
|
||||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
|
||||||
hash -r
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
|
|
||||||
PS1="${_OLD_VIRTUAL_PS1:-}"
|
|
||||||
export PS1
|
|
||||||
unset _OLD_VIRTUAL_PS1
|
|
||||||
fi
|
|
||||||
|
|
||||||
unset VIRTUAL_ENV
|
|
||||||
if [ ! "$1" = "nondestructive" ] ; then
|
|
||||||
# Self destruct!
|
|
||||||
unset -f deactivate
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# unset irrelevant variables
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
VIRTUAL_ENV="/root/projekti/TeraHz/utils/venv"
|
|
||||||
export VIRTUAL_ENV
|
|
||||||
|
|
||||||
_OLD_VIRTUAL_PATH="$PATH"
|
|
||||||
PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
export PATH
|
|
||||||
|
|
||||||
# unset PYTHONHOME if set
|
|
||||||
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
|
|
||||||
# could use `if (set -u; : $PYTHONHOME) ;` in bash
|
|
||||||
if [ -n "${PYTHONHOME:-}" ] ; then
|
|
||||||
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
|
|
||||||
unset PYTHONHOME
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
|
||||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
|
||||||
if [ "x(venv) " != x ] ; then
|
|
||||||
PS1="(venv) ${PS1:-}"
|
|
||||||
else
|
|
||||||
if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
|
|
||||||
# special case for Aspen magic directories
|
|
||||||
# see http://www.zetadev.com/software/aspen/
|
|
||||||
PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1"
|
|
||||||
else
|
|
||||||
PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
export PS1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# This should detect bash and zsh, which have a hash command that must
|
|
||||||
# be called to get it to forget past commands. Without forgetting
|
|
||||||
# past commands the $PATH changes we made may not be respected
|
|
||||||
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
|
|
||||||
hash -r
|
|
||||||
fi
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# This file must be used with "source bin/activate.csh" *from csh*.
|
|
||||||
# You cannot run it directly.
|
|
||||||
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
|
||||||
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
|
|
||||||
|
|
||||||
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; test "\!:*" != "nondestructive" && unalias deactivate'
|
|
||||||
|
|
||||||
# Unset irrelevant variables.
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
setenv VIRTUAL_ENV "/root/projekti/TeraHz/utils/venv"
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PATH="$PATH"
|
|
||||||
setenv PATH "$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
|
||||||
|
|
||||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
|
||||||
if ("venv" != "") then
|
|
||||||
set env_name = "venv"
|
|
||||||
else
|
|
||||||
if (`basename "VIRTUAL_ENV"` == "__") then
|
|
||||||
# special case for Aspen magic directories
|
|
||||||
# see http://www.zetadev.com/software/aspen/
|
|
||||||
set env_name = `basename \`dirname "$VIRTUAL_ENV"\``
|
|
||||||
else
|
|
||||||
set env_name = `basename "$VIRTUAL_ENV"`
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
set prompt = "[$env_name] $prompt"
|
|
||||||
unset env_name
|
|
||||||
endif
|
|
||||||
|
|
||||||
alias pydoc python -m pydoc
|
|
||||||
|
|
||||||
rehash
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# This file must be used with ". bin/activate.fish" *from fish* (http://fishshell.org)
|
|
||||||
# you cannot run it directly
|
|
||||||
|
|
||||||
function deactivate -d "Exit virtualenv and return to normal shell environment"
|
|
||||||
# reset old environment variables
|
|
||||||
if test -n "$_OLD_VIRTUAL_PATH"
|
|
||||||
set -gx PATH $_OLD_VIRTUAL_PATH
|
|
||||||
set -e _OLD_VIRTUAL_PATH
|
|
||||||
end
|
|
||||||
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
|
||||||
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
|
|
||||||
set -e _OLD_VIRTUAL_PYTHONHOME
|
|
||||||
end
|
|
||||||
|
|
||||||
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
|
||||||
functions -e fish_prompt
|
|
||||||
set -e _OLD_FISH_PROMPT_OVERRIDE
|
|
||||||
functions -c _old_fish_prompt fish_prompt
|
|
||||||
functions -e _old_fish_prompt
|
|
||||||
end
|
|
||||||
|
|
||||||
set -e VIRTUAL_ENV
|
|
||||||
if test "$argv[1]" != "nondestructive"
|
|
||||||
# Self destruct!
|
|
||||||
functions -e deactivate
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# unset irrelevant variables
|
|
||||||
deactivate nondestructive
|
|
||||||
|
|
||||||
set -gx VIRTUAL_ENV "/root/projekti/TeraHz/utils/venv"
|
|
||||||
|
|
||||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
|
||||||
set -gx PATH "$VIRTUAL_ENV/bin" $PATH
|
|
||||||
|
|
||||||
# unset PYTHONHOME if set
|
|
||||||
if set -q PYTHONHOME
|
|
||||||
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
|
||||||
set -e PYTHONHOME
|
|
||||||
end
|
|
||||||
|
|
||||||
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
|
||||||
# fish uses a function instead of an env var to generate the prompt.
|
|
||||||
|
|
||||||
# save the current fish_prompt function as the function _old_fish_prompt
|
|
||||||
functions -c fish_prompt _old_fish_prompt
|
|
||||||
|
|
||||||
# with the original prompt function renamed, we can override with our own.
|
|
||||||
function fish_prompt
|
|
||||||
# Save the return status of the last command
|
|
||||||
set -l old_status $status
|
|
||||||
|
|
||||||
# Prompt override?
|
|
||||||
if test -n "(venv) "
|
|
||||||
printf "%s%s" "(venv) " (set_color normal)
|
|
||||||
else
|
|
||||||
# ...Otherwise, prepend env
|
|
||||||
set -l _checkbase (basename "$VIRTUAL_ENV")
|
|
||||||
if test $_checkbase = "__"
|
|
||||||
# special case for Aspen magic directories
|
|
||||||
# see http://www.zetadev.com/software/aspen/
|
|
||||||
printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal)
|
|
||||||
else
|
|
||||||
printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Restore the return status of the previous command.
|
|
||||||
echo "exit $old_status" | .
|
|
||||||
_old_fish_prompt
|
|
||||||
end
|
|
||||||
|
|
||||||
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
|
||||||
end
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from setuptools.command.easy_install import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from setuptools.command.easy_install import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from numpy.f2py.f2py2e import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from numpy.f2py.f2py2e import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from numpy.f2py.f2py2e import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,976 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
#
|
|
||||||
# Very simple serial terminal
|
|
||||||
#
|
|
||||||
# This file is part of pySerial. https://github.com/pyserial/pyserial
|
|
||||||
# (C)2002-2015 Chris Liechti <cliechti@gmx.net>
|
|
||||||
#
|
|
||||||
# SPDX-License-Identifier: BSD-3-Clause
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
|
|
||||||
import serial
|
|
||||||
from serial.tools.list_ports import comports
|
|
||||||
from serial.tools import hexlify_codec
|
|
||||||
|
|
||||||
# pylint: disable=wrong-import-order,wrong-import-position
|
|
||||||
|
|
||||||
codecs.register(lambda c: hexlify_codec.getregentry() if c == 'hexlify' else None)
|
|
||||||
|
|
||||||
try:
|
|
||||||
raw_input
|
|
||||||
except NameError:
|
|
||||||
# pylint: disable=redefined-builtin,invalid-name
|
|
||||||
raw_input = input # in python3 it's "raw"
|
|
||||||
unichr = chr
|
|
||||||
|
|
||||||
|
|
||||||
def key_description(character):
|
|
||||||
"""generate a readable description for a key"""
|
|
||||||
ascii_code = ord(character)
|
|
||||||
if ascii_code < 32:
|
|
||||||
return 'Ctrl+{:c}'.format(ord('@') + ascii_code)
|
|
||||||
else:
|
|
||||||
return repr(character)
|
|
||||||
|
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
||||||
class ConsoleBase(object):
|
|
||||||
"""OS abstraction for console (input/output codec, no echo)"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
if sys.version_info >= (3, 0):
|
|
||||||
self.byte_output = sys.stdout.buffer
|
|
||||||
else:
|
|
||||||
self.byte_output = sys.stdout
|
|
||||||
self.output = sys.stdout
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
"""Set console to read single characters, no echo"""
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
"""Restore default console settings"""
|
|
||||||
|
|
||||||
def getkey(self):
|
|
||||||
"""Read a single key from the console"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
def write_bytes(self, byte_string):
|
|
||||||
"""Write bytes (already encoded)"""
|
|
||||||
self.byte_output.write(byte_string)
|
|
||||||
self.byte_output.flush()
|
|
||||||
|
|
||||||
def write(self, text):
|
|
||||||
"""Write string"""
|
|
||||||
self.output.write(text)
|
|
||||||
self.output.flush()
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
"""Cancel getkey operation"""
|
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - -
|
|
||||||
# context manager:
|
|
||||||
# switch terminal temporary to normal mode (e.g. to get user input)
|
|
||||||
|
|
||||||
def __enter__(self):
|
|
||||||
self.cleanup()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, *args, **kwargs):
|
|
||||||
self.setup()
|
|
||||||
|
|
||||||
|
|
||||||
if os.name == 'nt': # noqa
|
|
||||||
import msvcrt
|
|
||||||
import ctypes
|
|
||||||
|
|
||||||
class Out(object):
|
|
||||||
"""file-like wrapper that uses os.write"""
|
|
||||||
|
|
||||||
def __init__(self, fd):
|
|
||||||
self.fd = fd
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def write(self, s):
|
|
||||||
os.write(self.fd, s)
|
|
||||||
|
|
||||||
class Console(ConsoleBase):
|
|
||||||
def __init__(self):
|
|
||||||
super(Console, self).__init__()
|
|
||||||
self._saved_ocp = ctypes.windll.kernel32.GetConsoleOutputCP()
|
|
||||||
self._saved_icp = ctypes.windll.kernel32.GetConsoleCP()
|
|
||||||
ctypes.windll.kernel32.SetConsoleOutputCP(65001)
|
|
||||||
ctypes.windll.kernel32.SetConsoleCP(65001)
|
|
||||||
self.output = codecs.getwriter('UTF-8')(Out(sys.stdout.fileno()), 'replace')
|
|
||||||
# the change of the code page is not propagated to Python, manually fix it
|
|
||||||
sys.stderr = codecs.getwriter('UTF-8')(Out(sys.stderr.fileno()), 'replace')
|
|
||||||
sys.stdout = self.output
|
|
||||||
self.output.encoding = 'UTF-8' # needed for input
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
ctypes.windll.kernel32.SetConsoleOutputCP(self._saved_ocp)
|
|
||||||
ctypes.windll.kernel32.SetConsoleCP(self._saved_icp)
|
|
||||||
|
|
||||||
def getkey(self):
|
|
||||||
while True:
|
|
||||||
z = msvcrt.getwch()
|
|
||||||
if z == unichr(13):
|
|
||||||
return unichr(10)
|
|
||||||
elif z in (unichr(0), unichr(0x0e)): # functions keys, ignore
|
|
||||||
msvcrt.getwch()
|
|
||||||
else:
|
|
||||||
return z
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
# CancelIo, CancelSynchronousIo do not seem to work when using
|
|
||||||
# getwch, so instead, send a key to the window with the console
|
|
||||||
hwnd = ctypes.windll.kernel32.GetConsoleWindow()
|
|
||||||
ctypes.windll.user32.PostMessageA(hwnd, 0x100, 0x0d, 0)
|
|
||||||
|
|
||||||
elif os.name == 'posix':
|
|
||||||
import atexit
|
|
||||||
import termios
|
|
||||||
import fcntl
|
|
||||||
|
|
||||||
class Console(ConsoleBase):
|
|
||||||
def __init__(self):
|
|
||||||
super(Console, self).__init__()
|
|
||||||
self.fd = sys.stdin.fileno()
|
|
||||||
self.old = termios.tcgetattr(self.fd)
|
|
||||||
atexit.register(self.cleanup)
|
|
||||||
if sys.version_info < (3, 0):
|
|
||||||
self.enc_stdin = codecs.getreader(sys.stdin.encoding)(sys.stdin)
|
|
||||||
else:
|
|
||||||
self.enc_stdin = sys.stdin
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
new = termios.tcgetattr(self.fd)
|
|
||||||
new[3] = new[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG
|
|
||||||
new[6][termios.VMIN] = 1
|
|
||||||
new[6][termios.VTIME] = 0
|
|
||||||
termios.tcsetattr(self.fd, termios.TCSANOW, new)
|
|
||||||
|
|
||||||
def getkey(self):
|
|
||||||
c = self.enc_stdin.read(1)
|
|
||||||
if c == unichr(0x7f):
|
|
||||||
c = unichr(8) # map the BS key (which yields DEL) to backspace
|
|
||||||
return c
|
|
||||||
|
|
||||||
def cancel(self):
|
|
||||||
fcntl.ioctl(self.fd, termios.TIOCSTI, b'\0')
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Sorry no implementation for your platform ({}) available.'.format(sys.platform))
|
|
||||||
|
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
||||||
|
|
||||||
class Transform(object):
|
|
||||||
"""do-nothing: forward all data unchanged"""
|
|
||||||
def rx(self, text):
|
|
||||||
"""text received from serial port"""
|
|
||||||
return text
|
|
||||||
|
|
||||||
def tx(self, text):
|
|
||||||
"""text to be sent to serial port"""
|
|
||||||
return text
|
|
||||||
|
|
||||||
def echo(self, text):
|
|
||||||
"""text to be sent but displayed on console"""
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
class CRLF(Transform):
|
|
||||||
"""ENTER sends CR+LF"""
|
|
||||||
|
|
||||||
def tx(self, text):
|
|
||||||
return text.replace('\n', '\r\n')
|
|
||||||
|
|
||||||
|
|
||||||
class CR(Transform):
|
|
||||||
"""ENTER sends CR"""
|
|
||||||
|
|
||||||
def rx(self, text):
|
|
||||||
return text.replace('\r', '\n')
|
|
||||||
|
|
||||||
def tx(self, text):
|
|
||||||
return text.replace('\n', '\r')
|
|
||||||
|
|
||||||
|
|
||||||
class LF(Transform):
|
|
||||||
"""ENTER sends LF"""
|
|
||||||
|
|
||||||
|
|
||||||
class NoTerminal(Transform):
|
|
||||||
"""remove typical terminal control codes from input"""
|
|
||||||
|
|
||||||
REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32) if unichr(x) not in '\r\n\b\t')
|
|
||||||
REPLACEMENT_MAP.update(
|
|
||||||
{
|
|
||||||
0x7F: 0x2421, # DEL
|
|
||||||
0x9B: 0x2425, # CSI
|
|
||||||
})
|
|
||||||
|
|
||||||
def rx(self, text):
|
|
||||||
return text.translate(self.REPLACEMENT_MAP)
|
|
||||||
|
|
||||||
echo = rx
|
|
||||||
|
|
||||||
|
|
||||||
class NoControls(NoTerminal):
|
|
||||||
"""Remove all control codes, incl. CR+LF"""
|
|
||||||
|
|
||||||
REPLACEMENT_MAP = dict((x, 0x2400 + x) for x in range(32))
|
|
||||||
REPLACEMENT_MAP.update(
|
|
||||||
{
|
|
||||||
0x20: 0x2423, # visual space
|
|
||||||
0x7F: 0x2421, # DEL
|
|
||||||
0x9B: 0x2425, # CSI
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class Printable(Transform):
|
|
||||||
"""Show decimal code for all non-ASCII characters and replace most control codes"""
|
|
||||||
|
|
||||||
def rx(self, text):
|
|
||||||
r = []
|
|
||||||
for c in text:
|
|
||||||
if ' ' <= c < '\x7f' or c in '\r\n\b\t':
|
|
||||||
r.append(c)
|
|
||||||
elif c < ' ':
|
|
||||||
r.append(unichr(0x2400 + ord(c)))
|
|
||||||
else:
|
|
||||||
r.extend(unichr(0x2080 + ord(d) - 48) for d in '{:d}'.format(ord(c)))
|
|
||||||
r.append(' ')
|
|
||||||
return ''.join(r)
|
|
||||||
|
|
||||||
echo = rx
|
|
||||||
|
|
||||||
|
|
||||||
class Colorize(Transform):
|
|
||||||
"""Apply different colors for received and echo"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
# XXX make it configurable, use colorama?
|
|
||||||
self.input_color = '\x1b[37m'
|
|
||||||
self.echo_color = '\x1b[31m'
|
|
||||||
|
|
||||||
def rx(self, text):
|
|
||||||
return self.input_color + text
|
|
||||||
|
|
||||||
def echo(self, text):
|
|
||||||
return self.echo_color + text
|
|
||||||
|
|
||||||
|
|
||||||
class DebugIO(Transform):
|
|
||||||
"""Print what is sent and received"""
|
|
||||||
|
|
||||||
def rx(self, text):
|
|
||||||
sys.stderr.write(' [RX:{}] '.format(repr(text)))
|
|
||||||
sys.stderr.flush()
|
|
||||||
return text
|
|
||||||
|
|
||||||
def tx(self, text):
|
|
||||||
sys.stderr.write(' [TX:{}] '.format(repr(text)))
|
|
||||||
sys.stderr.flush()
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
# other ideas:
|
|
||||||
# - add date/time for each newline
|
|
||||||
# - insert newline after: a) timeout b) packet end character
|
|
||||||
|
|
||||||
EOL_TRANSFORMATIONS = {
|
|
||||||
'crlf': CRLF,
|
|
||||||
'cr': CR,
|
|
||||||
'lf': LF,
|
|
||||||
}
|
|
||||||
|
|
||||||
TRANSFORMATIONS = {
|
|
||||||
'direct': Transform, # no transformation
|
|
||||||
'default': NoTerminal,
|
|
||||||
'nocontrol': NoControls,
|
|
||||||
'printable': Printable,
|
|
||||||
'colorize': Colorize,
|
|
||||||
'debug': DebugIO,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
||||||
def ask_for_port():
|
|
||||||
"""\
|
|
||||||
Show a list of ports and ask the user for a choice. To make selection
|
|
||||||
easier on systems with long device names, also allow the input of an
|
|
||||||
index.
|
|
||||||
"""
|
|
||||||
sys.stderr.write('\n--- Available ports:\n')
|
|
||||||
ports = []
|
|
||||||
for n, (port, desc, hwid) in enumerate(sorted(comports()), 1):
|
|
||||||
sys.stderr.write('--- {:2}: {:20} {!r}\n'.format(n, port, desc))
|
|
||||||
ports.append(port)
|
|
||||||
while True:
|
|
||||||
port = raw_input('--- Enter port index or full name: ')
|
|
||||||
try:
|
|
||||||
index = int(port) - 1
|
|
||||||
if not 0 <= index < len(ports):
|
|
||||||
sys.stderr.write('--- Invalid index!\n')
|
|
||||||
continue
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
port = ports[index]
|
|
||||||
return port
|
|
||||||
|
|
||||||
|
|
||||||
class Miniterm(object):
|
|
||||||
"""\
|
|
||||||
Terminal application. Copy data from serial port to console and vice versa.
|
|
||||||
Handle special keys from the console to show menu etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, serial_instance, echo=False, eol='crlf', filters=()):
|
|
||||||
self.console = Console()
|
|
||||||
self.serial = serial_instance
|
|
||||||
self.echo = echo
|
|
||||||
self.raw = False
|
|
||||||
self.input_encoding = 'UTF-8'
|
|
||||||
self.output_encoding = 'UTF-8'
|
|
||||||
self.eol = eol
|
|
||||||
self.filters = filters
|
|
||||||
self.update_transformations()
|
|
||||||
self.exit_character = 0x1d # GS/CTRL+]
|
|
||||||
self.menu_character = 0x14 # Menu: CTRL+T
|
|
||||||
self.alive = None
|
|
||||||
self._reader_alive = None
|
|
||||||
self.receiver_thread = None
|
|
||||||
self.rx_decoder = None
|
|
||||||
self.tx_decoder = None
|
|
||||||
|
|
||||||
def _start_reader(self):
|
|
||||||
"""Start reader thread"""
|
|
||||||
self._reader_alive = True
|
|
||||||
# start serial->console thread
|
|
||||||
self.receiver_thread = threading.Thread(target=self.reader, name='rx')
|
|
||||||
self.receiver_thread.daemon = True
|
|
||||||
self.receiver_thread.start()
|
|
||||||
|
|
||||||
def _stop_reader(self):
|
|
||||||
"""Stop reader thread only, wait for clean exit of thread"""
|
|
||||||
self._reader_alive = False
|
|
||||||
if hasattr(self.serial, 'cancel_read'):
|
|
||||||
self.serial.cancel_read()
|
|
||||||
self.receiver_thread.join()
|
|
||||||
|
|
||||||
def start(self):
|
|
||||||
"""start worker threads"""
|
|
||||||
self.alive = True
|
|
||||||
self._start_reader()
|
|
||||||
# enter console->serial loop
|
|
||||||
self.transmitter_thread = threading.Thread(target=self.writer, name='tx')
|
|
||||||
self.transmitter_thread.daemon = True
|
|
||||||
self.transmitter_thread.start()
|
|
||||||
self.console.setup()
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
"""set flag to stop worker threads"""
|
|
||||||
self.alive = False
|
|
||||||
|
|
||||||
def join(self, transmit_only=False):
|
|
||||||
"""wait for worker threads to terminate"""
|
|
||||||
self.transmitter_thread.join()
|
|
||||||
if not transmit_only:
|
|
||||||
if hasattr(self.serial, 'cancel_read'):
|
|
||||||
self.serial.cancel_read()
|
|
||||||
self.receiver_thread.join()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
self.serial.close()
|
|
||||||
|
|
||||||
def update_transformations(self):
|
|
||||||
"""take list of transformation classes and instantiate them for rx and tx"""
|
|
||||||
transformations = [EOL_TRANSFORMATIONS[self.eol]] + [TRANSFORMATIONS[f]
|
|
||||||
for f in self.filters]
|
|
||||||
self.tx_transformations = [t() for t in transformations]
|
|
||||||
self.rx_transformations = list(reversed(self.tx_transformations))
|
|
||||||
|
|
||||||
def set_rx_encoding(self, encoding, errors='replace'):
|
|
||||||
"""set encoding for received data"""
|
|
||||||
self.input_encoding = encoding
|
|
||||||
self.rx_decoder = codecs.getincrementaldecoder(encoding)(errors)
|
|
||||||
|
|
||||||
def set_tx_encoding(self, encoding, errors='replace'):
|
|
||||||
"""set encoding for transmitted data"""
|
|
||||||
self.output_encoding = encoding
|
|
||||||
self.tx_encoder = codecs.getincrementalencoder(encoding)(errors)
|
|
||||||
|
|
||||||
def dump_port_settings(self):
|
|
||||||
"""Write current settings to sys.stderr"""
|
|
||||||
sys.stderr.write("\n--- Settings: {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits}\n".format(
|
|
||||||
p=self.serial))
|
|
||||||
sys.stderr.write('--- RTS: {:8} DTR: {:8} BREAK: {:8}\n'.format(
|
|
||||||
('active' if self.serial.rts else 'inactive'),
|
|
||||||
('active' if self.serial.dtr else 'inactive'),
|
|
||||||
('active' if self.serial.break_condition else 'inactive')))
|
|
||||||
try:
|
|
||||||
sys.stderr.write('--- CTS: {:8} DSR: {:8} RI: {:8} CD: {:8}\n'.format(
|
|
||||||
('active' if self.serial.cts else 'inactive'),
|
|
||||||
('active' if self.serial.dsr else 'inactive'),
|
|
||||||
('active' if self.serial.ri else 'inactive'),
|
|
||||||
('active' if self.serial.cd else 'inactive')))
|
|
||||||
except serial.SerialException:
|
|
||||||
# on RFC 2217 ports, it can happen if no modem state notification was
|
|
||||||
# yet received. ignore this error.
|
|
||||||
pass
|
|
||||||
sys.stderr.write('--- software flow control: {}\n'.format('active' if self.serial.xonxoff else 'inactive'))
|
|
||||||
sys.stderr.write('--- hardware flow control: {}\n'.format('active' if self.serial.rtscts else 'inactive'))
|
|
||||||
sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
|
|
||||||
sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
|
|
||||||
sys.stderr.write('--- EOL: {}\n'.format(self.eol.upper()))
|
|
||||||
sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
|
|
||||||
|
|
||||||
def reader(self):
|
|
||||||
"""loop and copy serial->console"""
|
|
||||||
try:
|
|
||||||
while self.alive and self._reader_alive:
|
|
||||||
# read all that is there or wait for one byte
|
|
||||||
data = self.serial.read(self.serial.in_waiting or 1)
|
|
||||||
if data:
|
|
||||||
if self.raw:
|
|
||||||
self.console.write_bytes(data)
|
|
||||||
else:
|
|
||||||
text = self.rx_decoder.decode(data)
|
|
||||||
for transformation in self.rx_transformations:
|
|
||||||
text = transformation.rx(text)
|
|
||||||
self.console.write(text)
|
|
||||||
except serial.SerialException:
|
|
||||||
self.alive = False
|
|
||||||
self.console.cancel()
|
|
||||||
raise # XXX handle instead of re-raise?
|
|
||||||
|
|
||||||
def writer(self):
|
|
||||||
"""\
|
|
||||||
Loop and copy console->serial until self.exit_character character is
|
|
||||||
found. When self.menu_character is found, interpret the next key
|
|
||||||
locally.
|
|
||||||
"""
|
|
||||||
menu_active = False
|
|
||||||
try:
|
|
||||||
while self.alive:
|
|
||||||
try:
|
|
||||||
c = self.console.getkey()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
c = '\x03'
|
|
||||||
if not self.alive:
|
|
||||||
break
|
|
||||||
if menu_active:
|
|
||||||
self.handle_menu_key(c)
|
|
||||||
menu_active = False
|
|
||||||
elif c == self.menu_character:
|
|
||||||
menu_active = True # next char will be for menu
|
|
||||||
elif c == self.exit_character:
|
|
||||||
self.stop() # exit app
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
#~ if self.raw:
|
|
||||||
text = c
|
|
||||||
for transformation in self.tx_transformations:
|
|
||||||
text = transformation.tx(text)
|
|
||||||
self.serial.write(self.tx_encoder.encode(text))
|
|
||||||
if self.echo:
|
|
||||||
echo_text = c
|
|
||||||
for transformation in self.tx_transformations:
|
|
||||||
echo_text = transformation.echo(echo_text)
|
|
||||||
self.console.write(echo_text)
|
|
||||||
except:
|
|
||||||
self.alive = False
|
|
||||||
raise
|
|
||||||
|
|
||||||
def handle_menu_key(self, c):
|
|
||||||
"""Implement a simple menu / settings"""
|
|
||||||
if c == self.menu_character or c == self.exit_character:
|
|
||||||
# Menu/exit character again -> send itself
|
|
||||||
self.serial.write(self.tx_encoder.encode(c))
|
|
||||||
if self.echo:
|
|
||||||
self.console.write(c)
|
|
||||||
elif c == '\x15': # CTRL+U -> upload file
|
|
||||||
self.upload_file()
|
|
||||||
elif c in '\x08hH?': # CTRL+H, h, H, ? -> Show help
|
|
||||||
sys.stderr.write(self.get_help_text())
|
|
||||||
elif c == '\x12': # CTRL+R -> Toggle RTS
|
|
||||||
self.serial.rts = not self.serial.rts
|
|
||||||
sys.stderr.write('--- RTS {} ---\n'.format('active' if self.serial.rts else 'inactive'))
|
|
||||||
elif c == '\x04': # CTRL+D -> Toggle DTR
|
|
||||||
self.serial.dtr = not self.serial.dtr
|
|
||||||
sys.stderr.write('--- DTR {} ---\n'.format('active' if self.serial.dtr else 'inactive'))
|
|
||||||
elif c == '\x02': # CTRL+B -> toggle BREAK condition
|
|
||||||
self.serial.break_condition = not self.serial.break_condition
|
|
||||||
sys.stderr.write('--- BREAK {} ---\n'.format('active' if self.serial.break_condition else 'inactive'))
|
|
||||||
elif c == '\x05': # CTRL+E -> toggle local echo
|
|
||||||
self.echo = not self.echo
|
|
||||||
sys.stderr.write('--- local echo {} ---\n'.format('active' if self.echo else 'inactive'))
|
|
||||||
elif c == '\x06': # CTRL+F -> edit filters
|
|
||||||
self.change_filter()
|
|
||||||
elif c == '\x0c': # CTRL+L -> EOL mode
|
|
||||||
modes = list(EOL_TRANSFORMATIONS) # keys
|
|
||||||
eol = modes.index(self.eol) + 1
|
|
||||||
if eol >= len(modes):
|
|
||||||
eol = 0
|
|
||||||
self.eol = modes[eol]
|
|
||||||
sys.stderr.write('--- EOL: {} ---\n'.format(self.eol.upper()))
|
|
||||||
self.update_transformations()
|
|
||||||
elif c == '\x01': # CTRL+A -> set encoding
|
|
||||||
self.change_encoding()
|
|
||||||
elif c == '\x09': # CTRL+I -> info
|
|
||||||
self.dump_port_settings()
|
|
||||||
#~ elif c == '\x01': # CTRL+A -> cycle escape mode
|
|
||||||
#~ elif c == '\x0c': # CTRL+L -> cycle linefeed mode
|
|
||||||
elif c in 'pP': # P -> change port
|
|
||||||
self.change_port()
|
|
||||||
elif c in 'sS': # S -> suspend / open port temporarily
|
|
||||||
self.suspend_port()
|
|
||||||
elif c in 'bB': # B -> change baudrate
|
|
||||||
self.change_baudrate()
|
|
||||||
elif c == '8': # 8 -> change to 8 bits
|
|
||||||
self.serial.bytesize = serial.EIGHTBITS
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c == '7': # 7 -> change to 8 bits
|
|
||||||
self.serial.bytesize = serial.SEVENBITS
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c in 'eE': # E -> change to even parity
|
|
||||||
self.serial.parity = serial.PARITY_EVEN
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c in 'oO': # O -> change to odd parity
|
|
||||||
self.serial.parity = serial.PARITY_ODD
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c in 'mM': # M -> change to mark parity
|
|
||||||
self.serial.parity = serial.PARITY_MARK
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c in 'sS': # S -> change to space parity
|
|
||||||
self.serial.parity = serial.PARITY_SPACE
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c in 'nN': # N -> change to no parity
|
|
||||||
self.serial.parity = serial.PARITY_NONE
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c == '1': # 1 -> change to 1 stop bits
|
|
||||||
self.serial.stopbits = serial.STOPBITS_ONE
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c == '2': # 2 -> change to 2 stop bits
|
|
||||||
self.serial.stopbits = serial.STOPBITS_TWO
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c == '3': # 3 -> change to 1.5 stop bits
|
|
||||||
self.serial.stopbits = serial.STOPBITS_ONE_POINT_FIVE
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c in 'xX': # X -> change software flow control
|
|
||||||
self.serial.xonxoff = (c == 'X')
|
|
||||||
self.dump_port_settings()
|
|
||||||
elif c in 'rR': # R -> change hardware flow control
|
|
||||||
self.serial.rtscts = (c == 'R')
|
|
||||||
self.dump_port_settings()
|
|
||||||
else:
|
|
||||||
sys.stderr.write('--- unknown menu character {} --\n'.format(key_description(c)))
|
|
||||||
|
|
||||||
def upload_file(self):
|
|
||||||
"""Ask user for filenname and send its contents"""
|
|
||||||
sys.stderr.write('\n--- File to upload: ')
|
|
||||||
sys.stderr.flush()
|
|
||||||
with self.console:
|
|
||||||
filename = sys.stdin.readline().rstrip('\r\n')
|
|
||||||
if filename:
|
|
||||||
try:
|
|
||||||
with open(filename, 'rb') as f:
|
|
||||||
sys.stderr.write('--- Sending file {} ---\n'.format(filename))
|
|
||||||
while True:
|
|
||||||
block = f.read(1024)
|
|
||||||
if not block:
|
|
||||||
break
|
|
||||||
self.serial.write(block)
|
|
||||||
# Wait for output buffer to drain.
|
|
||||||
self.serial.flush()
|
|
||||||
sys.stderr.write('.') # Progress indicator.
|
|
||||||
sys.stderr.write('\n--- File {} sent ---\n'.format(filename))
|
|
||||||
except IOError as e:
|
|
||||||
sys.stderr.write('--- ERROR opening file {}: {} ---\n'.format(filename, e))
|
|
||||||
|
|
||||||
def change_filter(self):
|
|
||||||
"""change the i/o transformations"""
|
|
||||||
sys.stderr.write('\n--- Available Filters:\n')
|
|
||||||
sys.stderr.write('\n'.join(
|
|
||||||
'--- {:<10} = {.__doc__}'.format(k, v)
|
|
||||||
for k, v in sorted(TRANSFORMATIONS.items())))
|
|
||||||
sys.stderr.write('\n--- Enter new filter name(s) [{}]: '.format(' '.join(self.filters)))
|
|
||||||
with self.console:
|
|
||||||
new_filters = sys.stdin.readline().lower().split()
|
|
||||||
if new_filters:
|
|
||||||
for f in new_filters:
|
|
||||||
if f not in TRANSFORMATIONS:
|
|
||||||
sys.stderr.write('--- unknown filter: {}\n'.format(repr(f)))
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.filters = new_filters
|
|
||||||
self.update_transformations()
|
|
||||||
sys.stderr.write('--- filters: {}\n'.format(' '.join(self.filters)))
|
|
||||||
|
|
||||||
def change_encoding(self):
|
|
||||||
"""change encoding on the serial port"""
|
|
||||||
sys.stderr.write('\n--- Enter new encoding name [{}]: '.format(self.input_encoding))
|
|
||||||
with self.console:
|
|
||||||
new_encoding = sys.stdin.readline().strip()
|
|
||||||
if new_encoding:
|
|
||||||
try:
|
|
||||||
codecs.lookup(new_encoding)
|
|
||||||
except LookupError:
|
|
||||||
sys.stderr.write('--- invalid encoding name: {}\n'.format(new_encoding))
|
|
||||||
else:
|
|
||||||
self.set_rx_encoding(new_encoding)
|
|
||||||
self.set_tx_encoding(new_encoding)
|
|
||||||
sys.stderr.write('--- serial input encoding: {}\n'.format(self.input_encoding))
|
|
||||||
sys.stderr.write('--- serial output encoding: {}\n'.format(self.output_encoding))
|
|
||||||
|
|
||||||
def change_baudrate(self):
|
|
||||||
"""change the baudrate"""
|
|
||||||
sys.stderr.write('\n--- Baudrate: ')
|
|
||||||
sys.stderr.flush()
|
|
||||||
with self.console:
|
|
||||||
backup = self.serial.baudrate
|
|
||||||
try:
|
|
||||||
self.serial.baudrate = int(sys.stdin.readline().strip())
|
|
||||||
except ValueError as e:
|
|
||||||
sys.stderr.write('--- ERROR setting baudrate: {} ---\n'.format(e))
|
|
||||||
self.serial.baudrate = backup
|
|
||||||
else:
|
|
||||||
self.dump_port_settings()
|
|
||||||
|
|
||||||
def change_port(self):
|
|
||||||
"""Have a conversation with the user to change the serial port"""
|
|
||||||
with self.console:
|
|
||||||
try:
|
|
||||||
port = ask_for_port()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
port = None
|
|
||||||
if port and port != self.serial.port:
|
|
||||||
# reader thread needs to be shut down
|
|
||||||
self._stop_reader()
|
|
||||||
# save settings
|
|
||||||
settings = self.serial.getSettingsDict()
|
|
||||||
try:
|
|
||||||
new_serial = serial.serial_for_url(port, do_not_open=True)
|
|
||||||
# restore settings and open
|
|
||||||
new_serial.applySettingsDict(settings)
|
|
||||||
new_serial.rts = self.serial.rts
|
|
||||||
new_serial.dtr = self.serial.dtr
|
|
||||||
new_serial.open()
|
|
||||||
new_serial.break_condition = self.serial.break_condition
|
|
||||||
except Exception as e:
|
|
||||||
sys.stderr.write('--- ERROR opening new port: {} ---\n'.format(e))
|
|
||||||
new_serial.close()
|
|
||||||
else:
|
|
||||||
self.serial.close()
|
|
||||||
self.serial = new_serial
|
|
||||||
sys.stderr.write('--- Port changed to: {} ---\n'.format(self.serial.port))
|
|
||||||
# and restart the reader thread
|
|
||||||
self._start_reader()
|
|
||||||
|
|
||||||
def suspend_port(self):
|
|
||||||
"""\
|
|
||||||
open port temporarily, allow reconnect, exit and port change to get
|
|
||||||
out of the loop
|
|
||||||
"""
|
|
||||||
# reader thread needs to be shut down
|
|
||||||
self._stop_reader()
|
|
||||||
self.serial.close()
|
|
||||||
sys.stderr.write('\n--- Port closed: {} ---\n'.format(self.serial.port))
|
|
||||||
do_change_port = False
|
|
||||||
while not self.serial.is_open:
|
|
||||||
sys.stderr.write('--- Quit: {exit} | p: port change | any other key to reconnect ---\n'.format(
|
|
||||||
exit=key_description(self.exit_character)))
|
|
||||||
k = self.console.getkey()
|
|
||||||
if k == self.exit_character:
|
|
||||||
self.stop() # exit app
|
|
||||||
break
|
|
||||||
elif k in 'pP':
|
|
||||||
do_change_port = True
|
|
||||||
break
|
|
||||||
try:
|
|
||||||
self.serial.open()
|
|
||||||
except Exception as e:
|
|
||||||
sys.stderr.write('--- ERROR opening port: {} ---\n'.format(e))
|
|
||||||
if do_change_port:
|
|
||||||
self.change_port()
|
|
||||||
else:
|
|
||||||
# and restart the reader thread
|
|
||||||
self._start_reader()
|
|
||||||
sys.stderr.write('--- Port opened: {} ---\n'.format(self.serial.port))
|
|
||||||
|
|
||||||
def get_help_text(self):
|
|
||||||
"""return the help text"""
|
|
||||||
# help text, starts with blank line!
|
|
||||||
return """
|
|
||||||
--- pySerial ({version}) - miniterm - help
|
|
||||||
---
|
|
||||||
--- {exit:8} Exit program
|
|
||||||
--- {menu:8} Menu escape key, followed by:
|
|
||||||
--- Menu keys:
|
|
||||||
--- {menu:7} Send the menu character itself to remote
|
|
||||||
--- {exit:7} Send the exit character itself to remote
|
|
||||||
--- {info:7} Show info
|
|
||||||
--- {upload:7} Upload file (prompt will be shown)
|
|
||||||
--- {repr:7} encoding
|
|
||||||
--- {filter:7} edit filters
|
|
||||||
--- Toggles:
|
|
||||||
--- {rts:7} RTS {dtr:7} DTR {brk:7} BREAK
|
|
||||||
--- {echo:7} echo {eol:7} EOL
|
|
||||||
---
|
|
||||||
--- Port settings ({menu} followed by the following):
|
|
||||||
--- p change port
|
|
||||||
--- 7 8 set data bits
|
|
||||||
--- N E O S M change parity (None, Even, Odd, Space, Mark)
|
|
||||||
--- 1 2 3 set stop bits (1, 2, 1.5)
|
|
||||||
--- b change baud rate
|
|
||||||
--- x X disable/enable software flow control
|
|
||||||
--- r R disable/enable hardware flow control
|
|
||||||
""".format(version=getattr(serial, 'VERSION', 'unknown version'),
|
|
||||||
exit=key_description(self.exit_character),
|
|
||||||
menu=key_description(self.menu_character),
|
|
||||||
rts=key_description('\x12'),
|
|
||||||
dtr=key_description('\x04'),
|
|
||||||
brk=key_description('\x02'),
|
|
||||||
echo=key_description('\x05'),
|
|
||||||
info=key_description('\x09'),
|
|
||||||
upload=key_description('\x15'),
|
|
||||||
repr=key_description('\x01'),
|
|
||||||
filter=key_description('\x06'),
|
|
||||||
eol=key_description('\x0c'))
|
|
||||||
|
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
||||||
# default args can be used to override when calling main() from an other script
|
|
||||||
# e.g to create a miniterm-my-device.py
|
|
||||||
def main(default_port=None, default_baudrate=9600, default_rts=None, default_dtr=None):
|
|
||||||
"""Command line tool, entry point"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Miniterm - A simple terminal program for the serial port.")
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"port",
|
|
||||||
nargs='?',
|
|
||||||
help="serial port name ('-' to show port list)",
|
|
||||||
default=default_port)
|
|
||||||
|
|
||||||
parser.add_argument(
|
|
||||||
"baudrate",
|
|
||||||
nargs='?',
|
|
||||||
type=int,
|
|
||||||
help="set baud rate, default: %(default)s",
|
|
||||||
default=default_baudrate)
|
|
||||||
|
|
||||||
group = parser.add_argument_group("port settings")
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--parity",
|
|
||||||
choices=['N', 'E', 'O', 'S', 'M'],
|
|
||||||
type=lambda c: c.upper(),
|
|
||||||
help="set parity, one of {N E O S M}, default: N",
|
|
||||||
default='N')
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--rtscts",
|
|
||||||
action="store_true",
|
|
||||||
help="enable RTS/CTS flow control (default off)",
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--xonxoff",
|
|
||||||
action="store_true",
|
|
||||||
help="enable software flow control (default off)",
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--rts",
|
|
||||||
type=int,
|
|
||||||
help="set initial RTS line state (possible values: 0, 1)",
|
|
||||||
default=default_rts)
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--dtr",
|
|
||||||
type=int,
|
|
||||||
help="set initial DTR line state (possible values: 0, 1)",
|
|
||||||
default=default_dtr)
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--ask",
|
|
||||||
action="store_true",
|
|
||||||
help="ask again for port when open fails",
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
group = parser.add_argument_group("data handling")
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"-e", "--echo",
|
|
||||||
action="store_true",
|
|
||||||
help="enable local echo (default off)",
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--encoding",
|
|
||||||
dest="serial_port_encoding",
|
|
||||||
metavar="CODEC",
|
|
||||||
help="set the encoding for the serial port (e.g. hexlify, Latin1, UTF-8), default: %(default)s",
|
|
||||||
default='UTF-8')
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"-f", "--filter",
|
|
||||||
action="append",
|
|
||||||
metavar="NAME",
|
|
||||||
help="add text transformation",
|
|
||||||
default=[])
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--eol",
|
|
||||||
choices=['CR', 'LF', 'CRLF'],
|
|
||||||
type=lambda c: c.upper(),
|
|
||||||
help="end of line mode",
|
|
||||||
default='CRLF')
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--raw",
|
|
||||||
action="store_true",
|
|
||||||
help="Do no apply any encodings/transformations",
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
group = parser.add_argument_group("hotkeys")
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--exit-char",
|
|
||||||
type=int,
|
|
||||||
metavar='NUM',
|
|
||||||
help="Unicode of special character that is used to exit the application, default: %(default)s",
|
|
||||||
default=0x1d) # GS/CTRL+]
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--menu-char",
|
|
||||||
type=int,
|
|
||||||
metavar='NUM',
|
|
||||||
help="Unicode code of special character that is used to control miniterm (menu), default: %(default)s",
|
|
||||||
default=0x14) # Menu: CTRL+T
|
|
||||||
|
|
||||||
group = parser.add_argument_group("diagnostics")
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"-q", "--quiet",
|
|
||||||
action="store_true",
|
|
||||||
help="suppress non-error messages",
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
group.add_argument(
|
|
||||||
"--develop",
|
|
||||||
action="store_true",
|
|
||||||
help="show Python traceback on error",
|
|
||||||
default=False)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
if args.menu_char == args.exit_char:
|
|
||||||
parser.error('--exit-char can not be the same as --menu-char')
|
|
||||||
|
|
||||||
if args.filter:
|
|
||||||
if 'help' in args.filter:
|
|
||||||
sys.stderr.write('Available filters:\n')
|
|
||||||
sys.stderr.write('\n'.join(
|
|
||||||
'{:<10} = {.__doc__}'.format(k, v)
|
|
||||||
for k, v in sorted(TRANSFORMATIONS.items())))
|
|
||||||
sys.stderr.write('\n')
|
|
||||||
sys.exit(1)
|
|
||||||
filters = args.filter
|
|
||||||
else:
|
|
||||||
filters = ['default']
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# no port given on command line -> ask user now
|
|
||||||
if args.port is None or args.port == '-':
|
|
||||||
try:
|
|
||||||
args.port = ask_for_port()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
sys.stderr.write('\n')
|
|
||||||
parser.error('user aborted and port is not given')
|
|
||||||
else:
|
|
||||||
if not args.port:
|
|
||||||
parser.error('port is not given')
|
|
||||||
try:
|
|
||||||
serial_instance = serial.serial_for_url(
|
|
||||||
args.port,
|
|
||||||
args.baudrate,
|
|
||||||
parity=args.parity,
|
|
||||||
rtscts=args.rtscts,
|
|
||||||
xonxoff=args.xonxoff,
|
|
||||||
do_not_open=True)
|
|
||||||
|
|
||||||
if not hasattr(serial_instance, 'cancel_read'):
|
|
||||||
# enable timeout for alive flag polling if cancel_read is not available
|
|
||||||
serial_instance.timeout = 1
|
|
||||||
|
|
||||||
if args.dtr is not None:
|
|
||||||
if not args.quiet:
|
|
||||||
sys.stderr.write('--- forcing DTR {}\n'.format('active' if args.dtr else 'inactive'))
|
|
||||||
serial_instance.dtr = args.dtr
|
|
||||||
if args.rts is not None:
|
|
||||||
if not args.quiet:
|
|
||||||
sys.stderr.write('--- forcing RTS {}\n'.format('active' if args.rts else 'inactive'))
|
|
||||||
serial_instance.rts = args.rts
|
|
||||||
|
|
||||||
serial_instance.open()
|
|
||||||
except serial.SerialException as e:
|
|
||||||
sys.stderr.write('could not open port {}: {}\n'.format(repr(args.port), e))
|
|
||||||
if args.develop:
|
|
||||||
raise
|
|
||||||
if not args.ask:
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
args.port = '-'
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
miniterm = Miniterm(
|
|
||||||
serial_instance,
|
|
||||||
echo=args.echo,
|
|
||||||
eol=args.eol.lower(),
|
|
||||||
filters=filters)
|
|
||||||
miniterm.exit_character = unichr(args.exit_char)
|
|
||||||
miniterm.menu_character = unichr(args.menu_char)
|
|
||||||
miniterm.raw = args.raw
|
|
||||||
miniterm.set_rx_encoding(args.serial_port_encoding)
|
|
||||||
miniterm.set_tx_encoding(args.serial_port_encoding)
|
|
||||||
|
|
||||||
if not args.quiet:
|
|
||||||
sys.stderr.write('--- Miniterm on {p.name} {p.baudrate},{p.bytesize},{p.parity},{p.stopbits} ---\n'.format(
|
|
||||||
p=miniterm.serial))
|
|
||||||
sys.stderr.write('--- Quit: {} | Menu: {} | Help: {} followed by {} ---\n'.format(
|
|
||||||
key_description(miniterm.exit_character),
|
|
||||||
key_description(miniterm.menu_character),
|
|
||||||
key_description(miniterm.menu_character),
|
|
||||||
key_description('\x08')))
|
|
||||||
|
|
||||||
miniterm.start()
|
|
||||||
try:
|
|
||||||
miniterm.join(True)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
pass
|
|
||||||
if not args.quiet:
|
|
||||||
sys.stderr.write("\n--- exit ---\n")
|
|
||||||
miniterm.join()
|
|
||||||
miniterm.close()
|
|
||||||
|
|
||||||
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pip._internal import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pip._internal import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/root/projekti/TeraHz/utils/venv/bin/python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from pip._internal import main
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
UNKNOWN
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pip
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
Metadata-Version: 2.0
|
|
||||||
Name: cycler
|
|
||||||
Version: 0.10.0
|
|
||||||
Summary: Composable style cycles
|
|
||||||
Home-page: http://github.com/matplotlib/cycler
|
|
||||||
Author: Thomas A Caswell
|
|
||||||
Author-email: matplotlib-users@python.org
|
|
||||||
License: BSD
|
|
||||||
Keywords: cycle kwargs
|
|
||||||
Platform: Cross platform (Linux
|
|
||||||
Platform: Mac OSX
|
|
||||||
Platform: Windows)
|
|
||||||
Classifier: Development Status :: 4 - Beta
|
|
||||||
Classifier: Programming Language :: Python :: 2
|
|
||||||
Classifier: Programming Language :: Python :: 2.6
|
|
||||||
Classifier: Programming Language :: Python :: 2.7
|
|
||||||
Classifier: Programming Language :: Python :: 3
|
|
||||||
Classifier: Programming Language :: Python :: 3.3
|
|
||||||
Classifier: Programming Language :: Python :: 3.4
|
|
||||||
Classifier: Programming Language :: Python :: 3.5
|
|
||||||
Requires-Dist: six
|
|
||||||
|
|
||||||
UNKNOWN
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
__pycache__/cycler.cpython-36.pyc,,
|
|
||||||
cycler-0.10.0.dist-info/DESCRIPTION.rst,sha256=OCTuuN6LcWulhHS3d5rfjdsQtW22n7HENFRh6jC6ego,10
|
|
||||||
cycler-0.10.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
cycler-0.10.0.dist-info/METADATA,sha256=aWX1pyo7D2hSDNZ2Q6Zl7DxhUQdpyu1O5uNABnvz000,722
|
|
||||||
cycler-0.10.0.dist-info/RECORD,,
|
|
||||||
cycler-0.10.0.dist-info/WHEEL,sha256=o2k-Qa-RMNIJmUdIc7KU6VWR_ErNRbWNlxDIpl7lm34,110
|
|
||||||
cycler-0.10.0.dist-info/metadata.json,sha256=CCBpg-KQU-VRL1unJcHPWKQeQbB84G0j7-BeCj7YUbU,875
|
|
||||||
cycler-0.10.0.dist-info/top_level.txt,sha256=D8BVVDdAAelLb2FOEz7lDpc6-AL21ylKPrMhtG6yzyE,7
|
|
||||||
cycler.py,sha256=ed3G39unvVEBrBZVDwnE0FFroRNsOLkbJ_TwIT5CjCU,15959
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: bdist_wheel (0.29.0)
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py2-none-any
|
|
||||||
Tag: py3-none-any
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"classifiers": ["Development Status :: 4 - Beta", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5"], "extensions": {"python.details": {"contacts": [{"email": "matplotlib-users@python.org", "name": "Thomas A Caswell", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/matplotlib/cycler"}}}, "extras": [], "generator": "bdist_wheel (0.29.0)", "keywords": ["cycle", "kwargs"], "license": "BSD", "metadata_version": "2.0", "name": "cycler", "platform": "Cross platform (Linux", "run_requires": [{"requires": ["six"]}], "summary": "Composable style cycles", "version": "0.10.0"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
cycler
|
|
||||||
@@ -1,558 +0,0 @@
|
|||||||
"""
|
|
||||||
Cycler
|
|
||||||
======
|
|
||||||
|
|
||||||
Cycling through combinations of values, producing dictionaries.
|
|
||||||
|
|
||||||
You can add cyclers::
|
|
||||||
|
|
||||||
from cycler import cycler
|
|
||||||
cc = (cycler(color=list('rgb')) +
|
|
||||||
cycler(linestyle=['-', '--', '-.']))
|
|
||||||
for d in cc:
|
|
||||||
print(d)
|
|
||||||
|
|
||||||
Results in::
|
|
||||||
|
|
||||||
{'color': 'r', 'linestyle': '-'}
|
|
||||||
{'color': 'g', 'linestyle': '--'}
|
|
||||||
{'color': 'b', 'linestyle': '-.'}
|
|
||||||
|
|
||||||
|
|
||||||
You can multiply cyclers::
|
|
||||||
|
|
||||||
from cycler import cycler
|
|
||||||
cc = (cycler(color=list('rgb')) *
|
|
||||||
cycler(linestyle=['-', '--', '-.']))
|
|
||||||
for d in cc:
|
|
||||||
print(d)
|
|
||||||
|
|
||||||
Results in::
|
|
||||||
|
|
||||||
{'color': 'r', 'linestyle': '-'}
|
|
||||||
{'color': 'r', 'linestyle': '--'}
|
|
||||||
{'color': 'r', 'linestyle': '-.'}
|
|
||||||
{'color': 'g', 'linestyle': '-'}
|
|
||||||
{'color': 'g', 'linestyle': '--'}
|
|
||||||
{'color': 'g', 'linestyle': '-.'}
|
|
||||||
{'color': 'b', 'linestyle': '-'}
|
|
||||||
{'color': 'b', 'linestyle': '--'}
|
|
||||||
{'color': 'b', 'linestyle': '-.'}
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import (absolute_import, division, print_function,
|
|
||||||
unicode_literals)
|
|
||||||
|
|
||||||
import six
|
|
||||||
from itertools import product, cycle
|
|
||||||
from six.moves import zip, reduce
|
|
||||||
from operator import mul, add
|
|
||||||
import copy
|
|
||||||
|
|
||||||
__version__ = '0.10.0'
|
|
||||||
|
|
||||||
|
|
||||||
def _process_keys(left, right):
|
|
||||||
"""
|
|
||||||
Helper function to compose cycler keys
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
left, right : iterable of dictionaries or None
|
|
||||||
The cyclers to be composed
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
keys : set
|
|
||||||
The keys in the composition of the two cyclers
|
|
||||||
"""
|
|
||||||
l_peek = next(iter(left)) if left is not None else {}
|
|
||||||
r_peek = next(iter(right)) if right is not None else {}
|
|
||||||
l_key = set(l_peek.keys())
|
|
||||||
r_key = set(r_peek.keys())
|
|
||||||
if l_key & r_key:
|
|
||||||
raise ValueError("Can not compose overlapping cycles")
|
|
||||||
return l_key | r_key
|
|
||||||
|
|
||||||
|
|
||||||
class Cycler(object):
|
|
||||||
"""
|
|
||||||
Composable cycles
|
|
||||||
|
|
||||||
This class has compositions methods:
|
|
||||||
|
|
||||||
``+``
|
|
||||||
for 'inner' products (zip)
|
|
||||||
|
|
||||||
``+=``
|
|
||||||
in-place ``+``
|
|
||||||
|
|
||||||
``*``
|
|
||||||
for outer products (itertools.product) and integer multiplication
|
|
||||||
|
|
||||||
``*=``
|
|
||||||
in-place ``*``
|
|
||||||
|
|
||||||
and supports basic slicing via ``[]``
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
left : Cycler or None
|
|
||||||
The 'left' cycler
|
|
||||||
|
|
||||||
right : Cycler or None
|
|
||||||
The 'right' cycler
|
|
||||||
|
|
||||||
op : func or None
|
|
||||||
Function which composes the 'left' and 'right' cyclers.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __call__(self):
|
|
||||||
return cycle(self)
|
|
||||||
|
|
||||||
def __init__(self, left, right=None, op=None):
|
|
||||||
"""Semi-private init
|
|
||||||
|
|
||||||
Do not use this directly, use `cycler` function instead.
|
|
||||||
"""
|
|
||||||
if isinstance(left, Cycler):
|
|
||||||
self._left = Cycler(left._left, left._right, left._op)
|
|
||||||
elif left is not None:
|
|
||||||
# Need to copy the dictionary or else that will be a residual
|
|
||||||
# mutable that could lead to strange errors
|
|
||||||
self._left = [copy.copy(v) for v in left]
|
|
||||||
else:
|
|
||||||
self._left = None
|
|
||||||
|
|
||||||
if isinstance(right, Cycler):
|
|
||||||
self._right = Cycler(right._left, right._right, right._op)
|
|
||||||
elif right is not None:
|
|
||||||
# Need to copy the dictionary or else that will be a residual
|
|
||||||
# mutable that could lead to strange errors
|
|
||||||
self._right = [copy.copy(v) for v in right]
|
|
||||||
else:
|
|
||||||
self._right = None
|
|
||||||
|
|
||||||
self._keys = _process_keys(self._left, self._right)
|
|
||||||
self._op = op
|
|
||||||
|
|
||||||
@property
|
|
||||||
def keys(self):
|
|
||||||
"""
|
|
||||||
The keys this Cycler knows about
|
|
||||||
"""
|
|
||||||
return set(self._keys)
|
|
||||||
|
|
||||||
def change_key(self, old, new):
|
|
||||||
"""
|
|
||||||
Change a key in this cycler to a new name.
|
|
||||||
Modification is performed in-place.
|
|
||||||
|
|
||||||
Does nothing if the old key is the same as the new key.
|
|
||||||
Raises a ValueError if the new key is already a key.
|
|
||||||
Raises a KeyError if the old key isn't a key.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if old == new:
|
|
||||||
return
|
|
||||||
if new in self._keys:
|
|
||||||
raise ValueError("Can't replace %s with %s, %s is already a key" %
|
|
||||||
(old, new, new))
|
|
||||||
if old not in self._keys:
|
|
||||||
raise KeyError("Can't replace %s with %s, %s is not a key" %
|
|
||||||
(old, new, old))
|
|
||||||
|
|
||||||
self._keys.remove(old)
|
|
||||||
self._keys.add(new)
|
|
||||||
|
|
||||||
if self._right is not None and old in self._right.keys:
|
|
||||||
self._right.change_key(old, new)
|
|
||||||
|
|
||||||
# self._left should always be non-None
|
|
||||||
# if self._keys is non-empty.
|
|
||||||
elif isinstance(self._left, Cycler):
|
|
||||||
self._left.change_key(old, new)
|
|
||||||
else:
|
|
||||||
# It should be completely safe at this point to
|
|
||||||
# assume that the old key can be found in each
|
|
||||||
# iteration.
|
|
||||||
self._left = [{new: entry[old]} for entry in self._left]
|
|
||||||
|
|
||||||
def _compose(self):
|
|
||||||
"""
|
|
||||||
Compose the 'left' and 'right' components of this cycle
|
|
||||||
with the proper operation (zip or product as of now)
|
|
||||||
"""
|
|
||||||
for a, b in self._op(self._left, self._right):
|
|
||||||
out = dict()
|
|
||||||
out.update(a)
|
|
||||||
out.update(b)
|
|
||||||
yield out
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _from_iter(cls, label, itr):
|
|
||||||
"""
|
|
||||||
Class method to create 'base' Cycler objects
|
|
||||||
that do not have a 'right' or 'op' and for which
|
|
||||||
the 'left' object is not another Cycler.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
label : str
|
|
||||||
The property key.
|
|
||||||
|
|
||||||
itr : iterable
|
|
||||||
Finite length iterable of the property values.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
cycler : Cycler
|
|
||||||
New 'base' `Cycler`
|
|
||||||
"""
|
|
||||||
ret = cls(None)
|
|
||||||
ret._left = list({label: v} for v in itr)
|
|
||||||
ret._keys = set([label])
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
# TODO : maybe add numpy style fancy slicing
|
|
||||||
if isinstance(key, slice):
|
|
||||||
trans = self.by_key()
|
|
||||||
return reduce(add, (_cycler(k, v[key])
|
|
||||||
for k, v in six.iteritems(trans)))
|
|
||||||
else:
|
|
||||||
raise ValueError("Can only use slices with Cycler.__getitem__")
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
if self._right is None:
|
|
||||||
return iter(dict(l) for l in self._left)
|
|
||||||
|
|
||||||
return self._compose()
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
"""
|
|
||||||
Pair-wise combine two equal length cycles (zip)
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
other : Cycler
|
|
||||||
The second Cycler
|
|
||||||
"""
|
|
||||||
if len(self) != len(other):
|
|
||||||
raise ValueError("Can only add equal length cycles, "
|
|
||||||
"not {0} and {1}".format(len(self), len(other)))
|
|
||||||
return Cycler(self, other, zip)
|
|
||||||
|
|
||||||
def __mul__(self, other):
|
|
||||||
"""
|
|
||||||
Outer product of two cycles (`itertools.product`) or integer
|
|
||||||
multiplication.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
other : Cycler or int
|
|
||||||
The second Cycler or integer
|
|
||||||
"""
|
|
||||||
if isinstance(other, Cycler):
|
|
||||||
return Cycler(self, other, product)
|
|
||||||
elif isinstance(other, int):
|
|
||||||
trans = self.by_key()
|
|
||||||
return reduce(add, (_cycler(k, v*other)
|
|
||||||
for k, v in six.iteritems(trans)))
|
|
||||||
else:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
def __rmul__(self, other):
|
|
||||||
return self * other
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
op_dict = {zip: min, product: mul}
|
|
||||||
if self._right is None:
|
|
||||||
return len(self._left)
|
|
||||||
l_len = len(self._left)
|
|
||||||
r_len = len(self._right)
|
|
||||||
return op_dict[self._op](l_len, r_len)
|
|
||||||
|
|
||||||
def __iadd__(self, other):
|
|
||||||
"""
|
|
||||||
In-place pair-wise combine two equal length cycles (zip)
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
other : Cycler
|
|
||||||
The second Cycler
|
|
||||||
"""
|
|
||||||
if not isinstance(other, Cycler):
|
|
||||||
raise TypeError("Cannot += with a non-Cycler object")
|
|
||||||
# True shallow copy of self is fine since this is in-place
|
|
||||||
old_self = copy.copy(self)
|
|
||||||
self._keys = _process_keys(old_self, other)
|
|
||||||
self._left = old_self
|
|
||||||
self._op = zip
|
|
||||||
self._right = Cycler(other._left, other._right, other._op)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __imul__(self, other):
|
|
||||||
"""
|
|
||||||
In-place outer product of two cycles (`itertools.product`)
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
other : Cycler
|
|
||||||
The second Cycler
|
|
||||||
"""
|
|
||||||
if not isinstance(other, Cycler):
|
|
||||||
raise TypeError("Cannot *= with a non-Cycler object")
|
|
||||||
# True shallow copy of self is fine since this is in-place
|
|
||||||
old_self = copy.copy(self)
|
|
||||||
self._keys = _process_keys(old_self, other)
|
|
||||||
self._left = old_self
|
|
||||||
self._op = product
|
|
||||||
self._right = Cycler(other._left, other._right, other._op)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
"""
|
|
||||||
Check equality
|
|
||||||
"""
|
|
||||||
if len(self) != len(other):
|
|
||||||
return False
|
|
||||||
if self.keys ^ other.keys:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return all(a == b for a, b in zip(self, other))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
op_map = {zip: '+', product: '*'}
|
|
||||||
if self._right is None:
|
|
||||||
lab = self.keys.pop()
|
|
||||||
itr = list(v[lab] for v in self)
|
|
||||||
return "cycler({lab!r}, {itr!r})".format(lab=lab, itr=itr)
|
|
||||||
else:
|
|
||||||
op = op_map.get(self._op, '?')
|
|
||||||
msg = "({left!r} {op} {right!r})"
|
|
||||||
return msg.format(left=self._left, op=op, right=self._right)
|
|
||||||
|
|
||||||
def _repr_html_(self):
|
|
||||||
# an table showing the value of each key through a full cycle
|
|
||||||
output = "<table>"
|
|
||||||
sorted_keys = sorted(self.keys, key=repr)
|
|
||||||
for key in sorted_keys:
|
|
||||||
output += "<th>{key!r}</th>".format(key=key)
|
|
||||||
for d in iter(self):
|
|
||||||
output += "<tr>"
|
|
||||||
for k in sorted_keys:
|
|
||||||
output += "<td>{val!r}</td>".format(val=d[k])
|
|
||||||
output += "</tr>"
|
|
||||||
output += "</table>"
|
|
||||||
return output
|
|
||||||
|
|
||||||
def by_key(self):
|
|
||||||
"""Values by key
|
|
||||||
|
|
||||||
This returns the transposed values of the cycler. Iterating
|
|
||||||
over a `Cycler` yields dicts with a single value for each key,
|
|
||||||
this method returns a `dict` of `list` which are the values
|
|
||||||
for the given key.
|
|
||||||
|
|
||||||
The returned value can be used to create an equivalent `Cycler`
|
|
||||||
using only `+`.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
transpose : dict
|
|
||||||
dict of lists of the values for each key.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# TODO : sort out if this is a bottle neck, if there is a better way
|
|
||||||
# and if we care.
|
|
||||||
|
|
||||||
keys = self.keys
|
|
||||||
# change this to dict comprehension when drop 2.6
|
|
||||||
out = dict((k, list()) for k in keys)
|
|
||||||
|
|
||||||
for d in self:
|
|
||||||
for k in keys:
|
|
||||||
out[k].append(d[k])
|
|
||||||
return out
|
|
||||||
|
|
||||||
# for back compatibility
|
|
||||||
_transpose = by_key
|
|
||||||
|
|
||||||
def simplify(self):
|
|
||||||
"""Simplify the Cycler
|
|
||||||
|
|
||||||
Returned as a composition using only sums (no multiplications)
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
simple : Cycler
|
|
||||||
An equivalent cycler using only summation"""
|
|
||||||
# TODO: sort out if it is worth the effort to make sure this is
|
|
||||||
# balanced. Currently it is is
|
|
||||||
# (((a + b) + c) + d) vs
|
|
||||||
# ((a + b) + (c + d))
|
|
||||||
# I would believe that there is some performance implications
|
|
||||||
|
|
||||||
trans = self.by_key()
|
|
||||||
return reduce(add, (_cycler(k, v) for k, v in six.iteritems(trans)))
|
|
||||||
|
|
||||||
def concat(self, other):
|
|
||||||
"""Concatenate this cycler and an other.
|
|
||||||
|
|
||||||
The keys must match exactly.
|
|
||||||
|
|
||||||
This returns a single Cycler which is equivalent to
|
|
||||||
`itertools.chain(self, other)`
|
|
||||||
|
|
||||||
Examples
|
|
||||||
--------
|
|
||||||
|
|
||||||
>>> num = cycler('a', range(3))
|
|
||||||
>>> let = cycler('a', 'abc')
|
|
||||||
>>> num.concat(let)
|
|
||||||
cycler('a', [0, 1, 2, 'a', 'b', 'c'])
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
other : `Cycler`
|
|
||||||
The `Cycler` to concatenate to this one.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
ret : `Cycler`
|
|
||||||
The concatenated `Cycler`
|
|
||||||
"""
|
|
||||||
return concat(self, other)
|
|
||||||
|
|
||||||
|
|
||||||
def concat(left, right):
|
|
||||||
"""Concatenate two cyclers.
|
|
||||||
|
|
||||||
The keys must match exactly.
|
|
||||||
|
|
||||||
This returns a single Cycler which is equivalent to
|
|
||||||
`itertools.chain(left, right)`
|
|
||||||
|
|
||||||
Examples
|
|
||||||
--------
|
|
||||||
|
|
||||||
>>> num = cycler('a', range(3))
|
|
||||||
>>> let = cycler('a', 'abc')
|
|
||||||
>>> num.concat(let)
|
|
||||||
cycler('a', [0, 1, 2, 'a', 'b', 'c'])
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
left, right : `Cycler`
|
|
||||||
The two `Cycler` instances to concatenate
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
ret : `Cycler`
|
|
||||||
The concatenated `Cycler`
|
|
||||||
"""
|
|
||||||
if left.keys != right.keys:
|
|
||||||
msg = '\n\t'.join(["Keys do not match:",
|
|
||||||
"Intersection: {both!r}",
|
|
||||||
"Disjoint: {just_one!r}"]).format(
|
|
||||||
both=left.keys & right.keys,
|
|
||||||
just_one=left.keys ^ right.keys)
|
|
||||||
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
_l = left.by_key()
|
|
||||||
_r = right.by_key()
|
|
||||||
return reduce(add, (_cycler(k, _l[k] + _r[k]) for k in left.keys))
|
|
||||||
|
|
||||||
|
|
||||||
def cycler(*args, **kwargs):
|
|
||||||
"""
|
|
||||||
Create a new `Cycler` object from a single positional argument,
|
|
||||||
a pair of positional arguments, or the combination of keyword arguments.
|
|
||||||
|
|
||||||
cycler(arg)
|
|
||||||
cycler(label1=itr1[, label2=iter2[, ...]])
|
|
||||||
cycler(label, itr)
|
|
||||||
|
|
||||||
Form 1 simply copies a given `Cycler` object.
|
|
||||||
|
|
||||||
Form 2 composes a `Cycler` as an inner product of the
|
|
||||||
pairs of keyword arguments. In other words, all of the
|
|
||||||
iterables are cycled simultaneously, as if through zip().
|
|
||||||
|
|
||||||
Form 3 creates a `Cycler` from a label and an iterable.
|
|
||||||
This is useful for when the label cannot be a keyword argument
|
|
||||||
(e.g., an integer or a name that has a space in it).
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
arg : Cycler
|
|
||||||
Copy constructor for Cycler (does a shallow copy of iterables).
|
|
||||||
|
|
||||||
label : name
|
|
||||||
The property key. In the 2-arg form of the function,
|
|
||||||
the label can be any hashable object. In the keyword argument
|
|
||||||
form of the function, it must be a valid python identifier.
|
|
||||||
|
|
||||||
itr : iterable
|
|
||||||
Finite length iterable of the property values.
|
|
||||||
Can be a single-property `Cycler` that would
|
|
||||||
be like a key change, but as a shallow copy.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
cycler : Cycler
|
|
||||||
New `Cycler` for the given property
|
|
||||||
|
|
||||||
"""
|
|
||||||
if args and kwargs:
|
|
||||||
raise TypeError("cyl() can only accept positional OR keyword "
|
|
||||||
"arguments -- not both.")
|
|
||||||
|
|
||||||
if len(args) == 1:
|
|
||||||
if not isinstance(args[0], Cycler):
|
|
||||||
raise TypeError("If only one positional argument given, it must "
|
|
||||||
" be a Cycler instance.")
|
|
||||||
return Cycler(args[0])
|
|
||||||
elif len(args) == 2:
|
|
||||||
return _cycler(*args)
|
|
||||||
elif len(args) > 2:
|
|
||||||
raise TypeError("Only a single Cycler can be accepted as the lone "
|
|
||||||
"positional argument. Use keyword arguments instead.")
|
|
||||||
|
|
||||||
if kwargs:
|
|
||||||
return reduce(add, (_cycler(k, v) for k, v in six.iteritems(kwargs)))
|
|
||||||
|
|
||||||
raise TypeError("Must have at least a positional OR keyword arguments")
|
|
||||||
|
|
||||||
|
|
||||||
def _cycler(label, itr):
|
|
||||||
"""
|
|
||||||
Create a new `Cycler` object from a property name and
|
|
||||||
iterable of values.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
label : hashable
|
|
||||||
The property key.
|
|
||||||
|
|
||||||
itr : iterable
|
|
||||||
Finite length iterable of the property values.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
cycler : Cycler
|
|
||||||
New `Cycler` for the given property
|
|
||||||
"""
|
|
||||||
if isinstance(itr, Cycler):
|
|
||||||
keys = itr.keys
|
|
||||||
if len(keys) != 1:
|
|
||||||
msg = "Can not create Cycler from a multi-property Cycler"
|
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
lab = keys.pop()
|
|
||||||
# Doesn't need to be a new list because
|
|
||||||
# _from_iter() will be creating that new list anyway.
|
|
||||||
itr = (v[lab] for v in itr)
|
|
||||||
|
|
||||||
return Cycler._from_iter(label, itr)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
try:
|
|
||||||
from ._version import version as __version__
|
|
||||||
except ImportError:
|
|
||||||
__version__ = 'unknown'
|
|
||||||
|
|
||||||
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
|
||||||
'utils', 'zoneinfo']
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
"""
|
|
||||||
Common code used in multiple modules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class weekday(object):
|
|
||||||
__slots__ = ["weekday", "n"]
|
|
||||||
|
|
||||||
def __init__(self, weekday, n=None):
|
|
||||||
self.weekday = weekday
|
|
||||||
self.n = n
|
|
||||||
|
|
||||||
def __call__(self, n):
|
|
||||||
if n == self.n:
|
|
||||||
return self
|
|
||||||
else:
|
|
||||||
return self.__class__(self.weekday, n)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
try:
|
|
||||||
if self.weekday != other.weekday or self.n != other.n:
|
|
||||||
return False
|
|
||||||
except AttributeError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((
|
|
||||||
self.weekday,
|
|
||||||
self.n,
|
|
||||||
))
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not (self == other)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
|
||||||
if not self.n:
|
|
||||||
return s
|
|
||||||
else:
|
|
||||||
return "%s(%+d)" % (s, self.n)
|
|
||||||
|
|
||||||
# vim:ts=4:sw=4:et
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# coding: utf-8
|
|
||||||
# file generated by setuptools_scm
|
|
||||||
# don't change, don't track in version control
|
|
||||||
version = '2.7.5'
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
This module offers a generic easter computing method for any given year, using
|
|
||||||
Western, Orthodox or Julian algorithms.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
|
||||||
|
|
||||||
EASTER_JULIAN = 1
|
|
||||||
EASTER_ORTHODOX = 2
|
|
||||||
EASTER_WESTERN = 3
|
|
||||||
|
|
||||||
|
|
||||||
def easter(year, method=EASTER_WESTERN):
|
|
||||||
"""
|
|
||||||
This method was ported from the work done by GM Arts,
|
|
||||||
on top of the algorithm by Claus Tondering, which was
|
|
||||||
based in part on the algorithm of Ouding (1940), as
|
|
||||||
quoted in "Explanatory Supplement to the Astronomical
|
|
||||||
Almanac", P. Kenneth Seidelmann, editor.
|
|
||||||
|
|
||||||
This algorithm implements three different easter
|
|
||||||
calculation methods:
|
|
||||||
|
|
||||||
1 - Original calculation in Julian calendar, valid in
|
|
||||||
dates after 326 AD
|
|
||||||
2 - Original method, with date converted to Gregorian
|
|
||||||
calendar, valid in years 1583 to 4099
|
|
||||||
3 - Revised method, in Gregorian calendar, valid in
|
|
||||||
years 1583 to 4099 as well
|
|
||||||
|
|
||||||
These methods are represented by the constants:
|
|
||||||
|
|
||||||
* ``EASTER_JULIAN = 1``
|
|
||||||
* ``EASTER_ORTHODOX = 2``
|
|
||||||
* ``EASTER_WESTERN = 3``
|
|
||||||
|
|
||||||
The default method is method 3.
|
|
||||||
|
|
||||||
More about the algorithm may be found at:
|
|
||||||
|
|
||||||
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
|
||||||
|
|
||||||
and
|
|
||||||
|
|
||||||
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not (1 <= method <= 3):
|
|
||||||
raise ValueError("invalid method")
|
|
||||||
|
|
||||||
# g - Golden year - 1
|
|
||||||
# c - Century
|
|
||||||
# h - (23 - Epact) mod 30
|
|
||||||
# i - Number of days from March 21 to Paschal Full Moon
|
|
||||||
# j - Weekday for PFM (0=Sunday, etc)
|
|
||||||
# p - Number of days from March 21 to Sunday on or before PFM
|
|
||||||
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
|
||||||
# e - Extra days to add for method 2 (converting Julian
|
|
||||||
# date to Gregorian date)
|
|
||||||
|
|
||||||
y = year
|
|
||||||
g = y % 19
|
|
||||||
e = 0
|
|
||||||
if method < 3:
|
|
||||||
# Old method
|
|
||||||
i = (19*g + 15) % 30
|
|
||||||
j = (y + y//4 + i) % 7
|
|
||||||
if method == 2:
|
|
||||||
# Extra dates to convert Julian to Gregorian date
|
|
||||||
e = 10
|
|
||||||
if y > 1600:
|
|
||||||
e = e + y//100 - 16 - (y//100 - 16)//4
|
|
||||||
else:
|
|
||||||
# New method
|
|
||||||
c = y//100
|
|
||||||
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
|
||||||
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
|
||||||
j = (y + y//4 + i + 2 - c + c//4) % 7
|
|
||||||
|
|
||||||
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
|
||||||
# (later dates apply to method 2, although 23 May never actually occurs)
|
|
||||||
p = i - j + e
|
|
||||||
d = 1 + (p + 27 + (p + 6)//40) % 31
|
|
||||||
m = 3 + (p + 26)//30
|
|
||||||
return datetime.date(int(y), int(m), int(d))
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from ._parser import parse, parser, parserinfo
|
|
||||||
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
|
||||||
from ._parser import UnknownTimezoneWarning
|
|
||||||
|
|
||||||
from ._parser import __doc__
|
|
||||||
|
|
||||||
from .isoparser import isoparser, isoparse
|
|
||||||
|
|
||||||
__all__ = ['parse', 'parser', 'parserinfo',
|
|
||||||
'isoparse', 'isoparser',
|
|
||||||
'UnknownTimezoneWarning']
|
|
||||||
|
|
||||||
|
|
||||||
###
|
|
||||||
# Deprecate portions of the private interface so that downstream code that
|
|
||||||
# is improperly relying on it is given *some* notice.
|
|
||||||
|
|
||||||
|
|
||||||
def __deprecated_private_func(f):
|
|
||||||
from functools import wraps
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
msg = ('{name} is a private function and may break without warning, '
|
|
||||||
'it will be moved and or renamed in future versions.')
|
|
||||||
msg = msg.format(name=f.__name__)
|
|
||||||
|
|
||||||
@wraps(f)
|
|
||||||
def deprecated_func(*args, **kwargs):
|
|
||||||
warnings.warn(msg, DeprecationWarning)
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return deprecated_func
|
|
||||||
|
|
||||||
def __deprecate_private_class(c):
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
msg = ('{name} is a private class and may break without warning, '
|
|
||||||
'it will be moved and or renamed in future versions.')
|
|
||||||
msg = msg.format(name=c.__name__)
|
|
||||||
|
|
||||||
class private_class(c):
|
|
||||||
__doc__ = c.__doc__
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
warnings.warn(msg, DeprecationWarning)
|
|
||||||
super(private_class, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
private_class.__name__ = c.__name__
|
|
||||||
|
|
||||||
return private_class
|
|
||||||
|
|
||||||
|
|
||||||
from ._parser import _timelex, _resultbase
|
|
||||||
from ._parser import _tzparser, _parsetz
|
|
||||||
|
|
||||||
_timelex = __deprecate_private_class(_timelex)
|
|
||||||
_tzparser = __deprecate_private_class(_tzparser)
|
|
||||||
_resultbase = __deprecate_private_class(_resultbase)
|
|
||||||
_parsetz = __deprecated_private_func(_parsetz)
|
|
||||||
@@ -1,406 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
This module offers a parser for ISO-8601 strings
|
|
||||||
|
|
||||||
It is intended to support all valid date, time and datetime formats per the
|
|
||||||
ISO-8601 specification.
|
|
||||||
|
|
||||||
..versionadded:: 2.7.0
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta, time, date
|
|
||||||
import calendar
|
|
||||||
from dateutil import tz
|
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
import re
|
|
||||||
import six
|
|
||||||
|
|
||||||
__all__ = ["isoparse", "isoparser"]
|
|
||||||
|
|
||||||
|
|
||||||
def _takes_ascii(f):
|
|
||||||
@wraps(f)
|
|
||||||
def func(self, str_in, *args, **kwargs):
|
|
||||||
# If it's a stream, read the whole thing
|
|
||||||
str_in = getattr(str_in, 'read', lambda: str_in)()
|
|
||||||
|
|
||||||
# If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
|
|
||||||
if isinstance(str_in, six.text_type):
|
|
||||||
# ASCII is the same in UTF-8
|
|
||||||
try:
|
|
||||||
str_in = str_in.encode('ascii')
|
|
||||||
except UnicodeEncodeError as e:
|
|
||||||
msg = 'ISO-8601 strings should contain only ASCII characters'
|
|
||||||
six.raise_from(ValueError(msg), e)
|
|
||||||
|
|
||||||
return f(self, str_in, *args, **kwargs)
|
|
||||||
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
class isoparser(object):
|
|
||||||
def __init__(self, sep=None):
|
|
||||||
"""
|
|
||||||
:param sep:
|
|
||||||
A single character that separates date and time portions. If
|
|
||||||
``None``, the parser will accept any single character.
|
|
||||||
For strict ISO-8601 adherence, pass ``'T'``.
|
|
||||||
"""
|
|
||||||
if sep is not None:
|
|
||||||
if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
|
|
||||||
raise ValueError('Separator must be a single, non-numeric ' +
|
|
||||||
'ASCII character')
|
|
||||||
|
|
||||||
sep = sep.encode('ascii')
|
|
||||||
|
|
||||||
self._sep = sep
|
|
||||||
|
|
||||||
@_takes_ascii
|
|
||||||
def isoparse(self, dt_str):
|
|
||||||
"""
|
|
||||||
Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
|
|
||||||
|
|
||||||
An ISO-8601 datetime string consists of a date portion, followed
|
|
||||||
optionally by a time portion - the date and time portions are separated
|
|
||||||
by a single character separator, which is ``T`` in the official
|
|
||||||
standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
|
|
||||||
combined with a time portion.
|
|
||||||
|
|
||||||
Supported date formats are:
|
|
||||||
|
|
||||||
Common:
|
|
||||||
|
|
||||||
- ``YYYY``
|
|
||||||
- ``YYYY-MM`` or ``YYYYMM``
|
|
||||||
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
|
||||||
|
|
||||||
Uncommon:
|
|
||||||
|
|
||||||
- ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
|
|
||||||
- ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
|
|
||||||
|
|
||||||
The ISO week and day numbering follows the same logic as
|
|
||||||
:func:`datetime.date.isocalendar`.
|
|
||||||
|
|
||||||
Supported time formats are:
|
|
||||||
|
|
||||||
- ``hh``
|
|
||||||
- ``hh:mm`` or ``hhmm``
|
|
||||||
- ``hh:mm:ss`` or ``hhmmss``
|
|
||||||
- ``hh:mm:ss.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits)
|
|
||||||
|
|
||||||
Midnight is a special case for `hh`, as the standard supports both
|
|
||||||
00:00 and 24:00 as a representation.
|
|
||||||
|
|
||||||
.. caution::
|
|
||||||
|
|
||||||
Support for fractional components other than seconds is part of the
|
|
||||||
ISO-8601 standard, but is not currently implemented in this parser.
|
|
||||||
|
|
||||||
Supported time zone offset formats are:
|
|
||||||
|
|
||||||
- `Z` (UTC)
|
|
||||||
- `±HH:MM`
|
|
||||||
- `±HHMM`
|
|
||||||
- `±HH`
|
|
||||||
|
|
||||||
Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
|
|
||||||
with the exception of UTC, which will be represented as
|
|
||||||
:class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
|
|
||||||
as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
|
|
||||||
|
|
||||||
:param dt_str:
|
|
||||||
A string or stream containing only an ISO-8601 datetime string
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns a :class:`datetime.datetime` representing the string.
|
|
||||||
Unspecified components default to their lowest value.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
As of version 2.7.0, the strictness of the parser should not be
|
|
||||||
considered a stable part of the contract. Any valid ISO-8601 string
|
|
||||||
that parses correctly with the default settings will continue to
|
|
||||||
parse correctly in future versions, but invalid strings that
|
|
||||||
currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
|
|
||||||
guaranteed to continue failing in future versions if they encode
|
|
||||||
a valid date.
|
|
||||||
|
|
||||||
.. versionadded:: 2.7.0
|
|
||||||
"""
|
|
||||||
components, pos = self._parse_isodate(dt_str)
|
|
||||||
|
|
||||||
if len(dt_str) > pos:
|
|
||||||
if self._sep is None or dt_str[pos:pos + 1] == self._sep:
|
|
||||||
components += self._parse_isotime(dt_str[pos + 1:])
|
|
||||||
else:
|
|
||||||
raise ValueError('String contains unknown ISO components')
|
|
||||||
|
|
||||||
return datetime(*components)
|
|
||||||
|
|
||||||
@_takes_ascii
|
|
||||||
def parse_isodate(self, datestr):
|
|
||||||
"""
|
|
||||||
Parse the date portion of an ISO string.
|
|
||||||
|
|
||||||
:param datestr:
|
|
||||||
The string portion of an ISO string, without a separator
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns a :class:`datetime.date` object
|
|
||||||
"""
|
|
||||||
components, pos = self._parse_isodate(datestr)
|
|
||||||
if pos < len(datestr):
|
|
||||||
raise ValueError('String contains unknown ISO ' +
|
|
||||||
'components: {}'.format(datestr))
|
|
||||||
return date(*components)
|
|
||||||
|
|
||||||
@_takes_ascii
|
|
||||||
def parse_isotime(self, timestr):
|
|
||||||
"""
|
|
||||||
Parse the time portion of an ISO string.
|
|
||||||
|
|
||||||
:param timestr:
|
|
||||||
The time portion of an ISO string, without a separator
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns a :class:`datetime.time` object
|
|
||||||
"""
|
|
||||||
return time(*self._parse_isotime(timestr))
|
|
||||||
|
|
||||||
@_takes_ascii
|
|
||||||
def parse_tzstr(self, tzstr, zero_as_utc=True):
|
|
||||||
"""
|
|
||||||
Parse a valid ISO time zone string.
|
|
||||||
|
|
||||||
See :func:`isoparser.isoparse` for details on supported formats.
|
|
||||||
|
|
||||||
:param tzstr:
|
|
||||||
A string representing an ISO time zone offset
|
|
||||||
|
|
||||||
:param zero_as_utc:
|
|
||||||
Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns :class:`dateutil.tz.tzoffset` for offsets and
|
|
||||||
:class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
|
|
||||||
specified) offsets equivalent to UTC.
|
|
||||||
"""
|
|
||||||
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
|
||||||
|
|
||||||
# Constants
|
|
||||||
_MICROSECOND_END_REGEX = re.compile(b'[-+Z]+')
|
|
||||||
_DATE_SEP = b'-'
|
|
||||||
_TIME_SEP = b':'
|
|
||||||
_MICRO_SEP = b'.'
|
|
||||||
|
|
||||||
def _parse_isodate(self, dt_str):
|
|
||||||
try:
|
|
||||||
return self._parse_isodate_common(dt_str)
|
|
||||||
except ValueError:
|
|
||||||
return self._parse_isodate_uncommon(dt_str)
|
|
||||||
|
|
||||||
def _parse_isodate_common(self, dt_str):
|
|
||||||
len_str = len(dt_str)
|
|
||||||
components = [1, 1, 1]
|
|
||||||
|
|
||||||
if len_str < 4:
|
|
||||||
raise ValueError('ISO string too short')
|
|
||||||
|
|
||||||
# Year
|
|
||||||
components[0] = int(dt_str[0:4])
|
|
||||||
pos = 4
|
|
||||||
if pos >= len_str:
|
|
||||||
return components, pos
|
|
||||||
|
|
||||||
has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
|
|
||||||
if has_sep:
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
# Month
|
|
||||||
if len_str - pos < 2:
|
|
||||||
raise ValueError('Invalid common month')
|
|
||||||
|
|
||||||
components[1] = int(dt_str[pos:pos + 2])
|
|
||||||
pos += 2
|
|
||||||
|
|
||||||
if pos >= len_str:
|
|
||||||
if has_sep:
|
|
||||||
return components, pos
|
|
||||||
else:
|
|
||||||
raise ValueError('Invalid ISO format')
|
|
||||||
|
|
||||||
if has_sep:
|
|
||||||
if dt_str[pos:pos + 1] != self._DATE_SEP:
|
|
||||||
raise ValueError('Invalid separator in ISO string')
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
# Day
|
|
||||||
if len_str - pos < 2:
|
|
||||||
raise ValueError('Invalid common day')
|
|
||||||
components[2] = int(dt_str[pos:pos + 2])
|
|
||||||
return components, pos + 2
|
|
||||||
|
|
||||||
def _parse_isodate_uncommon(self, dt_str):
|
|
||||||
if len(dt_str) < 4:
|
|
||||||
raise ValueError('ISO string too short')
|
|
||||||
|
|
||||||
# All ISO formats start with the year
|
|
||||||
year = int(dt_str[0:4])
|
|
||||||
|
|
||||||
has_sep = dt_str[4:5] == self._DATE_SEP
|
|
||||||
|
|
||||||
pos = 4 + has_sep # Skip '-' if it's there
|
|
||||||
if dt_str[pos:pos + 1] == b'W':
|
|
||||||
# YYYY-?Www-?D?
|
|
||||||
pos += 1
|
|
||||||
weekno = int(dt_str[pos:pos + 2])
|
|
||||||
pos += 2
|
|
||||||
|
|
||||||
dayno = 1
|
|
||||||
if len(dt_str) > pos:
|
|
||||||
if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
|
|
||||||
raise ValueError('Inconsistent use of dash separator')
|
|
||||||
|
|
||||||
pos += has_sep
|
|
||||||
|
|
||||||
dayno = int(dt_str[pos:pos + 1])
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
base_date = self._calculate_weekdate(year, weekno, dayno)
|
|
||||||
else:
|
|
||||||
# YYYYDDD or YYYY-DDD
|
|
||||||
if len(dt_str) - pos < 3:
|
|
||||||
raise ValueError('Invalid ordinal day')
|
|
||||||
|
|
||||||
ordinal_day = int(dt_str[pos:pos + 3])
|
|
||||||
pos += 3
|
|
||||||
|
|
||||||
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
|
||||||
raise ValueError('Invalid ordinal day' +
|
|
||||||
' {} for year {}'.format(ordinal_day, year))
|
|
||||||
|
|
||||||
base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
|
|
||||||
|
|
||||||
components = [base_date.year, base_date.month, base_date.day]
|
|
||||||
return components, pos
|
|
||||||
|
|
||||||
def _calculate_weekdate(self, year, week, day):
|
|
||||||
"""
|
|
||||||
Calculate the day of corresponding to the ISO year-week-day calendar.
|
|
||||||
|
|
||||||
This function is effectively the inverse of
|
|
||||||
:func:`datetime.date.isocalendar`.
|
|
||||||
|
|
||||||
:param year:
|
|
||||||
The year in the ISO calendar
|
|
||||||
|
|
||||||
:param week:
|
|
||||||
The week in the ISO calendar - range is [1, 53]
|
|
||||||
|
|
||||||
:param day:
|
|
||||||
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns a :class:`datetime.date`
|
|
||||||
"""
|
|
||||||
if not 0 < week < 54:
|
|
||||||
raise ValueError('Invalid week: {}'.format(week))
|
|
||||||
|
|
||||||
if not 0 < day < 8: # Range is 1-7
|
|
||||||
raise ValueError('Invalid weekday: {}'.format(day))
|
|
||||||
|
|
||||||
# Get week 1 for the specific year:
|
|
||||||
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
|
|
||||||
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
|
|
||||||
|
|
||||||
# Now add the specific number of weeks and days to get what we want
|
|
||||||
week_offset = (week - 1) * 7 + (day - 1)
|
|
||||||
return week_1 + timedelta(days=week_offset)
|
|
||||||
|
|
||||||
def _parse_isotime(self, timestr):
|
|
||||||
len_str = len(timestr)
|
|
||||||
components = [0, 0, 0, 0, None]
|
|
||||||
pos = 0
|
|
||||||
comp = -1
|
|
||||||
|
|
||||||
if len(timestr) < 2:
|
|
||||||
raise ValueError('ISO time too short')
|
|
||||||
|
|
||||||
has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP
|
|
||||||
|
|
||||||
while pos < len_str and comp < 5:
|
|
||||||
comp += 1
|
|
||||||
|
|
||||||
if timestr[pos:pos + 1] in b'-+Z':
|
|
||||||
# Detect time zone boundary
|
|
||||||
components[-1] = self._parse_tzstr(timestr[pos:])
|
|
||||||
pos = len_str
|
|
||||||
break
|
|
||||||
|
|
||||||
if comp < 3:
|
|
||||||
# Hour, minute, second
|
|
||||||
components[comp] = int(timestr[pos:pos + 2])
|
|
||||||
pos += 2
|
|
||||||
if (has_sep and pos < len_str and
|
|
||||||
timestr[pos:pos + 1] == self._TIME_SEP):
|
|
||||||
pos += 1
|
|
||||||
|
|
||||||
if comp == 3:
|
|
||||||
# Microsecond
|
|
||||||
if timestr[pos:pos + 1] != self._MICRO_SEP:
|
|
||||||
continue
|
|
||||||
|
|
||||||
pos += 1
|
|
||||||
us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6],
|
|
||||||
1)[0]
|
|
||||||
|
|
||||||
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
|
||||||
pos += len(us_str)
|
|
||||||
|
|
||||||
if pos < len_str:
|
|
||||||
raise ValueError('Unused components in ISO string')
|
|
||||||
|
|
||||||
if components[0] == 24:
|
|
||||||
# Standard supports 00:00 and 24:00 as representations of midnight
|
|
||||||
if any(component != 0 for component in components[1:4]):
|
|
||||||
raise ValueError('Hour may only be 24 at 24:00:00.000')
|
|
||||||
components[0] = 0
|
|
||||||
|
|
||||||
return components
|
|
||||||
|
|
||||||
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
|
||||||
if tzstr == b'Z':
|
|
||||||
return tz.tzutc()
|
|
||||||
|
|
||||||
if len(tzstr) not in {3, 5, 6}:
|
|
||||||
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
|
|
||||||
|
|
||||||
if tzstr[0:1] == b'-':
|
|
||||||
mult = -1
|
|
||||||
elif tzstr[0:1] == b'+':
|
|
||||||
mult = 1
|
|
||||||
else:
|
|
||||||
raise ValueError('Time zone offset requires sign')
|
|
||||||
|
|
||||||
hours = int(tzstr[1:3])
|
|
||||||
if len(tzstr) == 3:
|
|
||||||
minutes = 0
|
|
||||||
else:
|
|
||||||
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
|
|
||||||
|
|
||||||
if zero_as_utc and hours == 0 and minutes == 0:
|
|
||||||
return tz.tzutc()
|
|
||||||
else:
|
|
||||||
if minutes > 59:
|
|
||||||
raise ValueError('Invalid minutes in time zone offset')
|
|
||||||
|
|
||||||
if hours > 23:
|
|
||||||
raise ValueError('Invalid hours in time zone offset')
|
|
||||||
|
|
||||||
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_ISOPARSER = isoparser()
|
|
||||||
isoparse = DEFAULT_ISOPARSER.isoparse
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import datetime
|
|
||||||
import calendar
|
|
||||||
|
|
||||||
import operator
|
|
||||||
from math import copysign
|
|
||||||
|
|
||||||
from six import integer_types
|
|
||||||
from warnings import warn
|
|
||||||
|
|
||||||
from ._common import weekday
|
|
||||||
|
|
||||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
|
||||||
|
|
||||||
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
|
||||||
|
|
||||||
|
|
||||||
class relativedelta(object):
|
|
||||||
"""
|
|
||||||
The relativedelta type is based on the specification of the excellent
|
|
||||||
work done by M.-A. Lemburg in his
|
|
||||||
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
|
|
||||||
However, notice that this type does *NOT* implement the same algorithm as
|
|
||||||
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
|
||||||
|
|
||||||
There are two different ways to build a relativedelta instance. The
|
|
||||||
first one is passing it two date/datetime classes::
|
|
||||||
|
|
||||||
relativedelta(datetime1, datetime2)
|
|
||||||
|
|
||||||
The second one is passing it any number of the following keyword arguments::
|
|
||||||
|
|
||||||
relativedelta(arg1=x,arg2=y,arg3=z...)
|
|
||||||
|
|
||||||
year, month, day, hour, minute, second, microsecond:
|
|
||||||
Absolute information (argument is singular); adding or subtracting a
|
|
||||||
relativedelta with absolute information does not perform an arithmetic
|
|
||||||
operation, but rather REPLACES the corresponding value in the
|
|
||||||
original datetime with the value(s) in relativedelta.
|
|
||||||
|
|
||||||
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
|
||||||
Relative information, may be negative (argument is plural); adding
|
|
||||||
or subtracting a relativedelta with relative information performs
|
|
||||||
the corresponding aritmetic operation on the original datetime value
|
|
||||||
with the information in the relativedelta.
|
|
||||||
|
|
||||||
weekday:
|
|
||||||
One of the weekday instances (MO, TU, etc). These
|
|
||||||
instances may receive a parameter N, specifying the Nth
|
|
||||||
weekday, which could be positive or negative (like MO(+1)
|
|
||||||
or MO(-2). Not specifying it is the same as specifying
|
|
||||||
+1. You can also use an integer, where 0=MO. Notice that
|
|
||||||
if the calculated date is already Monday, for example,
|
|
||||||
using MO(1) or MO(-1) won't change the day.
|
|
||||||
|
|
||||||
leapdays:
|
|
||||||
Will add given days to the date found, if year is a leap
|
|
||||||
year, and the date found is post 28 of february.
|
|
||||||
|
|
||||||
yearday, nlyearday:
|
|
||||||
Set the yearday or the non-leap year day (jump leap days).
|
|
||||||
These are converted to day/month/leapdays information.
|
|
||||||
|
|
||||||
There are relative and absolute forms of the keyword
|
|
||||||
arguments. The plural is relative, and the singular is
|
|
||||||
absolute. For each argument in the order below, the absolute form
|
|
||||||
is applied first (by setting each attribute to that value) and
|
|
||||||
then the relative form (by adding the value to the attribute).
|
|
||||||
|
|
||||||
The order of attributes considered when this relativedelta is
|
|
||||||
added to a datetime is:
|
|
||||||
|
|
||||||
1. Year
|
|
||||||
2. Month
|
|
||||||
3. Day
|
|
||||||
4. Hours
|
|
||||||
5. Minutes
|
|
||||||
6. Seconds
|
|
||||||
7. Microseconds
|
|
||||||
|
|
||||||
Finally, weekday is applied, using the rule described above.
|
|
||||||
|
|
||||||
For example
|
|
||||||
|
|
||||||
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
|
||||||
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
|
||||||
datetime(2018, 4, 2, 14, 37, 0)
|
|
||||||
|
|
||||||
First, the day is set to 1 (the first of the month), then 25 hours
|
|
||||||
are added, to get to the 2nd day and 14th hour, finally the
|
|
||||||
weekday is applied, but since the 2nd is already a Monday there is
|
|
||||||
no effect.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, dt1=None, dt2=None,
|
|
||||||
years=0, months=0, days=0, leapdays=0, weeks=0,
|
|
||||||
hours=0, minutes=0, seconds=0, microseconds=0,
|
|
||||||
year=None, month=None, day=None, weekday=None,
|
|
||||||
yearday=None, nlyearday=None,
|
|
||||||
hour=None, minute=None, second=None, microsecond=None):
|
|
||||||
|
|
||||||
if dt1 and dt2:
|
|
||||||
# datetime is a subclass of date. So both must be date
|
|
||||||
if not (isinstance(dt1, datetime.date) and
|
|
||||||
isinstance(dt2, datetime.date)):
|
|
||||||
raise TypeError("relativedelta only diffs datetime/date")
|
|
||||||
|
|
||||||
# We allow two dates, or two datetimes, so we coerce them to be
|
|
||||||
# of the same type
|
|
||||||
if (isinstance(dt1, datetime.datetime) !=
|
|
||||||
isinstance(dt2, datetime.datetime)):
|
|
||||||
if not isinstance(dt1, datetime.datetime):
|
|
||||||
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
|
||||||
elif not isinstance(dt2, datetime.datetime):
|
|
||||||
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
|
||||||
|
|
||||||
self.years = 0
|
|
||||||
self.months = 0
|
|
||||||
self.days = 0
|
|
||||||
self.leapdays = 0
|
|
||||||
self.hours = 0
|
|
||||||
self.minutes = 0
|
|
||||||
self.seconds = 0
|
|
||||||
self.microseconds = 0
|
|
||||||
self.year = None
|
|
||||||
self.month = None
|
|
||||||
self.day = None
|
|
||||||
self.weekday = None
|
|
||||||
self.hour = None
|
|
||||||
self.minute = None
|
|
||||||
self.second = None
|
|
||||||
self.microsecond = None
|
|
||||||
self._has_time = 0
|
|
||||||
|
|
||||||
# Get year / month delta between the two
|
|
||||||
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
|
||||||
self._set_months(months)
|
|
||||||
|
|
||||||
# Remove the year/month delta so the timedelta is just well-defined
|
|
||||||
# time units (seconds, days and microseconds)
|
|
||||||
dtm = self.__radd__(dt2)
|
|
||||||
|
|
||||||
# If we've overshot our target, make an adjustment
|
|
||||||
if dt1 < dt2:
|
|
||||||
compare = operator.gt
|
|
||||||
increment = 1
|
|
||||||
else:
|
|
||||||
compare = operator.lt
|
|
||||||
increment = -1
|
|
||||||
|
|
||||||
while compare(dt1, dtm):
|
|
||||||
months += increment
|
|
||||||
self._set_months(months)
|
|
||||||
dtm = self.__radd__(dt2)
|
|
||||||
|
|
||||||
# Get the timedelta between the "months-adjusted" date and dt1
|
|
||||||
delta = dt1 - dtm
|
|
||||||
self.seconds = delta.seconds + delta.days * 86400
|
|
||||||
self.microseconds = delta.microseconds
|
|
||||||
else:
|
|
||||||
# Check for non-integer values in integer-only quantities
|
|
||||||
if any(x is not None and x != int(x) for x in (years, months)):
|
|
||||||
raise ValueError("Non-integer years and months are "
|
|
||||||
"ambiguous and not currently supported.")
|
|
||||||
|
|
||||||
# Relative information
|
|
||||||
self.years = int(years)
|
|
||||||
self.months = int(months)
|
|
||||||
self.days = days + weeks * 7
|
|
||||||
self.leapdays = leapdays
|
|
||||||
self.hours = hours
|
|
||||||
self.minutes = minutes
|
|
||||||
self.seconds = seconds
|
|
||||||
self.microseconds = microseconds
|
|
||||||
|
|
||||||
# Absolute information
|
|
||||||
self.year = year
|
|
||||||
self.month = month
|
|
||||||
self.day = day
|
|
||||||
self.hour = hour
|
|
||||||
self.minute = minute
|
|
||||||
self.second = second
|
|
||||||
self.microsecond = microsecond
|
|
||||||
|
|
||||||
if any(x is not None and int(x) != x
|
|
||||||
for x in (year, month, day, hour,
|
|
||||||
minute, second, microsecond)):
|
|
||||||
# For now we'll deprecate floats - later it'll be an error.
|
|
||||||
warn("Non-integer value passed as absolute information. " +
|
|
||||||
"This is not a well-defined condition and will raise " +
|
|
||||||
"errors in future versions.", DeprecationWarning)
|
|
||||||
|
|
||||||
if isinstance(weekday, integer_types):
|
|
||||||
self.weekday = weekdays[weekday]
|
|
||||||
else:
|
|
||||||
self.weekday = weekday
|
|
||||||
|
|
||||||
yday = 0
|
|
||||||
if nlyearday:
|
|
||||||
yday = nlyearday
|
|
||||||
elif yearday:
|
|
||||||
yday = yearday
|
|
||||||
if yearday > 59:
|
|
||||||
self.leapdays = -1
|
|
||||||
if yday:
|
|
||||||
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
|
||||||
243, 273, 304, 334, 366]
|
|
||||||
for idx, ydays in enumerate(ydayidx):
|
|
||||||
if yday <= ydays:
|
|
||||||
self.month = idx+1
|
|
||||||
if idx == 0:
|
|
||||||
self.day = yday
|
|
||||||
else:
|
|
||||||
self.day = yday-ydayidx[idx-1]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError("invalid year day (%d)" % yday)
|
|
||||||
|
|
||||||
self._fix()
|
|
||||||
|
|
||||||
def _fix(self):
|
|
||||||
if abs(self.microseconds) > 999999:
|
|
||||||
s = _sign(self.microseconds)
|
|
||||||
div, mod = divmod(self.microseconds * s, 1000000)
|
|
||||||
self.microseconds = mod * s
|
|
||||||
self.seconds += div * s
|
|
||||||
if abs(self.seconds) > 59:
|
|
||||||
s = _sign(self.seconds)
|
|
||||||
div, mod = divmod(self.seconds * s, 60)
|
|
||||||
self.seconds = mod * s
|
|
||||||
self.minutes += div * s
|
|
||||||
if abs(self.minutes) > 59:
|
|
||||||
s = _sign(self.minutes)
|
|
||||||
div, mod = divmod(self.minutes * s, 60)
|
|
||||||
self.minutes = mod * s
|
|
||||||
self.hours += div * s
|
|
||||||
if abs(self.hours) > 23:
|
|
||||||
s = _sign(self.hours)
|
|
||||||
div, mod = divmod(self.hours * s, 24)
|
|
||||||
self.hours = mod * s
|
|
||||||
self.days += div * s
|
|
||||||
if abs(self.months) > 11:
|
|
||||||
s = _sign(self.months)
|
|
||||||
div, mod = divmod(self.months * s, 12)
|
|
||||||
self.months = mod * s
|
|
||||||
self.years += div * s
|
|
||||||
if (self.hours or self.minutes or self.seconds or self.microseconds
|
|
||||||
or self.hour is not None or self.minute is not None or
|
|
||||||
self.second is not None or self.microsecond is not None):
|
|
||||||
self._has_time = 1
|
|
||||||
else:
|
|
||||||
self._has_time = 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def weeks(self):
|
|
||||||
return int(self.days / 7.0)
|
|
||||||
|
|
||||||
@weeks.setter
|
|
||||||
def weeks(self, value):
|
|
||||||
self.days = self.days - (self.weeks * 7) + value * 7
|
|
||||||
|
|
||||||
def _set_months(self, months):
|
|
||||||
self.months = months
|
|
||||||
if abs(self.months) > 11:
|
|
||||||
s = _sign(self.months)
|
|
||||||
div, mod = divmod(self.months * s, 12)
|
|
||||||
self.months = mod * s
|
|
||||||
self.years = div * s
|
|
||||||
else:
|
|
||||||
self.years = 0
|
|
||||||
|
|
||||||
def normalized(self):
|
|
||||||
"""
|
|
||||||
Return a version of this object represented entirely using integer
|
|
||||||
values for the relative attributes.
|
|
||||||
|
|
||||||
>>> relativedelta(days=1.5, hours=2).normalized()
|
|
||||||
relativedelta(days=1, hours=14)
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
|
||||||
"""
|
|
||||||
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
|
||||||
days = int(self.days)
|
|
||||||
|
|
||||||
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
|
||||||
hours = int(hours_f)
|
|
||||||
|
|
||||||
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
|
||||||
minutes = int(minutes_f)
|
|
||||||
|
|
||||||
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
|
||||||
seconds = int(seconds_f)
|
|
||||||
|
|
||||||
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
|
||||||
|
|
||||||
# Constructor carries overflow back up with call to _fix()
|
|
||||||
return self.__class__(years=self.years, months=self.months,
|
|
||||||
days=days, hours=hours, minutes=minutes,
|
|
||||||
seconds=seconds, microseconds=microseconds,
|
|
||||||
leapdays=self.leapdays, year=self.year,
|
|
||||||
month=self.month, day=self.day,
|
|
||||||
weekday=self.weekday, hour=self.hour,
|
|
||||||
minute=self.minute, second=self.second,
|
|
||||||
microsecond=self.microsecond)
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
if isinstance(other, relativedelta):
|
|
||||||
return self.__class__(years=other.years + self.years,
|
|
||||||
months=other.months + self.months,
|
|
||||||
days=other.days + self.days,
|
|
||||||
hours=other.hours + self.hours,
|
|
||||||
minutes=other.minutes + self.minutes,
|
|
||||||
seconds=other.seconds + self.seconds,
|
|
||||||
microseconds=(other.microseconds +
|
|
||||||
self.microseconds),
|
|
||||||
leapdays=other.leapdays or self.leapdays,
|
|
||||||
year=(other.year if other.year is not None
|
|
||||||
else self.year),
|
|
||||||
month=(other.month if other.month is not None
|
|
||||||
else self.month),
|
|
||||||
day=(other.day if other.day is not None
|
|
||||||
else self.day),
|
|
||||||
weekday=(other.weekday if other.weekday is not None
|
|
||||||
else self.weekday),
|
|
||||||
hour=(other.hour if other.hour is not None
|
|
||||||
else self.hour),
|
|
||||||
minute=(other.minute if other.minute is not None
|
|
||||||
else self.minute),
|
|
||||||
second=(other.second if other.second is not None
|
|
||||||
else self.second),
|
|
||||||
microsecond=(other.microsecond if other.microsecond
|
|
||||||
is not None else
|
|
||||||
self.microsecond))
|
|
||||||
if isinstance(other, datetime.timedelta):
|
|
||||||
return self.__class__(years=self.years,
|
|
||||||
months=self.months,
|
|
||||||
days=self.days + other.days,
|
|
||||||
hours=self.hours,
|
|
||||||
minutes=self.minutes,
|
|
||||||
seconds=self.seconds + other.seconds,
|
|
||||||
microseconds=self.microseconds + other.microseconds,
|
|
||||||
leapdays=self.leapdays,
|
|
||||||
year=self.year,
|
|
||||||
month=self.month,
|
|
||||||
day=self.day,
|
|
||||||
weekday=self.weekday,
|
|
||||||
hour=self.hour,
|
|
||||||
minute=self.minute,
|
|
||||||
second=self.second,
|
|
||||||
microsecond=self.microsecond)
|
|
||||||
if not isinstance(other, datetime.date):
|
|
||||||
return NotImplemented
|
|
||||||
elif self._has_time and not isinstance(other, datetime.datetime):
|
|
||||||
other = datetime.datetime.fromordinal(other.toordinal())
|
|
||||||
year = (self.year or other.year)+self.years
|
|
||||||
month = self.month or other.month
|
|
||||||
if self.months:
|
|
||||||
assert 1 <= abs(self.months) <= 12
|
|
||||||
month += self.months
|
|
||||||
if month > 12:
|
|
||||||
year += 1
|
|
||||||
month -= 12
|
|
||||||
elif month < 1:
|
|
||||||
year -= 1
|
|
||||||
month += 12
|
|
||||||
day = min(calendar.monthrange(year, month)[1],
|
|
||||||
self.day or other.day)
|
|
||||||
repl = {"year": year, "month": month, "day": day}
|
|
||||||
for attr in ["hour", "minute", "second", "microsecond"]:
|
|
||||||
value = getattr(self, attr)
|
|
||||||
if value is not None:
|
|
||||||
repl[attr] = value
|
|
||||||
days = self.days
|
|
||||||
if self.leapdays and month > 2 and calendar.isleap(year):
|
|
||||||
days += self.leapdays
|
|
||||||
ret = (other.replace(**repl)
|
|
||||||
+ datetime.timedelta(days=days,
|
|
||||||
hours=self.hours,
|
|
||||||
minutes=self.minutes,
|
|
||||||
seconds=self.seconds,
|
|
||||||
microseconds=self.microseconds))
|
|
||||||
if self.weekday:
|
|
||||||
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
|
||||||
jumpdays = (abs(nth) - 1) * 7
|
|
||||||
if nth > 0:
|
|
||||||
jumpdays += (7 - ret.weekday() + weekday) % 7
|
|
||||||
else:
|
|
||||||
jumpdays += (ret.weekday() - weekday) % 7
|
|
||||||
jumpdays *= -1
|
|
||||||
ret += datetime.timedelta(days=jumpdays)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def __radd__(self, other):
|
|
||||||
return self.__add__(other)
|
|
||||||
|
|
||||||
def __rsub__(self, other):
|
|
||||||
return self.__neg__().__radd__(other)
|
|
||||||
|
|
||||||
def __sub__(self, other):
|
|
||||||
if not isinstance(other, relativedelta):
|
|
||||||
return NotImplemented # In case the other object defines __rsub__
|
|
||||||
return self.__class__(years=self.years - other.years,
|
|
||||||
months=self.months - other.months,
|
|
||||||
days=self.days - other.days,
|
|
||||||
hours=self.hours - other.hours,
|
|
||||||
minutes=self.minutes - other.minutes,
|
|
||||||
seconds=self.seconds - other.seconds,
|
|
||||||
microseconds=self.microseconds - other.microseconds,
|
|
||||||
leapdays=self.leapdays or other.leapdays,
|
|
||||||
year=(self.year if self.year is not None
|
|
||||||
else other.year),
|
|
||||||
month=(self.month if self.month is not None else
|
|
||||||
other.month),
|
|
||||||
day=(self.day if self.day is not None else
|
|
||||||
other.day),
|
|
||||||
weekday=(self.weekday if self.weekday is not None else
|
|
||||||
other.weekday),
|
|
||||||
hour=(self.hour if self.hour is not None else
|
|
||||||
other.hour),
|
|
||||||
minute=(self.minute if self.minute is not None else
|
|
||||||
other.minute),
|
|
||||||
second=(self.second if self.second is not None else
|
|
||||||
other.second),
|
|
||||||
microsecond=(self.microsecond if self.microsecond
|
|
||||||
is not None else
|
|
||||||
other.microsecond))
|
|
||||||
|
|
||||||
def __abs__(self):
|
|
||||||
return self.__class__(years=abs(self.years),
|
|
||||||
months=abs(self.months),
|
|
||||||
days=abs(self.days),
|
|
||||||
hours=abs(self.hours),
|
|
||||||
minutes=abs(self.minutes),
|
|
||||||
seconds=abs(self.seconds),
|
|
||||||
microseconds=abs(self.microseconds),
|
|
||||||
leapdays=self.leapdays,
|
|
||||||
year=self.year,
|
|
||||||
month=self.month,
|
|
||||||
day=self.day,
|
|
||||||
weekday=self.weekday,
|
|
||||||
hour=self.hour,
|
|
||||||
minute=self.minute,
|
|
||||||
second=self.second,
|
|
||||||
microsecond=self.microsecond)
|
|
||||||
|
|
||||||
def __neg__(self):
|
|
||||||
return self.__class__(years=-self.years,
|
|
||||||
months=-self.months,
|
|
||||||
days=-self.days,
|
|
||||||
hours=-self.hours,
|
|
||||||
minutes=-self.minutes,
|
|
||||||
seconds=-self.seconds,
|
|
||||||
microseconds=-self.microseconds,
|
|
||||||
leapdays=self.leapdays,
|
|
||||||
year=self.year,
|
|
||||||
month=self.month,
|
|
||||||
day=self.day,
|
|
||||||
weekday=self.weekday,
|
|
||||||
hour=self.hour,
|
|
||||||
minute=self.minute,
|
|
||||||
second=self.second,
|
|
||||||
microsecond=self.microsecond)
|
|
||||||
|
|
||||||
def __bool__(self):
|
|
||||||
return not (not self.years and
|
|
||||||
not self.months and
|
|
||||||
not self.days and
|
|
||||||
not self.hours and
|
|
||||||
not self.minutes and
|
|
||||||
not self.seconds and
|
|
||||||
not self.microseconds and
|
|
||||||
not self.leapdays and
|
|
||||||
self.year is None and
|
|
||||||
self.month is None and
|
|
||||||
self.day is None and
|
|
||||||
self.weekday is None and
|
|
||||||
self.hour is None and
|
|
||||||
self.minute is None and
|
|
||||||
self.second is None and
|
|
||||||
self.microsecond is None)
|
|
||||||
# Compatibility with Python 2.x
|
|
||||||
__nonzero__ = __bool__
|
|
||||||
|
|
||||||
def __mul__(self, other):
|
|
||||||
try:
|
|
||||||
f = float(other)
|
|
||||||
except TypeError:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self.__class__(years=int(self.years * f),
|
|
||||||
months=int(self.months * f),
|
|
||||||
days=int(self.days * f),
|
|
||||||
hours=int(self.hours * f),
|
|
||||||
minutes=int(self.minutes * f),
|
|
||||||
seconds=int(self.seconds * f),
|
|
||||||
microseconds=int(self.microseconds * f),
|
|
||||||
leapdays=self.leapdays,
|
|
||||||
year=self.year,
|
|
||||||
month=self.month,
|
|
||||||
day=self.day,
|
|
||||||
weekday=self.weekday,
|
|
||||||
hour=self.hour,
|
|
||||||
minute=self.minute,
|
|
||||||
second=self.second,
|
|
||||||
microsecond=self.microsecond)
|
|
||||||
|
|
||||||
__rmul__ = __mul__
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, relativedelta):
|
|
||||||
return NotImplemented
|
|
||||||
if self.weekday or other.weekday:
|
|
||||||
if not self.weekday or not other.weekday:
|
|
||||||
return False
|
|
||||||
if self.weekday.weekday != other.weekday.weekday:
|
|
||||||
return False
|
|
||||||
n1, n2 = self.weekday.n, other.weekday.n
|
|
||||||
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
|
||||||
return False
|
|
||||||
return (self.years == other.years and
|
|
||||||
self.months == other.months and
|
|
||||||
self.days == other.days and
|
|
||||||
self.hours == other.hours and
|
|
||||||
self.minutes == other.minutes and
|
|
||||||
self.seconds == other.seconds and
|
|
||||||
self.microseconds == other.microseconds and
|
|
||||||
self.leapdays == other.leapdays and
|
|
||||||
self.year == other.year and
|
|
||||||
self.month == other.month and
|
|
||||||
self.day == other.day and
|
|
||||||
self.hour == other.hour and
|
|
||||||
self.minute == other.minute and
|
|
||||||
self.second == other.second and
|
|
||||||
self.microsecond == other.microsecond)
|
|
||||||
|
|
||||||
def __hash__(self):
|
|
||||||
return hash((
|
|
||||||
self.weekday,
|
|
||||||
self.years,
|
|
||||||
self.months,
|
|
||||||
self.days,
|
|
||||||
self.hours,
|
|
||||||
self.minutes,
|
|
||||||
self.seconds,
|
|
||||||
self.microseconds,
|
|
||||||
self.leapdays,
|
|
||||||
self.year,
|
|
||||||
self.month,
|
|
||||||
self.day,
|
|
||||||
self.hour,
|
|
||||||
self.minute,
|
|
||||||
self.second,
|
|
||||||
self.microsecond,
|
|
||||||
))
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
def __div__(self, other):
|
|
||||||
try:
|
|
||||||
reciprocal = 1 / float(other)
|
|
||||||
except TypeError:
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return self.__mul__(reciprocal)
|
|
||||||
|
|
||||||
__truediv__ = __div__
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
l = []
|
|
||||||
for attr in ["years", "months", "days", "leapdays",
|
|
||||||
"hours", "minutes", "seconds", "microseconds"]:
|
|
||||||
value = getattr(self, attr)
|
|
||||||
if value:
|
|
||||||
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
|
||||||
for attr in ["year", "month", "day", "weekday",
|
|
||||||
"hour", "minute", "second", "microsecond"]:
|
|
||||||
value = getattr(self, attr)
|
|
||||||
if value is not None:
|
|
||||||
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
|
||||||
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
|
||||||
attrs=", ".join(l))
|
|
||||||
|
|
||||||
|
|
||||||
def _sign(x):
|
|
||||||
return int(copysign(1, x))
|
|
||||||
|
|
||||||
# vim:ts=4:sw=4:et
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from .tz import *
|
|
||||||
from .tz import __doc__
|
|
||||||
|
|
||||||
#: Convenience constant providing a :class:`tzutc()` instance
|
|
||||||
#:
|
|
||||||
#: .. versionadded:: 2.7.0
|
|
||||||
UTC = tzutc()
|
|
||||||
|
|
||||||
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
|
||||||
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
|
||||||
"enfold", "datetime_ambiguous", "datetime_exists",
|
|
||||||
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
|
|
||||||
|
|
||||||
|
|
||||||
class DeprecatedTzFormatWarning(Warning):
|
|
||||||
"""Warning raised when time zones are parsed from deprecated formats."""
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
from six import PY3
|
|
||||||
|
|
||||||
from functools import wraps
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta, tzinfo
|
|
||||||
|
|
||||||
|
|
||||||
ZERO = timedelta(0)
|
|
||||||
|
|
||||||
__all__ = ['tzname_in_python2', 'enfold']
|
|
||||||
|
|
||||||
|
|
||||||
def tzname_in_python2(namefunc):
|
|
||||||
"""Change unicode output into bytestrings in Python 2
|
|
||||||
|
|
||||||
tzname() API changed in Python 3. It used to return bytes, but was changed
|
|
||||||
to unicode strings
|
|
||||||
"""
|
|
||||||
def adjust_encoding(*args, **kwargs):
|
|
||||||
name = namefunc(*args, **kwargs)
|
|
||||||
if name is not None and not PY3:
|
|
||||||
name = name.encode()
|
|
||||||
|
|
||||||
return name
|
|
||||||
|
|
||||||
return adjust_encoding
|
|
||||||
|
|
||||||
|
|
||||||
# The following is adapted from Alexander Belopolsky's tz library
|
|
||||||
# https://github.com/abalkin/tz
|
|
||||||
if hasattr(datetime, 'fold'):
|
|
||||||
# This is the pre-python 3.6 fold situation
|
|
||||||
def enfold(dt, fold=1):
|
|
||||||
"""
|
|
||||||
Provides a unified interface for assigning the ``fold`` attribute to
|
|
||||||
datetimes both before and after the implementation of PEP-495.
|
|
||||||
|
|
||||||
:param fold:
|
|
||||||
The value for the ``fold`` attribute in the returned datetime. This
|
|
||||||
should be either 0 or 1.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
|
||||||
``fold`` for all versions of Python. In versions prior to
|
|
||||||
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
|
||||||
subclass of :py:class:`datetime.datetime` with the ``fold``
|
|
||||||
attribute added, if ``fold`` is 1.
|
|
||||||
|
|
||||||
.. versionadded:: 2.6.0
|
|
||||||
"""
|
|
||||||
return dt.replace(fold=fold)
|
|
||||||
|
|
||||||
else:
|
|
||||||
class _DatetimeWithFold(datetime):
|
|
||||||
"""
|
|
||||||
This is a class designed to provide a PEP 495-compliant interface for
|
|
||||||
Python versions before 3.6. It is used only for dates in a fold, so
|
|
||||||
the ``fold`` attribute is fixed at ``1``.
|
|
||||||
|
|
||||||
.. versionadded:: 2.6.0
|
|
||||||
"""
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def replace(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Return a datetime with the same attributes, except for those
|
|
||||||
attributes given new values by whichever keyword arguments are
|
|
||||||
specified. Note that tzinfo=None can be specified to create a naive
|
|
||||||
datetime from an aware datetime with no conversion of date and time
|
|
||||||
data.
|
|
||||||
|
|
||||||
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
|
|
||||||
return a ``datetime.datetime`` even if ``fold`` is unchanged.
|
|
||||||
"""
|
|
||||||
argnames = (
|
|
||||||
'year', 'month', 'day', 'hour', 'minute', 'second',
|
|
||||||
'microsecond', 'tzinfo'
|
|
||||||
)
|
|
||||||
|
|
||||||
for arg, argname in zip(args, argnames):
|
|
||||||
if argname in kwargs:
|
|
||||||
raise TypeError('Duplicate argument: {}'.format(argname))
|
|
||||||
|
|
||||||
kwargs[argname] = arg
|
|
||||||
|
|
||||||
for argname in argnames:
|
|
||||||
if argname not in kwargs:
|
|
||||||
kwargs[argname] = getattr(self, argname)
|
|
||||||
|
|
||||||
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
|
|
||||||
|
|
||||||
return dt_class(**kwargs)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fold(self):
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def enfold(dt, fold=1):
|
|
||||||
"""
|
|
||||||
Provides a unified interface for assigning the ``fold`` attribute to
|
|
||||||
datetimes both before and after the implementation of PEP-495.
|
|
||||||
|
|
||||||
:param fold:
|
|
||||||
The value for the ``fold`` attribute in the returned datetime. This
|
|
||||||
should be either 0 or 1.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
|
||||||
``fold`` for all versions of Python. In versions prior to
|
|
||||||
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
|
||||||
subclass of :py:class:`datetime.datetime` with the ``fold``
|
|
||||||
attribute added, if ``fold`` is 1.
|
|
||||||
|
|
||||||
.. versionadded:: 2.6.0
|
|
||||||
"""
|
|
||||||
if getattr(dt, 'fold', 0) == fold:
|
|
||||||
return dt
|
|
||||||
|
|
||||||
args = dt.timetuple()[:6]
|
|
||||||
args += (dt.microsecond, dt.tzinfo)
|
|
||||||
|
|
||||||
if fold:
|
|
||||||
return _DatetimeWithFold(*args)
|
|
||||||
else:
|
|
||||||
return datetime(*args)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_fromutc_inputs(f):
|
|
||||||
"""
|
|
||||||
The CPython version of ``fromutc`` checks that the input is a ``datetime``
|
|
||||||
object and that ``self`` is attached as its ``tzinfo``.
|
|
||||||
"""
|
|
||||||
@wraps(f)
|
|
||||||
def fromutc(self, dt):
|
|
||||||
if not isinstance(dt, datetime):
|
|
||||||
raise TypeError("fromutc() requires a datetime argument")
|
|
||||||
if dt.tzinfo is not self:
|
|
||||||
raise ValueError("dt.tzinfo is not self")
|
|
||||||
|
|
||||||
return f(self, dt)
|
|
||||||
|
|
||||||
return fromutc
|
|
||||||
|
|
||||||
|
|
||||||
class _tzinfo(tzinfo):
|
|
||||||
"""
|
|
||||||
Base class for all ``dateutil`` ``tzinfo`` objects.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def is_ambiguous(self, dt):
|
|
||||||
"""
|
|
||||||
Whether or not the "wall time" of a given datetime is ambiguous in this
|
|
||||||
zone.
|
|
||||||
|
|
||||||
:param dt:
|
|
||||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
|
||||||
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
|
||||||
|
|
||||||
.. versionadded:: 2.6.0
|
|
||||||
"""
|
|
||||||
|
|
||||||
dt = dt.replace(tzinfo=self)
|
|
||||||
|
|
||||||
wall_0 = enfold(dt, fold=0)
|
|
||||||
wall_1 = enfold(dt, fold=1)
|
|
||||||
|
|
||||||
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
|
||||||
same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
|
|
||||||
|
|
||||||
return same_dt and not same_offset
|
|
||||||
|
|
||||||
def _fold_status(self, dt_utc, dt_wall):
|
|
||||||
"""
|
|
||||||
Determine the fold status of a "wall" datetime, given a representation
|
|
||||||
of the same datetime as a (naive) UTC datetime. This is calculated based
|
|
||||||
on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
|
|
||||||
datetimes, and that this offset is the actual number of hours separating
|
|
||||||
``dt_utc`` and ``dt_wall``.
|
|
||||||
|
|
||||||
:param dt_utc:
|
|
||||||
Representation of the datetime as UTC
|
|
||||||
|
|
||||||
:param dt_wall:
|
|
||||||
Representation of the datetime as "wall time". This parameter must
|
|
||||||
either have a `fold` attribute or have a fold-naive
|
|
||||||
:class:`datetime.tzinfo` attached, otherwise the calculation may
|
|
||||||
fail.
|
|
||||||
"""
|
|
||||||
if self.is_ambiguous(dt_wall):
|
|
||||||
delta_wall = dt_wall - dt_utc
|
|
||||||
_fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
|
|
||||||
else:
|
|
||||||
_fold = 0
|
|
||||||
|
|
||||||
return _fold
|
|
||||||
|
|
||||||
def _fold(self, dt):
|
|
||||||
return getattr(dt, 'fold', 0)
|
|
||||||
|
|
||||||
def _fromutc(self, dt):
|
|
||||||
"""
|
|
||||||
Given a timezone-aware datetime in a given timezone, calculates a
|
|
||||||
timezone-aware datetime in a new timezone.
|
|
||||||
|
|
||||||
Since this is the one time that we *know* we have an unambiguous
|
|
||||||
datetime object, we take this opportunity to determine whether the
|
|
||||||
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
|
||||||
occurence, chronologically, of the ambiguous datetime).
|
|
||||||
|
|
||||||
:param dt:
|
|
||||||
A timezone-aware :class:`datetime.datetime` object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Re-implement the algorithm from Python's datetime.py
|
|
||||||
dtoff = dt.utcoffset()
|
|
||||||
if dtoff is None:
|
|
||||||
raise ValueError("fromutc() requires a non-None utcoffset() "
|
|
||||||
"result")
|
|
||||||
|
|
||||||
# The original datetime.py code assumes that `dst()` defaults to
|
|
||||||
# zero during ambiguous times. PEP 495 inverts this presumption, so
|
|
||||||
# for pre-PEP 495 versions of python, we need to tweak the algorithm.
|
|
||||||
dtdst = dt.dst()
|
|
||||||
if dtdst is None:
|
|
||||||
raise ValueError("fromutc() requires a non-None dst() result")
|
|
||||||
delta = dtoff - dtdst
|
|
||||||
|
|
||||||
dt += delta
|
|
||||||
# Set fold=1 so we can default to being in the fold for
|
|
||||||
# ambiguous dates.
|
|
||||||
dtdst = enfold(dt, fold=1).dst()
|
|
||||||
if dtdst is None:
|
|
||||||
raise ValueError("fromutc(): dt.dst gave inconsistent "
|
|
||||||
"results; cannot convert")
|
|
||||||
return dt + dtdst
|
|
||||||
|
|
||||||
@_validate_fromutc_inputs
|
|
||||||
def fromutc(self, dt):
|
|
||||||
"""
|
|
||||||
Given a timezone-aware datetime in a given timezone, calculates a
|
|
||||||
timezone-aware datetime in a new timezone.
|
|
||||||
|
|
||||||
Since this is the one time that we *know* we have an unambiguous
|
|
||||||
datetime object, we take this opportunity to determine whether the
|
|
||||||
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
|
||||||
occurance, chronologically, of the ambiguous datetime).
|
|
||||||
|
|
||||||
:param dt:
|
|
||||||
A timezone-aware :class:`datetime.datetime` object.
|
|
||||||
"""
|
|
||||||
dt_wall = self._fromutc(dt)
|
|
||||||
|
|
||||||
# Calculate the fold status given the two datetimes.
|
|
||||||
_fold = self._fold_status(dt, dt_wall)
|
|
||||||
|
|
||||||
# Set the default fold value for ambiguous dates
|
|
||||||
return enfold(dt_wall, fold=_fold)
|
|
||||||
|
|
||||||
|
|
||||||
class tzrangebase(_tzinfo):
|
|
||||||
"""
|
|
||||||
This is an abstract base class for time zones represented by an annual
|
|
||||||
transition into and out of DST. Child classes should implement the following
|
|
||||||
methods:
|
|
||||||
|
|
||||||
* ``__init__(self, *args, **kwargs)``
|
|
||||||
* ``transitions(self, year)`` - this is expected to return a tuple of
|
|
||||||
datetimes representing the DST on and off transitions in standard
|
|
||||||
time.
|
|
||||||
|
|
||||||
A fully initialized ``tzrangebase`` subclass should also provide the
|
|
||||||
following attributes:
|
|
||||||
* ``hasdst``: Boolean whether or not the zone uses DST.
|
|
||||||
* ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
|
|
||||||
representing the respective UTC offsets.
|
|
||||||
* ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
|
|
||||||
abbreviations in DST and STD, respectively.
|
|
||||||
* ``_hasdst``: Whether or not the zone has DST.
|
|
||||||
|
|
||||||
.. versionadded:: 2.6.0
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
raise NotImplementedError('tzrangebase is an abstract base class')
|
|
||||||
|
|
||||||
def utcoffset(self, dt):
|
|
||||||
isdst = self._isdst(dt)
|
|
||||||
|
|
||||||
if isdst is None:
|
|
||||||
return None
|
|
||||||
elif isdst:
|
|
||||||
return self._dst_offset
|
|
||||||
else:
|
|
||||||
return self._std_offset
|
|
||||||
|
|
||||||
def dst(self, dt):
|
|
||||||
isdst = self._isdst(dt)
|
|
||||||
|
|
||||||
if isdst is None:
|
|
||||||
return None
|
|
||||||
elif isdst:
|
|
||||||
return self._dst_base_offset
|
|
||||||
else:
|
|
||||||
return ZERO
|
|
||||||
|
|
||||||
@tzname_in_python2
|
|
||||||
def tzname(self, dt):
|
|
||||||
if self._isdst(dt):
|
|
||||||
return self._dst_abbr
|
|
||||||
else:
|
|
||||||
return self._std_abbr
|
|
||||||
|
|
||||||
def fromutc(self, dt):
|
|
||||||
""" Given a datetime in UTC, return local time """
|
|
||||||
if not isinstance(dt, datetime):
|
|
||||||
raise TypeError("fromutc() requires a datetime argument")
|
|
||||||
|
|
||||||
if dt.tzinfo is not self:
|
|
||||||
raise ValueError("dt.tzinfo is not self")
|
|
||||||
|
|
||||||
# Get transitions - if there are none, fixed offset
|
|
||||||
transitions = self.transitions(dt.year)
|
|
||||||
if transitions is None:
|
|
||||||
return dt + self.utcoffset(dt)
|
|
||||||
|
|
||||||
# Get the transition times in UTC
|
|
||||||
dston, dstoff = transitions
|
|
||||||
|
|
||||||
dston -= self._std_offset
|
|
||||||
dstoff -= self._std_offset
|
|
||||||
|
|
||||||
utc_transitions = (dston, dstoff)
|
|
||||||
dt_utc = dt.replace(tzinfo=None)
|
|
||||||
|
|
||||||
isdst = self._naive_isdst(dt_utc, utc_transitions)
|
|
||||||
|
|
||||||
if isdst:
|
|
||||||
dt_wall = dt + self._dst_offset
|
|
||||||
else:
|
|
||||||
dt_wall = dt + self._std_offset
|
|
||||||
|
|
||||||
_fold = int(not isdst and self.is_ambiguous(dt_wall))
|
|
||||||
|
|
||||||
return enfold(dt_wall, fold=_fold)
|
|
||||||
|
|
||||||
def is_ambiguous(self, dt):
|
|
||||||
"""
|
|
||||||
Whether or not the "wall time" of a given datetime is ambiguous in this
|
|
||||||
zone.
|
|
||||||
|
|
||||||
:param dt:
|
|
||||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
|
||||||
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
|
||||||
|
|
||||||
.. versionadded:: 2.6.0
|
|
||||||
"""
|
|
||||||
if not self.hasdst:
|
|
||||||
return False
|
|
||||||
|
|
||||||
start, end = self.transitions(dt.year)
|
|
||||||
|
|
||||||
dt = dt.replace(tzinfo=None)
|
|
||||||
return (end <= dt < end + self._dst_base_offset)
|
|
||||||
|
|
||||||
def _isdst(self, dt):
|
|
||||||
if not self.hasdst:
|
|
||||||
return False
|
|
||||||
elif dt is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
transitions = self.transitions(dt.year)
|
|
||||||
|
|
||||||
if transitions is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
dt = dt.replace(tzinfo=None)
|
|
||||||
|
|
||||||
isdst = self._naive_isdst(dt, transitions)
|
|
||||||
|
|
||||||
# Handle ambiguous dates
|
|
||||||
if not isdst and self.is_ambiguous(dt):
|
|
||||||
return not self._fold(dt)
|
|
||||||
else:
|
|
||||||
return isdst
|
|
||||||
|
|
||||||
def _naive_isdst(self, dt, transitions):
|
|
||||||
dston, dstoff = transitions
|
|
||||||
|
|
||||||
dt = dt.replace(tzinfo=None)
|
|
||||||
|
|
||||||
if dston < dstoff:
|
|
||||||
isdst = dston <= dt < dstoff
|
|
||||||
else:
|
|
||||||
isdst = not dstoff <= dt < dston
|
|
||||||
|
|
||||||
return isdst
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _dst_base_offset(self):
|
|
||||||
return self._dst_offset - self._std_offset
|
|
||||||
|
|
||||||
__hash__ = None
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not (self == other)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "%s(...)" % self.__class__.__name__
|
|
||||||
|
|
||||||
__reduce__ = object.__reduce__
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
|
|
||||||
class _TzSingleton(type):
|
|
||||||
def __init__(cls, *args, **kwargs):
|
|
||||||
cls.__instance = None
|
|
||||||
super(_TzSingleton, cls).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def __call__(cls):
|
|
||||||
if cls.__instance is None:
|
|
||||||
cls.__instance = super(_TzSingleton, cls).__call__()
|
|
||||||
return cls.__instance
|
|
||||||
|
|
||||||
class _TzFactory(type):
|
|
||||||
def instance(cls, *args, **kwargs):
|
|
||||||
"""Alternate constructor that returns a fresh instance"""
|
|
||||||
return type.__call__(cls, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class _TzOffsetFactory(_TzFactory):
|
|
||||||
def __init__(cls, *args, **kwargs):
|
|
||||||
cls.__instances = {}
|
|
||||||
|
|
||||||
def __call__(cls, name, offset):
|
|
||||||
if isinstance(offset, timedelta):
|
|
||||||
key = (name, offset.total_seconds())
|
|
||||||
else:
|
|
||||||
key = (name, offset)
|
|
||||||
|
|
||||||
instance = cls.__instances.get(key, None)
|
|
||||||
if instance is None:
|
|
||||||
instance = cls.__instances.setdefault(key,
|
|
||||||
cls.instance(name, offset))
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
class _TzStrFactory(_TzFactory):
|
|
||||||
def __init__(cls, *args, **kwargs):
|
|
||||||
cls.__instances = {}
|
|
||||||
|
|
||||||
def __call__(cls, s, posix_offset=False):
|
|
||||||
key = (s, posix_offset)
|
|
||||||
instance = cls.__instances.get(key, None)
|
|
||||||
|
|
||||||
if instance is None:
|
|
||||||
instance = cls.__instances.setdefault(key,
|
|
||||||
cls.instance(s, posix_offset))
|
|
||||||
return instance
|
|
||||||
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
# This code was originally contributed by Jeffrey Harris.
|
|
||||||
import datetime
|
|
||||||
import struct
|
|
||||||
|
|
||||||
from six.moves import winreg
|
|
||||||
from six import text_type
|
|
||||||
|
|
||||||
try:
|
|
||||||
import ctypes
|
|
||||||
from ctypes import wintypes
|
|
||||||
except ValueError:
|
|
||||||
# ValueError is raised on non-Windows systems for some horrible reason.
|
|
||||||
raise ImportError("Running tzwin on non-Windows system")
|
|
||||||
|
|
||||||
from ._common import tzrangebase
|
|
||||||
|
|
||||||
__all__ = ["tzwin", "tzwinlocal", "tzres"]
|
|
||||||
|
|
||||||
ONEWEEK = datetime.timedelta(7)
|
|
||||||
|
|
||||||
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
|
||||||
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
|
||||||
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
|
||||||
|
|
||||||
|
|
||||||
def _settzkeyname():
|
|
||||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
|
||||||
try:
|
|
||||||
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
|
||||||
TZKEYNAME = TZKEYNAMENT
|
|
||||||
except WindowsError:
|
|
||||||
TZKEYNAME = TZKEYNAME9X
|
|
||||||
handle.Close()
|
|
||||||
return TZKEYNAME
|
|
||||||
|
|
||||||
|
|
||||||
TZKEYNAME = _settzkeyname()
|
|
||||||
|
|
||||||
|
|
||||||
class tzres(object):
|
|
||||||
"""
|
|
||||||
Class for accessing `tzres.dll`, which contains timezone name related
|
|
||||||
resources.
|
|
||||||
|
|
||||||
.. versionadded:: 2.5.0
|
|
||||||
"""
|
|
||||||
p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
|
|
||||||
|
|
||||||
def __init__(self, tzres_loc='tzres.dll'):
|
|
||||||
# Load the user32 DLL so we can load strings from tzres
|
|
||||||
user32 = ctypes.WinDLL('user32')
|
|
||||||
|
|
||||||
# Specify the LoadStringW function
|
|
||||||
user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
|
|
||||||
wintypes.UINT,
|
|
||||||
wintypes.LPWSTR,
|
|
||||||
ctypes.c_int)
|
|
||||||
|
|
||||||
self.LoadStringW = user32.LoadStringW
|
|
||||||
self._tzres = ctypes.WinDLL(tzres_loc)
|
|
||||||
self.tzres_loc = tzres_loc
|
|
||||||
|
|
||||||
def load_name(self, offset):
|
|
||||||
"""
|
|
||||||
Load a timezone name from a DLL offset (integer).
|
|
||||||
|
|
||||||
>>> from dateutil.tzwin import tzres
|
|
||||||
>>> tzr = tzres()
|
|
||||||
>>> print(tzr.load_name(112))
|
|
||||||
'Eastern Standard Time'
|
|
||||||
|
|
||||||
:param offset:
|
|
||||||
A positive integer value referring to a string from the tzres dll.
|
|
||||||
|
|
||||||
..note:
|
|
||||||
Offsets found in the registry are generally of the form
|
|
||||||
`@tzres.dll,-114`. The offset in this case if 114, not -114.
|
|
||||||
|
|
||||||
"""
|
|
||||||
resource = self.p_wchar()
|
|
||||||
lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
|
|
||||||
nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
|
|
||||||
return resource[:nchar]
|
|
||||||
|
|
||||||
def name_from_string(self, tzname_str):
|
|
||||||
"""
|
|
||||||
Parse strings as returned from the Windows registry into the time zone
|
|
||||||
name as defined in the registry.
|
|
||||||
|
|
||||||
>>> from dateutil.tzwin import tzres
|
|
||||||
>>> tzr = tzres()
|
|
||||||
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
|
||||||
'Dateline Daylight Time'
|
|
||||||
>>> print(tzr.name_from_string('Eastern Standard Time'))
|
|
||||||
'Eastern Standard Time'
|
|
||||||
|
|
||||||
:param tzname_str:
|
|
||||||
A timezone name string as returned from a Windows registry key.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns the localized timezone string from tzres.dll if the string
|
|
||||||
is of the form `@tzres.dll,-offset`, else returns the input string.
|
|
||||||
"""
|
|
||||||
if not tzname_str.startswith('@'):
|
|
||||||
return tzname_str
|
|
||||||
|
|
||||||
name_splt = tzname_str.split(',-')
|
|
||||||
try:
|
|
||||||
offset = int(name_splt[1])
|
|
||||||
except:
|
|
||||||
raise ValueError("Malformed timezone string.")
|
|
||||||
|
|
||||||
return self.load_name(offset)
|
|
||||||
|
|
||||||
|
|
||||||
class tzwinbase(tzrangebase):
|
|
||||||
"""tzinfo class based on win32's timezones available in the registry."""
|
|
||||||
def __init__(self):
|
|
||||||
raise NotImplementedError('tzwinbase is an abstract base class')
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
# Compare on all relevant dimensions, including name.
|
|
||||||
if not isinstance(other, tzwinbase):
|
|
||||||
return NotImplemented
|
|
||||||
|
|
||||||
return (self._std_offset == other._std_offset and
|
|
||||||
self._dst_offset == other._dst_offset and
|
|
||||||
self._stddayofweek == other._stddayofweek and
|
|
||||||
self._dstdayofweek == other._dstdayofweek and
|
|
||||||
self._stdweeknumber == other._stdweeknumber and
|
|
||||||
self._dstweeknumber == other._dstweeknumber and
|
|
||||||
self._stdhour == other._stdhour and
|
|
||||||
self._dsthour == other._dsthour and
|
|
||||||
self._stdminute == other._stdminute and
|
|
||||||
self._dstminute == other._dstminute and
|
|
||||||
self._std_abbr == other._std_abbr and
|
|
||||||
self._dst_abbr == other._dst_abbr)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def list():
|
|
||||||
"""Return a list of all time zones known to the system."""
|
|
||||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
|
||||||
with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
|
|
||||||
result = [winreg.EnumKey(tzkey, i)
|
|
||||||
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
|
||||||
return result
|
|
||||||
|
|
||||||
def display(self):
|
|
||||||
return self._display
|
|
||||||
|
|
||||||
def transitions(self, year):
|
|
||||||
"""
|
|
||||||
For a given year, get the DST on and off transition times, expressed
|
|
||||||
always on the standard time side. For zones with no transitions, this
|
|
||||||
function returns ``None``.
|
|
||||||
|
|
||||||
:param year:
|
|
||||||
The year whose transitions you would like to query.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
|
||||||
``(dston, dstoff)`` for zones with an annual DST transition, or
|
|
||||||
``None`` for fixed offset zones.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self.hasdst:
|
|
||||||
return None
|
|
||||||
|
|
||||||
dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
|
|
||||||
self._dsthour, self._dstminute,
|
|
||||||
self._dstweeknumber)
|
|
||||||
|
|
||||||
dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
|
|
||||||
self._stdhour, self._stdminute,
|
|
||||||
self._stdweeknumber)
|
|
||||||
|
|
||||||
# Ambiguous dates default to the STD side
|
|
||||||
dstoff -= self._dst_base_offset
|
|
||||||
|
|
||||||
return dston, dstoff
|
|
||||||
|
|
||||||
def _get_hasdst(self):
|
|
||||||
return self._dstmonth != 0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _dst_base_offset(self):
|
|
||||||
return self._dst_base_offset_
|
|
||||||
|
|
||||||
|
|
||||||
class tzwin(tzwinbase):
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
self._name = name
|
|
||||||
|
|
||||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
|
||||||
tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
|
|
||||||
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
|
||||||
keydict = valuestodict(tzkey)
|
|
||||||
|
|
||||||
self._std_abbr = keydict["Std"]
|
|
||||||
self._dst_abbr = keydict["Dlt"]
|
|
||||||
|
|
||||||
self._display = keydict["Display"]
|
|
||||||
|
|
||||||
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
|
||||||
tup = struct.unpack("=3l16h", keydict["TZI"])
|
|
||||||
stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
|
||||||
dstoffset = stdoffset-tup[2] # + DaylightBias * -1
|
|
||||||
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
|
||||||
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
|
||||||
|
|
||||||
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
|
||||||
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
|
||||||
(self._stdmonth,
|
|
||||||
self._stddayofweek, # Sunday = 0
|
|
||||||
self._stdweeknumber, # Last = 5
|
|
||||||
self._stdhour,
|
|
||||||
self._stdminute) = tup[4:9]
|
|
||||||
|
|
||||||
(self._dstmonth,
|
|
||||||
self._dstdayofweek, # Sunday = 0
|
|
||||||
self._dstweeknumber, # Last = 5
|
|
||||||
self._dsthour,
|
|
||||||
self._dstminute) = tup[12:17]
|
|
||||||
|
|
||||||
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
|
||||||
self.hasdst = self._get_hasdst()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "tzwin(%s)" % repr(self._name)
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
return (self.__class__, (self._name,))
|
|
||||||
|
|
||||||
|
|
||||||
class tzwinlocal(tzwinbase):
|
|
||||||
def __init__(self):
|
|
||||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
|
||||||
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
|
||||||
keydict = valuestodict(tzlocalkey)
|
|
||||||
|
|
||||||
self._std_abbr = keydict["StandardName"]
|
|
||||||
self._dst_abbr = keydict["DaylightName"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
|
|
||||||
sn=self._std_abbr)
|
|
||||||
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
|
||||||
_keydict = valuestodict(tzkey)
|
|
||||||
self._display = _keydict["Display"]
|
|
||||||
except OSError:
|
|
||||||
self._display = None
|
|
||||||
|
|
||||||
stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
|
||||||
dstoffset = stdoffset-keydict["DaylightBias"]
|
|
||||||
|
|
||||||
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
|
||||||
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
|
||||||
|
|
||||||
# For reasons unclear, in this particular key, the day of week has been
|
|
||||||
# moved to the END of the SYSTEMTIME structure.
|
|
||||||
tup = struct.unpack("=8h", keydict["StandardStart"])
|
|
||||||
|
|
||||||
(self._stdmonth,
|
|
||||||
self._stdweeknumber, # Last = 5
|
|
||||||
self._stdhour,
|
|
||||||
self._stdminute) = tup[1:5]
|
|
||||||
|
|
||||||
self._stddayofweek = tup[7]
|
|
||||||
|
|
||||||
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
|
||||||
|
|
||||||
(self._dstmonth,
|
|
||||||
self._dstweeknumber, # Last = 5
|
|
||||||
self._dsthour,
|
|
||||||
self._dstminute) = tup[1:5]
|
|
||||||
|
|
||||||
self._dstdayofweek = tup[7]
|
|
||||||
|
|
||||||
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
|
||||||
self.hasdst = self._get_hasdst()
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "tzwinlocal()"
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
# str will return the standard name, not the daylight name.
|
|
||||||
return "tzwinlocal(%s)" % repr(self._std_abbr)
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
return (self.__class__, ())
|
|
||||||
|
|
||||||
|
|
||||||
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
|
||||||
""" dayofweek == 0 means Sunday, whichweek 5 means last instance """
|
|
||||||
first = datetime.datetime(year, month, 1, hour, minute)
|
|
||||||
|
|
||||||
# This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
|
|
||||||
# Because 7 % 7 = 0
|
|
||||||
weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
|
|
||||||
wd = weekdayone + ((whichweek - 1) * ONEWEEK)
|
|
||||||
if (wd.month != month):
|
|
||||||
wd -= ONEWEEK
|
|
||||||
|
|
||||||
return wd
|
|
||||||
|
|
||||||
|
|
||||||
def valuestodict(key):
|
|
||||||
"""Convert a registry key's values to a dictionary."""
|
|
||||||
dout = {}
|
|
||||||
size = winreg.QueryInfoKey(key)[1]
|
|
||||||
tz_res = None
|
|
||||||
|
|
||||||
for i in range(size):
|
|
||||||
key_name, value, dtype = winreg.EnumValue(key, i)
|
|
||||||
if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
|
|
||||||
# If it's a DWORD (32-bit integer), it's stored as unsigned - convert
|
|
||||||
# that to a proper signed integer
|
|
||||||
if value & (1 << 31):
|
|
||||||
value = value - (1 << 32)
|
|
||||||
elif dtype == winreg.REG_SZ:
|
|
||||||
# If it's a reference to the tzres DLL, load the actual string
|
|
||||||
if value.startswith('@tzres'):
|
|
||||||
tz_res = tz_res or tzres()
|
|
||||||
value = tz_res.name_from_string(value)
|
|
||||||
|
|
||||||
value = value.rstrip('\x00') # Remove trailing nulls
|
|
||||||
|
|
||||||
dout[key_name] = value
|
|
||||||
|
|
||||||
return dout
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# tzwin has moved to dateutil.tz.win
|
|
||||||
from .tz.win import *
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
This module offers general convenience and utility functions for dealing with
|
|
||||||
datetimes.
|
|
||||||
|
|
||||||
.. versionadded:: 2.7.0
|
|
||||||
"""
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from datetime import datetime, time
|
|
||||||
|
|
||||||
|
|
||||||
def today(tzinfo=None):
|
|
||||||
"""
|
|
||||||
Returns a :py:class:`datetime` representing the current day at midnight
|
|
||||||
|
|
||||||
:param tzinfo:
|
|
||||||
The time zone to attach (also used to determine the current day).
|
|
||||||
|
|
||||||
:return:
|
|
||||||
A :py:class:`datetime.datetime` object representing the current day
|
|
||||||
at midnight.
|
|
||||||
"""
|
|
||||||
|
|
||||||
dt = datetime.now(tzinfo)
|
|
||||||
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
|
||||||
|
|
||||||
|
|
||||||
def default_tzinfo(dt, tzinfo):
|
|
||||||
"""
|
|
||||||
Sets the the ``tzinfo`` parameter on naive datetimes only
|
|
||||||
|
|
||||||
This is useful for example when you are provided a datetime that may have
|
|
||||||
either an implicit or explicit time zone, such as when parsing a time zone
|
|
||||||
string.
|
|
||||||
|
|
||||||
.. doctest::
|
|
||||||
|
|
||||||
>>> from dateutil.tz import tzoffset
|
|
||||||
>>> from dateutil.parser import parse
|
|
||||||
>>> from dateutil.utils import default_tzinfo
|
|
||||||
>>> dflt_tz = tzoffset("EST", -18000)
|
|
||||||
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
|
||||||
2014-01-01 12:30:00+00:00
|
|
||||||
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
|
||||||
2014-01-01 12:30:00-05:00
|
|
||||||
|
|
||||||
:param dt:
|
|
||||||
The datetime on which to replace the time zone
|
|
||||||
|
|
||||||
:param tzinfo:
|
|
||||||
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
|
||||||
``dt`` if (and only if) it is naive.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns an aware :py:class:`datetime.datetime`.
|
|
||||||
"""
|
|
||||||
if dt.tzinfo is not None:
|
|
||||||
return dt
|
|
||||||
else:
|
|
||||||
return dt.replace(tzinfo=tzinfo)
|
|
||||||
|
|
||||||
|
|
||||||
def within_delta(dt1, dt2, delta):
|
|
||||||
"""
|
|
||||||
Useful for comparing two datetimes that may a negilible difference
|
|
||||||
to be considered equal.
|
|
||||||
"""
|
|
||||||
delta = abs(delta)
|
|
||||||
difference = dt1 - dt2
|
|
||||||
return -delta <= difference <= delta
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import warnings
|
|
||||||
import json
|
|
||||||
|
|
||||||
from tarfile import TarFile
|
|
||||||
from pkgutil import get_data
|
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
from dateutil.tz import tzfile as _tzfile
|
|
||||||
|
|
||||||
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
|
||||||
|
|
||||||
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
|
||||||
METADATA_FN = 'METADATA'
|
|
||||||
|
|
||||||
|
|
||||||
class tzfile(_tzfile):
|
|
||||||
def __reduce__(self):
|
|
||||||
return (gettz, (self._filename,))
|
|
||||||
|
|
||||||
|
|
||||||
def getzoneinfofile_stream():
|
|
||||||
try:
|
|
||||||
return BytesIO(get_data(__name__, ZONEFILENAME))
|
|
||||||
except IOError as e: # TODO switch to FileNotFoundError?
|
|
||||||
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class ZoneInfoFile(object):
|
|
||||||
def __init__(self, zonefile_stream=None):
|
|
||||||
if zonefile_stream is not None:
|
|
||||||
with TarFile.open(fileobj=zonefile_stream) as tf:
|
|
||||||
self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
|
|
||||||
for zf in tf.getmembers()
|
|
||||||
if zf.isfile() and zf.name != METADATA_FN}
|
|
||||||
# deal with links: They'll point to their parent object. Less
|
|
||||||
# waste of memory
|
|
||||||
links = {zl.name: self.zones[zl.linkname]
|
|
||||||
for zl in tf.getmembers() if
|
|
||||||
zl.islnk() or zl.issym()}
|
|
||||||
self.zones.update(links)
|
|
||||||
try:
|
|
||||||
metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
|
|
||||||
metadata_str = metadata_json.read().decode('UTF-8')
|
|
||||||
self.metadata = json.loads(metadata_str)
|
|
||||||
except KeyError:
|
|
||||||
# no metadata in tar file
|
|
||||||
self.metadata = None
|
|
||||||
else:
|
|
||||||
self.zones = {}
|
|
||||||
self.metadata = None
|
|
||||||
|
|
||||||
def get(self, name, default=None):
|
|
||||||
"""
|
|
||||||
Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
|
|
||||||
for retrieving zones from the zone dictionary.
|
|
||||||
|
|
||||||
:param name:
|
|
||||||
The name of the zone to retrieve. (Generally IANA zone names)
|
|
||||||
|
|
||||||
:param default:
|
|
||||||
The value to return in the event of a missing key.
|
|
||||||
|
|
||||||
.. versionadded:: 2.6.0
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.zones.get(name, default)
|
|
||||||
|
|
||||||
|
|
||||||
# The current API has gettz as a module function, although in fact it taps into
|
|
||||||
# a stateful class. So as a workaround for now, without changing the API, we
|
|
||||||
# will create a new "global" class instance the first time a user requests a
|
|
||||||
# timezone. Ugly, but adheres to the api.
|
|
||||||
#
|
|
||||||
# TODO: Remove after deprecation period.
|
|
||||||
_CLASS_ZONE_INSTANCE = []
|
|
||||||
|
|
||||||
|
|
||||||
def get_zonefile_instance(new_instance=False):
|
|
||||||
"""
|
|
||||||
This is a convenience function which provides a :class:`ZoneInfoFile`
|
|
||||||
instance using the data provided by the ``dateutil`` package. By default, it
|
|
||||||
caches a single instance of the ZoneInfoFile object and returns that.
|
|
||||||
|
|
||||||
:param new_instance:
|
|
||||||
If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
|
|
||||||
used as the cached instance for the next call. Otherwise, new instances
|
|
||||||
are created only as necessary.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns a :class:`ZoneInfoFile` object.
|
|
||||||
|
|
||||||
.. versionadded:: 2.6
|
|
||||||
"""
|
|
||||||
if new_instance:
|
|
||||||
zif = None
|
|
||||||
else:
|
|
||||||
zif = getattr(get_zonefile_instance, '_cached_instance', None)
|
|
||||||
|
|
||||||
if zif is None:
|
|
||||||
zif = ZoneInfoFile(getzoneinfofile_stream())
|
|
||||||
|
|
||||||
get_zonefile_instance._cached_instance = zif
|
|
||||||
|
|
||||||
return zif
|
|
||||||
|
|
||||||
|
|
||||||
def gettz(name):
|
|
||||||
"""
|
|
||||||
This retrieves a time zone from the local zoneinfo tarball that is packaged
|
|
||||||
with dateutil.
|
|
||||||
|
|
||||||
:param name:
|
|
||||||
An IANA-style time zone name, as found in the zoneinfo file.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
Returns a :class:`dateutil.tz.tzfile` time zone object.
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
It is generally inadvisable to use this function, and it is only
|
|
||||||
provided for API compatibility with earlier versions. This is *not*
|
|
||||||
equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
|
|
||||||
time zone based on the inputs, favoring system zoneinfo. This is ONLY
|
|
||||||
for accessing the dateutil-specific zoneinfo (which may be out of
|
|
||||||
date compared to the system zoneinfo).
|
|
||||||
|
|
||||||
.. deprecated:: 2.6
|
|
||||||
If you need to use a specific zoneinfofile over the system zoneinfo,
|
|
||||||
instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
|
|
||||||
:func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
|
|
||||||
|
|
||||||
Use :func:`get_zonefile_instance` to retrieve an instance of the
|
|
||||||
dateutil-provided zoneinfo.
|
|
||||||
"""
|
|
||||||
warnings.warn("zoneinfo.gettz() will be removed in future versions, "
|
|
||||||
"to use the dateutil-provided zoneinfo files, instantiate a "
|
|
||||||
"ZoneInfoFile object and use ZoneInfoFile.zones.get() "
|
|
||||||
"instead. See the documentation for details.",
|
|
||||||
DeprecationWarning)
|
|
||||||
|
|
||||||
if len(_CLASS_ZONE_INSTANCE) == 0:
|
|
||||||
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
|
||||||
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
|
|
||||||
|
|
||||||
|
|
||||||
def gettz_db_metadata():
|
|
||||||
""" Get the zonefile metadata
|
|
||||||
|
|
||||||
See `zonefile_metadata`_
|
|
||||||
|
|
||||||
:returns:
|
|
||||||
A dictionary with the database metadata
|
|
||||||
|
|
||||||
.. deprecated:: 2.6
|
|
||||||
See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
|
|
||||||
query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
|
|
||||||
"""
|
|
||||||
warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
|
|
||||||
"versions, to use the dateutil-provided zoneinfo files, "
|
|
||||||
"ZoneInfoFile object and query the 'metadata' attribute "
|
|
||||||
"instead. See the documentation for details.",
|
|
||||||
DeprecationWarning)
|
|
||||||
|
|
||||||
if len(_CLASS_ZONE_INSTANCE) == 0:
|
|
||||||
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
|
||||||
return _CLASS_ZONE_INSTANCE[0].metadata
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
import shutil
|
|
||||||
import json
|
|
||||||
from subprocess import check_call
|
|
||||||
from tarfile import TarFile
|
|
||||||
|
|
||||||
from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
|
|
||||||
|
|
||||||
|
|
||||||
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
|
||||||
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
|
||||||
|
|
||||||
filename is the timezone tarball from ``ftp.iana.org/tz``.
|
|
||||||
|
|
||||||
"""
|
|
||||||
tmpdir = tempfile.mkdtemp()
|
|
||||||
zonedir = os.path.join(tmpdir, "zoneinfo")
|
|
||||||
moduledir = os.path.dirname(__file__)
|
|
||||||
try:
|
|
||||||
with TarFile.open(filename) as tf:
|
|
||||||
for name in zonegroups:
|
|
||||||
tf.extract(name, tmpdir)
|
|
||||||
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
|
|
||||||
try:
|
|
||||||
check_call(["zic", "-d", zonedir] + filepaths)
|
|
||||||
except OSError as e:
|
|
||||||
_print_on_nosuchfile(e)
|
|
||||||
raise
|
|
||||||
# write metadata file
|
|
||||||
with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
|
|
||||||
json.dump(metadata, f, indent=4, sort_keys=True)
|
|
||||||
target = os.path.join(moduledir, ZONEFILENAME)
|
|
||||||
with TarFile.open(target, "w:%s" % format) as tf:
|
|
||||||
for entry in os.listdir(zonedir):
|
|
||||||
entrypath = os.path.join(zonedir, entry)
|
|
||||||
tf.add(entrypath, entry)
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(tmpdir)
|
|
||||||
|
|
||||||
|
|
||||||
def _print_on_nosuchfile(e):
|
|
||||||
"""Print helpful troubleshooting message
|
|
||||||
|
|
||||||
e is an exception raised by subprocess.check_call()
|
|
||||||
|
|
||||||
"""
|
|
||||||
if e.errno == 2:
|
|
||||||
logging.error(
|
|
||||||
"Could not find zic. Perhaps you need to install "
|
|
||||||
"libc-bin or some other package that provides it, "
|
|
||||||
"or it's not in your PATH?")
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"""Run the EasyInstall command"""
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
from setuptools.command.easy_install import main
|
|
||||||
main()
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
Welcome to Kiwi
|
|
||||||
===============
|
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/nucleic/kiwi.svg?branch=master
|
|
||||||
:target: https://travis-ci.org/nucleic/kiwi
|
|
||||||
|
|
||||||
Kiwi is an efficient C++ implementation of the Cassowary constraint solving
|
|
||||||
algorithm. Kiwi is an implementation of the algorithm based on the seminal
|
|
||||||
Cassowary paper. It is *not* a refactoring of the original C++ solver. Kiwi
|
|
||||||
has been designed from the ground up to be lightweight and fast. Kiwi ranges
|
|
||||||
from 10x to 500x faster than the original Cassowary solver with typical use
|
|
||||||
cases gaining a 40x improvement. Memory savings are consistently > 5x.
|
|
||||||
|
|
||||||
In addition to the C++ solver, Kiwi ships with hand-rolled Python bindings.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pip
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
Metadata-Version: 2.0
|
|
||||||
Name: kiwisolver
|
|
||||||
Version: 1.0.1
|
|
||||||
Summary: A fast implementation of the Cassowary constraint solver
|
|
||||||
Home-page: https://github.com/nucleic/kiwi
|
|
||||||
Author: The Nucleic Development Team
|
|
||||||
Author-email: sccolbert@gmail.com
|
|
||||||
License: UNKNOWN
|
|
||||||
Description-Content-Type: UNKNOWN
|
|
||||||
Platform: UNKNOWN
|
|
||||||
Requires-Dist: setuptools
|
|
||||||
|
|
||||||
Welcome to Kiwi
|
|
||||||
===============
|
|
||||||
|
|
||||||
.. image:: https://travis-ci.org/nucleic/kiwi.svg?branch=master
|
|
||||||
:target: https://travis-ci.org/nucleic/kiwi
|
|
||||||
|
|
||||||
Kiwi is an efficient C++ implementation of the Cassowary constraint solving
|
|
||||||
algorithm. Kiwi is an implementation of the algorithm based on the seminal
|
|
||||||
Cassowary paper. It is *not* a refactoring of the original C++ solver. Kiwi
|
|
||||||
has been designed from the ground up to be lightweight and fast. Kiwi ranges
|
|
||||||
from 10x to 500x faster than the original Cassowary solver with typical use
|
|
||||||
cases gaining a 40x improvement. Memory savings are consistently > 5x.
|
|
||||||
|
|
||||||
In addition to the C++ solver, Kiwi ships with hand-rolled Python bindings.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
kiwisolver-1.0.1.dist-info/DESCRIPTION.rst,sha256=Qy5sjKaN4toH_Q7EUHWgwRVmpxBgeGUwEcyB75zRwSI,676
|
|
||||||
kiwisolver-1.0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
kiwisolver-1.0.1.dist-info/METADATA,sha256=bFtjRI423l1EopYow7-FVdw80WP942cTW4I37mnnunU,1006
|
|
||||||
kiwisolver-1.0.1.dist-info/RECORD,,
|
|
||||||
kiwisolver-1.0.1.dist-info/WHEEL,sha256=xLbWRW0PO79-mIB5Y7BBxUY9L97LDdQCs-obTuQSLEg,109
|
|
||||||
kiwisolver-1.0.1.dist-info/metadata.json,sha256=H-tQ6em1_t3jGeivswhlBiHFJ7CBAI6G_RZAsT-QHCs,535
|
|
||||||
kiwisolver-1.0.1.dist-info/top_level.txt,sha256=xqwWj7oSHlpIjcw2QMJb8puTFPdjDBO78AZp9gjTh9c,11
|
|
||||||
kiwisolver.cpython-36m-x86_64-linux-gnu.so,sha256=4GkBV_S_pJ93VLqTL5uPrsTtcIaTx6RIvdblto_AmVg,3860760
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: bdist_wheel (0.30.0)
|
|
||||||
Root-Is-Purelib: false
|
|
||||||
Tag: cp36-cp36m-manylinux1_x86_64
|
|
||||||
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"description_content_type": "UNKNOWN", "extensions": {"python.details": {"contacts": [{"email": "sccolbert@gmail.com", "name": "The Nucleic Development Team", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/nucleic/kiwi"}}}, "extras": [], "generator": "bdist_wheel (0.30.0)", "metadata_version": "2.0", "name": "kiwisolver", "run_requires": [{"requires": ["setuptools"]}], "summary": "A fast implementation of the Cassowary constraint solver", "version": "1.0.1"}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
kiwisolver
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('mpl_toolkits',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('mpl_toolkits', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('mpl_toolkits', [os.path.dirname(p)])));m = m or sys.modules.setdefault('mpl_toolkits', types.ModuleType('mpl_toolkits'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
|
|
||||||
import sys, types, os;has_mfs = sys.version_info > (3, 5);p = os.path.join(sys._getframe(1).f_locals['sitedir'], *('mpl_toolkits',));importlib = has_mfs and __import__('importlib.util');has_mfs and __import__('importlib.machinery');m = has_mfs and sys.modules.setdefault('mpl_toolkits', importlib.util.module_from_spec(importlib.machinery.PathFinder.find_spec('mpl_toolkits', [os.path.dirname(p)])));m = m or sys.modules.setdefault('mpl_toolkits', types.ModuleType('mpl_toolkits'));mp = (m or []) and m.__dict__.setdefault('__path__',[]);(p not in mp) and mp.append(p)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pip
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
Metadata-Version: 2.1
|
|
||||||
Name: matplotlib
|
|
||||||
Version: 3.0.2
|
|
||||||
Summary: Python plotting package
|
|
||||||
Home-page: http://matplotlib.org
|
|
||||||
Author: John D. Hunter, Michael Droettboom
|
|
||||||
Author-email: matplotlib-users@python.org
|
|
||||||
License: BSD
|
|
||||||
Download-URL: http://matplotlib.org/users/installing.html
|
|
||||||
Platform: any
|
|
||||||
Classifier: Development Status :: 5 - Production/Stable
|
|
||||||
Classifier: Intended Audience :: Science/Research
|
|
||||||
Classifier: License :: OSI Approved :: Python Software Foundation License
|
|
||||||
Classifier: Programming Language :: Python
|
|
||||||
Classifier: Programming Language :: Python :: 3
|
|
||||||
Classifier: Programming Language :: Python :: 3.5
|
|
||||||
Classifier: Programming Language :: Python :: 3.6
|
|
||||||
Classifier: Programming Language :: Python :: 3.7
|
|
||||||
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
||||||
Requires-Python: >=3.5
|
|
||||||
Requires-Dist: numpy (>=1.10.0)
|
|
||||||
Requires-Dist: cycler (>=0.10)
|
|
||||||
Requires-Dist: kiwisolver (>=1.0.1)
|
|
||||||
Requires-Dist: pyparsing (!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1)
|
|
||||||
Requires-Dist: python-dateutil (>=2.1)
|
|
||||||
|
|
||||||
|
|
||||||
Matplotlib strives to produce publication quality 2D graphics
|
|
||||||
for interactive graphing, scientific publishing, user interface
|
|
||||||
development and web application servers targeting multiple user
|
|
||||||
interfaces and hardcopy output formats.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,919 +0,0 @@
|
|||||||
__pycache__/pylab.cpython-36.pyc,,
|
|
||||||
matplotlib-3.0.2-py3.6-nspkg.pth,sha256=HBCg6BgtP04BPqHtaNyC13Cbp9tWa8jd68ltGEV1XF8,1138
|
|
||||||
matplotlib-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
|
||||||
matplotlib-3.0.2.dist-info/METADATA,sha256=fs4NGrFCD56DFwQffsSExcU818TFY8T9OTinV1dNP5w,1226
|
|
||||||
matplotlib-3.0.2.dist-info/RECORD,,
|
|
||||||
matplotlib-3.0.2.dist-info/WHEEL,sha256=d2ILPScH-y2UwGxsW1PeA2TT-KW0Git4AJ6LeOK8sQo,109
|
|
||||||
matplotlib-3.0.2.dist-info/namespace_packages.txt,sha256=LQMWCv385LtvVcrCFmS8Kk1axcWAaUG9ycvuMhV6yoA,26
|
|
||||||
matplotlib-3.0.2.dist-info/top_level.txt,sha256=9tEw2ni8DdgX8CceoYHqSH1s50vrJ9SDfgtLIG8e3Y4,30
|
|
||||||
matplotlib/.libs/libpng16-cfdb1654.so.16.21.0,sha256=Fo8LBDWTuCclLkpSng_KP5pI7wcQtuXA9opT1FFkXl0,275648
|
|
||||||
matplotlib/.libs/libz-a147dcb0.so.1.2.3,sha256=1IGoOjRpujOMRn7cZ29ERtAxBt6SxTUlRLBkSqa_lsk,87848
|
|
||||||
matplotlib/__init__.py,sha256=pWjrFCjP1acCOKaKs7AMHt1xVBiNQ_EXQtq6DUJJQUU,64067
|
|
||||||
matplotlib/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_animation_data.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_cm.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_cm_listed.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_color_data.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_constrained_layout.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_layoutbox.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_mathtext_data.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_pylab_helpers.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/_version.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/afm.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/animation.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/artist.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/axis.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/backend_bases.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/backend_managers.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/backend_tools.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/bezier.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/blocking_input.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/category.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/cm.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/collections.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/colorbar.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/colors.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/container.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/contour.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/dates.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/docstring.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/dviread.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/figure.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/font_manager.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/fontconfig_pattern.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/gridspec.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/hatch.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/image.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/legend.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/legend_handler.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/lines.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/markers.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/mathtext.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/mlab.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/offsetbox.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/patches.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/path.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/patheffects.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/pylab.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/pyplot.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/quiver.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/rcsetup.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/sankey.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/scale.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/spines.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/stackplot.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/streamplot.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/table.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/texmanager.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/text.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/textpath.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/ticker.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/tight_bbox.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/tight_layout.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/transforms.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/type1font.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/units.cpython-36.pyc,,
|
|
||||||
matplotlib/__pycache__/widgets.cpython-36.pyc,,
|
|
||||||
matplotlib/_animation_data.py,sha256=CCdf8YwNX_08FS3YFPYzr2wi3id_WXIKLHrqM50HM8g,6157
|
|
||||||
matplotlib/_cm.py,sha256=C3xi_H7o8WF6qSv3Jl0DA1T2vbT-4wIDLFYfuJe5Zpg,66609
|
|
||||||
matplotlib/_cm_listed.py,sha256=EpTjQ6pZ9E_UeY4kDa6fU9za_VHBvhuOmCG6fwNRvlk,98362
|
|
||||||
matplotlib/_color_data.py,sha256=9hUzbyqLpEe-2LjEeAN3ja2ANuryNvtTseU8vuJXfsI,34776
|
|
||||||
matplotlib/_constrained_layout.py,sha256=1ygKBGfY0ttpPFhEWucL0wVnwYP-U_ZX8OxHlCUXXjQ,29304
|
|
||||||
matplotlib/_contour.cpython-36m-x86_64-linux-gnu.so,sha256=Gd6D_VYbA-imr5k186kroDG91DrOUYiltJS3QuARruQ,95144
|
|
||||||
matplotlib/_image.cpython-36m-x86_64-linux-gnu.so,sha256=akH-urj3-vOY9iYC-N9lORYlpSVVMPUKMBnCAVPrqc0,242496
|
|
||||||
matplotlib/_layoutbox.py,sha256=yXrJA2hBYKdzPNzbrsLl0Lv8zAPtnPulQ9-BbfhcVWM,24354
|
|
||||||
matplotlib/_mathtext_data.py,sha256=CmKFRW6mXCJqgZSQaiNOSG_VUn9WiSx5Hrg-4qKIn14,89371
|
|
||||||
matplotlib/_path.cpython-36m-x86_64-linux-gnu.so,sha256=jEhQzvD031IYQRSbo4Fxobi5KpZF-NV_7iuDDRaVBDU,190216
|
|
||||||
matplotlib/_png.cpython-36m-x86_64-linux-gnu.so,sha256=DlT39C99TemBT-i-7MuJuwJRg6eVwRhMMR4AxInU2QI,48144
|
|
||||||
matplotlib/_pylab_helpers.py,sha256=lC5IY7NOGCPVD5brJj-PdtJkzA53Fs4hrn9exdzWDFg,3528
|
|
||||||
matplotlib/_qhull.cpython-36m-x86_64-linux-gnu.so,sha256=dpM763p27zLpJIW8u045wQW71Oz3JwP9BHCHW6M-pHY,382640
|
|
||||||
matplotlib/_tri.cpython-36m-x86_64-linux-gnu.so,sha256=7InLl8WYUN4CyAtbrAWCXzycxTUs7_xNZx4QZKcifZg,128616
|
|
||||||
matplotlib/_version.py,sha256=bHCJPWh4TT1eI2_VgNQWyLUAJMklrvz_FkivkMGVXmk,471
|
|
||||||
matplotlib/afm.py,sha256=Cfj2v5Rgsr5GaDzuKySLd-aqsBdJBLX2T1gt7TQ175o,16934
|
|
||||||
matplotlib/animation.py,sha256=WyFTZBvyIN2tmvnT9LzlGZDOvqWgPn0SHOfzl0e1LlY,67680
|
|
||||||
matplotlib/artist.py,sha256=VbAnAHa4CPr39s_99S0yAD9-tBUMPxc-BLvhWeykSLk,48885
|
|
||||||
matplotlib/axes/__init__.py,sha256=npQuBvs_xEBEGUP2-BBZzCrelsAQYgB1U96kSZTSWIs,46
|
|
||||||
matplotlib/axes/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/axes/__pycache__/_axes.cpython-36.pyc,,
|
|
||||||
matplotlib/axes/__pycache__/_base.cpython-36.pyc,,
|
|
||||||
matplotlib/axes/__pycache__/_subplots.cpython-36.pyc,,
|
|
||||||
matplotlib/axes/_axes.py,sha256=o_hBMnccGhT_BedP7n12Kk6Y-qhEmL7S3guI5EF7r_M,306901
|
|
||||||
matplotlib/axes/_base.py,sha256=-gd9r0IAp785gRHpHpaN1IibgC5J069NNPKH2d_UbHQ,156195
|
|
||||||
matplotlib/axes/_subplots.py,sha256=pPG7GjDBFGDnjVXdojw3_ROHUO5kjZM2s7FsUwIH7sE,9479
|
|
||||||
matplotlib/axis.py,sha256=s2rcQiyNOCskQ3e4J9YeG-dI0GMbH8v3SWbrC7ymtPo,90811
|
|
||||||
matplotlib/backend_bases.py,sha256=7EaSuF58KKjm5sRYnmsUECkSJc0BcbsYmGd3gMpcpJM,111487
|
|
||||||
matplotlib/backend_managers.py,sha256=R3ZGS7NiyfAYutPX9KRqRXiZur8VVFb2_yQPNKtQ2Os,12983
|
|
||||||
matplotlib/backend_tools.py,sha256=nCruryzfTxOT0v8wyQPzPvCuNdOx5eWXTiiVniGoQmg,35810
|
|
||||||
matplotlib/backends/__init__.py,sha256=Y2fIYrWuxw3DsTGBgaYTkoExgiv5V-fpFMoL6Xluv1U,3593
|
|
||||||
matplotlib/backends/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/_backend_tk.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/_gtk3_compat.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_agg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_cairo.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_gtk3.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_gtk3agg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_gtk3cairo.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_macosx.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_mixed.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_nbagg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_pdf.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_pgf.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_ps.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_qt4.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_qt4agg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_qt4cairo.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_qt5.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_qt5agg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_qt5cairo.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_svg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_template.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_tkagg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_tkcairo.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_webagg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_webagg_core.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_wx.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_wxagg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/backend_wxcairo.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/qt_compat.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/tkagg.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/windowing.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/__pycache__/wx_compat.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/_backend_agg.cpython-36m-x86_64-linux-gnu.so,sha256=7egU7A_XSGu5zAhXxcs7ubTrUwDx9wCah09ET3u9BTg,358384
|
|
||||||
matplotlib/backends/_backend_tk.py,sha256=oG6iWxbo96fmE24Q1-2p62MX2sSXwp-bDsCiteqw7bw,37878
|
|
||||||
matplotlib/backends/_gtk3_compat.py,sha256=_mQjgaLq7PAdWCmkYkN3ZEc7SFms4T47Y6X1bmc3UlI,1458
|
|
||||||
matplotlib/backends/_tkagg.cpython-36m-x86_64-linux-gnu.so,sha256=ghIFRXt4_3htN74rENcBbpBc_Ly2pRg94F6h_xxE7vI,29080
|
|
||||||
matplotlib/backends/backend_agg.py,sha256=oWgXuSgkOr_w_Ix3xB07QwaW34CIrDeFr3Ar38KWLnI,21136
|
|
||||||
matplotlib/backends/backend_cairo.py,sha256=F_Er53Cssgiaz22qKUExxLvgBssxmsl0vRTenkckDJA,23049
|
|
||||||
matplotlib/backends/backend_gtk3.py,sha256=7yldMkTpLnLqd9GLi905Ba2Se0gBe2bNNjrojNYDKMM,34061
|
|
||||||
matplotlib/backends/backend_gtk3agg.py,sha256=Sdv_sVpJab05fFqZEz4a8hv2jB6194l0S7I9kioDCBc,2863
|
|
||||||
matplotlib/backends/backend_gtk3cairo.py,sha256=qvEc-eOfNhYhvG_4COnmCt3e_L-dInGMMnWJY3EG1s8,1516
|
|
||||||
matplotlib/backends/backend_macosx.py,sha256=cQLOUeHfteu3UzK5VhdBwNRohFidcNGsJ9NNCpcx52Y,6534
|
|
||||||
matplotlib/backends/backend_mixed.py,sha256=w0npmW09OKYhaHlpaKXY9vBhKNpUrL_gplxhjlm_re0,5738
|
|
||||||
matplotlib/backends/backend_nbagg.py,sha256=981WrP82fEZJIdND77C2JW4DpS3k82J6PyXthy18-jM,8707
|
|
||||||
matplotlib/backends/backend_pdf.py,sha256=L6GwqJKiPaFlqPRuhFbJQ9xGhZf8DGkL-FQ5W_nSamw,96965
|
|
||||||
matplotlib/backends/backend_pgf.py,sha256=8a9VSoU32ZatMBik36IjmrM56GkU0QFc3w1o3z3y89w,43189
|
|
||||||
matplotlib/backends/backend_ps.py,sha256=M47hHy6DgqRev1emHxv-K8diIoP5aYsNKW1sMex2h2A,61117
|
|
||||||
matplotlib/backends/backend_qt4.py,sha256=x9KHRXMxJfmlJ-6lMdmKrw9cGe5RlZAAGjk1Ga1xX2c,410
|
|
||||||
matplotlib/backends/backend_qt4agg.py,sha256=xTZMUL161hqcsOKSeU4sEs9kYloQrC7_OJjfyuYUAp0,245
|
|
||||||
matplotlib/backends/backend_qt4cairo.py,sha256=B-LD_AECxVVsZA6Zb-oxoN39iM-GUNl81LR7nVaJ8q4,159
|
|
||||||
matplotlib/backends/backend_qt5.py,sha256=HTxAS2-cOPD_pS40BP4foUl-UfQb3XHmQrNJsuWE8Vs,41475
|
|
||||||
matplotlib/backends/backend_qt5agg.py,sha256=plgjHJqJ0IawKuEH8Re_mTOLLrH90mRcMs65nbdTLvE,3237
|
|
||||||
matplotlib/backends/backend_qt5cairo.py,sha256=wTVbxWW9f5REOesXDIzI2eSmE5HS-nwWNcg2m-YvyLI,1977
|
|
||||||
matplotlib/backends/backend_svg.py,sha256=I_-tR0sEpRFt7jlM8S-dJX8vgjwMifEJTa5N3maJWC0,45518
|
|
||||||
matplotlib/backends/backend_template.py,sha256=drJi5J7r4ZMpmwvYiVp3uVfWDJ0VtZSEd2A4OD93fUU,9149
|
|
||||||
matplotlib/backends/backend_tkagg.py,sha256=WMslLWYmtxlmAaBH4tx4HjmRDWMKiSV91KHF9yeMRng,676
|
|
||||||
matplotlib/backends/backend_tkcairo.py,sha256=dVCh7ZD_2OR0DBQ0N3icD8cDV1SeEzCsRja446wWhPw,1069
|
|
||||||
matplotlib/backends/backend_webagg.py,sha256=kqDsqw1a0mZA3-ouBGegg8BlWys5iAA0-fAtxTvUZ5o,11124
|
|
||||||
matplotlib/backends/backend_webagg_core.py,sha256=c8FqOMph-FdYJ-kNauNG23enXWwZwbmQ3QzjhYud4IM,17554
|
|
||||||
matplotlib/backends/backend_wx.py,sha256=gQTIV29utVqq_W6iqRE5gqGwhDZ3sPg2s0fxXDkmcCY,74559
|
|
||||||
matplotlib/backends/backend_wxagg.py,sha256=TGdhDULgRzqhLNOzq7QAt0oe_AqHoT-IyZOO8RlE4Bw,4015
|
|
||||||
matplotlib/backends/backend_wxcairo.py,sha256=VC5TyJaX8TPLSgHv5ckAreoGrY_KiNRMQjVInMLlcFk,1843
|
|
||||||
matplotlib/backends/qt_compat.py,sha256=gdDU16Oe1HfwJJSGmvLvEJmrKDX6T8Tt1WiSUITR3qU,6658
|
|
||||||
matplotlib/backends/qt_editor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
matplotlib/backends/qt_editor/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/qt_editor/__pycache__/figureoptions.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/qt_editor/__pycache__/formlayout.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/qt_editor/__pycache__/formsubplottool.cpython-36.pyc,,
|
|
||||||
matplotlib/backends/qt_editor/figureoptions.py,sha256=_TD7NJY_LzXDoljckLqKaVBYa3FSJwEFNrccYpbKMTM,9133
|
|
||||||
matplotlib/backends/qt_editor/formlayout.py,sha256=qCAmYnZUEwgBRMVsPWuu7e1fqCU0OSM-I5Snw7ShcBI,19573
|
|
||||||
matplotlib/backends/qt_editor/formsubplottool.py,sha256=HiiXkwCotra_hI9JU208KOs8Q9JuGH1uAW3mV5l3Evg,1934
|
|
||||||
matplotlib/backends/tkagg.py,sha256=ro4lL5U0cLMslYmLXQRhWwR1mxlcZYGO9awistZzL1E,1319
|
|
||||||
matplotlib/backends/web_backend/all_figures.html,sha256=GiIHkdjLO94c_GAHVX4Zk5R88uEnIUwW2CJgm3qCCv0,1512
|
|
||||||
matplotlib/backends/web_backend/css/boilerplate.css,sha256=qui16QXRnQFNJDbcMasfH6KtN9hLjv8883U9cJmsVCE,2310
|
|
||||||
matplotlib/backends/web_backend/css/fbm.css,sha256=Us0osu_rK8EUAdp_GXrh89tN_hUNCN-r7N1T1NvmmwI,1473
|
|
||||||
matplotlib/backends/web_backend/css/page.css,sha256=Djf6ZNMFaM6_hVaizSkDFoqk-jn81qgduwles4AroGk,1599
|
|
||||||
matplotlib/backends/web_backend/ipython_inline_figure.html,sha256=mzi-yWg4fcO6PdtTBCfiNuvcv04T53lcRQi-8hphwuE,1305
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_diagonals-thick_18_b81900_40x40.png,sha256=xRM8xoz-7ahUtdsImL_ZC6s8kJT2QnE04-cji0ZQfsE,418
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_diagonals-thick_20_666666_40x40.png,sha256=T9PQekCQeDFHwAxdZMQs_QzAqaqtimo0uLCotJ0xSqg,312
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_flat_10_000000_40x100.png,sha256=r1CSqnKXLAw-qLRMm5LbIAnqQWB3OLzAqdpZesaoc-o,205
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_glass_100_f6f6f6_1x400.png,sha256=_ws_N6fYaeQspm2VHS5Wm-QV4CXhlUoSqGQ-lmW3xm0,262
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_glass_100_fdf5ce_1x400.png,sha256=-VfVTyNvfByi7t0zq33BBb1mLO5iuIBvsso4Ye2ysXw,348
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_glass_65_ffffff_1x400.png,sha256=JFJf1ebnJmze7uXUNtoLcgn-dTh-bJxVxXVLIuQDGrU,207
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_gloss-wave_35_f6a828_500x100.png,sha256=Av4RPyWlnMm1at-9VqlutTAvTKW7637sDlQamg2ozNA,5815
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_highlight-soft_100_eeeeee_1x100.png,sha256=80rH2tcJybpprH1zkHIN1U_aVhUcZOc9mv9OEYavhRA,278
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-bg_highlight-soft_75_ffe45c_1x100.png,sha256=JRY-0684rYlKOQKRQmXYJ5YiPhHS3kNPZmUVa3xi3hg,328
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_222222_256x240.png,sha256=_htyYBLdV3XU9kp9QnMKIQ8pBX6OgU8zkE05EsTZq9s,6922
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_228ef1_256x240.png,sha256=Z8eq2yeIM45jXe3uMbiKqOqQjA2MKR20tWcBTVc6bC8,4549
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_ef8c08_256x240.png,sha256=loEi-IH5oyL8PSSjAGPT549tcpGfRzIbFEaqPFhR8ck,4549
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_ffd27a_256x240.png,sha256=O-dEGxrQchuZxTdpQJQ4LEirQLT1OA5lBNYO9O8mJo8,4549
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/images/ui-icons_ffffff_256x240.png,sha256=sVicDAn7JIIW-osNHhgSqa-bSRWOtS4G94BitP_MAds,6299
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/jquery-ui.css,sha256=zs9cWf98KIv5DMYiF1a9lhJGQwhVe5LKVPJ9HNEI880,35348
|
|
||||||
matplotlib/backends/web_backend/jquery/css/themes/base/jquery-ui.min.css,sha256=VQzrlVm7QjdSeQn_IecZgE9rnfM390H3VoIcDJljOSs,30163
|
|
||||||
matplotlib/backends/web_backend/jquery/js/jquery-1.11.3.js,sha256=kaIBSZlozqJ4IeqPwOlDOi97qhNHWTchpUedwo5-gMU,284395
|
|
||||||
matplotlib/backends/web_backend/jquery/js/jquery-1.11.3.min.js,sha256=7LkWEzqTdpEfELxcZZlS6wAx5Ff13zZ83lYO2_ujj7g,95957
|
|
||||||
matplotlib/backends/web_backend/jquery/js/jquery-ui.js,sha256=DI6NdAhhFRnO2k51mumYeDShet3I8AKCQf_tf7ARNhI,470596
|
|
||||||
matplotlib/backends/web_backend/jquery/js/jquery-ui.min.js,sha256=xNjb53_rY-WmG-4L6tTl9m6PpqknWZvRt0rO1SRnJzw,240427
|
|
||||||
matplotlib/backends/web_backend/js/mpl.js,sha256=SqYBZRsHYoQY9d-5eCZsVpIBnlR4T7t1JiYYSZOs6Uw,16964
|
|
||||||
matplotlib/backends/web_backend/js/mpl_tornado.js,sha256=lSxC7-yqF1GYY-6SheaHanx6SujMdcG7Vx2_3qbi-9Q,272
|
|
||||||
matplotlib/backends/web_backend/js/nbagg_mpl.js,sha256=WfV96-6LXzUSnzCmvQ93JopqO7KPqnfbT_07Q1XJeT4,7467
|
|
||||||
matplotlib/backends/web_backend/nbagg_uat.ipynb,sha256=y1N8hQzBJ05rJ2hZla2_Mw6tOUfNP1UHKo636W1e098,15933
|
|
||||||
matplotlib/backends/web_backend/single_figure.html,sha256=56UAOD6qgZ-T6wE2EjGpTBG4iwAmfG3QsnX5lLQp2dI,1203
|
|
||||||
matplotlib/backends/windowing.py,sha256=q-hSo_vvsjst6EjNkr94NIDbUucMocrMj9MSjVwkfY4,822
|
|
||||||
matplotlib/backends/wx_compat.py,sha256=RQFVDMaMLjT8YmazLiq3BcjCcCi6xuzS_AwmxtYd2vg,968
|
|
||||||
matplotlib/bezier.py,sha256=zwC07cDX9iyh9KWnZsPBQaFJ4CTue0NQpm1VwS2PEuM,15415
|
|
||||||
matplotlib/blocking_input.py,sha256=E6r1m5acvx_KLDp7rZXftnA5OBtJnjy_zGWBUO9k_OA,11112
|
|
||||||
matplotlib/category.py,sha256=AWYm1nucRySTh-6cXN2FeNYQOpwJPeUl9iFkjcr4oW0,5928
|
|
||||||
matplotlib/cbook/__init__.py,sha256=bFrhSeVf8Ya6pNEtkqOTVABSR--ADZLbTlsLqd3BUV4,65667
|
|
||||||
matplotlib/cbook/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/cbook/__pycache__/deprecation.cpython-36.pyc,,
|
|
||||||
matplotlib/cbook/deprecation.py,sha256=5ghVk2E7syukLQuADs7UtHuBRJYaiuvylSymAh_axDQ,9402
|
|
||||||
matplotlib/cm.py,sha256=MHQtLJ9UGDuyxHa-qyMxL2vikkhXB5OXhYdxUy-_68M,12843
|
|
||||||
matplotlib/collections.py,sha256=HqR0G7ydei6YkX06MYwp0TGrVZaXz7BDNiwEPGk0Fro,66851
|
|
||||||
matplotlib/colorbar.py,sha256=_UImtSk1sRHbLGNn_gLTy86mFYqzjovJMvioqcvn57w,59164
|
|
||||||
matplotlib/colors.py,sha256=HeZGu5MwSkMLEABhU5qYmJw4ZbvpxeOrdV2ytY_klSg,71636
|
|
||||||
matplotlib/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
matplotlib/compat/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/compat/__pycache__/subprocess.cpython-36.pyc,,
|
|
||||||
matplotlib/compat/subprocess.py,sha256=-K36IW-t5SjjWcP4nL80GAE-y7v0HZE6nQ-hscwmkvg,1562
|
|
||||||
matplotlib/container.py,sha256=Sbz7JTBAFirjClv-nSpVojuSLKIBoUo-dAve8QAFjl8,5245
|
|
||||||
matplotlib/contour.py,sha256=XiEg_unL8WQaQf3f3c41KBHltZPXJ8Fhk2W0OGvuacc,70231
|
|
||||||
matplotlib/dates.py,sha256=XsBhin-wZcY7FPcMrDQDBEYjYXNtmc-YbtwZiysKeqQ,61547
|
|
||||||
matplotlib/docstring.py,sha256=M5wraMzJf7veSfzUJUArSY68bM5lkH3q6niSmW0q1xw,3787
|
|
||||||
matplotlib/dviread.py,sha256=s_N3IU68PIVm03TFnBP8_UakGnIYwYCv1vxLDjpWsk4,37282
|
|
||||||
matplotlib/figure.py,sha256=p9yke2KjrUkOJ5ZUt-q44GqMjCU5woMnFMAXLR3-DJk,95506
|
|
||||||
matplotlib/font_manager.py,sha256=uWPJ8AR9gzeIljZz8baIRTDacgsAMORLgmV1lT6Ve_s,45972
|
|
||||||
matplotlib/fontconfig_pattern.py,sha256=LYKBekGIwiHNYyYe36Lfki4JyEkZMsBinmhVzBc1wns,6593
|
|
||||||
matplotlib/ft2font.cpython-36m-x86_64-linux-gnu.so,sha256=xKwvjhkVt1lLcRA4Swcz9iHqhWTk78UGl6CLyR5tgvw,894456
|
|
||||||
matplotlib/gridspec.py,sha256=C5rAu1tokWgaegWBEocG6phEfemgMF9dch-HwVfKC0A,20056
|
|
||||||
matplotlib/hatch.py,sha256=kmq09LW_AJxPY0LGJxJrrC9Yf6tPQYi209EMAveqQoE,6988
|
|
||||||
matplotlib/image.py,sha256=LGgYY2UmfjldiGhqAU6Qwi-3I_CJAzbL6b9hFMhCWHQ,54632
|
|
||||||
matplotlib/legend.py,sha256=M6PCguGj32SpnEuLvPhRKfWZuCofn1JYSubhRb9e5Yk,48770
|
|
||||||
matplotlib/legend_handler.py,sha256=gmQCUHjMmvcJ4g8dSqayhaXj82jkUK36i0-4DK6ik-A,25524
|
|
||||||
matplotlib/lines.py,sha256=uKymslFnSniquZR_5N266mXvkQJ_4eKu_sg5iP5t1rA,48503
|
|
||||||
matplotlib/markers.py,sha256=yILg87-gXelLwbu2o1xHNdCikXVHG50frJQJ6ywY8OY,34345
|
|
||||||
matplotlib/mathtext.py,sha256=vXlAVF9T7xA7-uRHnBu4T6LBZhr97a6obWEAr-X-mA4,121307
|
|
||||||
matplotlib/mlab.py,sha256=mhkOI6_Wgw0aaaqPDuCE53tHLNxl8tj_ajgoCZbRUcY,124056
|
|
||||||
matplotlib/mpl-data/fonts/afm/cmex10.afm,sha256=blR3ERmrVBV5XKkAnDCj4NMeYVgzH7cXtJ3u59u9GuE,12070
|
|
||||||
matplotlib/mpl-data/fonts/afm/cmmi10.afm,sha256=5qwEOpedEo76bDUahyuuF1q0cD84tRrX-VQ4p3MlfBo,10416
|
|
||||||
matplotlib/mpl-data/fonts/afm/cmr10.afm,sha256=WDvgC_D3UkGJg9u-J0U6RaT02lF4oz3lQxHtg1r3lYw,10101
|
|
||||||
matplotlib/mpl-data/fonts/afm/cmsy10.afm,sha256=AbmzvCVWBceHRfmRfeJ9E6xzOQTFLk0U1zDfpf3_MaM,8295
|
|
||||||
matplotlib/mpl-data/fonts/afm/cmtt10.afm,sha256=4ji7_mTpeWMa93o_UHBWPKCnqsBfhJJNllat1lJArP4,6501
|
|
||||||
matplotlib/mpl-data/fonts/afm/pagd8a.afm,sha256=jjFrigwkTpYLqa26cpzZvKQNBo-PuF4bmDVqaM4pMWw,17183
|
|
||||||
matplotlib/mpl-data/fonts/afm/pagdo8a.afm,sha256=sgNQdeYyx8J-itGw9h31y95aMBiTCRvmNSPTXwwS7xg,17255
|
|
||||||
matplotlib/mpl-data/fonts/afm/pagk8a.afm,sha256=ZUtfHPloNqcvGMHMxaKDSlshhOcjwheUx143RwpGdIU,17241
|
|
||||||
matplotlib/mpl-data/fonts/afm/pagko8a.afm,sha256=Yj1wBg6Jsqqz1KBfhRoJ3ACR-CMQol8Fj_ZM5NZ1gDk,17346
|
|
||||||
matplotlib/mpl-data/fonts/afm/pbkd8a.afm,sha256=Zl5o6J_di9Y5j2EpHtjew-_sfg7-WoeVmO9PzOYSTUc,15157
|
|
||||||
matplotlib/mpl-data/fonts/afm/pbkdi8a.afm,sha256=JAOno930iTyfZILMf11vWtiaTgrJcPpP6FRTRhEMMD4,15278
|
|
||||||
matplotlib/mpl-data/fonts/afm/pbkl8a.afm,sha256=UJqJjOJ6xQDgDBLX157mKpohIJFVmHM-N6x2-DiGv14,15000
|
|
||||||
matplotlib/mpl-data/fonts/afm/pbkli8a.afm,sha256=AWislZ2hDbs0ox_qOWREugsbS8_8lpL48LPMR40qpi0,15181
|
|
||||||
matplotlib/mpl-data/fonts/afm/pcrb8a.afm,sha256=6j1TS2Uc7DWSc-8l42TGDc1u0Fg8JspeWfxFayjUwi8,15352
|
|
||||||
matplotlib/mpl-data/fonts/afm/pcrbo8a.afm,sha256=smg3mjl9QaBDtQIt06ko5GvaxLsO9QtTvYANuE5hfG0,15422
|
|
||||||
matplotlib/mpl-data/fonts/afm/pcrr8a.afm,sha256=7nxFr0Ehz4E5KG_zSE5SZOhxRH8MyfnCbw-7x5wu7tw,15339
|
|
||||||
matplotlib/mpl-data/fonts/afm/pcrro8a.afm,sha256=NKEz7XtdFkh9cA8MvY-S3UOZlV2Y_J3tMEWFFxj7QSg,15443
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvb8a.afm,sha256=NAx4M4HjL7vANCJbc-tk04Vkol-T0oaXeQ3T2h-XUvM,17155
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvb8an.afm,sha256=8e_myD-AQkNF7q9XNLb2m76_lX2TUr3a5wog_LIE1sk,17086
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvbo8a.afm,sha256=8fkBRmJ-SWY2YrBg8fFyjJyrJp8daQ6JPO6LvhM8xPI,17230
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvbo8an.afm,sha256=aeVRvV4r15BBvxuRJ0MG8ZHuH2HViuIiCYkvuapmkmM,17195
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvl8a.afm,sha256=IyMYM-bgl-gI6rG0EuZZ2OLzlxJfGeSh8xqsh0t-eJQ,15627
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvlo8a.afm,sha256=s12C-eNnIDHJ_UVbuiprjxBjCiHIbS3Y8ORTC-qTpuI,15729
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvr8a.afm,sha256=Kt8KaRidts89EBIK29X2JomDUEDxvroeaJz_RNTi6r4,17839
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvr8an.afm,sha256=lL5fAHTRwODl-sB5mH7IfsD1tnnea4yRUK-_Ca2bQHM,17781
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvro8a.afm,sha256=3KqK3eejiR4hIFBUynuSX_4lMdE2V2T58xOF8lX-fwc,17919
|
|
||||||
matplotlib/mpl-data/fonts/afm/phvro8an.afm,sha256=Vx9rRf3YfasMY7tz-njSxz67xHKk-fNkN7yBi0X2IP0,17877
|
|
||||||
matplotlib/mpl-data/fonts/afm/pncb8a.afm,sha256=aoXepTcDQtQa_mspflMJkEFKefzXHoyjz6ioJVI0YNc,16028
|
|
||||||
matplotlib/mpl-data/fonts/afm/pncbi8a.afm,sha256=pCWW1MYgy0EmvwaYsaYJaAI_LfrsKmDANHu7Pk0RaiU,17496
|
|
||||||
matplotlib/mpl-data/fonts/afm/pncr8a.afm,sha256=0CIB2BLe9r-6_Wl5ObRTTf98UOrezmGQ8ZOuBX5kLks,16665
|
|
||||||
matplotlib/mpl-data/fonts/afm/pncri8a.afm,sha256=5R-pLZOnaHNG8pjV6MP3Ai-d2OTQYR_cYCb5zQhzfSU,16920
|
|
||||||
matplotlib/mpl-data/fonts/afm/pplb8a.afm,sha256=3EzUbNnXr5Ft5eFLY00W9oWu59rHORgDXUuJaOoKN58,15662
|
|
||||||
matplotlib/mpl-data/fonts/afm/pplbi8a.afm,sha256=X_9tVspvrcMer3OS8qvdwjFFqpAXYZneyCL2NHA902g,15810
|
|
||||||
matplotlib/mpl-data/fonts/afm/pplr8a.afm,sha256=ijMb497FDJ9nVdVMb21F7W3-cu9sb_9nF0oriFpSn8k,15752
|
|
||||||
matplotlib/mpl-data/fonts/afm/pplri8a.afm,sha256=8KITbarcUUMi_hdoRLLmNHtlqs0TtOSKqtPFft7X5nY,15733
|
|
||||||
matplotlib/mpl-data/fonts/afm/psyr.afm,sha256=Iyt8ajE4B2Tm34oBj2pKtctIf9kPfq05suQefq8p3Ro,9644
|
|
||||||
matplotlib/mpl-data/fonts/afm/ptmb8a.afm,sha256=bL1fA1NC4_nW14Zrnxz4nHlXJb4dzELJPvodqKnYeMg,17983
|
|
||||||
matplotlib/mpl-data/fonts/afm/ptmbi8a.afm,sha256=-_Ui6XlKaFTHEnkoS_-1GtIr5VtGa3gFQ2ezLOYHs08,18070
|
|
||||||
matplotlib/mpl-data/fonts/afm/ptmr8a.afm,sha256=IEcsWcmzJyjCwkgsw4o6hIMmzlyXUglJat9s1PZNnEU,17942
|
|
||||||
matplotlib/mpl-data/fonts/afm/ptmri8a.afm,sha256=49fQMg5fIGguZ7rgc_2styMK55Pv5bPTs7wCzqpcGpk,18068
|
|
||||||
matplotlib/mpl-data/fonts/afm/putb8a.afm,sha256=qMaHTdpkrNL-m4DWhjpxJCSmgYkCv1qIzLlFfM0rl40,21532
|
|
||||||
matplotlib/mpl-data/fonts/afm/putbi8a.afm,sha256=g7AVJyiTxeMpNk_1cSfmYgM09uNUfPlZyWGv3D1vcAk,21931
|
|
||||||
matplotlib/mpl-data/fonts/afm/putr8a.afm,sha256=XYmNC5GQgSVAZKTIYdYeNksE6znNm9GF_0SmQlriqx0,22148
|
|
||||||
matplotlib/mpl-data/fonts/afm/putri8a.afm,sha256=i7fVe-iLyLtQxCfAa4IxdxH-ufcHmMk7hbCGG5TxAY4,21891
|
|
||||||
matplotlib/mpl-data/fonts/afm/pzcmi8a.afm,sha256=wyuoIWEZOcoXrSl1tPzLkEahik7kGi91JJj-tkFRG4A,16250
|
|
||||||
matplotlib/mpl-data/fonts/afm/pzdr.afm,sha256=MyjLAnzKYRdQBfof1W3k_hf30MvqOkqL__G22mQ5xww,9467
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Courier-Bold.afm,sha256=sIDDI-B82VZ3C0mI_mHFITCZ7PVn37AIYMv1CrHX4sE,15333
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Courier-BoldOblique.afm,sha256=zg61QobD3YU9UBfCXmvmhBNaFKno-xj8sY0b2RpgfLw,15399
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Courier-Oblique.afm,sha256=vRQm5j1sTUN4hicT1PcVZ9P9DTTUHhEzfPXqUUzVZhE,15441
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Courier.afm,sha256=Mdcq2teZEBJrIqVXnsnhee7oZnTs6-P8_292kWGTrw4,15335
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Helvetica-Bold.afm,sha256=i2l4gcjuYXoXf28uK7yIVwuf0rnw6J7PwPVQeHj5iPw,69269
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Helvetica-BoldOblique.afm,sha256=Um5O6qK11DXLt8uj_0IoWkc84TKqHK3bObSKUswQqvY,69365
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Helvetica-Oblique.afm,sha256=hVYDg2b52kqtbVeCzmiv25bW1yYdpkZS-LXlGREN2Rs,74392
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Helvetica.afm,sha256=23cvKDD7bQAJB3kdjSahJSTZaUOppznlIO6FXGslyW8,74292
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Symbol.afm,sha256=P5UaoXr4y0qh4SiMa5uqijDT6ZDr2-jPmj1ayry593E,9740
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Times-Bold.afm,sha256=cQTmr2LFPwKQE_sGQageMcmFicjye16mKJslsJLHQyE,64251
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Times-BoldItalic.afm,sha256=pzWOdycm6RqocBWgAVY5Jq0z3Fp7LuqWgLNMx4q6OFw,59642
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Times-Italic.afm,sha256=bK5puSMpGT_YUILwyJrXoxjfj7XJOdfv5TQ_iKsJRzw,66328
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/Times-Roman.afm,sha256=hhNrUnpazuDDKD1WpraPxqPWCYLrO7D7bMVOg-zI13o,60460
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/ZapfDingbats.afm,sha256=ZuOmt9GcKofjdOq8kqhPhtAIhOwkL2rTJTmZxAjFakA,9527
|
|
||||||
matplotlib/mpl-data/fonts/pdfcorefonts/readme.txt,sha256=MRv8ppSITYYAb7lt5EOw9DWWNZIblfxsFhu5TQE7cpI,828
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSans-Bold.ttf,sha256=sYS4njwQdfIva3FXW2_CDUlys8_TsjMiym_Vltyu8Wc,704128
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSans-BoldOblique.ttf,sha256=bt8CgxYBhq9FHL7nHnuEXy5Mq_Jku5ks5mjIPCVGXm8,641720
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSans-Oblique.ttf,sha256=zN90s1DxH9PdV3TeUOXmNGoaXaH1t9X7g1kGZel6UhM,633840
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSans.ttf,sha256=P99pyr8GBJ6nCgC1kZNA4s4ebQKwzDxLRPtoAb0eDSI,756072
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSansDisplay.ttf,sha256=ggmdz7paqGjN_CdFGYlSX-MpL3N_s8ngMozpzvWWUvY,25712
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Bold.ttf,sha256=uq2ppRcv4giGJRr_BDP8OEYZEtXa8HKH577lZiCo2pY,331536
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-BoldOblique.ttf,sha256=ppCBwVx2yCfgonpaf1x0thNchDSZlVSV_6jCDTqYKIs,253116
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSansMono-Oblique.ttf,sha256=KAUoE_enCfyJ9S0ZLcmV708P3Fw9e3OknWhJsZFtDNA,251472
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSansMono.ttf,sha256=YC7Ia4lIz82VZIL-ZPlMNshndwFJ7y95HUYT9EO87LM,340240
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Bold.ttf,sha256=w3U_Lta8Zz8VhG3EWt2-s7nIcvMvsY_VOiHxvvHtdnY,355692
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSerif-BoldItalic.ttf,sha256=2T7-x6nS6CZ2jRou6VuVhw4V4pWZqE80hK8d4c7C4YE,347064
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSerif-Italic.ttf,sha256=PnmU-8VPoQzjNSpC1Uj63X2crbacsRCbydlg9trFfwQ,345612
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSerif.ttf,sha256=EHJElW6ZYrnpb6zNxVGCXgrgiYrhNzcTPhuSGi_TX_o,379740
|
|
||||||
matplotlib/mpl-data/fonts/ttf/DejaVuSerifDisplay.ttf,sha256=KRTzLkfHd8J75Wd6-ufbTeefnkXeb8kJfZlJwjwU99U,14300
|
|
||||||
matplotlib/mpl-data/fonts/ttf/LICENSE_DEJAVU,sha256=11k43sCY8G8Kw8AIUwZdlPAgvhw8Yu8dwpdboVtNmw4,4816
|
|
||||||
matplotlib/mpl-data/fonts/ttf/LICENSE_STIX,sha256=cxFOZdp1AxNhXR6XxCzf5iJpNcu-APm-geOHhD-s0h8,5475
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXGeneral.ttf,sha256=FnN4Ax4t3cYhbWeBnJJg6aBv_ExHjk4jy5im_USxg8I,448228
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXGeneralBol.ttf,sha256=6FM9xwg_o0a9oZM9YOpKg7Z9CUW86vGzVB-CtKDixqA,237360
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXGeneralBolIta.ttf,sha256=mHiP1LpI37sr0CbA4gokeosGxzcoeWKLemuw1bsJc2w,181152
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXGeneralItalic.ttf,sha256=bPyzM9IrfDxiO9_UAXTxTIXD1nMcphZsHtyAFA6uhSc,175040
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXNonUni.ttf,sha256=Ulb34CEzWsSFTRgPDovxmJZOwvyCAXYnbhaqvGU3u1c,59108
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXNonUniBol.ttf,sha256=XRBqW3jR_8MBdFU0ObhiV7-kXwiBIMs7QVClHcT5tgs,30512
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXNonUniBolIta.ttf,sha256=pb22DnbDf2yQqizotc3wBDqFGC_g27YcCGJivH9-Le8,41272
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXNonUniIta.ttf,sha256=BMr9pWiBv2YIZdq04X4c3CgL6NPLUPrl64aV1N4w9Ug,46752
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizFiveSymReg.ttf,sha256=wYuH1gYUpCuusqItRH5kf9p_s6mUD-9X3L5RvRtKSxs,13656
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizFourSymBol.ttf,sha256=yNdvjUoSmsZCULmD7SVq9HabndG9P4dPhboL1JpAf0s,12228
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizFourSymReg.ttf,sha256=-9xVMYL4_1rcO8FiCKrCfR4PaSmKtA42ddLGqwtei1w,15972
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizOneSymBol.ttf,sha256=cYexyo8rZcdqMlpa9fNF5a2IoXLUTZuIvh0JD1Qp0i4,12556
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizOneSymReg.ttf,sha256=0lbHzpndzJmO8S42mlkhsz5NbvJLQCaH5Mcc7QZRDzc,19760
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymBol.ttf,sha256=3eBc-VtYbhQU3BnxiypfO6eAzEu8BdDvtIJSFbkS2oY,12192
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizThreeSymReg.ttf,sha256=XFSKCptbESM8uxHtUFSAV2cybwxhSjd8dWVByq6f3w0,15836
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymBol.ttf,sha256=MUCYHrA0ZqFiSE_PjIGlJZgMuv79aUgQqE7Dtu3kuo0,12116
|
|
||||||
matplotlib/mpl-data/fonts/ttf/STIXSizTwoSymReg.ttf,sha256=_sdxDuEwBDtADpu9CyIXQxV7sIqA2TZVBCUiUjq5UCk,15704
|
|
||||||
matplotlib/mpl-data/fonts/ttf/cmb10.ttf,sha256=B0SXtQxD6ldZcYFZH5iT04_BKofpUQT1ZX_CSB9hojo,25680
|
|
||||||
matplotlib/mpl-data/fonts/ttf/cmex10.ttf,sha256=ryjwwXByOsd2pxv6WVrKCemNFa5cPVTOGa_VYZyWqQU,21092
|
|
||||||
matplotlib/mpl-data/fonts/ttf/cmmi10.ttf,sha256=MJKWW4gR_WpnZXmWZIRRgfwd0TMLk3-RWAjEhdMWI00,32560
|
|
||||||
matplotlib/mpl-data/fonts/ttf/cmr10.ttf,sha256=Tdl2GwWMAJ25shRfVe5mF9CTwnPdPWxbPkP_YRD6m_Y,26348
|
|
||||||
matplotlib/mpl-data/fonts/ttf/cmss10.ttf,sha256=ffkag9BbLkcexjjLC0NaNgo8eSsJ_EKn2mfpHy55EVo,20376
|
|
||||||
matplotlib/mpl-data/fonts/ttf/cmsy10.ttf,sha256=uyJu2TLz8QDNDlL15JEu5VO0G2nnv9uNOFTbDrZgUjI,29396
|
|
||||||
matplotlib/mpl-data/fonts/ttf/cmtt10.ttf,sha256=YhHwmuk1mZka_alwwkZp2tGnfiU9kVYk-_IS9wLwcdc,28136
|
|
||||||
matplotlib/mpl-data/fonts/ttf/local.conf,sha256=wcg11V6oGPKn4c43Z_bGGo9TCyvjMWq9msuzMmdtRTo,988
|
|
||||||
matplotlib/mpl-data/images/back.gif,sha256=sdkxFRAh-Mgs44DTvruO5OxcI3Av9CS1g5MqMA_DDkQ,608
|
|
||||||
matplotlib/mpl-data/images/back.pdf,sha256=ZR7CJo_dAeCM-KlaGvskgtHQyRtrPIolc8REOmcoqJk,1623
|
|
||||||
matplotlib/mpl-data/images/back.png,sha256=E4dGf4Gnz1xJ1v2tMygHV0YNQgShreDeVApaMb-74mU,380
|
|
||||||
matplotlib/mpl-data/images/back.svg,sha256=yRdMiKsa-awUm2x_JE_rEV20rNTa7FInbFBEoMo-6ik,1512
|
|
||||||
matplotlib/mpl-data/images/back_large.gif,sha256=tqCtecrxNrPuDCUj7FGs8UXWftljKcwgp5cSBBhXwiQ,799
|
|
||||||
matplotlib/mpl-data/images/back_large.png,sha256=9A6hUSQeszhYONE4ZuH3kvOItM0JfDVu6tkfromCbsQ,620
|
|
||||||
matplotlib/mpl-data/images/filesave.gif,sha256=wAyNwOPd9c-EIPwcUAlqHSfLmxq167nhDVppOWPy9UA,723
|
|
||||||
matplotlib/mpl-data/images/filesave.pdf,sha256=P1EPPV2g50WTt8UaX-6kFoTZM1xVqo6S2H6FJ6Zd1ec,1734
|
|
||||||
matplotlib/mpl-data/images/filesave.png,sha256=b7ctucrM_F2mG-DycTedG_a_y4pHkx3F-zM7l18GLhk,458
|
|
||||||
matplotlib/mpl-data/images/filesave.svg,sha256=oxPVbLS9Pzelz71C1GCJWB34DZ0sx_pUVPRHBrCZrGs,2029
|
|
||||||
matplotlib/mpl-data/images/filesave_large.gif,sha256=IXrenlwu3wwO8WTRvxHt_q62NF6ZWyqk3jZhm6GE-G8,1498
|
|
||||||
matplotlib/mpl-data/images/filesave_large.png,sha256=LNbRD5KZ3Kf7nbp-stx_a1_6XfGBSWUfDdpgmnzoRvk,720
|
|
||||||
matplotlib/mpl-data/images/forward.gif,sha256=VNL9R-dECOX7wUAYPtU_DWn5hwi3SwLR17DhmBvUIxE,590
|
|
||||||
matplotlib/mpl-data/images/forward.pdf,sha256=KIqIL4YId43LkcOxV_TT5uvz1SP8k5iUNUeJmAElMV8,1630
|
|
||||||
matplotlib/mpl-data/images/forward.png,sha256=pKbLepgGiGeyY2TCBl8svjvm7Z4CS3iysFxcq4GR-wk,357
|
|
||||||
matplotlib/mpl-data/images/forward.svg,sha256=NnQDOenfjsn-o0aJMUfErrP320Zcx9XHZkLh0cjMHsk,1531
|
|
||||||
matplotlib/mpl-data/images/forward_large.gif,sha256=H6Jbcc7qJwHJAE294YqI5Bm-5irofX40cKRvYdrG_Ig,786
|
|
||||||
matplotlib/mpl-data/images/forward_large.png,sha256=36h7m7DZDHql6kkdpNPckyi2LKCe_xhhyavWARz_2kQ,593
|
|
||||||
matplotlib/mpl-data/images/hand.gif,sha256=3lRfmAqQU7A2t1YXXsB9IbwzK7FaRh-IZO84D5-xCrw,1267
|
|
||||||
matplotlib/mpl-data/images/hand.pdf,sha256=hspwkNY915KPD7AMWnVQs7LFPOtlcj0VUiLu76dMabQ,4172
|
|
||||||
matplotlib/mpl-data/images/hand.png,sha256=2cchRETGKa0hYNKUxnJABwkyYXEBPqJy_VqSPlT0W2Q,979
|
|
||||||
matplotlib/mpl-data/images/hand.svg,sha256=tsVIES_nINrAbH4FqdsCGOx0SVE37vcofSYBhnnaOP0,4888
|
|
||||||
matplotlib/mpl-data/images/hand_large.gif,sha256=H5IHmVTvOqHQb9FZ_7g7AlPt9gv-zRq0L5_Q9B7OuvU,973
|
|
||||||
matplotlib/mpl-data/images/help.pdf,sha256=CeE978IMi0YWznWKjIT1R8IrP4KhZ0S7usPUvreSgcA,1813
|
|
||||||
matplotlib/mpl-data/images/help.png,sha256=s4pQrqaQ0py8I7vc9hv3BI3DO_tky-7YBMpaHuBDCBY,472
|
|
||||||
matplotlib/mpl-data/images/help.ppm,sha256=mVPvgwcddzCM-nGZd8Lnl_CorzDkRIXQE17b7qo8vlU,1741
|
|
||||||
matplotlib/mpl-data/images/help.svg,sha256=KXabvQhqIWen_t2SvZuddFYa3S0iI3W8cAKm3s1fI8Q,1870
|
|
||||||
matplotlib/mpl-data/images/help_large.png,sha256=1IwEyWfGRgnoCWM-r9CJHEogTJVD5n1c8LXTK4AJ4RE,747
|
|
||||||
matplotlib/mpl-data/images/help_large.ppm,sha256=MiCSKp1Su88FXOi9MTtkQDA2srwbX3w5navi6cneAi4,6925
|
|
||||||
matplotlib/mpl-data/images/home.gif,sha256=NKuFM7tTtFngdfsOpJ4AxYTL8PYS5GWKAoiJjBMwLlU,666
|
|
||||||
matplotlib/mpl-data/images/home.pdf,sha256=e0e0pI-XRtPmvUCW2VTKL1DeYu1pvPmUUeRSgEbWmik,1737
|
|
||||||
matplotlib/mpl-data/images/home.png,sha256=IcFdAAUa6_A0qt8IO3I8p4rpXpQgAlJ8ndBECCh7C1w,468
|
|
||||||
matplotlib/mpl-data/images/home.svg,sha256=n_AosjJVXET3McymFuHgXbUr5vMLdXK2PDgghX8Cch4,1891
|
|
||||||
matplotlib/mpl-data/images/home_large.gif,sha256=k86PJCgED46sCFkOlUYHA0s5U7OjRsc517bpAtU2JSw,1422
|
|
||||||
matplotlib/mpl-data/images/home_large.png,sha256=uxS2O3tWOHh1iau7CaVV4ermIJaZ007ibm5Z3i8kXYg,790
|
|
||||||
matplotlib/mpl-data/images/matplotlib.pdf,sha256=BkSUf-2xoij-eXfpV2t7y1JFKG1zD1gtV6aAg3Xi_wE,22852
|
|
||||||
matplotlib/mpl-data/images/matplotlib.png,sha256=w8KLRYVa-voUZXa41hgJauQuoois23f3NFfdc72pUYY,1283
|
|
||||||
matplotlib/mpl-data/images/matplotlib.ppm,sha256=voTyfqUGvirNbkrsJGMwJT0z0g_KFWAnO8Hn8J4XXh8,1741
|
|
||||||
matplotlib/mpl-data/images/matplotlib.svg,sha256=QiTIcqlQwGaVPtHsEk-vtmJk1wxwZSvijhqBe_b9VCI,62087
|
|
||||||
matplotlib/mpl-data/images/matplotlib_large.png,sha256=ElRoue9grUqkZXJngk-nvh4GKfpvJ4gE69WryjCbX5U,3088
|
|
||||||
matplotlib/mpl-data/images/move.gif,sha256=FN52MptH4FZiwmV2rQgYCO2FvO3m5LtqYv8jk6Xbeyk,679
|
|
||||||
matplotlib/mpl-data/images/move.pdf,sha256=CXk3PGK9WL5t-5J-G2X5Tl-nb6lcErTBS5oUj2St6aU,1867
|
|
||||||
matplotlib/mpl-data/images/move.png,sha256=TmjR41IzSzxGbhiUcV64X0zx2BjrxbWH3cSKvnG2vzc,481
|
|
||||||
matplotlib/mpl-data/images/move.svg,sha256=_ZKpcwGD6DMTkZlbyj0nQbT8Ygt5vslEZ0OqXaXGd4E,2509
|
|
||||||
matplotlib/mpl-data/images/move_large.gif,sha256=RMIAr-G9OOY7vWC04oN6qv5TAHJxhQGhLsw_bNsvWbg,951
|
|
||||||
matplotlib/mpl-data/images/move_large.png,sha256=Skjz2nW_RTA5s_0g88gdq2hrVbm6DOcfYW4Fu42Fn9U,767
|
|
||||||
matplotlib/mpl-data/images/qt4_editor_options.pdf,sha256=2qu6GVyBrJvVHxychQoJUiXPYxBylbH2j90QnytXs_w,1568
|
|
||||||
matplotlib/mpl-data/images/qt4_editor_options.png,sha256=EryQjQ5hh2dwmIxtzCFiMN1U6Tnd11p1CDfgH5ZHjNM,380
|
|
||||||
matplotlib/mpl-data/images/qt4_editor_options.svg,sha256=E00YoX7u4NrxMHm_L1TM8PDJ88bX5qRdCrO-Uj59CEA,1244
|
|
||||||
matplotlib/mpl-data/images/qt4_editor_options_large.png,sha256=-Pd-9Vh5aIr3PZa8O6Ge_BLo41kiEnpmkdDj8a11JkY,619
|
|
||||||
matplotlib/mpl-data/images/subplots.gif,sha256=QfhmUdcrko08-WtrzCJUjrVFDTvUZCJEXpARNtzEwkg,691
|
|
||||||
matplotlib/mpl-data/images/subplots.pdf,sha256=Q0syPMI5EvtgM-CE-YXKOkL9eFUAZnj_X2Ihoj6R4p4,1714
|
|
||||||
matplotlib/mpl-data/images/subplots.png,sha256=MUfCItq3_yzb9yRieGOglpn0Y74h8IA7m5i70B63iRc,445
|
|
||||||
matplotlib/mpl-data/images/subplots.svg,sha256=8acBogXIr9OWGn1iD6mUkgahdFZgDybww385zLCLoIs,2130
|
|
||||||
matplotlib/mpl-data/images/subplots_large.gif,sha256=Ff3ERmtVAaGP9i1QGUNnIIKac6LGuSW2Qf4DrockZSI,1350
|
|
||||||
matplotlib/mpl-data/images/subplots_large.png,sha256=Edu9SwVMQEXJZ5ogU5cyW7VLcwXJdhdf-EtxxmxdkIs,662
|
|
||||||
matplotlib/mpl-data/images/zoom_to_rect.gif,sha256=mTX6h9fh2W9zmvUYqeibK0TZ7qIMKOB1nAXMpD_jDys,696
|
|
||||||
matplotlib/mpl-data/images/zoom_to_rect.pdf,sha256=SEvPc24gfZRpl-dHv7nx8KkxPyU66Kq4zgQTvGFm9KA,1609
|
|
||||||
matplotlib/mpl-data/images/zoom_to_rect.png,sha256=aNz3QZBrIgxu9E-fFfaQweCVNitGuDUFoC27e5NU2L4,530
|
|
||||||
matplotlib/mpl-data/images/zoom_to_rect.svg,sha256=1vRxr3cl8QTwTuRlQzD1jxu0fXZofTJ2PMgG97E7Bco,1479
|
|
||||||
matplotlib/mpl-data/images/zoom_to_rect_large.gif,sha256=nx5LUpTAH6ZynM3ZfZDS-wR87jbMUsUnyQ27NGkV0_c,1456
|
|
||||||
matplotlib/mpl-data/images/zoom_to_rect_large.png,sha256=V6pkxmm6VwFExdg_PEJWdK37HB7k3cE_corLa7RbUMk,1016
|
|
||||||
matplotlib/mpl-data/matplotlibrc,sha256=-CfOXSRZ7WaduxxmjJjX-_PLETPwuzleTpGhTLd7ylU,33061
|
|
||||||
matplotlib/mpl-data/sample_data/Minduka_Present_Blue_Pack.png,sha256=XnKGiCanpDKalQ5anvo5NZSAeDP7fyflzQAaivuc0IE,13634
|
|
||||||
matplotlib/mpl-data/sample_data/None_vs_nearest-pdf.png,sha256=5CPvcG3SDNfOXx39CMKHCNS9JKZ-fmOUwIfpppNXsQ0,106228
|
|
||||||
matplotlib/mpl-data/sample_data/README.txt,sha256=ABz19VBKfGewdY39QInG9Qccgn1MTYV3bT5Ph7TCy2Y,128
|
|
||||||
matplotlib/mpl-data/sample_data/aapl.npz,sha256=GssVYka_EccteiXbNRJJ5GsuqU7G8F597qX7srYXZsw,107503
|
|
||||||
matplotlib/mpl-data/sample_data/ada.png,sha256=X1hjJK1_1Nc8DN-EEhey3G7Sq8jBwQDKNSl4cCAE0uY,308313
|
|
||||||
matplotlib/mpl-data/sample_data/axes_grid/bivariate_normal.npy,sha256=DpWZ9udAh6ospYqneEa27D6EkRgORFwHosacZXVu98U,1880
|
|
||||||
matplotlib/mpl-data/sample_data/ct.raw.gz,sha256=LDvvgH-mycRQF2D29-w5MW94ZI0opvwKUoFI8euNpMk,256159
|
|
||||||
matplotlib/mpl-data/sample_data/data_x_x2_x3.csv,sha256=A0SU3buOUGhT-NI_6LQ6p70fFSIU3iLFdgzvzrKR6SE,132
|
|
||||||
matplotlib/mpl-data/sample_data/demodata.csv,sha256=MRybziqnyrqMCH9qG7Mr6BwcohIhftVG5dejXV2AX2M,659
|
|
||||||
matplotlib/mpl-data/sample_data/eeg.dat,sha256=KGVjFt8ABKz7p6XZirNfcxSTOpGGNuyA8JYErRKLRBc,25600
|
|
||||||
matplotlib/mpl-data/sample_data/embedding_in_wx3.xrc,sha256=cUqVw5vDHNSZoaO4J0ebZUf5SrJP36775abs7R9Bclg,2186
|
|
||||||
matplotlib/mpl-data/sample_data/goog.npz,sha256=QAkXzzDmtmT3sNqT18dFhg06qQCNqLfxYNLdEuajGLE,22845
|
|
||||||
matplotlib/mpl-data/sample_data/grace_hopper.jpg,sha256=qMptc0dlcDsJcoq0f-WfRz2Trjln_CTHwCiMPHrbcTA,61306
|
|
||||||
matplotlib/mpl-data/sample_data/grace_hopper.png,sha256=MCf0ju2kpC40srQ0xw4HEyOoKhLL4khP3jHfU9_dR7s,628280
|
|
||||||
matplotlib/mpl-data/sample_data/jacksboro_fault_dem.npz,sha256=1JP1CjPoKkQgSUxU0fyhU50Xe9wnqxkLxf5ukvYvtjc,174061
|
|
||||||
matplotlib/mpl-data/sample_data/logo2.png,sha256=ITxkJUsan2oqXgJDy6DJvwJ4aHviKeWGnxPkTjXUt7A,33541
|
|
||||||
matplotlib/mpl-data/sample_data/membrane.dat,sha256=q3lbQpIBpbtXXGNw1eFwkN_PwxdDGqk4L46IE2b0M1c,48000
|
|
||||||
matplotlib/mpl-data/sample_data/msft.csv,sha256=GArKb0O3DgKZRsKdJf6lX3rMSf-PCekIiBoLNdgF7Mk,3211
|
|
||||||
matplotlib/mpl-data/sample_data/percent_bachelors_degrees_women_usa.csv,sha256=TzoqamsV_N3d3lW7SKmj14zZVX4FOOg9jJcsC5U9pbA,5681
|
|
||||||
matplotlib/mpl-data/sample_data/s1045.ima.gz,sha256=MrQk1k9it-ccsk0p_VOTitVmTWCAVaZ6srKvQ2n4uJ4,33229
|
|
||||||
matplotlib/mpl-data/stylelib/Solarize_Light2.mplstyle,sha256=PECeO60wwJe2sSDvxapBJRuKGek0qLcoaN8qOX6tgNQ,1255
|
|
||||||
matplotlib/mpl-data/stylelib/_classic_test.mplstyle,sha256=XnegNNz-4tr8vnTgI1IakyHYPryuInJD1GidF9a8n6E,25458
|
|
||||||
matplotlib/mpl-data/stylelib/bmh.mplstyle,sha256=-KbhaI859BITHIoyUZIfpQDjfckgLAlDAS_ydKsm6mc,712
|
|
||||||
matplotlib/mpl-data/stylelib/classic.mplstyle,sha256=brJE6RZ114bTIVwl7yBm2sd6s0TyRrUq9t-qi--Ih20,25639
|
|
||||||
matplotlib/mpl-data/stylelib/dark_background.mplstyle,sha256=-EGmoFm_35Zk7oRp29UalT56HsOSuJbYMeQGdAATnz4,477
|
|
||||||
matplotlib/mpl-data/stylelib/fast.mplstyle,sha256=yTa2YEIIP9xi5V_G0p2vSlxghuhNwjRi9gPECMxyRiM,288
|
|
||||||
matplotlib/mpl-data/stylelib/fivethirtyeight.mplstyle,sha256=WNUmAFuBPcqQPVgt6AS1ldy8Be2XO01N-1YQL__Q6ZY,832
|
|
||||||
matplotlib/mpl-data/stylelib/ggplot.mplstyle,sha256=xhjLwr8hiikEXKy8APMy0Bmvtz1g0WnG84gX7e9lArs,957
|
|
||||||
matplotlib/mpl-data/stylelib/grayscale.mplstyle,sha256=KCLg-pXpns9cnKDXKN2WH6mV41OH-6cbT-5zKQotSdw,526
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-bright.mplstyle,sha256=pDqn3-NUyVLvlfkYs8n8HzNZvmslVMChkeH-HtZuJIc,144
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-colorblind.mplstyle,sha256=eCSzFj5_2vR6n5qu1rHE46wvSVGZcdVqz85ov40ZsH8,148
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-dark-palette.mplstyle,sha256=p5ABKNQHRG7bk4HXqMQrRBjDlxGAo3RCXHdQmP7g-Ng,142
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-dark.mplstyle,sha256=I4xQ75vE5_9X4k0cNDiqhhnF3OcrZ2xlPX8Ll7OCkoE,667
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-darkgrid.mplstyle,sha256=2bXOSzS5gmPzRBrRmzVWyhg_7ZaBRQ6t_-O-cRuyZoA,670
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-deep.mplstyle,sha256=44dLcXjjRgR-6yaopgGRInaVgz3jk8VJVQTbBIcxRB0,142
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-muted.mplstyle,sha256=T4o3jvqKD_ImXDkp66XFOV_xrBVFUolJU34JDFk1Xkk,143
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-notebook.mplstyle,sha256=PcvZQbYrDdducrNlavBPmQ1g2minio_9GkUUFRdgtoM,382
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-paper.mplstyle,sha256=n0mboUp2C4Usq2j6tNWcu4TZ_YT4-kKgrYO0t-rz1yw,393
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-pastel.mplstyle,sha256=8nV8qRpbUrnFZeyE6VcQ1oRuZPLil2W74M2U37DNMOE,144
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-poster.mplstyle,sha256=dUaKqTE4MRfUq2rWVXbbou7kzD7Z9PE9Ko8aXLza8JA,403
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-talk.mplstyle,sha256=7FnBaBEdWBbncTm6_ER-EQVa_bZgU7dncgez-ez8R74,403
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-ticks.mplstyle,sha256=CITZmZFUFp40MK2Oz8tI8a7WRoCizQU9Z4J172YWfWw,665
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-white.mplstyle,sha256=WjJ6LEU6rlCwUugToawciAbKP9oERFHr9rfFlUrdTx0,665
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn-whitegrid.mplstyle,sha256=ec4BjsNzmOvHptcJ3mdPxULF3S1_U1EUocuqfIpw-Nk,664
|
|
||||||
matplotlib/mpl-data/stylelib/seaborn.mplstyle,sha256=_Xu6qXKzi4b3GymCOB1b1-ykKTQ8xhDliZ8ezHGTiAs,1130
|
|
||||||
matplotlib/mpl-data/stylelib/tableau-colorblind10.mplstyle,sha256=BsirZVd1LmPWT4tBIz6loZPjZcInoQrIGfC7rvzqmJw,190
|
|
||||||
matplotlib/offsetbox.py,sha256=KZdfpLd6rYRjmRAboaRBuKag8lDUCzMEeykgmp4irx8,55449
|
|
||||||
matplotlib/patches.py,sha256=wUAZISlGrUWm_bxDh88ryGoWkOXrJZttyanOnZWdDQ4,151034
|
|
||||||
matplotlib/path.py,sha256=Ys7EYJOyCTAQm0fROCvHdgg2Kxp5AZwkJhG42hw3QZc,37139
|
|
||||||
matplotlib/patheffects.py,sha256=l7huZDCPG8RS3LLq67EztQg7s64zngoUN1R60zxpp5k,14139
|
|
||||||
matplotlib/projections/__init__.py,sha256=-_LPKYaHOdfgxYdCtPs-nDJvjQ8ZTXIuAcC0v_1_zFY,2874
|
|
||||||
matplotlib/projections/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/projections/__pycache__/geo.cpython-36.pyc,,
|
|
||||||
matplotlib/projections/__pycache__/polar.cpython-36.pyc,,
|
|
||||||
matplotlib/projections/geo.py,sha256=EI7YSw7UbZTsb31dCTAH0aJRXQct-cOnm45alvAj3Cc,18909
|
|
||||||
matplotlib/projections/polar.py,sha256=6X8t4p4LAx8h7O7WJ-T5SDhbKaNsA8X6dcXIl4z4wgM,55006
|
|
||||||
matplotlib/pylab.py,sha256=lNqCpuoTGfjprzltmvpQ3DYthdj_FJf6PRFfeEv3HhY,10331
|
|
||||||
matplotlib/pyplot.py,sha256=5AwtjpRSBFNyRyKTVrWIWSl_EPVPyPQ6RpZdOidI1nI,110436
|
|
||||||
matplotlib/quiver.py,sha256=9eP6xfF6pOOQpQ3zxbvedZewUYE236OS9gwRNZhPvws,45910
|
|
||||||
matplotlib/rcsetup.py,sha256=aFy_3zYioc0oCEmRdvqIGlCRbSA6hj7BKKTO-k2__Gw,58203
|
|
||||||
matplotlib/sankey.py,sha256=tMlZvy0CmG55hpDVY6Dqk461x6vvSxvhDmkIXJG_qDo,38734
|
|
||||||
matplotlib/scale.py,sha256=l9r0dmYGeMCLkqcrcJRIBFj6tJq6Znvc9ZoRTynHLoI,19164
|
|
||||||
matplotlib/sphinxext/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
||||||
matplotlib/sphinxext/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/__pycache__/mathmpl.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/__pycache__/plot_directive.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/mathmpl.py,sha256=sdWYQ5aBB9hGiEY5E3j9By-DaFENDNHtTHxLMRQXrWc,3919
|
|
||||||
matplotlib/sphinxext/plot_directive.py,sha256=uMBuVvlmjuLUr6r1iDm42L1yN7A1r_knUXrcfxPtTBU,27082
|
|
||||||
matplotlib/sphinxext/tests/__init__.py,sha256=gTnBimTaMh3t3WC49SvyeSH0rsbWxQgjAsr5H8HRDig,23
|
|
||||||
matplotlib/sphinxext/tests/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/tests/__pycache__/conftest.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/tests/__pycache__/test_tinypages.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/tests/conftest.py,sha256=Ph6QZKdfAnkPwU52StddC-uwtCHfANKX1dDXgtX122g,213
|
|
||||||
matplotlib/sphinxext/tests/test_tinypages.py,sha256=uU72lyon36bSL2xF_90zfkblEolK_Cj5VGrfEpv0_wU,2058
|
|
||||||
matplotlib/sphinxext/tests/tinypages/__pycache__/conf.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/tests/tinypages/__pycache__/range4.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/tests/tinypages/__pycache__/range6.cpython-36.pyc,,
|
|
||||||
matplotlib/sphinxext/tests/tinypages/_static/README.txt,sha256=1nnoizmUuHn5GKx8RL6MwJPlkyGmu_KHhYIMTDSWUNM,303
|
|
||||||
matplotlib/sphinxext/tests/tinypages/conf.py,sha256=0_a4wyqPA9oaOFpLLpSEzkZI-hwtyRbqLWBx9nf0sLA,8432
|
|
||||||
matplotlib/sphinxext/tests/tinypages/index.rst,sha256=kLSy7c3SoIAVsKOFkbzB4zFVzk3HW3d_rJHlHcNGBAg,446
|
|
||||||
matplotlib/sphinxext/tests/tinypages/range4.py,sha256=fs2krzi9sY9ysmJRQCdGs_Jh1L9vDXDrNse7c0aX_Rw,81
|
|
||||||
matplotlib/sphinxext/tests/tinypages/range6.py,sha256=a2EaHrNwXz4GJqhRuc7luqRpt7sqLKhTKeid9Drt2QQ,281
|
|
||||||
matplotlib/sphinxext/tests/tinypages/some_plots.rst,sha256=C9rwV9UVlhFvxm8VqV6PoAP1AQ8Kk0LGZI9va4joif0,2156
|
|
||||||
matplotlib/spines.py,sha256=dctV_-aWkJuc52EgZof2aPjcelWVKOXttz66sZyrPkk,21217
|
|
||||||
matplotlib/stackplot.py,sha256=4YbKAU349muVPKAkNSdt9r0tG_hcHj5iCy1Dp9otWGA,3977
|
|
||||||
matplotlib/streamplot.py,sha256=_ARll6zMPC0-eLPBW-svHOs4wcG8W2tTABIcFWVwN3I,21933
|
|
||||||
matplotlib/style/__init__.py,sha256=EExOAUAq3u_rscUwkfKtZoEgLA5npmltCrYZOP9ftjw,67
|
|
||||||
matplotlib/style/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/style/__pycache__/core.cpython-36.pyc,,
|
|
||||||
matplotlib/style/core.py,sha256=8LwOz9KtNuYLOzYiUyeUJNN_w-y_LsAGRhTN_SvCftA,7901
|
|
||||||
matplotlib/table.py,sha256=fp3kcp88hOthPbc02LwZbnvbQ3__vne_LpOk2Bw_e2o,21917
|
|
||||||
matplotlib/testing/__init__.py,sha256=z-NqrY_YBuiQGl4QVqRYAGOZcyGnSIcI16XjAbmIsjM,1498
|
|
||||||
matplotlib/testing/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/__pycache__/compare.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/__pycache__/conftest.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/__pycache__/decorators.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/__pycache__/determinism.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/__pycache__/disable_internet.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/__pycache__/exceptions.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/compare.py,sha256=vklvca9q772lqZ7wsaHxI2n3MY65MCHnKTjFKV48R6s,17552
|
|
||||||
matplotlib/testing/conftest.py,sha256=NYJUijf2Rm25TwLysGfCNFKPEtD_zK9sWG5lcfUZS58,3253
|
|
||||||
matplotlib/testing/decorators.py,sha256=zOMJfeD75zD8OA5MrxsRCTOLiDGlZaMERNGw5LyBrN8,17704
|
|
||||||
matplotlib/testing/determinism.py,sha256=4GADMpjbO3127YtWNK2bNB2HTUWUkyEEQB397LGCpB0,4389
|
|
||||||
matplotlib/testing/disable_internet.py,sha256=YE5szJX8tNHyK8j90uPUOJO76MmUHKrXFqRnj_xGyu0,4747
|
|
||||||
matplotlib/testing/exceptions.py,sha256=72QmjiHG7DwxSvlJf8mei-hRit5AH3NKh0-osBo4YbY,138
|
|
||||||
matplotlib/testing/jpl_units/Duration.py,sha256=FDR6rn_Yvc3XlfLuJe4KI6ALMSJCytZGZX5xIQz-yuc,6875
|
|
||||||
matplotlib/testing/jpl_units/Epoch.py,sha256=1Htkm5XtuToVC5otGBWgsOlhPVH4FcV8EA9CkNHNwI8,7859
|
|
||||||
matplotlib/testing/jpl_units/EpochConverter.py,sha256=NbxO32pLLg7hMuiiB2QiAuiipGAfT6q2tSSf9Xys26o,5424
|
|
||||||
matplotlib/testing/jpl_units/StrConverter.py,sha256=g2EEkhg4ZdT8PSB-4MjPDNRbilRp7Wi72mTnsv7Ty7g,5295
|
|
||||||
matplotlib/testing/jpl_units/UnitDbl.py,sha256=b_4G6NaHJUl4Xd-NE1BZzombIHyOGO0i6Vb0MQGBeuw,9725
|
|
||||||
matplotlib/testing/jpl_units/UnitDblConverter.py,sha256=9AIYgnnR78G0E0D3rI20IgTawiCYC2bA_izCmy5csNo,5419
|
|
||||||
matplotlib/testing/jpl_units/UnitDblFormatter.py,sha256=bE8adtYRXG5gzQObrzR-ROLwkFkpru7GDjjUIlRh7Ss,1416
|
|
||||||
matplotlib/testing/jpl_units/__init__.py,sha256=fWVROJbodccLPtdnFzhV8ItE1Dl1uinQc9HcYz4hmpE,3062
|
|
||||||
matplotlib/testing/jpl_units/__pycache__/Duration.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/jpl_units/__pycache__/Epoch.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/jpl_units/__pycache__/EpochConverter.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/jpl_units/__pycache__/StrConverter.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/jpl_units/__pycache__/UnitDbl.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/jpl_units/__pycache__/UnitDblConverter.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/jpl_units/__pycache__/UnitDblFormatter.cpython-36.pyc,,
|
|
||||||
matplotlib/testing/jpl_units/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__init__.py,sha256=g3AQvrbsKOFFnkrCoQzlqr1cXCDV0LvGsPnvPhNOrYs,463
|
|
||||||
matplotlib/tests/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/conftest.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_afm.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_agg.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_animation.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_arrow_patches.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_artist.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_axes.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_bases.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_nbagg.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_pdf.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_pgf.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_ps.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_qt4.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_qt5.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_svg.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backend_tools.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_backends_interactive.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_basic.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_bbox_tight.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_category.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_cbook.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_collections.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_colorbar.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_colors.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_compare_images.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_constrainedlayout.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_container.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_contour.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_cycles.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_dates.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_dviread.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_figure.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_font_manager.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_gridspec.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_image.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_legend.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_lines.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_marker.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_mathtext.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_matplotlib.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_mlab.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_offsetbox.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_patches.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_path.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_patheffects.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_pickle.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_png.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_preprocess_data.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_pyplot.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_quiver.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_rcparams.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_sankey.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_scale.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_simplification.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_skew.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_spines.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_streamplot.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_style.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_subplots.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_table.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_texmanager.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_text.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_ticker.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_tightlayout.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_transforms.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_triangulation.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_ttconv.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_type1font.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_units.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_usetex.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/__pycache__/test_widgets.cpython-36.pyc,,
|
|
||||||
matplotlib/tests/cmr10.pfb,sha256=_c7eh5QBjfXytY8JBfsgorQY7Y9ntz7hJEWFXfvlsb4,35752
|
|
||||||
matplotlib/tests/conftest.py,sha256=QtpdWPUoXL_9F8WIytDc3--h0nPjbo8PToig7svIT1Y,258
|
|
||||||
matplotlib/tests/mpltest.ttf,sha256=Jwb2O5KRVk_2CMqnhL0igeI3iGQCY3eChyS16N589zE,2264
|
|
||||||
matplotlib/tests/test_afm.py,sha256=64Qvm_dkFOh88o8oBouswePl1kgSUE_37jrRN0__jng,2218
|
|
||||||
matplotlib/tests/test_agg.py,sha256=aorEpO-NshLjutQQV-yF1ZikLIqss9fF7ouXsjG7o3s,7299
|
|
||||||
matplotlib/tests/test_animation.py,sha256=rMZwOf3WEVT7xGYDYc2xkx6XjW8_mQ0kFL3wouIaZ0g,8267
|
|
||||||
matplotlib/tests/test_arrow_patches.py,sha256=dfRDXMnKZt0FBY5JAtjKx8qbCnHCzAQoFhuEy3EzhPg,5950
|
|
||||||
matplotlib/tests/test_artist.py,sha256=PjaIk-F4jN0QEhPm3ZinWIMOB04LesHnMe0xmLmlm4g,9203
|
|
||||||
matplotlib/tests/test_axes.py,sha256=QQbwWolkRT1jytrYIp1tzsJN1tbpqF7tL0M4mIAa148,189994
|
|
||||||
matplotlib/tests/test_backend_bases.py,sha256=_jib8QrQa98qAarlE01aIk7Hkfs1gQbcVDIMGzsVr2U,3585
|
|
||||||
matplotlib/tests/test_backend_nbagg.py,sha256=TvfgH4gOzFxGRvNohjn1c9vBqEKL4OI6IWwBnZdBWgg,1066
|
|
||||||
matplotlib/tests/test_backend_pdf.py,sha256=6D7Yah7exx8xFwGsfm5ZcjYcxnRtZs0D8l3z1UZ7qPI,7925
|
|
||||||
matplotlib/tests/test_backend_pgf.py,sha256=GEYeAku09gn3erePaZMHRWVUBAFwuzdAXmrnFl5a4mc,8157
|
|
||||||
matplotlib/tests/test_backend_ps.py,sha256=KX7Xak4Yb2UMD6KEiM1-UJTtA5MwbSYhRg3l5gyofWI,4505
|
|
||||||
matplotlib/tests/test_backend_qt4.py,sha256=gNbJ4BFnwhZ-LGZ9HlXMYtQPsgPAHoYSCs8TDCFmPUA,4199
|
|
||||||
matplotlib/tests/test_backend_qt5.py,sha256=Pf4B8pNrh8qeh-vKeVeip6_fr2UhCy1fH6HDxLC0Edg,6574
|
|
||||||
matplotlib/tests/test_backend_svg.py,sha256=fbp1hAcPoxetMsfRdZ50ZttMX-vY1eqSiBfFtCXKB80,5439
|
|
||||||
matplotlib/tests/test_backend_tools.py,sha256=C-B7NCkyWsQ5KzQEnI5Be16DsAHHZJU9P5v9--wsF-o,501
|
|
||||||
matplotlib/tests/test_backends_interactive.py,sha256=AuioLNYNgZZ4Q5HJWh58CLsX5CRW0ZJBQrPYZn6DE68,5012
|
|
||||||
matplotlib/tests/test_basic.py,sha256=jexF5eJw4gwb0hYMx6_u8wJa3hgtwKclag1Lhws_f7U,730
|
|
||||||
matplotlib/tests/test_bbox_tight.py,sha256=ZcHz4qXTRhiVOiGQgSaqQgA7IjsRh84qDYOoVe82IH4,3280
|
|
||||||
matplotlib/tests/test_category.py,sha256=HJ1oBEZD0A6wHUnrz49kymv93UX0fkkdFCclIBrI09w,10303
|
|
||||||
matplotlib/tests/test_cbook.py,sha256=Tm8U0CgUpWthx7xQ_DicpslnOHlXnG9Wr0bO9E-YJAM,15647
|
|
||||||
matplotlib/tests/test_collections.py,sha256=no4WHkfoo6s6Mlc0hrrd_ssl9bpapGGTMJ7WsvCh8Hg,22100
|
|
||||||
matplotlib/tests/test_colorbar.py,sha256=YbP1IZp9HftMxpUa_yvVKFJexKCGabXQkZSaPam8qvM,17071
|
|
||||||
matplotlib/tests/test_colors.py,sha256=JrmZ9kvwN21bUwwa2YMAhrlURLRYDGDAA0yMlOHN5nc,25629
|
|
||||||
matplotlib/tests/test_compare_images.py,sha256=n3Uoukid0GcjyQpd6ZrqIY9u3RLNE2XAPWwtcHIsqto,3155
|
|
||||||
matplotlib/tests/test_constrainedlayout.py,sha256=easxoL_jBZe5ChE7eQcjz6aqx4LPxQde9FkRsWoOg5I,13261
|
|
||||||
matplotlib/tests/test_container.py,sha256=ijZ2QDT_GLdMwKW393K4WU6R7A5jJNaH1uhLRDwoXXI,550
|
|
||||||
matplotlib/tests/test_contour.py,sha256=2Yl9crBjJegsAEco-mo6oa31TfoJkupLbqlwl9IFkWU,12909
|
|
||||||
matplotlib/tests/test_cycles.py,sha256=75QI7uIkh4b5ckGd0B7irJuqhIivYx8CmxRAciWRj1g,7490
|
|
||||||
matplotlib/tests/test_dates.py,sha256=SDos5lnmY_dNNIT-hjVZ3e5OpmSePAOIWFJoA_9tDdM,25085
|
|
||||||
matplotlib/tests/test_dviread.py,sha256=kTk9Qv6q9Kk3fJcDAEWm35HF-sKsP6Ybec6N8jEHasE,2342
|
|
||||||
matplotlib/tests/test_figure.py,sha256=LVk5dyf6H8V8Ot2bIQXAnscaqLegeDfud1wtLXusJXo,14204
|
|
||||||
matplotlib/tests/test_font_manager.py,sha256=8dVBlum-bY9gmy_YVXWERbWtbGNQllwzCyCNxffGCFI,3271
|
|
||||||
matplotlib/tests/test_gridspec.py,sha256=zahj5Rd4pB0xtAc_3KX7fQWyBys0P-IQk-Cq0cs8VgY,626
|
|
||||||
matplotlib/tests/test_image.py,sha256=uAMkqy06wYZeJASFKIt-hdWCG310dL-AFp6cnLdnjlM,28263
|
|
||||||
matplotlib/tests/test_legend.py,sha256=2Q6Qn2C72wRqZvY6Mt9v_udD0bVARDTbWcqBZD1-oCk,19782
|
|
||||||
matplotlib/tests/test_lines.py,sha256=4n7pdODOXqLvX1zing9SuXXxChPDe7nEr42gaiIG8d8,6221
|
|
||||||
matplotlib/tests/test_marker.py,sha256=fhHHW93wCl5KbrZRL2iEVziv2BLBZU6zSt66hgsI5jY,739
|
|
||||||
matplotlib/tests/test_mathtext.py,sha256=3G0W1S2J5QOHs0EfQiAdP5M114sh81sJtDxITrF1gzs,12696
|
|
||||||
matplotlib/tests/test_matplotlib.py,sha256=DIBqISzUIYanSxNWJL9n2oob1dRLOOAr6TIz2BTWK1I,706
|
|
||||||
matplotlib/tests/test_mlab.py,sha256=gpd4pJ8fIQNCXdwJV1C1__EKBMdnkvQLVgyFdgNgrZc,96981
|
|
||||||
matplotlib/tests/test_offsetbox.py,sha256=u_VCL-lWvQ66IGTzuotzRxq23ySzcB86Gjvmeib-S3w,4198
|
|
||||||
matplotlib/tests/test_patches.py,sha256=IbeCN3mVunvkukneSZKjVbZKGJg0h2oqmOzrI400HQ0,16218
|
|
||||||
matplotlib/tests/test_path.py,sha256=EwvnGhKmdMFl5w4PC2KWwKVm2qKLuRksM70kb-MUVTU,7164
|
|
||||||
matplotlib/tests/test_patheffects.py,sha256=Sl90AY4wri37bEvRuS3QEthE_GV5i5drh0Yk2oGJjc0,5372
|
|
||||||
matplotlib/tests/test_pickle.py,sha256=38XMrLSsH7RgNVesFLB68UAD6HZS7Eylbs3DktWmkBw,5533
|
|
||||||
matplotlib/tests/test_png.py,sha256=3TieTdSRvSL9mS7bhxpozw1CClQxKYunK83TLPU4bnA,1786
|
|
||||||
matplotlib/tests/test_preprocess_data.py,sha256=oJuYg8kpqgLrA1wYEUzMLULRoKM3IRf9_ejRzPzM0AU,15990
|
|
||||||
matplotlib/tests/test_pyplot.py,sha256=wOld704X_pAZVNvnL3WD2KUHliRPtAoqLBh0-OrdeGg,974
|
|
||||||
matplotlib/tests/test_quiver.py,sha256=hAKbP4S6npdpIfwjw6efhMr6En2A-0fJAjLOiMofxWk,5789
|
|
||||||
matplotlib/tests/test_rcparams.py,sha256=bmDAFAz3g0ZLHLVqVU9F4POt-X5HbAqpLNfBp4m81N8,20498
|
|
||||||
matplotlib/tests/test_rcparams.rc,sha256=zwPbYzajd7FTIYURvpwTBAn8i060Do3OVDGZ2xHZeLw,74
|
|
||||||
matplotlib/tests/test_sankey.py,sha256=ZZBtNqIsFcJLoZTCKSM2xaUfrtMjoSica3Mi-FtMysw,162
|
|
||||||
matplotlib/tests/test_scale.py,sha256=8V-tt5E79R-P_Zz6e7iH_7KCHU6e1Ql2SLnQdpt7BmQ,4100
|
|
||||||
matplotlib/tests/test_simplification.py,sha256=c1GSkJwiGxksnUj5WgYbvsr3yDcmbEKHmB_12W_jp7s,10895
|
|
||||||
matplotlib/tests/test_skew.py,sha256=zOhGb5V-9A531ZpmHlqFsdTL9xfdj2-RL3N7OQJbV20,7058
|
|
||||||
matplotlib/tests/test_spines.py,sha256=1cN5KequShVG83DggeUxt5QWE9uIkfmyONaoVNJOojw,2326
|
|
||||||
matplotlib/tests/test_streamplot.py,sha256=F-ilyMfXm47d-XCz6L06GE8KQ-bEHbhKLqIS0EVKv0I,3497
|
|
||||||
matplotlib/tests/test_style.py,sha256=JAcu_9wDS_Gvtisce9Z6rHrO-SHZVjyPeBDYfKgDZ1c,5333
|
|
||||||
matplotlib/tests/test_subplots.py,sha256=yMzfFiUpZBcBt2FpLFySHzxmoj2B4jevjLviFWkPouo,5551
|
|
||||||
matplotlib/tests/test_table.py,sha256=Qoe4Sm-yyog-NOHASdBOjjGQrFQOo2amm01KAgP5MJo,5906
|
|
||||||
matplotlib/tests/test_texmanager.py,sha256=zCtJ3JnZNfP2AQNy7q2LQAgaflSe7S5htJkJNylQSGE,459
|
|
||||||
matplotlib/tests/test_text.py,sha256=Vg5HlZU9ob584X5AZAxQuIm6PFj_GO9xnxIBKZU7O2c,15435
|
|
||||||
matplotlib/tests/test_ticker.py,sha256=OPuJl75xgr_AZ8IDUuyi90PgAXBVT91EnORB2h31mUQ,32802
|
|
||||||
matplotlib/tests/test_tightlayout.py,sha256=7w_LPOY1EM0Z6i7GPt1--w4z0wmf-5M6rvpd0wauqKg,10016
|
|
||||||
matplotlib/tests/test_transforms.py,sha256=Y3FWBzEiBBydD5X4id-bgpzRSFCBxyZFtPTbto2KUv0,24713
|
|
||||||
matplotlib/tests/test_triangulation.py,sha256=XXCtxqhP6jHFe2mbIbB0-KNsdkzxhUDI1V9bDz1A_Eg,44838
|
|
||||||
matplotlib/tests/test_ttconv.py,sha256=xilgvzZpTpHSnumaUlHvQ_zdIQ7U7xBD1Aflx34I-xU,641
|
|
||||||
matplotlib/tests/test_type1font.py,sha256=C0pCPBGOv49SR2xxDOq6LSXAEH_ZNvIWvr_jG-23Gmc,2097
|
|
||||||
matplotlib/tests/test_units.py,sha256=yUnl2ds8QGdJdZWME2YHysQbSr9DG4aEkgIIstMZyMc,5196
|
|
||||||
matplotlib/tests/test_usetex.py,sha256=9ANPkY6aKfyY0_DopPLivplxlT__v5y5wHX2_8-Z4Z8,918
|
|
||||||
matplotlib/tests/test_utf32_be_rcparams.rc,sha256=K66jcKehDKcG1yTXJCOSsmp1iteU9Kvsd_eobV5wNW4,56
|
|
||||||
matplotlib/tests/test_widgets.py,sha256=7sBak0W8XT-NhZ9zNEZC8i_PA6LJIkBgiE_gB42h2cE,16787
|
|
||||||
matplotlib/texmanager.py,sha256=KBU8HQhgj0DNOTwpCZhH_pJ1IqyfEnF1IgUOBDUpRqQ,17284
|
|
||||||
matplotlib/text.py,sha256=-q9B-fS_GmCoAEHhEDbY7m9fidyFLjSn3600KAcROkE,81684
|
|
||||||
matplotlib/textpath.py,sha256=FLg0qVzTVf9_CFbE3O39Pw7-4hiAYiWoqz_dwKMmHYY,17917
|
|
||||||
matplotlib/ticker.py,sha256=Pb6Eyaiz6ncDoDjE7Vew8h63i1s-x9hNR-aRHP9-fhs,88149
|
|
||||||
matplotlib/tight_bbox.py,sha256=Yf5X-HVlMe3AqwR5tUJQBH0LLZqOGoh2uP0oJnU6ErA,2463
|
|
||||||
matplotlib/tight_layout.py,sha256=iQt1J_nr40VPf3GCgqed_r_umsYE5lnvjWxLdh3-uPs,14562
|
|
||||||
matplotlib/transforms.py,sha256=2pDKJqB7Fnx2WLvWUKY8wG7ry9xk0xtVq9dDu32HvLc,99669
|
|
||||||
matplotlib/tri/__init__.py,sha256=XMaejh88uov7Neu7MuYMyaNQqaxg49nXaiJfvjifrRM,256
|
|
||||||
matplotlib/tri/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/__pycache__/triangulation.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/__pycache__/tricontour.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/__pycache__/trifinder.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/__pycache__/triinterpolate.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/__pycache__/tripcolor.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/__pycache__/triplot.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/__pycache__/trirefine.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/__pycache__/tritools.cpython-36.pyc,,
|
|
||||||
matplotlib/tri/triangulation.py,sha256=YM3AIH44SK4LOJiLPB4Wo-DQ1d2q75SdUQ_W2kaxrO4,8427
|
|
||||||
matplotlib/tri/tricontour.py,sha256=Uz3bHK7xw3fHnjFstOlurnz9-btm6dfa0lKE7Vt_P0k,9375
|
|
||||||
matplotlib/tri/trifinder.py,sha256=_S-whwBCe5m9byOzcdAXFJXs0gAIXqy9rVGkXKiM14U,3505
|
|
||||||
matplotlib/tri/triinterpolate.py,sha256=uWh1PPiaN0nMM30txiEVoAxtFZXvX0hxvPnWzKP3xoc,64969
|
|
||||||
matplotlib/tri/tripcolor.py,sha256=DwBFSsZ_jBrFKIPrYetMXNqy_i9GS9-BQUDjPig2WOw,5326
|
|
||||||
matplotlib/tri/triplot.py,sha256=aZ9O_VVLH0AOne31u11ltLlyVyhqKtyzec7WH3b3pkk,2857
|
|
||||||
matplotlib/tri/trirefine.py,sha256=DZS_gihMxkUMzuxAijKnEDo4Po_ahIHY7-uGnjUY1Eg,14142
|
|
||||||
matplotlib/tri/tritools.py,sha256=dC_OcwrFN3gunCe3SgHjQTH_dHBCncM1Fex0bQ_b1Jg,12498
|
|
||||||
matplotlib/ttconv.cpython-36m-x86_64-linux-gnu.so,sha256=tnAP4SNjDwMwdCZFJjzoDYLYa0wf3LAMZmKd9M_QmS4,83888
|
|
||||||
matplotlib/type1font.py,sha256=aBak-e5VKpZpH_LyYqNUyAX4vgRisZq4sfeEcV45-j4,12173
|
|
||||||
matplotlib/units.py,sha256=URBYLJTwt1n-w7A0LcznVlMCMMnLaJFEVrhEHTt0Yv8,6665
|
|
||||||
matplotlib/widgets.py,sha256=vAYlyIJUze6G7CW9CUmY1LePR5L5XFJrPqvVIYycWzY,94072
|
|
||||||
mpl_toolkits/axes_grid/__init__.py,sha256=d0j8ET68OmR22G59uzWO4BQ3Jv2kCmJC15nZRAf227M,559
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/anchored_artists.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/angle_helper.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/axes_divider.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/axes_grid.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/axes_rgb.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/axes_size.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/axis_artist.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/axisline_style.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/axislines.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/clip_path.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/colorbar.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/floating_axes.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/grid_finder.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/grid_helper_curvelinear.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/inset_locator.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/__pycache__/parasite_axes.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid/anchored_artists.py,sha256=_F6-9iacZidb5JpJ8jCOZ9PdiZaR5qpfBjf-3VjTzNc,291
|
|
||||||
mpl_toolkits/axes_grid/angle_helper.py,sha256=Tb4Mb_NGkUdkisebe2dqfBdFmUZiSmGyUnftiSeSIls,51
|
|
||||||
mpl_toolkits/axes_grid/axes_divider.py,sha256=Q1NvDXXtKVuX7iUoKFFRw11Wg1eHEGt3-qDWG7DOVxg,269
|
|
||||||
mpl_toolkits/axes_grid/axes_grid.py,sha256=t2Fc8fM-_qINumuDxctOEYhMI3M1ZfqEVc3th-cnz5g,152
|
|
||||||
mpl_toolkits/axes_grid/axes_rgb.py,sha256=nKv0IWpKHN2NW5eZqx_rbaZqqMWvYw9GK94gIAVEmE0,301
|
|
||||||
mpl_toolkits/axes_grid/axes_size.py,sha256=v4Nhxe7DVp1FkKX03DqJJ1aevDanDvgKT9r0ouDzTxw,48
|
|
||||||
mpl_toolkits/axes_grid/axis_artist.py,sha256=zUlJFUHueDsMtzLi_mK2_Wf-nSBQgiTsMOFpo_SngZ0,50
|
|
||||||
mpl_toolkits/axes_grid/axisline_style.py,sha256=lNVHXkFWhSWPXOOfF-wlVkDPzmzuStJyJzF-NS5Wf_g,53
|
|
||||||
mpl_toolkits/axes_grid/axislines.py,sha256=kVyhb6laiImmuNE53QTQh3kgxz0sO1mcSMpnqIdjylA,48
|
|
||||||
mpl_toolkits/axes_grid/clip_path.py,sha256=s-d36hUiy9I9BSr9wpxjgoAACCQrczHjw072JvArNvE,48
|
|
||||||
mpl_toolkits/axes_grid/colorbar.py,sha256=DckRf6tadLeTNjx-Zk1u3agnSGZgizDjd0Dxw1-GRdw,171
|
|
||||||
mpl_toolkits/axes_grid/floating_axes.py,sha256=i35OfV1ZMF-DkLo4bKmzFZP6LgCwXfdDKxYlGqjyKOM,52
|
|
||||||
mpl_toolkits/axes_grid/grid_finder.py,sha256=Y221c-Jh_AFd3Oolzvr0B1Zrz9MoXPatUABQdLsFdpw,50
|
|
||||||
mpl_toolkits/axes_grid/grid_helper_curvelinear.py,sha256=nRl_B-755X7UpVqqdwkqc_IwiTmM48z3eOMHuvJT5HI,62
|
|
||||||
mpl_toolkits/axes_grid/inset_locator.py,sha256=qqXlT8JWokP0kV-8NHknZDINtK-jbXfkutH_1tcRe_o,216
|
|
||||||
mpl_toolkits/axes_grid/parasite_axes.py,sha256=kCFtaRTd0O8ePL78GOYvhEKqn8rE9bk61v0kVgMb6UE,469
|
|
||||||
mpl_toolkits/axes_grid1/__init__.py,sha256=SEWPa2ggZnKkFVX4yaJOPN7KgyV_T-cyjr8UjIjjhPs,272
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/anchored_artists.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/axes_divider.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/axes_grid.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/axes_rgb.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/axes_size.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/colorbar.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/inset_locator.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/mpl_axes.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/__pycache__/parasite_axes.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axes_grid1/anchored_artists.py,sha256=SPXVgw8CLMGTyPScN3Q2WHeWJbhtQo52Fo3DaLJ8yrY,21162
|
|
||||||
mpl_toolkits/axes_grid1/axes_divider.py,sha256=RNaghesva1N6F4h6J-5Amy15LVqzQyW-FelkfQ_m9n8,29873
|
|
||||||
mpl_toolkits/axes_grid1/axes_grid.py,sha256=BOh_MSGrTK9_-rAFuY5UST84PRDLObCj2tOg71wYjXI,27243
|
|
||||||
mpl_toolkits/axes_grid1/axes_rgb.py,sha256=UlErJuhcqtN0skRLckf_v-GvUtAWDTVW401wUzwXOxI,6603
|
|
||||||
mpl_toolkits/axes_grid1/axes_size.py,sha256=m4LSknVO9c6vcpT1bEZBKYUGkoJ7BOrPnLRTcLmnmFQ,8933
|
|
||||||
mpl_toolkits/axes_grid1/colorbar.py,sha256=BLNBORudFV18ShwQiiVdceUOTJESQZ4yefVu23yVex0,27428
|
|
||||||
mpl_toolkits/axes_grid1/inset_locator.py,sha256=EeLwbA6sUhCBFzPVA1MwCTGE8N1APdKZXq4xSiCXG1Y,23722
|
|
||||||
mpl_toolkits/axes_grid1/mpl_axes.py,sha256=nUXeFjye-NBR1SCXYx1qirV6QIYSB2e01AEeoWlzm7w,4680
|
|
||||||
mpl_toolkits/axes_grid1/parasite_axes.py,sha256=TjNMInmARhT4tZn2PBafcecuyammO3KBuCnUUYyhbdc,12908
|
|
||||||
mpl_toolkits/axisartist/__init__.py,sha256=2zsgjqTtP_NXv78MEaKabmfmkjA7yhy77pIcaR57YWs,748
|
|
||||||
mpl_toolkits/axisartist/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/angle_helper.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/axes_divider.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/axes_grid.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/axes_rgb.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/axis_artist.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/axisline_style.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/axislines.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/clip_path.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/floating_axes.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/grid_finder.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/grid_helper_curvelinear.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/__pycache__/parasite_axes.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/axisartist/angle_helper.py,sha256=csNOzIHc3s1fvJm7XpuZy74kQY2oeC5przHcVSSFVLc,12932
|
|
||||||
mpl_toolkits/axisartist/axes_divider.py,sha256=FqYC72nAYkmU9oaawDb7TjMxb1NSjhbYocD1vxwCrvM,509
|
|
||||||
mpl_toolkits/axisartist/axes_grid.py,sha256=vfd_EXHuYQ7iIVK2FOm6inLhb7huZxtOSvFyOVW2GmU,610
|
|
||||||
mpl_toolkits/axisartist/axes_rgb.py,sha256=TpJCB8eA0wHZVXOxxfFoy1Tk_KFj68sZvo74doDeHYE,179
|
|
||||||
mpl_toolkits/axisartist/axis_artist.py,sha256=4IdmKz2zRHnRCerIO-lYnf7LiUphSE7fe_Hq5VapDms,44753
|
|
||||||
mpl_toolkits/axisartist/axisline_style.py,sha256=It8dzmdESmoAmwwEjOs-YjtBydKlNmb41vV6v8tZ1-s,5107
|
|
||||||
mpl_toolkits/axisartist/axislines.py,sha256=63q7XMODxvM3mwHCmBvtczaOgre-dGigNqavgrDZ844,22082
|
|
||||||
mpl_toolkits/axisartist/clip_path.py,sha256=_fxHB05pFazHxDmNRXN7xO5EfJaxFPHMwFfqwfAs2uA,3736
|
|
||||||
mpl_toolkits/axisartist/floating_axes.py,sha256=B3_1_qTFDSWIpfbRxMf9TXyoRzQmwMISdpqU46nC-Uc,16491
|
|
||||||
mpl_toolkits/axisartist/grid_finder.py,sha256=E_JrpMeAIUj9FZ6Q7_rd4cEYeDbb5TgjoOsJ_5YQIoc,11513
|
|
||||||
mpl_toolkits/axisartist/grid_helper_curvelinear.py,sha256=yH4--wkTm2C2FSSc_TQcvU-24wJlJLj18JphW5Kzz80,15491
|
|
||||||
mpl_toolkits/axisartist/parasite_axes.py,sha256=1sQwBEYuXHpaEeObb7cXh0I1xWroYtcvFiEmwrzqK3w,447
|
|
||||||
mpl_toolkits/mplot3d/__init__.py,sha256=V2iPIP9VyRhoJsFWnQf5AkfyI1GSSP9H6hICEe9edJo,27
|
|
||||||
mpl_toolkits/mplot3d/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/mplot3d/__pycache__/art3d.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/mplot3d/__pycache__/axes3d.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/mplot3d/__pycache__/axis3d.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/mplot3d/__pycache__/proj3d.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/mplot3d/art3d.py,sha256=Juj1shJAObe_JDhF3Fm4qtMVPmvLMUYt2lKp8dakteQ,24879
|
|
||||||
mpl_toolkits/mplot3d/axes3d.py,sha256=EDZc-nuqbI6HNt7oioFRL36EKODH8HTA68EsElSW_II,103257
|
|
||||||
mpl_toolkits/mplot3d/axis3d.py,sha256=z8Q20DsMzFvA-jYWFsbbo-fFB7Yzegdlg7Wz1Ws5VPE,18574
|
|
||||||
mpl_toolkits/mplot3d/proj3d.py,sha256=JNb8VcfoAOwRJMLAOT6pdX2PDE7fCx5L48PGdIACzvU,4612
|
|
||||||
mpl_toolkits/tests/__init__.py,sha256=iPdasxJf0vpIi11tQ98OVSQgS0UaPUyOEGGfAryAhIA,381
|
|
||||||
mpl_toolkits/tests/__pycache__/__init__.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/conftest.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axes_grid.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axes_grid1.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axisartist_angle_helper.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axisartist_axis_artist.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axisartist_axislines.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axisartist_clip_path.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axisartist_floating_axes.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axisartist_grid_finder.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_axisartist_grid_helper_curvelinear.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/__pycache__/test_mplot3d.cpython-36.pyc,,
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid/imagegrid_cbar_mode.png,sha256=yvo6erXXc3Z9aO0rrEezBooCc6KhAw7wKv4WngOQmFA,87393
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/anchored_direction_arrows.png,sha256=XMZGgG7_9k96bKhI2G--XBVKpct5O5psbGH2Wvj5YA0,10784
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/anchored_direction_arrows_many_args.png,sha256=fkPsdmhd4S1g-QxMb55M63iAgWmC2G4ytcLOT9tMAD0,11039
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/divider_append_axes.pdf,sha256=eW2CuM_T4d95dC-DU0PmmQD7gqRQIO0rcQpvp-zu1i4,25446
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/divider_append_axes.png,sha256=VfRfs6p4akgjGxxNm6Bu83Pg0v1KmU7WPu97_-kzNFc,48825
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/divider_append_axes.svg,sha256=usfsa3y-s-N2KMOzsOZHTq-PZXgAPXsSM-lkxJ3ZUi0,172812
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/fill_facecolor.png,sha256=Tkrylxebxm8SuWZjQK0qXSX8m9QsQU6kYm7L2dgt4yg,14845
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/image_grid.png,sha256=EStruex2GqxBIGm49SXqrn8lO4-_XhsAnvs5CmtUawc,3872
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/inset_axes.png,sha256=RQmR39E6Vskvl7G4LInHibW9E1VK0QgCvI-hBlb-E2E,9928
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/inset_locator.png,sha256=bQKKKUuoU_EZwZT_9FzzeVKsKwUUBOZV55g4vVUbnCU,9490
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/inverted_zoomed_axes.png,sha256=rvglsLg8Kl9jE_JukTJ5B3EHozsIYJsaYA0JIOicZL8,25997
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/twin_axes_empty_and_removed.png,sha256=0YzkFhxs4SBG_FEmnWB10bXIxl9aq7WJveQAqHm0JrQ,37701
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axes_grid1/zoomed_axes.png,sha256=mUu8zJtz8FMb7h5l4Cfp3oBi9jaNR5OoyaDgwqpAZp4,25893
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist.png,sha256=qdlk9UPScCAN9RBOhoNqLmJvmkXt8pCuwuQtrz5E8Bs,10151
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_labelbase.png,sha256=An5lovtvAiNA1NZI-E8kOj6eYTruQMqwf3J7pXwdk4A,10598
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_ticklabels.png,sha256=7vuAKkIqcpgJrc2AF7oslf-E_sDfSlCoymyc87u4AWs,5696
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_ticks.png,sha256=CkVtCWG13ViW0w2DsbzfXSvoFWHYaaqQYeEYpbKbOg8,5695
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_axislines/ParasiteAxesAuxTrans_meshplot.png,sha256=FOgl-Glmzhdp6V8mz4StofTsFXGysFkEcUeaWtmJDZs,34354
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_axislines/Subplot.png,sha256=tRpYCjR5zUkafA85DVmY3duTEouwCZq6jDwSF4UsBS8,26919
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_axislines/SubplotZero.png,sha256=3kCrz7HQMYrK3iDgYgf8kyigxRtIGFBbcUzJPtiXh_E,28682
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_clip_path/clip_path.png,sha256=BtMyb7ZawcgId9jl1_qW72lU_ZyxLN780uQ9bCLjbHA,25701
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_floating_axes/curvelinear3.png,sha256=4th7Y74_9YV6X25RqJW0Op9WDzGRCcxF1kfNogkgozE,52835
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_floating_axes/curvelinear4.png,sha256=cYjrSiH6Mvor-VhmwNUgX7Le3_k1rurpd8L5vhTf16s,29374
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/axis_direction.png,sha256=3fue92dg-ntYI0XX0nB31IFpgRT2V3izqjrmLvEdYN4,40536
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/custom_transform.png,sha256=4cQhIFK1z8oPUVyvkHNZ_m-GCbikmUbTvkvYVGy6U4o,15118
|
|
||||||
mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/polar_box.png,sha256=wWaPM3I7_435SkVQqIggb8BHrWBMWrsSVyMZQQJ6fE4,62526
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_cla.png,sha256=htnP1CA8dd85KqdnOsHVlsehT90MUoQD8kFTyra0AuE,51409
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_labelpad.png,sha256=zrLsk8t7s970yaY3cqj6SOMbI6UY8Loe0Zbp0WqFtwQ,66817
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_ortho.pdf,sha256=zkfSOR2bJYC_X425qnXHMmGJPSlLSpFs53YB_R760Gw,6603
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_ortho.png,sha256=SoyN30SsuvEECZyB_ReGP3ZKGZJazOp05dXa3YUn7Jc,47796
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_ortho.svg,sha256=Kb_zdIzZG6JKnztMcmOUG4esPsuteljB_A2sxrhIA3Y,18046
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.pdf,sha256=YI5gzs8NK6fWo6JB0wf8xeZ-FrHlS3P8DSCccsLU4fE,7197
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.png,sha256=Qw909B4nDmV9DlMuo1DKk7y5ndjtvni5d_DcysmG9VA,100466
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.svg,sha256=iU1Pk60SDC89km6bwz9Li9mXdNdZ_Vn15bkbAUG2iKo,30591
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_notshaded.png,sha256=p9nV0cr7ZqhmM5VRHYcByR_QWKh91b8jjt71nkrq3Bc,66289
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_shaded.png,sha256=9RXKAlcPSrQYmhvqH9JMnlLaR62satjwyUq1joXcy6g,64595
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contour3d.pdf,sha256=CtzH5MJNMY_hZGAyp9py9PLI0a8kJ-jNnpQKQYtoQEE,25170
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contour3d.png,sha256=tii1IakS8MC_Vuwd95HhcyM0iq4zGN5DxgRFfB9mKu8,83161
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contour3d.svg,sha256=3VjmHaN5hRXJ-HH9duS1M6nR8gkgBOCtb3zruWcSujU,67618
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d.pdf,sha256=H7FyZjuiA12CzP8FDDnVNf42HxGfMlg_8rA57HA1sIo,52812
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d.png,sha256=Jb-fhAcgogE8jn9DSsaqInUfWC7D_5Pf3QRf7XWAX2Q,42575
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d.svg,sha256=cz9TicdIGJ_8UdHTKQZ76n2rAv6Rx2EELPg8AyAEgMU,173077
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d_fill.pdf,sha256=raTRNOdfYPFJvZiM7BbRbdWRPElQe0sn6jsKH63TTyA,3950
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d_fill.png,sha256=dE8eHoj43eePB44F1nLM2RLj8iqw8rCYI3D0VD3gUg0,39694
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d_fill.svg,sha256=BM-PcmZJ-8iyB4wUWQxcMuskmDXemrsX52LKPYEz850,11093
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.pdf,sha256=6pKrI8lUIPxRwi3ofm8DK8UqNeZsBYprXwX51vwFv1Y,4354
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.png,sha256=DQT-NruHCeG5LKpjG-dlLln3aCoPKhua5PQnHTafBGU,60217
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.svg,sha256=c32m_X6tFYlY5PCVYJ0cPzvZDTbgRA-GIwzDUVTNKYA,12919
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/mixedsubplot.pdf,sha256=Y-C6q8kG4QdVsBpck1Qg90W87rSnbOzrDQP1v7qFZDI,53962
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/mixedsubplot.png,sha256=rGHpqTyXqt1PvCSvSrP7Dnd0uNeeEmPPgJwADxXdfM8,39763
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/mixedsubplot.svg,sha256=TQ9KQlKC0BU4lnayB2S8ArbsfAh87qKjFEh5WKjWbvk,286241
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/plot_3d_from_2d.png,sha256=AWos5EJWMerD0tgVZyvBofz-5hnCq6fhGHKmQi-izAg,56593
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_closed.pdf,sha256=mJYYIj01eM9ouCPpoeWu-KDrXe3_o2um4_JddGAzWGg,2680
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_closed.png,sha256=ePzSA-iFaQbmH603vw1jhs9vyIt45xXnbpIuUF3a1l8,52065
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_closed.svg,sha256=K-D9vp-jB8KFTilVotfIQvuhG3qTMT04XZ6LUIfpZ60,8594
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_axes_cube.png,sha256=AJ0EoayvdBoywpOUWcxbMQ0oB7cTzcoWGgGyx2qgQMU,23182
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_axes_cube_ortho.png,sha256=5Phz7BclSciZpg4SDu-eUQ-v_ikHbEqReQWCdeHywQk,16210
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_lines_dists.png,sha256=XCd4hX2ckc5GCxcgenkRJ8MT7pX-3iMLylD2rCjNl-4,18898
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d.pdf,sha256=T4nKDMCEi2zKpoq5FOsdyMQJd0igS9yBeyUL84a0oso,18589
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d.png,sha256=PBllNI1kHf1rz-oPK507OwsPNE9DPwivXAVJM9DemBI,104755
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d.svg,sha256=_cwXN4aH-mQx5ADarZtXsY-vaeRFg4dPuVmP6S9NZbM,128623
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_empty.pdf,sha256=DSFSucOBU22R3zmG8eKlWWtLy5Wb7L7wqZ1B7CMybKc,7405
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_empty.png,sha256=98D3k5QIL7KugUwzqJhdLtp9dgDGgx8hGa9_u8cvX6o,37954
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_empty.svg,sha256=H_b_HtjccyLTNDjIqUek6DwQugfsyb88nVBepBzFdTo,7745
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_masked.pdf,sha256=_VIBNh32vX_K9QrH-0o4z13FAE0JZQuOzOfkCTsSe7c,10939
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_masked.png,sha256=67yp7-6f-vDiYTmCqMFfuIEGly5UHCCUOV84YJtLsX8,80392
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_masked.svg,sha256=JO8n2RUeQANeCpq_PmtyjmVPqMmuqvZQg7vk_3hsv8I,68796
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_pivot_middle.png,sha256=N4o26wMzfnyxndPbZ2VnsjIAiNYrFN9Aa40ficwO9AM,104735
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_pivot_tail.png,sha256=Ff_UrWxD-VIMQLN1uXy5u_Yd5e1P427YfGM05nvU2kE,104951
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d.pdf,sha256=93umAGrsz8bYekxMETAlU67eCTY3tzycxpVmm2M7eEE,5847
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d.png,sha256=MDaocusHz6Itinjm2j6fnDh-rl1fqVjnqM89nP8bwZs,43155
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d.svg,sha256=yc-PIx36-DdoCE1Vd8JRih45GoBR7eVtdi6wMZVZUXU,12494
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d_color.png,sha256=Y7De9BIFLp0Ova4fk9IcXloNjiwmifTrFA1IfVJA3aE,41598
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d.pdf,sha256=OSvMZiGJzmVdtv_e0XHIBw4bLSY7DLtWGDAYgh6vNuo,48096
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d.png,sha256=Ok0UmO2DELze2yK8mRx0CifmRAgvjyS1IvERsBRvFlU,54712
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d.svg,sha256=CaqTIT-jdKSKfCqp8GC_QAc_3Dxwgr97uvDR-a4xdcw,270325
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d_shaded.png,sha256=tW1q8o6BAlqGD-ILqAXXTBxEt08fwIwEnK-L7n4fHFo,43405
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.pdf,sha256=CfWlocbvwHK48CADjhNjjXJ2eHsSvoNWWpz33RvkuQQ,13906
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.png,sha256=cuahU107LG85pT3kyHngOPV2GchTUwu1AkLYbb4UDd8,77741
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.svg,sha256=MmcVy3u0Syymf933rIsaUY8XOv6b9r6ScVXx33NFxBg,36357
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/tricontour.png,sha256=8IjYmJP6cBhnPGLz-WDyn7UUMYZ10Kz2MpjOFwDUVow,71328
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d.pdf,sha256=OVbK3ausYMApR7_UJPx0pf3bt4A3CpZumhAVhzQMbs0,169155
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d.png,sha256=nO0gJBIluLEX3mlxXY3C6bx-9Jf_xJyXAnTXKnqrIkQ,99103
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d.svg,sha256=PFUTTF34KiJcol6J5JmRjkaillzRWAgsbfxgEhpWxmA,103333
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d_shaded.png,sha256=45y5oF9MzbR07cP-T9CNF7yLWo8qUDoG3E91kc6jueE,94492
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-alpha.png,sha256=4BUCO65oRVuxgOs3i06OFVNWi_UJLelUQzJ7WCzhdOQ,179128
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-edge-style.png,sha256=DMDM2KV84Z0TH7BlWJAuESle0NbFcCsmDF9Q0rPmP3w,65236
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-named-colors.png,sha256=ze6vMiuPmRj7FOX_1ltXMWwQNaRD5jl3drzXbdMKcFs,96319
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-rgb-data.png,sha256=6X8fQ-dYqDlRt9oSoeYbic2mQb1DDlqPfAUpMbh2O9U,136235
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-simple.png,sha256=mG67qqJF-9TKYbacsbVknSnUOj8D9m0sYmy0imfwY2M,60940
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-xyz.png,sha256=OeInzIzGNXG490czp9McRMqEMcgiz0ABkUky7QlTwfE,122177
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3d.pdf,sha256=3gYea_CtpNg1UmurOhyWt3vHZSkLX1-trEY779yrxbc,36169
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3d.png,sha256=epmsR4rWGzh7prW3RL_t8ZcUEsphM5bc0t5j__iauOY,108371
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3d.svg,sha256=-hjWYF-WSE3aDuCSpHYztjeV_yktEqatDK2yZyn_rhE,85892
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3dzerocstride.png,sha256=WaO_3NcaZPFzlui5SQYJ-TbUylHkSbieneCYPffNgAA,81117
|
|
||||||
mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3dzerorstride.png,sha256=y1JvfuVOBiNhJoJ2HdOXyBYBBkQm-oaPcoekfT-cMso,84284
|
|
||||||
mpl_toolkits/tests/conftest.py,sha256=Ph6QZKdfAnkPwU52StddC-uwtCHfANKX1dDXgtX122g,213
|
|
||||||
mpl_toolkits/tests/test_axes_grid.py,sha256=UCQFk5p-9sbTMCS6RKk7BfyCiTb9yRnsarH23eUem0o,1638
|
|
||||||
mpl_toolkits/tests/test_axes_grid1.py,sha256=mHdjXAd-3X1OEW0yS9iap1i2GUuvqtH4-UnnOICT_U4,15028
|
|
||||||
mpl_toolkits/tests/test_axisartist_angle_helper.py,sha256=2jLmTrH4fw3Xty2CwaBsRJISb8qxWWn8ALRa4HL2FQA,5702
|
|
||||||
mpl_toolkits/tests/test_axisartist_axis_artist.py,sha256=h8UXVxnt-fsfvjEOLxnyrwA4z0b7Lf-1UtvIh_SNaI4,2893
|
|
||||||
mpl_toolkits/tests/test_axisartist_axislines.py,sha256=nzxykaFzR1XaQ7KlJNf9VNDXoXRQY4BZKuH_iIyiCHk,2266
|
|
||||||
mpl_toolkits/tests/test_axisartist_clip_path.py,sha256=7K1Y-2DPbDdyvpAHq3XEDaXfsikw-u8v7olwaqwZ53o,1054
|
|
||||||
mpl_toolkits/tests/test_axisartist_floating_axes.py,sha256=bTaH-fTMJum4DAjH5h4t-hH3W5BNNw9LuiWPaC24j6Q,4165
|
|
||||||
mpl_toolkits/tests/test_axisartist_grid_finder.py,sha256=e65sLudWFIXeU08Sis3_SI1JEI6eq8YqKj-80F_Nohk,325
|
|
||||||
mpl_toolkits/tests/test_axisartist_grid_helper_curvelinear.py,sha256=TpV3ShQOPAk57KV-97dWfpULRws0zyAOQwn3JpejTe4,7487
|
|
||||||
mpl_toolkits/tests/test_mplot3d.py,sha256=BUvMUV5OkJskzLZ1KBIIFgKIWoiWaw38FOYBJT0_xRM,26306
|
|
||||||
pylab.py,sha256=u_By3CHla-rBMg57egFXIxZ3P_J6zEkSu_dNpBcH5pw,90
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: bdist_wheel (0.31.1)
|
|
||||||
Root-Is-Purelib: false
|
|
||||||
Tag: cp36-cp36m-manylinux1_x86_64
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
mpl_toolkits
|
|
||||||
mpl_toolkits
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
matplotlib
|
|
||||||
mpl_toolkits
|
|
||||||
pylab
|
|
||||||
@@ -1,210 +0,0 @@
|
|||||||
# Javascript template for HTMLWriter
|
|
||||||
JS_INCLUDE = """
|
|
||||||
<link rel="stylesheet"
|
|
||||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/
|
|
||||||
css/font-awesome.min.css">
|
|
||||||
<script language="javascript">
|
|
||||||
/* Define the Animation class */
|
|
||||||
function Animation(frames, img_id, slider_id, interval, loop_select_id){
|
|
||||||
this.img_id = img_id;
|
|
||||||
this.slider_id = slider_id;
|
|
||||||
this.loop_select_id = loop_select_id;
|
|
||||||
this.interval = interval;
|
|
||||||
this.current_frame = 0;
|
|
||||||
this.direction = 0;
|
|
||||||
this.timer = null;
|
|
||||||
this.frames = new Array(frames.length);
|
|
||||||
|
|
||||||
for (var i=0; i<frames.length; i++)
|
|
||||||
{
|
|
||||||
this.frames[i] = new Image();
|
|
||||||
this.frames[i].src = frames[i];
|
|
||||||
}
|
|
||||||
document.getElementById(this.slider_id).max = this.frames.length - 1;
|
|
||||||
this.set_frame(this.current_frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.get_loop_state = function(){
|
|
||||||
var button_group = document[this.loop_select_id].state;
|
|
||||||
for (var i = 0; i < button_group.length; i++) {
|
|
||||||
var button = button_group[i];
|
|
||||||
if (button.checked) {
|
|
||||||
return button.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.set_frame = function(frame){
|
|
||||||
this.current_frame = frame;
|
|
||||||
document.getElementById(this.img_id).src =
|
|
||||||
this.frames[this.current_frame].src;
|
|
||||||
document.getElementById(this.slider_id).value = this.current_frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.next_frame = function()
|
|
||||||
{
|
|
||||||
this.set_frame(Math.min(this.frames.length - 1, this.current_frame + 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.previous_frame = function()
|
|
||||||
{
|
|
||||||
this.set_frame(Math.max(0, this.current_frame - 1));
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.first_frame = function()
|
|
||||||
{
|
|
||||||
this.set_frame(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.last_frame = function()
|
|
||||||
{
|
|
||||||
this.set_frame(this.frames.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.slower = function()
|
|
||||||
{
|
|
||||||
this.interval /= 0.7;
|
|
||||||
if(this.direction > 0){this.play_animation();}
|
|
||||||
else if(this.direction < 0){this.reverse_animation();}
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.faster = function()
|
|
||||||
{
|
|
||||||
this.interval *= 0.7;
|
|
||||||
if(this.direction > 0){this.play_animation();}
|
|
||||||
else if(this.direction < 0){this.reverse_animation();}
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.anim_step_forward = function()
|
|
||||||
{
|
|
||||||
this.current_frame += 1;
|
|
||||||
if(this.current_frame < this.frames.length){
|
|
||||||
this.set_frame(this.current_frame);
|
|
||||||
}else{
|
|
||||||
var loop_state = this.get_loop_state();
|
|
||||||
if(loop_state == "loop"){
|
|
||||||
this.first_frame();
|
|
||||||
}else if(loop_state == "reflect"){
|
|
||||||
this.last_frame();
|
|
||||||
this.reverse_animation();
|
|
||||||
}else{
|
|
||||||
this.pause_animation();
|
|
||||||
this.last_frame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.anim_step_reverse = function()
|
|
||||||
{
|
|
||||||
this.current_frame -= 1;
|
|
||||||
if(this.current_frame >= 0){
|
|
||||||
this.set_frame(this.current_frame);
|
|
||||||
}else{
|
|
||||||
var loop_state = this.get_loop_state();
|
|
||||||
if(loop_state == "loop"){
|
|
||||||
this.last_frame();
|
|
||||||
}else if(loop_state == "reflect"){
|
|
||||||
this.first_frame();
|
|
||||||
this.play_animation();
|
|
||||||
}else{
|
|
||||||
this.pause_animation();
|
|
||||||
this.first_frame();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.pause_animation = function()
|
|
||||||
{
|
|
||||||
this.direction = 0;
|
|
||||||
if (this.timer){
|
|
||||||
clearInterval(this.timer);
|
|
||||||
this.timer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.play_animation = function()
|
|
||||||
{
|
|
||||||
this.pause_animation();
|
|
||||||
this.direction = 1;
|
|
||||||
var t = this;
|
|
||||||
if (!this.timer) this.timer = setInterval(function() {
|
|
||||||
t.anim_step_forward();
|
|
||||||
}, this.interval);
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation.prototype.reverse_animation = function()
|
|
||||||
{
|
|
||||||
this.pause_animation();
|
|
||||||
this.direction = -1;
|
|
||||||
var t = this;
|
|
||||||
if (!this.timer) this.timer = setInterval(function() {
|
|
||||||
t.anim_step_reverse();
|
|
||||||
}, this.interval);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# HTML template for HTMLWriter
|
|
||||||
DISPLAY_TEMPLATE = """
|
|
||||||
<div class="animation" align="center">
|
|
||||||
<img id="_anim_img{id}">
|
|
||||||
<br>
|
|
||||||
<input id="_anim_slider{id}" type="range" style="width:350px"
|
|
||||||
name="points" min="0" max="1" step="1" value="0"
|
|
||||||
onchange="anim{id}.set_frame(parseInt(this.value));"></input>
|
|
||||||
<br>
|
|
||||||
<button onclick="anim{id}.slower()"><i class="fa fa-minus"></i></button>
|
|
||||||
<button onclick="anim{id}.first_frame()"><i class="fa fa-fast-backward">
|
|
||||||
</i></button>
|
|
||||||
<button onclick="anim{id}.previous_frame()">
|
|
||||||
<i class="fa fa-step-backward"></i></button>
|
|
||||||
<button onclick="anim{id}.reverse_animation()">
|
|
||||||
<i class="fa fa-play fa-flip-horizontal"></i></button>
|
|
||||||
<button onclick="anim{id}.pause_animation()"><i class="fa fa-pause">
|
|
||||||
</i></button>
|
|
||||||
<button onclick="anim{id}.play_animation()"><i class="fa fa-play"></i>
|
|
||||||
</button>
|
|
||||||
<button onclick="anim{id}.next_frame()"><i class="fa fa-step-forward">
|
|
||||||
</i></button>
|
|
||||||
<button onclick="anim{id}.last_frame()"><i class="fa fa-fast-forward">
|
|
||||||
</i></button>
|
|
||||||
<button onclick="anim{id}.faster()"><i class="fa fa-plus"></i></button>
|
|
||||||
<form action="#n" name="_anim_loop_select{id}" class="anim_control">
|
|
||||||
<input type="radio" name="state"
|
|
||||||
value="once" {once_checked}> Once </input>
|
|
||||||
<input type="radio" name="state"
|
|
||||||
value="loop" {loop_checked}> Loop </input>
|
|
||||||
<input type="radio" name="state"
|
|
||||||
value="reflect" {reflect_checked}> Reflect </input>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<script language="javascript">
|
|
||||||
/* Instantiate the Animation class. */
|
|
||||||
/* The IDs given should match those used in the template above. */
|
|
||||||
(function() {{
|
|
||||||
var img_id = "_anim_img{id}";
|
|
||||||
var slider_id = "_anim_slider{id}";
|
|
||||||
var loop_select_id = "_anim_loop_select{id}";
|
|
||||||
var frames = new Array({Nframes});
|
|
||||||
{fill_frames}
|
|
||||||
|
|
||||||
/* set a timeout to make sure all the above elements are created before
|
|
||||||
the object is initialized. */
|
|
||||||
setTimeout(function() {{
|
|
||||||
anim{id} = new Animation(frames, img_id, slider_id, {interval},
|
|
||||||
loop_select_id);
|
|
||||||
}}, 0);
|
|
||||||
}})()
|
|
||||||
</script>
|
|
||||||
"""
|
|
||||||
|
|
||||||
INCLUDED_FRAMES = """
|
|
||||||
for (var i=0; i<{Nframes}; i++){{
|
|
||||||
frames[i] = "{frame_dir}/frame" + ("0000000" + i).slice(-7) +
|
|
||||||
".{frame_format}";
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
@@ -1,729 +0,0 @@
|
|||||||
"""
|
|
||||||
This module provides the routine to adjust subplot layouts so that there are
|
|
||||||
no overlapping axes or axes decorations. All axes decorations are dealt with
|
|
||||||
(labels, ticks, titles, ticklabels) and some dependent artists are also dealt
|
|
||||||
with (colorbar, suptitle, legend).
|
|
||||||
|
|
||||||
Layout is done via :meth:`~matplotlib.gridspec`, with one constraint per
|
|
||||||
gridspec, so it is possible to have overlapping axes if the gridspecs
|
|
||||||
overlap (i.e. using :meth:`~matplotlib.gridspec.GridSpecFromSubplotSpec`).
|
|
||||||
Axes placed using ``figure.subplots()`` or ``figure.add_subplots()`` will
|
|
||||||
participate in the layout. Axes manually placed via ``figure.add_axes()``
|
|
||||||
will not.
|
|
||||||
|
|
||||||
See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide`
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Development Notes:
|
|
||||||
|
|
||||||
# What gets a layoutbox:
|
|
||||||
# - figure
|
|
||||||
# - gridspec
|
|
||||||
# - subplotspec
|
|
||||||
# EITHER:
|
|
||||||
# - axes + pos for the axes (i.e. the total area taken by axis and
|
|
||||||
# the actual "position" argument that needs to be sent to
|
|
||||||
# ax.set_position.)
|
|
||||||
# - The axes layout box will also encomapss the legend, and that is
|
|
||||||
# how legends get included (axes legeneds, not figure legends)
|
|
||||||
# - colorbars are sibblings of the axes if they are single-axes
|
|
||||||
# colorbars
|
|
||||||
# OR:
|
|
||||||
# - a gridspec can be inside a subplotspec.
|
|
||||||
# - subplotspec
|
|
||||||
# EITHER:
|
|
||||||
# - axes...
|
|
||||||
# OR:
|
|
||||||
# - gridspec... with arbitrary nesting...
|
|
||||||
# - colorbars are siblings of the subplotspecs if they are multi-axes
|
|
||||||
# colorbars.
|
|
||||||
# - suptitle:
|
|
||||||
# - right now suptitles are just stacked atop everything else in figure.
|
|
||||||
# Could imagine suptitles being gridspec suptitles, but not implimented
|
|
||||||
#
|
|
||||||
# Todo: AnchoredOffsetbox connected to gridspecs or axes. This would
|
|
||||||
# be more general way to add extra-axes annotations.
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import logging
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from matplotlib.legend import Legend
|
|
||||||
import matplotlib.transforms as transforms
|
|
||||||
import matplotlib._layoutbox as layoutbox
|
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def _in_same_column(colnum0min, colnum0max, colnumCmin, colnumCmax):
|
|
||||||
return (colnumCmin <= colnum0min <= colnumCmax
|
|
||||||
or colnumCmin <= colnum0max <= colnumCmax)
|
|
||||||
|
|
||||||
|
|
||||||
def _in_same_row(rownum0min, rownum0max, rownumCmin, rownumCmax):
|
|
||||||
return (rownumCmin <= rownum0min <= rownumCmax
|
|
||||||
or rownumCmin <= rownum0max <= rownumCmax)
|
|
||||||
|
|
||||||
|
|
||||||
def _axes_all_finite_sized(fig):
|
|
||||||
"""
|
|
||||||
helper function to make sure all axes in the
|
|
||||||
figure have a finite width and height. If not, return False
|
|
||||||
"""
|
|
||||||
for ax in fig.axes:
|
|
||||||
if ax._layoutbox is not None:
|
|
||||||
newpos = ax._poslayoutbox.get_rect()
|
|
||||||
if newpos[2] <= 0 or newpos[3] <= 0:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
######################################################
|
|
||||||
def do_constrained_layout(fig, renderer, h_pad, w_pad,
|
|
||||||
hspace=None, wspace=None):
|
|
||||||
|
|
||||||
"""
|
|
||||||
Do the constrained_layout. Called at draw time in
|
|
||||||
``figure.constrained_layout()``
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
|
|
||||||
|
|
||||||
fig: Figure
|
|
||||||
is the ``figure`` instance to do the layout in.
|
|
||||||
|
|
||||||
renderer: Renderer
|
|
||||||
the renderer to use.
|
|
||||||
|
|
||||||
h_pad, w_pad : float
|
|
||||||
are in figure-normalized units, and are a padding around the axes
|
|
||||||
elements.
|
|
||||||
|
|
||||||
hspace, wspace : float
|
|
||||||
are in fractions of the subplot sizes.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
''' Steps:
|
|
||||||
|
|
||||||
1. get a list of unique gridspecs in this figure. Each gridspec will be
|
|
||||||
constrained separately.
|
|
||||||
2. Check for gaps in the gridspecs. i.e. if not every axes slot in the
|
|
||||||
gridspec has been filled. If empty, add a ghost axis that is made so
|
|
||||||
that it cannot be seen (though visible=True). This is needed to make
|
|
||||||
a blank spot in the layout.
|
|
||||||
3. Compare the tight_bbox of each axes to its `position`, and assume that
|
|
||||||
the difference is the space needed by the elements around the edge of
|
|
||||||
the axes (decorations) like the title, ticklabels, x-labels, etc. This
|
|
||||||
can include legends who overspill the axes boundaries.
|
|
||||||
4. Constrain gridspec elements to line up:
|
|
||||||
a) if colnum0 neq colnumC, the two subplotspecs are stacked next to
|
|
||||||
each other, with the appropriate order.
|
|
||||||
b) if colnum0 == columnC line up the left or right side of the
|
|
||||||
_poslayoutbox (depending if it is the min or max num that is equal).
|
|
||||||
c) do the same for rows...
|
|
||||||
5. The above doesn't constrain relative sizes of the _poslayoutboxes at
|
|
||||||
all, and indeed zero-size is a solution that the solver often finds more
|
|
||||||
convenient than expanding the sizes. Right now the solution is to compare
|
|
||||||
subplotspec sizes (i.e. drowsC and drows0) and constrain the larger
|
|
||||||
_poslayoutbox to be larger than the ratio of the sizes. i.e. if drows0 >
|
|
||||||
drowsC, then ax._poslayoutbox > axc._poslayoutbox * drowsC / drows0. This
|
|
||||||
works fine *if* the decorations are similar between the axes. If the
|
|
||||||
larger subplotspec has much larger axes decorations, then the constraint
|
|
||||||
above is incorrect.
|
|
||||||
|
|
||||||
We need the greater than in the above, in general, rather than an equals
|
|
||||||
sign. Consider the case of the left column having 2 rows, and the right
|
|
||||||
column having 1 row. We want the top and bottom of the _poslayoutboxes to
|
|
||||||
line up. So that means if there are decorations on the left column axes
|
|
||||||
they will be smaller than half as large as the right hand axis.
|
|
||||||
|
|
||||||
This can break down if the decoration size for the right hand axis (the
|
|
||||||
margins) is very large. There must be a math way to check for this case.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
invTransFig = fig.transFigure.inverted().transform_bbox
|
|
||||||
|
|
||||||
# list of unique gridspecs that contain child axes:
|
|
||||||
gss = set()
|
|
||||||
for ax in fig.axes:
|
|
||||||
if hasattr(ax, 'get_subplotspec'):
|
|
||||||
gs = ax.get_subplotspec().get_gridspec()
|
|
||||||
if gs._layoutbox is not None:
|
|
||||||
gss.add(gs)
|
|
||||||
if len(gss) == 0:
|
|
||||||
warnings.warn('There are no gridspecs with layoutboxes. '
|
|
||||||
'Possibly did not call parent GridSpec with the figure= '
|
|
||||||
'keyword')
|
|
||||||
|
|
||||||
if fig._layoutbox.constrained_layout_called < 1:
|
|
||||||
for gs in gss:
|
|
||||||
# fill in any empty gridspec slots w/ ghost axes...
|
|
||||||
_make_ghost_gridspec_slots(fig, gs)
|
|
||||||
|
|
||||||
for nnn in range(2):
|
|
||||||
# do the algrithm twice. This has to be done because decorators
|
|
||||||
# change size after the first re-position (i.e. x/yticklabels get
|
|
||||||
# larger/smaller). This second reposition tends to be much milder,
|
|
||||||
# so doing twice makes things work OK.
|
|
||||||
for ax in fig.axes:
|
|
||||||
_log.debug(ax._layoutbox)
|
|
||||||
if ax._layoutbox is not None:
|
|
||||||
# make margins for each layout box based on the size of
|
|
||||||
# the decorators.
|
|
||||||
_make_layout_margins(ax, renderer, h_pad, w_pad)
|
|
||||||
|
|
||||||
# do layout for suptitle.
|
|
||||||
if fig._suptitle is not None and fig._suptitle._layoutbox is not None:
|
|
||||||
sup = fig._suptitle
|
|
||||||
bbox = invTransFig(sup.get_window_extent(renderer=renderer))
|
|
||||||
height = bbox.y1 - bbox.y0
|
|
||||||
sup._layoutbox.edit_height(height+h_pad)
|
|
||||||
|
|
||||||
# OK, the above lines up ax._poslayoutbox with ax._layoutbox
|
|
||||||
# now we need to
|
|
||||||
# 1) arrange the subplotspecs. We do it at this level because
|
|
||||||
# the subplotspecs are meant to contain other dependent axes
|
|
||||||
# like colorbars or legends.
|
|
||||||
# 2) line up the right and left side of the ax._poslayoutbox
|
|
||||||
# that have the same subplotspec maxes.
|
|
||||||
|
|
||||||
if fig._layoutbox.constrained_layout_called < 1:
|
|
||||||
# arrange the subplotspecs... This is all done relative to each
|
|
||||||
# other. Some subplotspecs conatain axes, and others contain
|
|
||||||
# gridspecs the ones that contain gridspecs are a set proportion
|
|
||||||
# of their parent gridspec. The ones that contain axes are
|
|
||||||
# not so constrained.
|
|
||||||
figlb = fig._layoutbox
|
|
||||||
for child in figlb.children:
|
|
||||||
if child._is_gridspec_layoutbox():
|
|
||||||
# This routine makes all the subplot spec containers
|
|
||||||
# have the correct arrangement. It just stacks the
|
|
||||||
# subplot layoutboxes in the correct order...
|
|
||||||
_arrange_subplotspecs(child, hspace=hspace, wspace=wspace)
|
|
||||||
|
|
||||||
for gs in gss:
|
|
||||||
_align_spines(fig, gs)
|
|
||||||
|
|
||||||
fig._layoutbox.constrained_layout_called += 1
|
|
||||||
fig._layoutbox.update_variables()
|
|
||||||
|
|
||||||
# check if any axes collapsed to zero. If not, don't change positions:
|
|
||||||
if _axes_all_finite_sized(fig):
|
|
||||||
# Now set the position of the axes...
|
|
||||||
for ax in fig.axes:
|
|
||||||
if ax._layoutbox is not None:
|
|
||||||
newpos = ax._poslayoutbox.get_rect()
|
|
||||||
# Now set the new position.
|
|
||||||
# ax.set_position will zero out the layout for
|
|
||||||
# this axis, allowing users to hard-code the position,
|
|
||||||
# so this does the same w/o zeroing layout.
|
|
||||||
ax._set_position(newpos, which='original')
|
|
||||||
else:
|
|
||||||
warnings.warn('constrained_layout not applied. At least '
|
|
||||||
'one axes collapsed to zero width or height.')
|
|
||||||
|
|
||||||
|
|
||||||
def _make_ghost_gridspec_slots(fig, gs):
|
|
||||||
"""
|
|
||||||
Check for unoccupied gridspec slots and make ghost axes for these
|
|
||||||
slots... Do for each gs separately. This is a pretty big kludge
|
|
||||||
but shoudn't have too much ill effect. The worst is that
|
|
||||||
someone querrying the figure will wonder why there are more
|
|
||||||
axes than they thought.
|
|
||||||
"""
|
|
||||||
nrows, ncols = gs.get_geometry()
|
|
||||||
hassubplotspec = np.zeros(nrows * ncols, dtype=bool)
|
|
||||||
axs = []
|
|
||||||
for ax in fig.axes:
|
|
||||||
if (hasattr(ax, 'get_subplotspec')
|
|
||||||
and ax._layoutbox is not None
|
|
||||||
and ax.get_subplotspec().get_gridspec() == gs):
|
|
||||||
axs += [ax]
|
|
||||||
for ax in axs:
|
|
||||||
ss0 = ax.get_subplotspec()
|
|
||||||
if ss0.num2 is None:
|
|
||||||
ss0.num2 = ss0.num1
|
|
||||||
hassubplotspec[ss0.num1:(ss0.num2 + 1)] = True
|
|
||||||
for nn, hss in enumerate(hassubplotspec):
|
|
||||||
if not hss:
|
|
||||||
# this gridspec slot doesn't have an axis so we
|
|
||||||
# make a "ghost".
|
|
||||||
ax = fig.add_subplot(gs[nn])
|
|
||||||
ax.set_frame_on(False)
|
|
||||||
ax.set_xticks([])
|
|
||||||
ax.set_yticks([])
|
|
||||||
ax.set_facecolor((1, 0, 0, 0))
|
|
||||||
|
|
||||||
|
|
||||||
def _make_layout_margins(ax, renderer, h_pad, w_pad):
|
|
||||||
"""
|
|
||||||
For each axes, make a margin between the *pos* layoutbox and the
|
|
||||||
*axes* layoutbox be a minimum size that can accommodate the
|
|
||||||
decorations on the axis.
|
|
||||||
"""
|
|
||||||
fig = ax.figure
|
|
||||||
invTransFig = fig.transFigure.inverted().transform_bbox
|
|
||||||
|
|
||||||
pos = ax.get_position(original=True)
|
|
||||||
tightbbox = ax.get_tightbbox(renderer=renderer)
|
|
||||||
bbox = invTransFig(tightbbox)
|
|
||||||
# use stored h_pad if it exists
|
|
||||||
h_padt = ax._poslayoutbox.h_pad
|
|
||||||
if h_padt is None:
|
|
||||||
h_padt = h_pad
|
|
||||||
w_padt = ax._poslayoutbox.w_pad
|
|
||||||
if w_padt is None:
|
|
||||||
w_padt = w_pad
|
|
||||||
ax._poslayoutbox.edit_left_margin_min(-bbox.x0 +
|
|
||||||
pos.x0 + w_padt)
|
|
||||||
ax._poslayoutbox.edit_right_margin_min(bbox.x1 -
|
|
||||||
pos.x1 + w_padt)
|
|
||||||
ax._poslayoutbox.edit_bottom_margin_min(
|
|
||||||
-bbox.y0 + pos.y0 + h_padt)
|
|
||||||
ax._poslayoutbox.edit_top_margin_min(bbox.y1-pos.y1+h_padt)
|
|
||||||
_log.debug('left %f', (-bbox.x0 + pos.x0 + w_pad))
|
|
||||||
_log.debug('right %f', (bbox.x1 - pos.x1 + w_pad))
|
|
||||||
_log.debug('bottom %f', (-bbox.y0 + pos.y0 + h_padt))
|
|
||||||
# Sometimes its possible for the solver to collapse
|
|
||||||
# rather than expand axes, so they all have zero height
|
|
||||||
# or width. This stops that... It *should* have been
|
|
||||||
# taken into account w/ pref_width...
|
|
||||||
if fig._layoutbox.constrained_layout_called < 1:
|
|
||||||
ax._poslayoutbox.constrain_height_min(20, strength='weak')
|
|
||||||
ax._poslayoutbox.constrain_width_min(20, strength='weak')
|
|
||||||
ax._layoutbox.constrain_height_min(20, strength='weak')
|
|
||||||
ax._layoutbox.constrain_width_min(20, strength='weak')
|
|
||||||
ax._poslayoutbox.constrain_top_margin(0, strength='weak')
|
|
||||||
ax._poslayoutbox.constrain_bottom_margin(0,
|
|
||||||
strength='weak')
|
|
||||||
ax._poslayoutbox.constrain_right_margin(0, strength='weak')
|
|
||||||
ax._poslayoutbox.constrain_left_margin(0, strength='weak')
|
|
||||||
|
|
||||||
|
|
||||||
def _align_spines(fig, gs):
|
|
||||||
"""
|
|
||||||
- Align right/left and bottom/top spines of appropriate subplots.
|
|
||||||
- Compare size of subplotspec including height and width ratios
|
|
||||||
and make sure that the axes spines are at least as large
|
|
||||||
as they should be.
|
|
||||||
"""
|
|
||||||
# for each gridspec...
|
|
||||||
nrows, ncols = gs.get_geometry()
|
|
||||||
width_ratios = gs.get_width_ratios()
|
|
||||||
height_ratios = gs.get_height_ratios()
|
|
||||||
if width_ratios is None:
|
|
||||||
width_ratios = np.ones(ncols)
|
|
||||||
if height_ratios is None:
|
|
||||||
height_ratios = np.ones(nrows)
|
|
||||||
|
|
||||||
# get axes in this gridspec....
|
|
||||||
axs = []
|
|
||||||
for ax in fig.axes:
|
|
||||||
if (hasattr(ax, 'get_subplotspec')
|
|
||||||
and ax._layoutbox is not None):
|
|
||||||
if ax.get_subplotspec().get_gridspec() == gs:
|
|
||||||
axs += [ax]
|
|
||||||
rownummin = np.zeros(len(axs), dtype=np.int8)
|
|
||||||
rownummax = np.zeros(len(axs), dtype=np.int8)
|
|
||||||
colnummin = np.zeros(len(axs), dtype=np.int8)
|
|
||||||
colnummax = np.zeros(len(axs), dtype=np.int8)
|
|
||||||
width = np.zeros(len(axs))
|
|
||||||
height = np.zeros(len(axs))
|
|
||||||
|
|
||||||
for n, ax in enumerate(axs):
|
|
||||||
ss0 = ax.get_subplotspec()
|
|
||||||
if ss0.num2 is None:
|
|
||||||
ss0.num2 = ss0.num1
|
|
||||||
rownummin[n], colnummin[n] = divmod(ss0.num1, ncols)
|
|
||||||
rownummax[n], colnummax[n] = divmod(ss0.num2, ncols)
|
|
||||||
width[n] = np.sum(
|
|
||||||
width_ratios[colnummin[n]:(colnummax[n] + 1)])
|
|
||||||
height[n] = np.sum(
|
|
||||||
height_ratios[rownummin[n]:(rownummax[n] + 1)])
|
|
||||||
|
|
||||||
for nn, ax in enumerate(axs[:-1]):
|
|
||||||
ss0 = ax.get_subplotspec()
|
|
||||||
|
|
||||||
# now compare ax to all the axs:
|
|
||||||
#
|
|
||||||
# If the subplotspecs have the same colnumXmax, then line
|
|
||||||
# up their right sides. If they have the same min, then
|
|
||||||
# line up their left sides (and vertical equivalents).
|
|
||||||
rownum0min, colnum0min = rownummin[nn], colnummin[nn]
|
|
||||||
rownum0max, colnum0max = rownummax[nn], colnummax[nn]
|
|
||||||
width0, height0 = width[nn], height[nn]
|
|
||||||
alignleft = False
|
|
||||||
alignright = False
|
|
||||||
alignbot = False
|
|
||||||
aligntop = False
|
|
||||||
alignheight = False
|
|
||||||
alignwidth = False
|
|
||||||
for mm in range(nn+1, len(axs)):
|
|
||||||
axc = axs[mm]
|
|
||||||
rownumCmin, colnumCmin = rownummin[mm], colnummin[mm]
|
|
||||||
rownumCmax, colnumCmax = rownummax[mm], colnummax[mm]
|
|
||||||
widthC, heightC = width[mm], height[mm]
|
|
||||||
# Horizontally align axes spines if they have the
|
|
||||||
# same min or max:
|
|
||||||
if not alignleft and colnum0min == colnumCmin:
|
|
||||||
# we want the _poslayoutboxes to line up on left
|
|
||||||
# side of the axes spines...
|
|
||||||
layoutbox.align([ax._poslayoutbox,
|
|
||||||
axc._poslayoutbox],
|
|
||||||
'left')
|
|
||||||
alignleft = True
|
|
||||||
|
|
||||||
if not alignright and colnum0max == colnumCmax:
|
|
||||||
# line up right sides of _poslayoutbox
|
|
||||||
layoutbox.align([ax._poslayoutbox,
|
|
||||||
axc._poslayoutbox],
|
|
||||||
'right')
|
|
||||||
alignright = True
|
|
||||||
# Vertically align axes spines if they have the
|
|
||||||
# same min or max:
|
|
||||||
if not aligntop and rownum0min == rownumCmin:
|
|
||||||
# line up top of _poslayoutbox
|
|
||||||
_log.debug('rownum0min == rownumCmin')
|
|
||||||
layoutbox.align([ax._poslayoutbox, axc._poslayoutbox],
|
|
||||||
'top')
|
|
||||||
aligntop = True
|
|
||||||
|
|
||||||
if not alignbot and rownum0max == rownumCmax:
|
|
||||||
# line up bottom of _poslayoutbox
|
|
||||||
_log.debug('rownum0max == rownumCmax')
|
|
||||||
layoutbox.align([ax._poslayoutbox, axc._poslayoutbox],
|
|
||||||
'bottom')
|
|
||||||
alignbot = True
|
|
||||||
###########
|
|
||||||
# Now we make the widths and heights of position boxes
|
|
||||||
# similar. (i.e the spine locations)
|
|
||||||
# This allows vertically stacked subplots to have
|
|
||||||
# different sizes if they occupy different amounts
|
|
||||||
# of the gridspec: i.e.
|
|
||||||
# gs = gridspec.GridSpec(3,1)
|
|
||||||
# ax1 = gs[0,:]
|
|
||||||
# ax2 = gs[1:,:]
|
|
||||||
# then drows0 = 1, and drowsC = 2, and ax2
|
|
||||||
# should be at least twice as large as ax1.
|
|
||||||
# But it can be more than twice as large because
|
|
||||||
# it needs less room for the labeling.
|
|
||||||
#
|
|
||||||
# For height, this only needs to be done if the
|
|
||||||
# subplots share a column. For width if they
|
|
||||||
# share a row.
|
|
||||||
|
|
||||||
drowsC = (rownumCmax - rownumCmin + 1)
|
|
||||||
drows0 = (rownum0max - rownum0min + 1)
|
|
||||||
dcolsC = (colnumCmax - colnumCmin + 1)
|
|
||||||
dcols0 = (colnum0max - colnum0min + 1)
|
|
||||||
|
|
||||||
if not alignheight and drows0 == drowsC:
|
|
||||||
ax._poslayoutbox.constrain_height(
|
|
||||||
axc._poslayoutbox.height * height0 / heightC)
|
|
||||||
alignheight = True
|
|
||||||
elif _in_same_column(colnum0min, colnum0max,
|
|
||||||
colnumCmin, colnumCmax):
|
|
||||||
if height0 > heightC:
|
|
||||||
ax._poslayoutbox.constrain_height_min(
|
|
||||||
axc._poslayoutbox.height * height0 / heightC)
|
|
||||||
# these constraints stop the smaller axes from
|
|
||||||
# being allowed to go to zero height...
|
|
||||||
axc._poslayoutbox.constrain_height_min(
|
|
||||||
ax._poslayoutbox.height * heightC /
|
|
||||||
(height0*1.8))
|
|
||||||
elif height0 < heightC:
|
|
||||||
axc._poslayoutbox.constrain_height_min(
|
|
||||||
ax._poslayoutbox.height * heightC / height0)
|
|
||||||
ax._poslayoutbox.constrain_height_min(
|
|
||||||
ax._poslayoutbox.height * height0 /
|
|
||||||
(heightC*1.8))
|
|
||||||
# widths...
|
|
||||||
if not alignwidth and dcols0 == dcolsC:
|
|
||||||
ax._poslayoutbox.constrain_width(
|
|
||||||
axc._poslayoutbox.width * width0 / widthC)
|
|
||||||
alignwidth = True
|
|
||||||
elif _in_same_row(rownum0min, rownum0max,
|
|
||||||
rownumCmin, rownumCmax):
|
|
||||||
if width0 > widthC:
|
|
||||||
ax._poslayoutbox.constrain_width_min(
|
|
||||||
axc._poslayoutbox.width * width0 / widthC)
|
|
||||||
axc._poslayoutbox.constrain_width_min(
|
|
||||||
ax._poslayoutbox.width * widthC /
|
|
||||||
(width0*1.8))
|
|
||||||
elif width0 < widthC:
|
|
||||||
axc._poslayoutbox.constrain_width_min(
|
|
||||||
ax._poslayoutbox.width * widthC / width0)
|
|
||||||
ax._poslayoutbox.constrain_width_min(
|
|
||||||
axc._poslayoutbox.width * width0 /
|
|
||||||
(widthC*1.8))
|
|
||||||
|
|
||||||
|
|
||||||
def _arrange_subplotspecs(gs, hspace=0, wspace=0):
|
|
||||||
"""
|
|
||||||
arrange the subplotspec children of this gridspec, and then recursively
|
|
||||||
do the same of any gridspec children of those gridspecs...
|
|
||||||
"""
|
|
||||||
sschildren = []
|
|
||||||
for child in gs.children:
|
|
||||||
if child._is_subplotspec_layoutbox():
|
|
||||||
for child2 in child.children:
|
|
||||||
# check for gridspec children...
|
|
||||||
if child2._is_gridspec_layoutbox():
|
|
||||||
_arrange_subplotspecs(child2, hspace=hspace, wspace=wspace)
|
|
||||||
sschildren += [child]
|
|
||||||
# now arrange the subplots...
|
|
||||||
for child0 in sschildren:
|
|
||||||
ss0 = child0.artist
|
|
||||||
nrows, ncols = ss0.get_gridspec().get_geometry()
|
|
||||||
if ss0.num2 is None:
|
|
||||||
ss0.num2 = ss0.num1
|
|
||||||
rowNum0min, colNum0min = divmod(ss0.num1, ncols)
|
|
||||||
rowNum0max, colNum0max = divmod(ss0.num2, ncols)
|
|
||||||
sschildren = sschildren[1:]
|
|
||||||
for childc in sschildren:
|
|
||||||
ssc = childc.artist
|
|
||||||
rowNumCmin, colNumCmin = divmod(ssc.num1, ncols)
|
|
||||||
if ssc.num2 is None:
|
|
||||||
ssc.num2 = ssc.num1
|
|
||||||
rowNumCmax, colNumCmax = divmod(ssc.num2, ncols)
|
|
||||||
# OK, this tells us the relative layout of ax
|
|
||||||
# with axc
|
|
||||||
thepad = wspace / ncols
|
|
||||||
if colNum0max < colNumCmin:
|
|
||||||
layoutbox.hstack([ss0._layoutbox, ssc._layoutbox],
|
|
||||||
padding=thepad)
|
|
||||||
if colNumCmax < colNum0min:
|
|
||||||
layoutbox.hstack([ssc._layoutbox, ss0._layoutbox],
|
|
||||||
padding=thepad)
|
|
||||||
|
|
||||||
####
|
|
||||||
# vertical alignment
|
|
||||||
thepad = hspace / nrows
|
|
||||||
if rowNum0max < rowNumCmin:
|
|
||||||
layoutbox.vstack([ss0._layoutbox,
|
|
||||||
ssc._layoutbox],
|
|
||||||
padding=thepad)
|
|
||||||
if rowNumCmax < rowNum0min:
|
|
||||||
layoutbox.vstack([ssc._layoutbox,
|
|
||||||
ss0._layoutbox],
|
|
||||||
padding=thepad)
|
|
||||||
|
|
||||||
|
|
||||||
def layoutcolorbarsingle(ax, cax, shrink, aspect, location, pad=0.05):
|
|
||||||
"""
|
|
||||||
Do the layout for a colorbar, to not oeverly pollute colorbar.py
|
|
||||||
|
|
||||||
`pad` is in fraction of the original axis size.
|
|
||||||
"""
|
|
||||||
axlb = ax._layoutbox
|
|
||||||
axpos = ax._poslayoutbox
|
|
||||||
axsslb = ax.get_subplotspec()._layoutbox
|
|
||||||
lb = layoutbox.LayoutBox(
|
|
||||||
parent=axsslb,
|
|
||||||
name=axsslb.name + '.cbar',
|
|
||||||
artist=cax)
|
|
||||||
|
|
||||||
if location in ('left', 'right'):
|
|
||||||
lbpos = layoutbox.LayoutBox(
|
|
||||||
parent=lb,
|
|
||||||
name=lb.name + '.pos',
|
|
||||||
tightwidth=False,
|
|
||||||
pos=True,
|
|
||||||
subplot=False,
|
|
||||||
artist=cax)
|
|
||||||
|
|
||||||
if location == 'right':
|
|
||||||
# arrange to right of parent axis
|
|
||||||
layoutbox.hstack([axlb, lb], padding=pad * axlb.width,
|
|
||||||
strength='strong')
|
|
||||||
else:
|
|
||||||
layoutbox.hstack([lb, axlb], padding=pad * axlb.width)
|
|
||||||
# constrain the height and center...
|
|
||||||
layoutbox.match_heights([axpos, lbpos], [1, shrink])
|
|
||||||
layoutbox.align([axpos, lbpos], 'v_center')
|
|
||||||
# set the width of the pos box
|
|
||||||
lbpos.constrain_width(shrink * axpos.height * (1/aspect),
|
|
||||||
strength='strong')
|
|
||||||
elif location in ('bottom', 'top'):
|
|
||||||
lbpos = layoutbox.LayoutBox(
|
|
||||||
parent=lb,
|
|
||||||
name=lb.name + '.pos',
|
|
||||||
tightheight=True,
|
|
||||||
pos=True,
|
|
||||||
subplot=False,
|
|
||||||
artist=cax)
|
|
||||||
|
|
||||||
if location == 'bottom':
|
|
||||||
layoutbox.vstack([axlb, lb], padding=pad * axlb.height)
|
|
||||||
else:
|
|
||||||
layoutbox.vstack([lb, axlb], padding=pad * axlb.height)
|
|
||||||
# constrain the height and center...
|
|
||||||
layoutbox.match_widths([axpos, lbpos],
|
|
||||||
[1, shrink], strength='strong')
|
|
||||||
layoutbox.align([axpos, lbpos], 'h_center')
|
|
||||||
# set the height of the pos box
|
|
||||||
lbpos.constrain_height(axpos.width * aspect * shrink,
|
|
||||||
strength='medium')
|
|
||||||
|
|
||||||
return lb, lbpos
|
|
||||||
|
|
||||||
|
|
||||||
def _getmaxminrowcolumn(axs):
|
|
||||||
# helper to get the min/max rows and columns of a list of axes.
|
|
||||||
maxrow = -100000
|
|
||||||
minrow = 1000000
|
|
||||||
maxax = None
|
|
||||||
minax = None
|
|
||||||
maxcol = -100000
|
|
||||||
mincol = 1000000
|
|
||||||
maxax_col = None
|
|
||||||
minax_col = None
|
|
||||||
|
|
||||||
for ax in axs:
|
|
||||||
subspec = ax.get_subplotspec()
|
|
||||||
nrows, ncols, row_start, row_stop, col_start, col_stop = \
|
|
||||||
subspec.get_rows_columns()
|
|
||||||
if row_stop > maxrow:
|
|
||||||
maxrow = row_stop
|
|
||||||
maxax = ax
|
|
||||||
if row_start < minrow:
|
|
||||||
minrow = row_start
|
|
||||||
minax = ax
|
|
||||||
if col_stop > maxcol:
|
|
||||||
maxcol = col_stop
|
|
||||||
maxax_col = ax
|
|
||||||
if col_start < mincol:
|
|
||||||
mincol = col_start
|
|
||||||
minax_col = ax
|
|
||||||
return (minrow, maxrow, minax, maxax, mincol, maxcol, minax_col, maxax_col)
|
|
||||||
|
|
||||||
|
|
||||||
def layoutcolorbargridspec(parents, cax, shrink, aspect, location, pad=0.05):
|
|
||||||
"""
|
|
||||||
Do the layout for a colorbar, to not oeverly pollute colorbar.py
|
|
||||||
|
|
||||||
`pad` is in fraction of the original axis size.
|
|
||||||
"""
|
|
||||||
|
|
||||||
gs = parents[0].get_subplotspec().get_gridspec()
|
|
||||||
# parent layout box....
|
|
||||||
gslb = gs._layoutbox
|
|
||||||
|
|
||||||
lb = layoutbox.LayoutBox(parent=gslb.parent,
|
|
||||||
name=gslb.parent.name + '.cbar',
|
|
||||||
artist=cax)
|
|
||||||
# figure out the row and column extent of the parents.
|
|
||||||
(minrow, maxrow, minax_row, maxax_row,
|
|
||||||
mincol, maxcol, minax_col, maxax_col) = _getmaxminrowcolumn(parents)
|
|
||||||
|
|
||||||
if location in ('left', 'right'):
|
|
||||||
lbpos = layoutbox.LayoutBox(
|
|
||||||
parent=lb,
|
|
||||||
name=lb.name + '.pos',
|
|
||||||
tightwidth=False,
|
|
||||||
pos=True,
|
|
||||||
subplot=False,
|
|
||||||
artist=cax)
|
|
||||||
for ax in parents:
|
|
||||||
if location == 'right':
|
|
||||||
order = [ax._layoutbox, lb]
|
|
||||||
else:
|
|
||||||
order = [lb, ax._layoutbox]
|
|
||||||
layoutbox.hstack(order, padding=pad * gslb.width,
|
|
||||||
strength='strong')
|
|
||||||
# constrain the height and center...
|
|
||||||
# This isn't quite right. We'd like the colorbar
|
|
||||||
# pos to line up w/ the axes poss, not the size of the
|
|
||||||
# gs.
|
|
||||||
|
|
||||||
# Horizontal Layout: need to check all the axes in this gridspec
|
|
||||||
for ch in gslb.children:
|
|
||||||
subspec = ch.artist
|
|
||||||
nrows, ncols, row_start, row_stop, col_start, col_stop = \
|
|
||||||
subspec.get_rows_columns()
|
|
||||||
if location == 'right':
|
|
||||||
if col_stop <= maxcol:
|
|
||||||
order = [subspec._layoutbox, lb]
|
|
||||||
# arrange to right of the parents
|
|
||||||
if col_start > maxcol:
|
|
||||||
order = [lb, subspec._layoutbox]
|
|
||||||
elif location == 'left':
|
|
||||||
if col_start >= mincol:
|
|
||||||
order = [lb, subspec._layoutbox]
|
|
||||||
if col_stop < mincol:
|
|
||||||
order = [subspec._layoutbox, lb]
|
|
||||||
layoutbox.hstack(order, padding=pad * gslb.width,
|
|
||||||
strength='strong')
|
|
||||||
|
|
||||||
# Vertical layout:
|
|
||||||
maxposlb = minax_row._poslayoutbox
|
|
||||||
minposlb = maxax_row._poslayoutbox
|
|
||||||
# now we want the height of the colorbar pos to be
|
|
||||||
# set by the top and bottom of the min/max axes...
|
|
||||||
# bottom top
|
|
||||||
# b t
|
|
||||||
# h = (top-bottom)*shrink
|
|
||||||
# b = bottom + (top-bottom - h) / 2.
|
|
||||||
lbpos.constrain_height(
|
|
||||||
(maxposlb.top - minposlb.bottom) *
|
|
||||||
shrink, strength='strong')
|
|
||||||
lbpos.constrain_bottom(
|
|
||||||
(maxposlb.top - minposlb.bottom) *
|
|
||||||
(1 - shrink)/2 + minposlb.bottom,
|
|
||||||
strength='strong')
|
|
||||||
|
|
||||||
# set the width of the pos box
|
|
||||||
lbpos.constrain_width(lbpos.height * (shrink / aspect),
|
|
||||||
strength='strong')
|
|
||||||
elif location in ('bottom', 'top'):
|
|
||||||
lbpos = layoutbox.LayoutBox(
|
|
||||||
parent=lb,
|
|
||||||
name=lb.name + '.pos',
|
|
||||||
tightheight=True,
|
|
||||||
pos=True,
|
|
||||||
subplot=False,
|
|
||||||
artist=cax)
|
|
||||||
|
|
||||||
for ax in parents:
|
|
||||||
if location == 'bottom':
|
|
||||||
order = [ax._layoutbox, lb]
|
|
||||||
else:
|
|
||||||
order = [lb, ax._layoutbox]
|
|
||||||
layoutbox.vstack(order, padding=pad * gslb.width,
|
|
||||||
strength='strong')
|
|
||||||
|
|
||||||
# Vertical Layout: need to check all the axes in this gridspec
|
|
||||||
for ch in gslb.children:
|
|
||||||
subspec = ch.artist
|
|
||||||
nrows, ncols, row_start, row_stop, col_start, col_stop = \
|
|
||||||
subspec.get_rows_columns()
|
|
||||||
if location == 'bottom':
|
|
||||||
if row_stop <= minrow:
|
|
||||||
order = [subspec._layoutbox, lb]
|
|
||||||
if row_start > maxrow:
|
|
||||||
order = [lb, subspec._layoutbox]
|
|
||||||
elif location == 'top':
|
|
||||||
if row_stop < minrow:
|
|
||||||
order = [subspec._layoutbox, lb]
|
|
||||||
if row_start >= maxrow:
|
|
||||||
order = [lb, subspec._layoutbox]
|
|
||||||
layoutbox.vstack(order, padding=pad * gslb.width,
|
|
||||||
strength='strong')
|
|
||||||
|
|
||||||
# Do horizontal layout...
|
|
||||||
maxposlb = maxax_col._poslayoutbox
|
|
||||||
minposlb = minax_col._poslayoutbox
|
|
||||||
lbpos.constrain_width((maxposlb.right - minposlb.left) *
|
|
||||||
shrink)
|
|
||||||
lbpos.constrain_left(
|
|
||||||
(maxposlb.right - minposlb.left) *
|
|
||||||
(1-shrink)/2 + minposlb.left)
|
|
||||||
# set the height of the pos box
|
|
||||||
lbpos.constrain_height(lbpos.width * shrink * aspect,
|
|
||||||
strength='medium')
|
|
||||||
|
|
||||||
return lb, lbpos
|
|
||||||
@@ -1,735 +0,0 @@
|
|||||||
"""
|
|
||||||
|
|
||||||
Conventions:
|
|
||||||
|
|
||||||
"constrain_x" means to constrain the variable with either
|
|
||||||
another kiwisolver variable, or a float. i.e. `constrain_width(0.2)`
|
|
||||||
will set a constraint that the width has to be 0.2 and this constraint is
|
|
||||||
permanent - i.e. it will not be removed if it becomes obsolete.
|
|
||||||
|
|
||||||
"edit_x" means to set x to a value (just a float), and that this value can
|
|
||||||
change. So `edit_width(0.2)` will set width to be 0.2, but `edit_width(0.3)`
|
|
||||||
will allow it to change to 0.3 later. Note that these values are still just
|
|
||||||
"suggestions" in `kiwisolver` parlance, and could be over-ridden by
|
|
||||||
other constrains.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import itertools
|
|
||||||
import kiwisolver as kiwi
|
|
||||||
import logging
|
|
||||||
import numpy as np
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# renderers can be complicated
|
|
||||||
def get_renderer(fig):
|
|
||||||
if fig._cachedRenderer:
|
|
||||||
renderer = fig._cachedRenderer
|
|
||||||
else:
|
|
||||||
canvas = fig.canvas
|
|
||||||
if canvas and hasattr(canvas, "get_renderer"):
|
|
||||||
renderer = canvas.get_renderer()
|
|
||||||
else:
|
|
||||||
# not sure if this can happen
|
|
||||||
# seems to with PDF...
|
|
||||||
_log.info("constrained_layout : falling back to Agg renderer")
|
|
||||||
from matplotlib.backends.backend_agg import FigureCanvasAgg
|
|
||||||
canvas = FigureCanvasAgg(fig)
|
|
||||||
renderer = canvas.get_renderer()
|
|
||||||
|
|
||||||
return renderer
|
|
||||||
|
|
||||||
|
|
||||||
class LayoutBox(object):
|
|
||||||
"""
|
|
||||||
Basic rectangle representation using kiwi solver variables
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent=None, name='', tightwidth=False,
|
|
||||||
tightheight=False, artist=None,
|
|
||||||
lower_left=(0, 0), upper_right=(1, 1), pos=False,
|
|
||||||
subplot=False, h_pad=None, w_pad=None):
|
|
||||||
Variable = kiwi.Variable
|
|
||||||
self.parent = parent
|
|
||||||
self.name = name
|
|
||||||
sn = self.name + '_'
|
|
||||||
if parent is None:
|
|
||||||
self.solver = kiwi.Solver()
|
|
||||||
self.constrained_layout_called = 0
|
|
||||||
else:
|
|
||||||
self.solver = parent.solver
|
|
||||||
self.constrained_layout_called = None
|
|
||||||
# parent wants to know about this child!
|
|
||||||
parent.add_child(self)
|
|
||||||
# keep track of artist associated w/ this layout. Can be none
|
|
||||||
self.artist = artist
|
|
||||||
# keep track if this box is supposed to be a pos that is constrained
|
|
||||||
# by the parent.
|
|
||||||
self.pos = pos
|
|
||||||
# keep track of whether we need to match this subplot up with others.
|
|
||||||
self.subplot = subplot
|
|
||||||
|
|
||||||
# we need the str below for Py 2 which complains the string is unicode
|
|
||||||
self.top = Variable(str(sn + 'top'))
|
|
||||||
self.bottom = Variable(str(sn + 'bottom'))
|
|
||||||
self.left = Variable(str(sn + 'left'))
|
|
||||||
self.right = Variable(str(sn + 'right'))
|
|
||||||
|
|
||||||
self.width = Variable(str(sn + 'width'))
|
|
||||||
self.height = Variable(str(sn + 'height'))
|
|
||||||
self.h_center = Variable(str(sn + 'h_center'))
|
|
||||||
self.v_center = Variable(str(sn + 'v_center'))
|
|
||||||
|
|
||||||
self.min_width = Variable(str(sn + 'min_width'))
|
|
||||||
self.min_height = Variable(str(sn + 'min_height'))
|
|
||||||
self.pref_width = Variable(str(sn + 'pref_width'))
|
|
||||||
self.pref_height = Variable(str(sn + 'pref_height'))
|
|
||||||
# margis are only used for axes-position layout boxes. maybe should
|
|
||||||
# be a separate subclass:
|
|
||||||
self.left_margin = Variable(str(sn + 'left_margin'))
|
|
||||||
self.right_margin = Variable(str(sn + 'right_margin'))
|
|
||||||
self.bottom_margin = Variable(str(sn + 'bottom_margin'))
|
|
||||||
self.top_margin = Variable(str(sn + 'top_margin'))
|
|
||||||
# mins
|
|
||||||
self.left_margin_min = Variable(str(sn + 'left_margin_min'))
|
|
||||||
self.right_margin_min = Variable(str(sn + 'right_margin_min'))
|
|
||||||
self.bottom_margin_min = Variable(str(sn + 'bottom_margin_min'))
|
|
||||||
self.top_margin_min = Variable(str(sn + 'top_margin_min'))
|
|
||||||
|
|
||||||
right, top = upper_right
|
|
||||||
left, bottom = lower_left
|
|
||||||
self.tightheight = tightheight
|
|
||||||
self.tightwidth = tightwidth
|
|
||||||
self.add_constraints()
|
|
||||||
self.children = []
|
|
||||||
self.subplotspec = None
|
|
||||||
if self.pos:
|
|
||||||
self.constrain_margins()
|
|
||||||
self.h_pad = h_pad
|
|
||||||
self.w_pad = w_pad
|
|
||||||
|
|
||||||
def constrain_margins(self):
|
|
||||||
"""
|
|
||||||
Only do this for pos. This sets a variable distance
|
|
||||||
margin between the position of the axes and the outer edge of
|
|
||||||
the axes.
|
|
||||||
|
|
||||||
Margins are variable because they change with the fogure size.
|
|
||||||
|
|
||||||
Margin minimums are set to make room for axes decorations. However,
|
|
||||||
the margins can be larger if we are mathicng the position size to
|
|
||||||
otehr axes.
|
|
||||||
"""
|
|
||||||
sol = self.solver
|
|
||||||
|
|
||||||
# left
|
|
||||||
if not sol.hasEditVariable(self.left_margin_min):
|
|
||||||
sol.addEditVariable(self.left_margin_min, 'strong')
|
|
||||||
sol.suggestValue(self.left_margin_min, 0.0001)
|
|
||||||
c = (self.left_margin == self.left - self.parent.left)
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
c = (self.left_margin >= self.left_margin_min)
|
|
||||||
self.solver.addConstraint(c | 'strong')
|
|
||||||
|
|
||||||
# right
|
|
||||||
if not sol.hasEditVariable(self.right_margin_min):
|
|
||||||
sol.addEditVariable(self.right_margin_min, 'strong')
|
|
||||||
sol.suggestValue(self.right_margin_min, 0.0001)
|
|
||||||
c = (self.right_margin == self.parent.right - self.right)
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
c = (self.right_margin >= self.right_margin_min)
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
# bottom
|
|
||||||
if not sol.hasEditVariable(self.bottom_margin_min):
|
|
||||||
sol.addEditVariable(self.bottom_margin_min, 'strong')
|
|
||||||
sol.suggestValue(self.bottom_margin_min, 0.0001)
|
|
||||||
c = (self.bottom_margin == self.bottom - self.parent.bottom)
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
c = (self.bottom_margin >= self.bottom_margin_min)
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
# top
|
|
||||||
if not sol.hasEditVariable(self.top_margin_min):
|
|
||||||
sol.addEditVariable(self.top_margin_min, 'strong')
|
|
||||||
sol.suggestValue(self.top_margin_min, 0.0001)
|
|
||||||
c = (self.top_margin == self.parent.top - self.top)
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
c = (self.top_margin >= self.top_margin_min)
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
|
|
||||||
def add_child(self, child):
|
|
||||||
self.children += [child]
|
|
||||||
|
|
||||||
def remove_child(self, child):
|
|
||||||
try:
|
|
||||||
self.children.remove(child)
|
|
||||||
except ValueError:
|
|
||||||
_log.info("Tried to remove child that doesn't belong to parent")
|
|
||||||
|
|
||||||
def add_constraints(self):
|
|
||||||
sol = self.solver
|
|
||||||
# never let width and height go negative.
|
|
||||||
for i in [self.min_width, self.min_height]:
|
|
||||||
sol.addEditVariable(i, 1e9)
|
|
||||||
sol.suggestValue(i, 0.0)
|
|
||||||
# define relation ships between things thing width and right and left
|
|
||||||
self.hard_constraints()
|
|
||||||
# self.soft_constraints()
|
|
||||||
if self.parent:
|
|
||||||
self.parent_constrain()
|
|
||||||
# sol.updateVariables()
|
|
||||||
|
|
||||||
def parent_constrain(self):
|
|
||||||
parent = self.parent
|
|
||||||
hc = [self.left >= parent.left,
|
|
||||||
self.bottom >= parent.bottom,
|
|
||||||
self.top <= parent.top,
|
|
||||||
self.right <= parent.right]
|
|
||||||
for c in hc:
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
|
|
||||||
def hard_constraints(self):
|
|
||||||
hc = [self.width == self.right - self.left,
|
|
||||||
self.height == self.top - self.bottom,
|
|
||||||
self.h_center == (self.left + self.right) * 0.5,
|
|
||||||
self.v_center == (self.top + self.bottom) * 0.5,
|
|
||||||
self.width >= self.min_width,
|
|
||||||
self.height >= self.min_height]
|
|
||||||
for c in hc:
|
|
||||||
self.solver.addConstraint(c | 'required')
|
|
||||||
|
|
||||||
def soft_constraints(self):
|
|
||||||
sol = self.solver
|
|
||||||
if self.tightwidth:
|
|
||||||
suggest = 0.
|
|
||||||
else:
|
|
||||||
suggest = 20.
|
|
||||||
c = (self.pref_width == suggest)
|
|
||||||
for i in c:
|
|
||||||
sol.addConstraint(i | 'required')
|
|
||||||
if self.tightheight:
|
|
||||||
suggest = 0.
|
|
||||||
else:
|
|
||||||
suggest = 20.
|
|
||||||
c = (self.pref_height == suggest)
|
|
||||||
for i in c:
|
|
||||||
sol.addConstraint(i | 'required')
|
|
||||||
|
|
||||||
c = [(self.width >= suggest),
|
|
||||||
(self.height >= suggest)]
|
|
||||||
for i in c:
|
|
||||||
sol.addConstraint(i | 150000)
|
|
||||||
|
|
||||||
def set_parent(self, parent):
|
|
||||||
''' replace the parent of this with the new parent
|
|
||||||
'''
|
|
||||||
self.parent = parent
|
|
||||||
self.parent_constrain()
|
|
||||||
|
|
||||||
def constrain_geometry(self, left, bottom, right, top, strength='strong'):
|
|
||||||
hc = [self.left == left,
|
|
||||||
self.right == right,
|
|
||||||
self.bottom == bottom,
|
|
||||||
self.top == top]
|
|
||||||
for c in hc:
|
|
||||||
self.solver.addConstraint((c | strength))
|
|
||||||
# self.solver.updateVariables()
|
|
||||||
|
|
||||||
def constrain_same(self, other, strength='strong'):
|
|
||||||
"""
|
|
||||||
Make the layoutbox have same position as other layoutbox
|
|
||||||
"""
|
|
||||||
hc = [self.left == other.left,
|
|
||||||
self.right == other.right,
|
|
||||||
self.bottom == other.bottom,
|
|
||||||
self.top == other.top]
|
|
||||||
for c in hc:
|
|
||||||
self.solver.addConstraint((c | strength))
|
|
||||||
|
|
||||||
def constrain_left_margin(self, margin, strength='strong'):
|
|
||||||
c = (self.left == self.parent.left + margin)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def edit_left_margin_min(self, margin):
|
|
||||||
self.solver.suggestValue(self.left_margin_min, margin)
|
|
||||||
|
|
||||||
def constrain_right_margin(self, margin, strength='strong'):
|
|
||||||
c = (self.right == self.parent.right - margin)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def edit_right_margin_min(self, margin):
|
|
||||||
self.solver.suggestValue(self.right_margin_min, margin)
|
|
||||||
|
|
||||||
def constrain_bottom_margin(self, margin, strength='strong'):
|
|
||||||
c = (self.bottom == self.parent.bottom + margin)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def edit_bottom_margin_min(self, margin):
|
|
||||||
self.solver.suggestValue(self.bottom_margin_min, margin)
|
|
||||||
|
|
||||||
def constrain_top_margin(self, margin, strength='strong'):
|
|
||||||
c = (self.top == self.parent.top - margin)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def edit_top_margin_min(self, margin):
|
|
||||||
self.solver.suggestValue(self.top_margin_min, margin)
|
|
||||||
|
|
||||||
def get_rect(self):
|
|
||||||
return (self.left.value(), self.bottom.value(),
|
|
||||||
self.width.value(), self.height.value())
|
|
||||||
|
|
||||||
def update_variables(self):
|
|
||||||
'''
|
|
||||||
Update *all* the variables that are part of the solver this LayoutBox
|
|
||||||
is created with
|
|
||||||
'''
|
|
||||||
self.solver.updateVariables()
|
|
||||||
|
|
||||||
def edit_height(self, height, strength='strong'):
|
|
||||||
'''
|
|
||||||
Set the height of the layout box.
|
|
||||||
|
|
||||||
This is done as an editable variable so that the value can change
|
|
||||||
due to resizing.
|
|
||||||
'''
|
|
||||||
sol = self.solver
|
|
||||||
for i in [self.height]:
|
|
||||||
if not sol.hasEditVariable(i):
|
|
||||||
sol.addEditVariable(i, strength)
|
|
||||||
sol.suggestValue(self.height, height)
|
|
||||||
|
|
||||||
def constrain_height(self, height, strength='strong'):
|
|
||||||
'''
|
|
||||||
Constrain the height of the layout box. height is
|
|
||||||
either a float or a layoutbox.height.
|
|
||||||
'''
|
|
||||||
c = (self.height == height)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def constrain_height_min(self, height, strength='strong'):
|
|
||||||
c = (self.height >= height)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def edit_width(self, width, strength='strong'):
|
|
||||||
sol = self.solver
|
|
||||||
for i in [self.width]:
|
|
||||||
if not sol.hasEditVariable(i):
|
|
||||||
sol.addEditVariable(i, strength)
|
|
||||||
sol.suggestValue(self.width, width)
|
|
||||||
|
|
||||||
def constrain_width(self, width, strength='strong'):
|
|
||||||
'''
|
|
||||||
Constrain the width of the layout box. `width` is
|
|
||||||
either a float or a layoutbox.width.
|
|
||||||
'''
|
|
||||||
c = (self.width == width)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def constrain_width_min(self, width, strength='strong'):
|
|
||||||
c = (self.width >= width)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def constrain_left(self, left, strength='strong'):
|
|
||||||
c = (self.left == left)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def constrain_bottom(self, bottom, strength='strong'):
|
|
||||||
c = (self.bottom == bottom)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def constrain_right(self, right, strength='strong'):
|
|
||||||
c = (self.right == right)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def constrain_top(self, top, strength='strong'):
|
|
||||||
c = (self.top == top)
|
|
||||||
self.solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
def _is_subplotspec_layoutbox(self):
|
|
||||||
'''
|
|
||||||
Helper to check if this layoutbox is the layoutbox of a
|
|
||||||
subplotspec
|
|
||||||
'''
|
|
||||||
name = (self.name).split('.')[-1]
|
|
||||||
return name[:2] == 'ss'
|
|
||||||
|
|
||||||
def _is_gridspec_layoutbox(self):
|
|
||||||
'''
|
|
||||||
Helper to check if this layoutbox is the layoutbox of a
|
|
||||||
gridspec
|
|
||||||
'''
|
|
||||||
name = (self.name).split('.')[-1]
|
|
||||||
return name[:8] == 'gridspec'
|
|
||||||
|
|
||||||
def find_child_subplots(self):
|
|
||||||
'''
|
|
||||||
Find children of this layout box that are subplots. We want to line
|
|
||||||
poss up, and this is an easy way to find them all.
|
|
||||||
'''
|
|
||||||
if self.subplot:
|
|
||||||
subplots = [self]
|
|
||||||
else:
|
|
||||||
subplots = []
|
|
||||||
for child in self.children:
|
|
||||||
subplots += child.find_child_subplots()
|
|
||||||
return subplots
|
|
||||||
|
|
||||||
def layout_from_subplotspec(self, subspec,
|
|
||||||
name='', artist=None, pos=False):
|
|
||||||
''' Make a layout box from a subplotspec. The layout box is
|
|
||||||
constrained to be a fraction of the width/height of the parent,
|
|
||||||
and be a fraction of the parent width/height from the left/bottom
|
|
||||||
of the parent. Therefore the parent can move around and the
|
|
||||||
layout for the subplot spec should move with it.
|
|
||||||
|
|
||||||
The parent is *usually* the gridspec that made the subplotspec.??
|
|
||||||
'''
|
|
||||||
lb = LayoutBox(parent=self, name=name, artist=artist, pos=pos)
|
|
||||||
gs = subspec.get_gridspec()
|
|
||||||
nrows, ncols = gs.get_geometry()
|
|
||||||
parent = self.parent
|
|
||||||
|
|
||||||
# OK, now, we want to set the position of this subplotspec
|
|
||||||
# based on its subplotspec parameters. The new gridspec will inherit.
|
|
||||||
|
|
||||||
# from gridspec. prob should be new method in gridspec
|
|
||||||
left = 0.0
|
|
||||||
right = 1.0
|
|
||||||
bottom = 0.0
|
|
||||||
top = 1.0
|
|
||||||
totWidth = right-left
|
|
||||||
totHeight = top-bottom
|
|
||||||
hspace = 0.
|
|
||||||
wspace = 0.
|
|
||||||
|
|
||||||
# calculate accumulated heights of columns
|
|
||||||
cellH = totHeight / (nrows + hspace * (nrows - 1))
|
|
||||||
sepH = hspace*cellH
|
|
||||||
|
|
||||||
if gs._row_height_ratios is not None:
|
|
||||||
netHeight = cellH * nrows
|
|
||||||
tr = float(sum(gs._row_height_ratios))
|
|
||||||
cellHeights = [netHeight*r/tr for r in gs._row_height_ratios]
|
|
||||||
else:
|
|
||||||
cellHeights = [cellH] * nrows
|
|
||||||
|
|
||||||
sepHeights = [0] + ([sepH] * (nrows - 1))
|
|
||||||
cellHs = np.add.accumulate(np.ravel(
|
|
||||||
list(zip(sepHeights, cellHeights))))
|
|
||||||
|
|
||||||
# calculate accumulated widths of rows
|
|
||||||
cellW = totWidth/(ncols + wspace * (ncols - 1))
|
|
||||||
sepW = wspace*cellW
|
|
||||||
|
|
||||||
if gs._col_width_ratios is not None:
|
|
||||||
netWidth = cellW * ncols
|
|
||||||
tr = float(sum(gs._col_width_ratios))
|
|
||||||
cellWidths = [netWidth * r / tr for r in gs._col_width_ratios]
|
|
||||||
else:
|
|
||||||
cellWidths = [cellW] * ncols
|
|
||||||
|
|
||||||
sepWidths = [0] + ([sepW] * (ncols - 1))
|
|
||||||
cellWs = np.add.accumulate(np.ravel(list(zip(sepWidths, cellWidths))))
|
|
||||||
|
|
||||||
figTops = [top - cellHs[2 * rowNum] for rowNum in range(nrows)]
|
|
||||||
figBottoms = [top - cellHs[2 * rowNum + 1] for rowNum in range(nrows)]
|
|
||||||
figLefts = [left + cellWs[2 * colNum] for colNum in range(ncols)]
|
|
||||||
figRights = [left + cellWs[2 * colNum + 1] for colNum in range(ncols)]
|
|
||||||
|
|
||||||
rowNum, colNum = divmod(subspec.num1, ncols)
|
|
||||||
figBottom = figBottoms[rowNum]
|
|
||||||
figTop = figTops[rowNum]
|
|
||||||
figLeft = figLefts[colNum]
|
|
||||||
figRight = figRights[colNum]
|
|
||||||
|
|
||||||
if subspec.num2 is not None:
|
|
||||||
|
|
||||||
rowNum2, colNum2 = divmod(subspec.num2, ncols)
|
|
||||||
figBottom2 = figBottoms[rowNum2]
|
|
||||||
figTop2 = figTops[rowNum2]
|
|
||||||
figLeft2 = figLefts[colNum2]
|
|
||||||
figRight2 = figRights[colNum2]
|
|
||||||
|
|
||||||
figBottom = min(figBottom, figBottom2)
|
|
||||||
figLeft = min(figLeft, figLeft2)
|
|
||||||
figTop = max(figTop, figTop2)
|
|
||||||
figRight = max(figRight, figRight2)
|
|
||||||
# These are numbers relative to 0,0,1,1. Need to constrain
|
|
||||||
# relative to parent.
|
|
||||||
|
|
||||||
width = figRight - figLeft
|
|
||||||
height = figTop - figBottom
|
|
||||||
parent = self.parent
|
|
||||||
cs = [self.left == parent.left + parent.width * figLeft,
|
|
||||||
self.bottom == parent.bottom + parent.height * figBottom,
|
|
||||||
self.width == parent.width * width,
|
|
||||||
self.height == parent.height * height]
|
|
||||||
for c in cs:
|
|
||||||
self.solver.addConstraint((c | 'required'))
|
|
||||||
|
|
||||||
return lb
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
args = (self.name, self.left.value(), self.bottom.value(),
|
|
||||||
self.right.value(), self.top.value())
|
|
||||||
return ('LayoutBox: %25s, (left: %1.3f) (bot: %1.3f) '
|
|
||||||
'(right: %1.3f) (top: %1.3f) ') % args
|
|
||||||
|
|
||||||
|
|
||||||
# Utility functions that act on layoutboxes...
|
|
||||||
def hstack(boxes, padding=0, strength='strong'):
|
|
||||||
'''
|
|
||||||
Stack LayoutBox instances from left to right.
|
|
||||||
`padding` is in figure-relative units.
|
|
||||||
'''
|
|
||||||
|
|
||||||
for i in range(1, len(boxes)):
|
|
||||||
c = (boxes[i-1].right + padding <= boxes[i].left)
|
|
||||||
boxes[i].solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
|
|
||||||
def hpack(boxes, padding=0, strength='strong'):
|
|
||||||
'''
|
|
||||||
Stack LayoutBox instances from left to right.
|
|
||||||
'''
|
|
||||||
|
|
||||||
for i in range(1, len(boxes)):
|
|
||||||
c = (boxes[i-1].right + padding == boxes[i].left)
|
|
||||||
boxes[i].solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
|
|
||||||
def vstack(boxes, padding=0, strength='strong'):
|
|
||||||
'''
|
|
||||||
Stack LayoutBox instances from top to bottom
|
|
||||||
'''
|
|
||||||
|
|
||||||
for i in range(1, len(boxes)):
|
|
||||||
c = (boxes[i-1].bottom - padding >= boxes[i].top)
|
|
||||||
boxes[i].solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
|
|
||||||
def vpack(boxes, padding=0, strength='strong'):
|
|
||||||
'''
|
|
||||||
Stack LayoutBox instances from top to bottom
|
|
||||||
'''
|
|
||||||
|
|
||||||
for i in range(1, len(boxes)):
|
|
||||||
c = (boxes[i-1].bottom - padding >= boxes[i].top)
|
|
||||||
boxes[i].solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
|
|
||||||
def match_heights(boxes, height_ratios=None, strength='medium'):
|
|
||||||
'''
|
|
||||||
Stack LayoutBox instances from top to bottom
|
|
||||||
'''
|
|
||||||
|
|
||||||
if height_ratios is None:
|
|
||||||
height_ratios = np.ones(len(boxes))
|
|
||||||
for i in range(1, len(boxes)):
|
|
||||||
c = (boxes[i-1].height ==
|
|
||||||
boxes[i].height*height_ratios[i-1]/height_ratios[i])
|
|
||||||
boxes[i].solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
|
|
||||||
def match_widths(boxes, width_ratios=None, strength='medium'):
|
|
||||||
'''
|
|
||||||
Stack LayoutBox instances from top to bottom
|
|
||||||
'''
|
|
||||||
|
|
||||||
if width_ratios is None:
|
|
||||||
width_ratios = np.ones(len(boxes))
|
|
||||||
for i in range(1, len(boxes)):
|
|
||||||
c = (boxes[i-1].width ==
|
|
||||||
boxes[i].width*width_ratios[i-1]/width_ratios[i])
|
|
||||||
boxes[i].solver.addConstraint(c | strength)
|
|
||||||
|
|
||||||
|
|
||||||
def vstackeq(boxes, padding=0, height_ratios=None):
|
|
||||||
vstack(boxes, padding=padding)
|
|
||||||
match_heights(boxes, height_ratios=height_ratios)
|
|
||||||
|
|
||||||
|
|
||||||
def hstackeq(boxes, padding=0, width_ratios=None):
|
|
||||||
hstack(boxes, padding=padding)
|
|
||||||
match_widths(boxes, width_ratios=width_ratios)
|
|
||||||
|
|
||||||
|
|
||||||
def align(boxes, attr, strength='strong'):
|
|
||||||
cons = []
|
|
||||||
for box in boxes[1:]:
|
|
||||||
cons = (getattr(boxes[0], attr) == getattr(box, attr))
|
|
||||||
boxes[0].solver.addConstraint(cons | strength)
|
|
||||||
|
|
||||||
|
|
||||||
def match_top_margins(boxes, levels=1):
|
|
||||||
box0 = boxes[0]
|
|
||||||
top0 = box0
|
|
||||||
for n in range(levels):
|
|
||||||
top0 = top0.parent
|
|
||||||
for box in boxes[1:]:
|
|
||||||
topb = box
|
|
||||||
for n in range(levels):
|
|
||||||
topb = topb.parent
|
|
||||||
c = (box0.top-top0.top == box.top-topb.top)
|
|
||||||
box0.solver.addConstraint(c | 'strong')
|
|
||||||
|
|
||||||
|
|
||||||
def match_bottom_margins(boxes, levels=1):
|
|
||||||
box0 = boxes[0]
|
|
||||||
top0 = box0
|
|
||||||
for n in range(levels):
|
|
||||||
top0 = top0.parent
|
|
||||||
for box in boxes[1:]:
|
|
||||||
topb = box
|
|
||||||
for n in range(levels):
|
|
||||||
topb = topb.parent
|
|
||||||
c = (box0.bottom-top0.bottom == box.bottom-topb.bottom)
|
|
||||||
box0.solver.addConstraint(c | 'strong')
|
|
||||||
|
|
||||||
|
|
||||||
def match_left_margins(boxes, levels=1):
|
|
||||||
box0 = boxes[0]
|
|
||||||
top0 = box0
|
|
||||||
for n in range(levels):
|
|
||||||
top0 = top0.parent
|
|
||||||
for box in boxes[1:]:
|
|
||||||
topb = box
|
|
||||||
for n in range(levels):
|
|
||||||
topb = topb.parent
|
|
||||||
c = (box0.left-top0.left == box.left-topb.left)
|
|
||||||
box0.solver.addConstraint(c | 'strong')
|
|
||||||
|
|
||||||
|
|
||||||
def match_right_margins(boxes, levels=1):
|
|
||||||
box0 = boxes[0]
|
|
||||||
top0 = box0
|
|
||||||
for n in range(levels):
|
|
||||||
top0 = top0.parent
|
|
||||||
for box in boxes[1:]:
|
|
||||||
topb = box
|
|
||||||
for n in range(levels):
|
|
||||||
topb = topb.parent
|
|
||||||
c = (box0.right-top0.right == box.right-topb.right)
|
|
||||||
box0.solver.addConstraint(c | 'strong')
|
|
||||||
|
|
||||||
|
|
||||||
def match_width_margins(boxes, levels=1):
|
|
||||||
match_left_margins(boxes, levels=levels)
|
|
||||||
match_right_margins(boxes, levels=levels)
|
|
||||||
|
|
||||||
|
|
||||||
def match_height_margins(boxes, levels=1):
|
|
||||||
match_top_margins(boxes, levels=levels)
|
|
||||||
match_bottom_margins(boxes, levels=levels)
|
|
||||||
|
|
||||||
|
|
||||||
def match_margins(boxes, levels=1):
|
|
||||||
match_width_margins(boxes, levels=levels)
|
|
||||||
match_height_margins(boxes, levels=levels)
|
|
||||||
|
|
||||||
|
|
||||||
_layoutboxobjnum = itertools.count()
|
|
||||||
|
|
||||||
|
|
||||||
def seq_id():
|
|
||||||
'''
|
|
||||||
Generate a short sequential id for layoutbox objects...
|
|
||||||
'''
|
|
||||||
|
|
||||||
global _layoutboxobjnum
|
|
||||||
|
|
||||||
return ('%06d' % (next(_layoutboxobjnum)))
|
|
||||||
|
|
||||||
|
|
||||||
def print_children(lb):
|
|
||||||
'''
|
|
||||||
Print the children of the layoutbox
|
|
||||||
'''
|
|
||||||
print(lb)
|
|
||||||
for child in lb.children:
|
|
||||||
print_children(child)
|
|
||||||
|
|
||||||
|
|
||||||
def nonetree(lb):
|
|
||||||
'''
|
|
||||||
Make all elements in this tree none... This signals not to do any more
|
|
||||||
layout.
|
|
||||||
'''
|
|
||||||
if lb is not None:
|
|
||||||
if lb.parent is None:
|
|
||||||
# Clear the solver. Hopefully this garbage collects.
|
|
||||||
lb.solver.reset()
|
|
||||||
nonechildren(lb)
|
|
||||||
else:
|
|
||||||
nonetree(lb.parent)
|
|
||||||
|
|
||||||
|
|
||||||
def nonechildren(lb):
|
|
||||||
for child in lb.children:
|
|
||||||
nonechildren(child)
|
|
||||||
lb.artist._layoutbox = None
|
|
||||||
lb = None
|
|
||||||
|
|
||||||
|
|
||||||
def print_tree(lb):
|
|
||||||
'''
|
|
||||||
Print the tree of layoutboxes
|
|
||||||
'''
|
|
||||||
|
|
||||||
if lb.parent is None:
|
|
||||||
print('LayoutBox Tree\n')
|
|
||||||
print('==============\n')
|
|
||||||
print_children(lb)
|
|
||||||
print('\n')
|
|
||||||
else:
|
|
||||||
print_tree(lb.parent)
|
|
||||||
|
|
||||||
|
|
||||||
def plot_children(fig, box, level=0, printit=True):
|
|
||||||
'''
|
|
||||||
Simple plotting to show where boxes are
|
|
||||||
'''
|
|
||||||
import matplotlib
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
if isinstance(fig, matplotlib.figure.Figure):
|
|
||||||
ax = fig.add_axes([0., 0., 1., 1.])
|
|
||||||
ax.set_facecolor([1., 1., 1., 0.7])
|
|
||||||
ax.set_alpha(0.3)
|
|
||||||
fig.draw(fig.canvas.get_renderer())
|
|
||||||
else:
|
|
||||||
ax = fig
|
|
||||||
|
|
||||||
import matplotlib.patches as patches
|
|
||||||
colors = plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
|
||||||
if printit:
|
|
||||||
print("Level:", level)
|
|
||||||
for child in box.children:
|
|
||||||
rect = child.get_rect()
|
|
||||||
if printit:
|
|
||||||
print(child)
|
|
||||||
ax.add_patch(
|
|
||||||
patches.Rectangle(
|
|
||||||
(child.left.value(), child.bottom.value()), # (x,y)
|
|
||||||
child.width.value(), # width
|
|
||||||
child.height.value(), # height
|
|
||||||
fc='none',
|
|
||||||
alpha=0.8,
|
|
||||||
ec=colors[level]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if level > 0:
|
|
||||||
name = child.name.split('.')[-1]
|
|
||||||
if level % 2 == 0:
|
|
||||||
ax.text(child.left.value(), child.bottom.value(), name,
|
|
||||||
size=12-level, color=colors[level])
|
|
||||||
else:
|
|
||||||
ax.text(child.right.value(), child.top.value(), name,
|
|
||||||
ha='right', va='top', size=12-level,
|
|
||||||
color=colors[level])
|
|
||||||
|
|
||||||
plot_children(ax, child, level=level+1, printit=printit)
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
"""
|
|
||||||
Manage figures for pyplot interface.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import atexit
|
|
||||||
import gc
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class Gcf(object):
|
|
||||||
"""
|
|
||||||
Singleton to manage a set of integer-numbered figures.
|
|
||||||
|
|
||||||
This class is never instantiated; it consists of two class
|
|
||||||
attributes (a list and a dictionary), and a set of static
|
|
||||||
methods that operate on those attributes, accessing them
|
|
||||||
directly as class attributes.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
|
|
||||||
*figs*:
|
|
||||||
dictionary of the form {*num*: *manager*, ...}
|
|
||||||
|
|
||||||
*_activeQue*:
|
|
||||||
list of *managers*, with active one at the end
|
|
||||||
|
|
||||||
"""
|
|
||||||
_activeQue = []
|
|
||||||
figs = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_fig_manager(cls, num):
|
|
||||||
"""
|
|
||||||
If figure manager *num* exists, make it the active
|
|
||||||
figure and return the manager; otherwise return *None*.
|
|
||||||
"""
|
|
||||||
manager = cls.figs.get(num, None)
|
|
||||||
if manager is not None:
|
|
||||||
cls.set_active(manager)
|
|
||||||
return manager
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def destroy(cls, num):
|
|
||||||
"""
|
|
||||||
Try to remove all traces of figure *num*.
|
|
||||||
|
|
||||||
In the interactive backends, this is bound to the
|
|
||||||
window "destroy" and "delete" events.
|
|
||||||
"""
|
|
||||||
if not cls.has_fignum(num):
|
|
||||||
return
|
|
||||||
manager = cls.figs[num]
|
|
||||||
manager.canvas.mpl_disconnect(manager._cidgcf)
|
|
||||||
cls._activeQue.remove(manager)
|
|
||||||
del cls.figs[num]
|
|
||||||
manager.destroy()
|
|
||||||
gc.collect(1)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def destroy_fig(cls, fig):
|
|
||||||
"*fig* is a Figure instance"
|
|
||||||
num = next((manager.num for manager in cls.figs.values()
|
|
||||||
if manager.canvas.figure == fig), None)
|
|
||||||
if num is not None:
|
|
||||||
cls.destroy(num)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def destroy_all(cls):
|
|
||||||
# this is need to ensure that gc is available in corner cases
|
|
||||||
# where modules are being torn down after install with easy_install
|
|
||||||
import gc # noqa
|
|
||||||
for manager in list(cls.figs.values()):
|
|
||||||
manager.canvas.mpl_disconnect(manager._cidgcf)
|
|
||||||
manager.destroy()
|
|
||||||
|
|
||||||
cls._activeQue = []
|
|
||||||
cls.figs.clear()
|
|
||||||
gc.collect(1)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def has_fignum(cls, num):
|
|
||||||
"""
|
|
||||||
Return *True* if figure *num* exists.
|
|
||||||
"""
|
|
||||||
return num in cls.figs
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_fig_managers(cls):
|
|
||||||
"""
|
|
||||||
Return a list of figure managers.
|
|
||||||
"""
|
|
||||||
return list(cls.figs.values())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_num_fig_managers(cls):
|
|
||||||
"""
|
|
||||||
Return the number of figures being managed.
|
|
||||||
"""
|
|
||||||
return len(cls.figs)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_active(cls):
|
|
||||||
"""
|
|
||||||
Return the manager of the active figure, or *None*.
|
|
||||||
"""
|
|
||||||
if len(cls._activeQue) == 0:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return cls._activeQue[-1]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_active(cls, manager):
|
|
||||||
"""
|
|
||||||
Make the figure corresponding to *manager* the active one.
|
|
||||||
"""
|
|
||||||
oldQue = cls._activeQue[:]
|
|
||||||
cls._activeQue = []
|
|
||||||
for m in oldQue:
|
|
||||||
if m != manager:
|
|
||||||
cls._activeQue.append(m)
|
|
||||||
cls._activeQue.append(manager)
|
|
||||||
cls.figs[manager.num] = manager
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def draw_all(cls, force=False):
|
|
||||||
"""
|
|
||||||
Redraw all figures registered with the pyplot
|
|
||||||
state machine.
|
|
||||||
"""
|
|
||||||
for f_mgr in cls.get_all_fig_managers():
|
|
||||||
if force or f_mgr.canvas.figure.stale:
|
|
||||||
f_mgr.canvas.draw_idle()
|
|
||||||
|
|
||||||
atexit.register(Gcf.destroy_all)
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
|
|
||||||
# This file was generated by 'versioneer.py' (0.15) from
|
|
||||||
# revision-control system data, or from the parent directory name of an
|
|
||||||
# unpacked source archive. Distribution tarballs contain a pre-generated copy
|
|
||||||
# of this file.
|
|
||||||
|
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
|
|
||||||
version_json = '''
|
|
||||||
{
|
|
||||||
"dirty": false,
|
|
||||||
"error": null,
|
|
||||||
"full-revisionid": "8858a0d1bdd149a0897789e8503ac586be14676d",
|
|
||||||
"version": "3.0.2"
|
|
||||||
}
|
|
||||||
''' # END VERSION_JSON
|
|
||||||
|
|
||||||
|
|
||||||
def get_versions():
|
|
||||||
return json.loads(version_json)
|
|
||||||
@@ -1,562 +0,0 @@
|
|||||||
"""
|
|
||||||
This is a python interface to Adobe Font Metrics Files. Although a
|
|
||||||
number of other python implementations exist, and may be more complete
|
|
||||||
than this, it was decided not to go with them because they were
|
|
||||||
either:
|
|
||||||
|
|
||||||
1) copyrighted or used a non-BSD compatible license
|
|
||||||
|
|
||||||
2) had too many dependencies and a free standing lib was needed
|
|
||||||
|
|
||||||
3) Did more than needed and it was easier to write afresh rather than
|
|
||||||
figure out how to get just what was needed.
|
|
||||||
|
|
||||||
It is pretty easy to use, and requires only built-in python libs:
|
|
||||||
|
|
||||||
>>> from matplotlib import rcParams
|
|
||||||
>>> import os.path
|
|
||||||
>>> afm_fname = os.path.join(rcParams['datapath'],
|
|
||||||
... 'fonts', 'afm', 'ptmr8a.afm')
|
|
||||||
>>>
|
|
||||||
>>> from matplotlib.afm import AFM
|
|
||||||
>>> with open(afm_fname, 'rb') as fh:
|
|
||||||
... afm = AFM(fh)
|
|
||||||
>>> afm.string_width_height('What the heck?')
|
|
||||||
(6220.0, 694)
|
|
||||||
>>> afm.get_fontname()
|
|
||||||
'Times-Roman'
|
|
||||||
>>> afm.get_kern_dist('A', 'f')
|
|
||||||
0
|
|
||||||
>>> afm.get_kern_dist('A', 'y')
|
|
||||||
-92.0
|
|
||||||
>>> afm.get_bbox_char('!')
|
|
||||||
[130, -9, 238, 676]
|
|
||||||
|
|
||||||
As in the Adobe Font Metrics File Format Specification, all dimensions
|
|
||||||
are given in units of 1/1000 of the scale factor (point size) of the font
|
|
||||||
being used.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from collections import namedtuple
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from ._mathtext_data import uni2type1
|
|
||||||
from matplotlib.cbook import deprecated
|
|
||||||
|
|
||||||
|
|
||||||
# some afm files have floats where we are expecting ints -- there is
|
|
||||||
# probably a better way to handle this (support floats, round rather
|
|
||||||
# than truncate). But I don't know what the best approach is now and
|
|
||||||
# this change to _to_int should at least prevent mpl from crashing on
|
|
||||||
# these JDH (2009-11-06)
|
|
||||||
|
|
||||||
def _to_int(x):
|
|
||||||
return int(float(x))
|
|
||||||
|
|
||||||
|
|
||||||
_to_float = float
|
|
||||||
|
|
||||||
|
|
||||||
def _to_str(x):
|
|
||||||
return x.decode('utf8')
|
|
||||||
|
|
||||||
|
|
||||||
def _to_list_of_ints(s):
|
|
||||||
s = s.replace(b',', b' ')
|
|
||||||
return [_to_int(val) for val in s.split()]
|
|
||||||
|
|
||||||
|
|
||||||
def _to_list_of_floats(s):
|
|
||||||
return [_to_float(val) for val in s.split()]
|
|
||||||
|
|
||||||
|
|
||||||
def _to_bool(s):
|
|
||||||
if s.lower().strip() in (b'false', b'0', b'no'):
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _sanity_check(fh):
|
|
||||||
"""
|
|
||||||
Check if the file at least looks like AFM.
|
|
||||||
If not, raise :exc:`RuntimeError`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Remember the file position in case the caller wants to
|
|
||||||
# do something else with the file.
|
|
||||||
pos = fh.tell()
|
|
||||||
try:
|
|
||||||
line = next(fh)
|
|
||||||
finally:
|
|
||||||
fh.seek(pos, 0)
|
|
||||||
|
|
||||||
# AFM spec, Section 4: The StartFontMetrics keyword [followed by a
|
|
||||||
# version number] must be the first line in the file, and the
|
|
||||||
# EndFontMetrics keyword must be the last non-empty line in the
|
|
||||||
# file. We just check the first line.
|
|
||||||
if not line.startswith(b'StartFontMetrics'):
|
|
||||||
raise RuntimeError('Not an AFM file')
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_header(fh):
|
|
||||||
"""
|
|
||||||
Reads the font metrics header (up to the char metrics) and returns
|
|
||||||
a dictionary mapping *key* to *val*. *val* will be converted to the
|
|
||||||
appropriate python type as necessary; e.g.:
|
|
||||||
|
|
||||||
* 'False'->False
|
|
||||||
* '0'->0
|
|
||||||
* '-168 -218 1000 898'-> [-168, -218, 1000, 898]
|
|
||||||
|
|
||||||
Dictionary keys are
|
|
||||||
|
|
||||||
StartFontMetrics, FontName, FullName, FamilyName, Weight,
|
|
||||||
ItalicAngle, IsFixedPitch, FontBBox, UnderlinePosition,
|
|
||||||
UnderlineThickness, Version, Notice, EncodingScheme, CapHeight,
|
|
||||||
XHeight, Ascender, Descender, StartCharMetrics
|
|
||||||
|
|
||||||
"""
|
|
||||||
headerConverters = {
|
|
||||||
b'StartFontMetrics': _to_float,
|
|
||||||
b'FontName': _to_str,
|
|
||||||
b'FullName': _to_str,
|
|
||||||
b'FamilyName': _to_str,
|
|
||||||
b'Weight': _to_str,
|
|
||||||
b'ItalicAngle': _to_float,
|
|
||||||
b'IsFixedPitch': _to_bool,
|
|
||||||
b'FontBBox': _to_list_of_ints,
|
|
||||||
b'UnderlinePosition': _to_int,
|
|
||||||
b'UnderlineThickness': _to_int,
|
|
||||||
b'Version': _to_str,
|
|
||||||
b'Notice': _to_str,
|
|
||||||
b'EncodingScheme': _to_str,
|
|
||||||
b'CapHeight': _to_float, # Is the second version a mistake, or
|
|
||||||
b'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS
|
|
||||||
b'XHeight': _to_float,
|
|
||||||
b'Ascender': _to_float,
|
|
||||||
b'Descender': _to_float,
|
|
||||||
b'StdHW': _to_float,
|
|
||||||
b'StdVW': _to_float,
|
|
||||||
b'StartCharMetrics': _to_int,
|
|
||||||
b'CharacterSet': _to_str,
|
|
||||||
b'Characters': _to_int,
|
|
||||||
}
|
|
||||||
|
|
||||||
d = {}
|
|
||||||
for line in fh:
|
|
||||||
line = line.rstrip()
|
|
||||||
if line.startswith(b'Comment'):
|
|
||||||
continue
|
|
||||||
lst = line.split(b' ', 1)
|
|
||||||
|
|
||||||
key = lst[0]
|
|
||||||
if len(lst) == 2:
|
|
||||||
val = lst[1]
|
|
||||||
else:
|
|
||||||
val = b''
|
|
||||||
|
|
||||||
try:
|
|
||||||
d[key] = headerConverters[key](val)
|
|
||||||
except ValueError:
|
|
||||||
print('Value error parsing header in AFM:', key, val,
|
|
||||||
file=sys.stderr)
|
|
||||||
continue
|
|
||||||
except KeyError:
|
|
||||||
print('Found an unknown keyword in AFM header (was %r)' % key,
|
|
||||||
file=sys.stderr)
|
|
||||||
continue
|
|
||||||
if key == b'StartCharMetrics':
|
|
||||||
return d
|
|
||||||
raise RuntimeError('Bad parse')
|
|
||||||
|
|
||||||
|
|
||||||
CharMetrics = namedtuple('CharMetrics', 'width, name, bbox')
|
|
||||||
CharMetrics.__doc__ = """
|
|
||||||
Represents the character metrics of a single character.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
The fields do currently only describe a subset of character metrics
|
|
||||||
information defined in the AFM standard.
|
|
||||||
"""
|
|
||||||
CharMetrics.width.__doc__ = """The character width (WX)."""
|
|
||||||
CharMetrics.name.__doc__ = """The character name (N)."""
|
|
||||||
CharMetrics.bbox.__doc__ = """
|
|
||||||
The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*)."""
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_char_metrics(fh):
|
|
||||||
"""
|
|
||||||
Parse the given filehandle for character metrics information and return
|
|
||||||
the information as dicts.
|
|
||||||
|
|
||||||
It is assumed that the file cursor is on the line behind
|
|
||||||
'StartCharMetrics'.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
ascii_d : dict
|
|
||||||
A mapping "ASCII num of the character" to `.CharMetrics`.
|
|
||||||
name_d : dict
|
|
||||||
A mapping "character name" to `.CharMetrics`.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
This function is incomplete per the standard, but thus far parses
|
|
||||||
all the sample afm files tried.
|
|
||||||
"""
|
|
||||||
required_keys = {'C', 'WX', 'N', 'B'}
|
|
||||||
|
|
||||||
ascii_d = {}
|
|
||||||
name_d = {}
|
|
||||||
for line in fh:
|
|
||||||
# We are defensively letting values be utf8. The spec requires
|
|
||||||
# ascii, but there are non-compliant fonts in circulation
|
|
||||||
line = _to_str(line.rstrip()) # Convert from byte-literal
|
|
||||||
if line.startswith('EndCharMetrics'):
|
|
||||||
return ascii_d, name_d
|
|
||||||
# Split the metric line into a dictionary, keyed by metric identifiers
|
|
||||||
vals = dict(s.strip().split(' ', 1) for s in line.split(';') if s)
|
|
||||||
# There may be other metrics present, but only these are needed
|
|
||||||
if not required_keys.issubset(vals):
|
|
||||||
raise RuntimeError('Bad char metrics line: %s' % line)
|
|
||||||
num = _to_int(vals['C'])
|
|
||||||
wx = _to_float(vals['WX'])
|
|
||||||
name = vals['N']
|
|
||||||
bbox = _to_list_of_floats(vals['B'])
|
|
||||||
bbox = list(map(int, bbox))
|
|
||||||
metrics = CharMetrics(wx, name, bbox)
|
|
||||||
# Workaround: If the character name is 'Euro', give it the
|
|
||||||
# corresponding character code, according to WinAnsiEncoding (see PDF
|
|
||||||
# Reference).
|
|
||||||
if name == 'Euro':
|
|
||||||
num = 128
|
|
||||||
if num != -1:
|
|
||||||
ascii_d[num] = metrics
|
|
||||||
name_d[name] = metrics
|
|
||||||
raise RuntimeError('Bad parse')
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_kern_pairs(fh):
|
|
||||||
"""
|
|
||||||
Return a kern pairs dictionary; keys are (*char1*, *char2*) tuples and
|
|
||||||
values are the kern pair value. For example, a kern pairs line like
|
|
||||||
``KPX A y -50``
|
|
||||||
|
|
||||||
will be represented as::
|
|
||||||
|
|
||||||
d[ ('A', 'y') ] = -50
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
line = next(fh)
|
|
||||||
if not line.startswith(b'StartKernPairs'):
|
|
||||||
raise RuntimeError('Bad start of kern pairs data: %s' % line)
|
|
||||||
|
|
||||||
d = {}
|
|
||||||
for line in fh:
|
|
||||||
line = line.rstrip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith(b'EndKernPairs'):
|
|
||||||
next(fh) # EndKernData
|
|
||||||
return d
|
|
||||||
vals = line.split()
|
|
||||||
if len(vals) != 4 or vals[0] != b'KPX':
|
|
||||||
raise RuntimeError('Bad kern pairs line: %s' % line)
|
|
||||||
c1, c2, val = _to_str(vals[1]), _to_str(vals[2]), _to_float(vals[3])
|
|
||||||
d[(c1, c2)] = val
|
|
||||||
raise RuntimeError('Bad kern pairs parse')
|
|
||||||
|
|
||||||
|
|
||||||
CompositePart = namedtuple('CompositePart', 'name, dx, dy')
|
|
||||||
CompositePart.__doc__ = """
|
|
||||||
Represents the information on a composite element of a composite char."""
|
|
||||||
CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'."""
|
|
||||||
CompositePart.dx.__doc__ = """x-displacement of the part from the origin."""
|
|
||||||
CompositePart.dy.__doc__ = """y-displacement of the part from the origin."""
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_composites(fh):
|
|
||||||
"""
|
|
||||||
Parse the given filehandle for composites information return them as a
|
|
||||||
dict.
|
|
||||||
|
|
||||||
It is assumed that the file cursor is on the line behind 'StartComposites'.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
composites : dict
|
|
||||||
A dict mapping composite character names to a parts list. The parts
|
|
||||||
list is a list of `.CompositePart` entries describing the parts of
|
|
||||||
the composite.
|
|
||||||
|
|
||||||
Example
|
|
||||||
-------
|
|
||||||
A composite definition line::
|
|
||||||
|
|
||||||
CC Aacute 2 ; PCC A 0 0 ; PCC acute 160 170 ;
|
|
||||||
|
|
||||||
will be represented as::
|
|
||||||
|
|
||||||
composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0),
|
|
||||||
CompositePart(name='acute', dx=160, dy=170)]
|
|
||||||
|
|
||||||
"""
|
|
||||||
composites = {}
|
|
||||||
for line in fh:
|
|
||||||
line = line.rstrip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
if line.startswith(b'EndComposites'):
|
|
||||||
return composites
|
|
||||||
vals = line.split(b';')
|
|
||||||
cc = vals[0].split()
|
|
||||||
name, numParts = cc[1], _to_int(cc[2])
|
|
||||||
pccParts = []
|
|
||||||
for s in vals[1:-1]:
|
|
||||||
pcc = s.split()
|
|
||||||
part = CompositePart(pcc[1], _to_float(pcc[2]), _to_float(pcc[3]))
|
|
||||||
pccParts.append(part)
|
|
||||||
composites[name] = pccParts
|
|
||||||
|
|
||||||
raise RuntimeError('Bad composites parse')
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_optional(fh):
|
|
||||||
"""
|
|
||||||
Parse the optional fields for kern pair data and composites.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
kern_data : dict
|
|
||||||
A dict containing kerning information. May be empty.
|
|
||||||
See `._parse_kern_pairs`.
|
|
||||||
composites : dict
|
|
||||||
A dict containing composite information. May be empty.
|
|
||||||
See `._parse_composites`.
|
|
||||||
"""
|
|
||||||
optional = {
|
|
||||||
b'StartKernData': _parse_kern_pairs,
|
|
||||||
b'StartComposites': _parse_composites,
|
|
||||||
}
|
|
||||||
|
|
||||||
d = {b'StartKernData': {},
|
|
||||||
b'StartComposites': {}}
|
|
||||||
for line in fh:
|
|
||||||
line = line.rstrip()
|
|
||||||
if not line:
|
|
||||||
continue
|
|
||||||
key = line.split()[0]
|
|
||||||
|
|
||||||
if key in optional:
|
|
||||||
d[key] = optional[key](fh)
|
|
||||||
|
|
||||||
return d[b'StartKernData'], d[b'StartComposites']
|
|
||||||
|
|
||||||
|
|
||||||
@deprecated("3.0", "Use the class AFM instead.")
|
|
||||||
def parse_afm(fh):
|
|
||||||
return _parse_afm(fh)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_afm(fh):
|
|
||||||
"""
|
|
||||||
Parse the Adobe Font Metrics file in file handle *fh*.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
header : dict
|
|
||||||
A header dict. See :func:`_parse_header`.
|
|
||||||
cmetrics_by_ascii : dict
|
|
||||||
From :func:`_parse_char_metrics`.
|
|
||||||
cmetrics_by_name : dict
|
|
||||||
From :func:`_parse_char_metrics`.
|
|
||||||
kernpairs : dict
|
|
||||||
From :func:`_parse_kern_pairs`.
|
|
||||||
composites : dict
|
|
||||||
From :func:`_parse_composites`
|
|
||||||
|
|
||||||
"""
|
|
||||||
_sanity_check(fh)
|
|
||||||
header = _parse_header(fh)
|
|
||||||
cmetrics_by_ascii, cmetrics_by_name = _parse_char_metrics(fh)
|
|
||||||
kernpairs, composites = _parse_optional(fh)
|
|
||||||
return header, cmetrics_by_ascii, cmetrics_by_name, kernpairs, composites
|
|
||||||
|
|
||||||
|
|
||||||
class AFM(object):
|
|
||||||
|
|
||||||
def __init__(self, fh):
|
|
||||||
"""Parse the AFM file in file object *fh*."""
|
|
||||||
(self._header,
|
|
||||||
self._metrics,
|
|
||||||
self._metrics_by_name,
|
|
||||||
self._kern,
|
|
||||||
self._composite) = _parse_afm(fh)
|
|
||||||
|
|
||||||
def get_bbox_char(self, c, isord=False):
|
|
||||||
if not isord:
|
|
||||||
c = ord(c)
|
|
||||||
return self._metrics[c].bbox
|
|
||||||
|
|
||||||
def string_width_height(self, s):
|
|
||||||
"""
|
|
||||||
Return the string width (including kerning) and string height
|
|
||||||
as a (*w*, *h*) tuple.
|
|
||||||
"""
|
|
||||||
if not len(s):
|
|
||||||
return 0, 0
|
|
||||||
total_width = 0
|
|
||||||
namelast = None
|
|
||||||
miny = 1e9
|
|
||||||
maxy = 0
|
|
||||||
for c in s:
|
|
||||||
if c == '\n':
|
|
||||||
continue
|
|
||||||
wx, name, bbox = self._metrics[ord(c)]
|
|
||||||
|
|
||||||
total_width += wx + self._kern.get((namelast, name), 0)
|
|
||||||
l, b, w, h = bbox
|
|
||||||
miny = min(miny, b)
|
|
||||||
maxy = max(maxy, b + h)
|
|
||||||
|
|
||||||
namelast = name
|
|
||||||
|
|
||||||
return total_width, maxy - miny
|
|
||||||
|
|
||||||
def get_str_bbox_and_descent(self, s):
|
|
||||||
"""Return the string bounding box and the maximal descent."""
|
|
||||||
if not len(s):
|
|
||||||
return 0, 0, 0, 0, 0
|
|
||||||
total_width = 0
|
|
||||||
namelast = None
|
|
||||||
miny = 1e9
|
|
||||||
maxy = 0
|
|
||||||
left = 0
|
|
||||||
if not isinstance(s, str):
|
|
||||||
s = _to_str(s)
|
|
||||||
for c in s:
|
|
||||||
if c == '\n':
|
|
||||||
continue
|
|
||||||
name = uni2type1.get(ord(c), 'question')
|
|
||||||
try:
|
|
||||||
wx, _, bbox = self._metrics_by_name[name]
|
|
||||||
except KeyError:
|
|
||||||
name = 'question'
|
|
||||||
wx, _, bbox = self._metrics_by_name[name]
|
|
||||||
total_width += wx + self._kern.get((namelast, name), 0)
|
|
||||||
l, b, w, h = bbox
|
|
||||||
left = min(left, l)
|
|
||||||
miny = min(miny, b)
|
|
||||||
maxy = max(maxy, b + h)
|
|
||||||
|
|
||||||
namelast = name
|
|
||||||
|
|
||||||
return left, miny, total_width, maxy - miny, -miny
|
|
||||||
|
|
||||||
def get_str_bbox(self, s):
|
|
||||||
"""Return the string bounding box."""
|
|
||||||
return self.get_str_bbox_and_descent(s)[:4]
|
|
||||||
|
|
||||||
def get_name_char(self, c, isord=False):
|
|
||||||
"""Get the name of the character, i.e., ';' is 'semicolon'."""
|
|
||||||
if not isord:
|
|
||||||
c = ord(c)
|
|
||||||
return self._metrics[c].name
|
|
||||||
|
|
||||||
def get_width_char(self, c, isord=False):
|
|
||||||
"""
|
|
||||||
Get the width of the character from the character metric WX field.
|
|
||||||
"""
|
|
||||||
if not isord:
|
|
||||||
c = ord(c)
|
|
||||||
return self._metrics[c].width
|
|
||||||
|
|
||||||
def get_width_from_char_name(self, name):
|
|
||||||
"""Get the width of the character from a type1 character name."""
|
|
||||||
return self._metrics_by_name[name].width
|
|
||||||
|
|
||||||
def get_height_char(self, c, isord=False):
|
|
||||||
"""Get the bounding box (ink) height of character *c* (space is 0)."""
|
|
||||||
if not isord:
|
|
||||||
c = ord(c)
|
|
||||||
return self._metrics[c].bbox[-1]
|
|
||||||
|
|
||||||
def get_kern_dist(self, c1, c2):
|
|
||||||
"""
|
|
||||||
Return the kerning pair distance (possibly 0) for chars *c1* and *c2*.
|
|
||||||
"""
|
|
||||||
name1, name2 = self.get_name_char(c1), self.get_name_char(c2)
|
|
||||||
return self.get_kern_dist_from_name(name1, name2)
|
|
||||||
|
|
||||||
def get_kern_dist_from_name(self, name1, name2):
|
|
||||||
"""
|
|
||||||
Return the kerning pair distance (possibly 0) for chars
|
|
||||||
*name1* and *name2*.
|
|
||||||
"""
|
|
||||||
return self._kern.get((name1, name2), 0)
|
|
||||||
|
|
||||||
def get_fontname(self):
|
|
||||||
"""Return the font name, e.g., 'Times-Roman'."""
|
|
||||||
return self._header[b'FontName']
|
|
||||||
|
|
||||||
def get_fullname(self):
|
|
||||||
"""Return the font full name, e.g., 'Times-Roman'."""
|
|
||||||
name = self._header.get(b'FullName')
|
|
||||||
if name is None: # use FontName as a substitute
|
|
||||||
name = self._header[b'FontName']
|
|
||||||
return name
|
|
||||||
|
|
||||||
def get_familyname(self):
|
|
||||||
"""Return the font family name, e.g., 'Times'."""
|
|
||||||
name = self._header.get(b'FamilyName')
|
|
||||||
if name is not None:
|
|
||||||
return name
|
|
||||||
|
|
||||||
# FamilyName not specified so we'll make a guess
|
|
||||||
name = self.get_fullname()
|
|
||||||
extras = (r'(?i)([ -](regular|plain|italic|oblique|bold|semibold|'
|
|
||||||
r'light|ultralight|extra|condensed))+$')
|
|
||||||
return re.sub(extras, '', name)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def family_name(self):
|
|
||||||
"""The font family name, e.g., 'Times'."""
|
|
||||||
return self.get_familyname()
|
|
||||||
|
|
||||||
def get_weight(self):
|
|
||||||
"""Return the font weight, e.g., 'Bold' or 'Roman'."""
|
|
||||||
return self._header[b'Weight']
|
|
||||||
|
|
||||||
def get_angle(self):
|
|
||||||
"""Return the fontangle as float."""
|
|
||||||
return self._header[b'ItalicAngle']
|
|
||||||
|
|
||||||
def get_capheight(self):
|
|
||||||
"""Return the cap height as float."""
|
|
||||||
return self._header[b'CapHeight']
|
|
||||||
|
|
||||||
def get_xheight(self):
|
|
||||||
"""Return the xheight as float."""
|
|
||||||
return self._header[b'XHeight']
|
|
||||||
|
|
||||||
def get_underline_thickness(self):
|
|
||||||
"""Return the underline thickness as float."""
|
|
||||||
return self._header[b'UnderlineThickness']
|
|
||||||
|
|
||||||
def get_horizontal_stem_width(self):
|
|
||||||
"""
|
|
||||||
Return the standard horizontal stem width as float, or *None* if
|
|
||||||
not specified in AFM file.
|
|
||||||
"""
|
|
||||||
return self._header.get(b'StdHW', None)
|
|
||||||
|
|
||||||
def get_vertical_stem_width(self):
|
|
||||||
"""
|
|
||||||
Return the standard vertical stem width as float, or *None* if
|
|
||||||
not specified in AFM file.
|
|
||||||
"""
|
|
||||||
return self._header.get(b'StdVW', None)
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
from ._subplots import *
|
|
||||||
from ._axes import *
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
import functools
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from matplotlib import docstring
|
|
||||||
import matplotlib.artist as martist
|
|
||||||
from matplotlib.axes._axes import Axes
|
|
||||||
from matplotlib.gridspec import GridSpec, SubplotSpec
|
|
||||||
import matplotlib._layoutbox as layoutbox
|
|
||||||
|
|
||||||
|
|
||||||
class SubplotBase(object):
|
|
||||||
"""
|
|
||||||
Base class for subplots, which are :class:`Axes` instances with
|
|
||||||
additional methods to facilitate generating and manipulating a set
|
|
||||||
of :class:`Axes` within a figure.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, fig, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
*fig* is a :class:`matplotlib.figure.Figure` instance.
|
|
||||||
|
|
||||||
*args* is the tuple (*numRows*, *numCols*, *plotNum*), where
|
|
||||||
the array of subplots in the figure has dimensions *numRows*,
|
|
||||||
*numCols*, and where *plotNum* is the number of the subplot
|
|
||||||
being created. *plotNum* starts at 1 in the upper left
|
|
||||||
corner and increases to the right.
|
|
||||||
|
|
||||||
If *numRows* <= *numCols* <= *plotNum* < 10, *args* can be the
|
|
||||||
decimal integer *numRows* * 100 + *numCols* * 10 + *plotNum*.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.figure = fig
|
|
||||||
|
|
||||||
if len(args) == 1:
|
|
||||||
if isinstance(args[0], SubplotSpec):
|
|
||||||
self._subplotspec = args[0]
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
s = str(int(args[0]))
|
|
||||||
rows, cols, num = map(int, s)
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError('Single argument to subplot must be '
|
|
||||||
'a 3-digit integer')
|
|
||||||
self._subplotspec = GridSpec(rows, cols,
|
|
||||||
figure=self.figure)[num - 1]
|
|
||||||
# num - 1 for converting from MATLAB to python indexing
|
|
||||||
elif len(args) == 3:
|
|
||||||
rows, cols, num = args
|
|
||||||
rows = int(rows)
|
|
||||||
cols = int(cols)
|
|
||||||
if isinstance(num, tuple) and len(num) == 2:
|
|
||||||
num = [int(n) for n in num]
|
|
||||||
self._subplotspec = GridSpec(
|
|
||||||
rows, cols,
|
|
||||||
figure=self.figure)[(num[0] - 1):num[1]]
|
|
||||||
else:
|
|
||||||
if num < 1 or num > rows*cols:
|
|
||||||
raise ValueError(
|
|
||||||
("num must be 1 <= num <= {maxn}, not {num}"
|
|
||||||
).format(maxn=rows*cols, num=num))
|
|
||||||
self._subplotspec = GridSpec(
|
|
||||||
rows, cols, figure=self.figure)[int(num) - 1]
|
|
||||||
# num - 1 for converting from MATLAB to python indexing
|
|
||||||
else:
|
|
||||||
raise ValueError('Illegal argument(s) to subplot: %s' % (args,))
|
|
||||||
|
|
||||||
self.update_params()
|
|
||||||
|
|
||||||
# _axes_class is set in the subplot_class_factory
|
|
||||||
self._axes_class.__init__(self, fig, self.figbox, **kwargs)
|
|
||||||
# add a layout box to this, for both the full axis, and the poss
|
|
||||||
# of the axis. We need both because the axes may become smaller
|
|
||||||
# due to parasitic axes and hence no longer fill the subplotspec.
|
|
||||||
if self._subplotspec._layoutbox is None:
|
|
||||||
self._layoutbox = None
|
|
||||||
self._poslayoutbox = None
|
|
||||||
else:
|
|
||||||
name = self._subplotspec._layoutbox.name + '.ax'
|
|
||||||
name = name + layoutbox.seq_id()
|
|
||||||
self._layoutbox = layoutbox.LayoutBox(
|
|
||||||
parent=self._subplotspec._layoutbox,
|
|
||||||
name=name,
|
|
||||||
artist=self)
|
|
||||||
self._poslayoutbox = layoutbox.LayoutBox(
|
|
||||||
parent=self._layoutbox,
|
|
||||||
name=self._layoutbox.name+'.pos',
|
|
||||||
pos=True, subplot=True, artist=self)
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
# get the first axes class which does not inherit from a subplotbase
|
|
||||||
axes_class = next(
|
|
||||||
c for c in type(self).__mro__
|
|
||||||
if issubclass(c, Axes) and not issubclass(c, SubplotBase))
|
|
||||||
return (_picklable_subplot_class_constructor,
|
|
||||||
(axes_class,),
|
|
||||||
self.__getstate__())
|
|
||||||
|
|
||||||
def get_geometry(self):
|
|
||||||
"""get the subplot geometry, e.g., 2,2,3"""
|
|
||||||
rows, cols, num1, num2 = self.get_subplotspec().get_geometry()
|
|
||||||
return rows, cols, num1 + 1 # for compatibility
|
|
||||||
|
|
||||||
# COVERAGE NOTE: Never used internally or from examples
|
|
||||||
def change_geometry(self, numrows, numcols, num):
|
|
||||||
"""change subplot geometry, e.g., from 1,1,1 to 2,2,3"""
|
|
||||||
self._subplotspec = GridSpec(numrows, numcols,
|
|
||||||
figure=self.figure)[num - 1]
|
|
||||||
self.update_params()
|
|
||||||
self.set_position(self.figbox)
|
|
||||||
|
|
||||||
def get_subplotspec(self):
|
|
||||||
"""get the SubplotSpec instance associated with the subplot"""
|
|
||||||
return self._subplotspec
|
|
||||||
|
|
||||||
def set_subplotspec(self, subplotspec):
|
|
||||||
"""set the SubplotSpec instance associated with the subplot"""
|
|
||||||
self._subplotspec = subplotspec
|
|
||||||
|
|
||||||
def get_gridspec(self):
|
|
||||||
"""get the GridSpec instance associated with the subplot"""
|
|
||||||
return self._subplotspec.get_gridspec()
|
|
||||||
|
|
||||||
def update_params(self):
|
|
||||||
"""update the subplot position from fig.subplotpars"""
|
|
||||||
|
|
||||||
self.figbox, self.rowNum, self.colNum, self.numRows, self.numCols = \
|
|
||||||
self.get_subplotspec().get_position(self.figure,
|
|
||||||
return_all=True)
|
|
||||||
|
|
||||||
def is_first_col(self):
|
|
||||||
return self.colNum == 0
|
|
||||||
|
|
||||||
def is_first_row(self):
|
|
||||||
return self.rowNum == 0
|
|
||||||
|
|
||||||
def is_last_row(self):
|
|
||||||
return self.rowNum == self.numRows - 1
|
|
||||||
|
|
||||||
def is_last_col(self):
|
|
||||||
return self.colNum == self.numCols - 1
|
|
||||||
|
|
||||||
# COVERAGE NOTE: Never used internally.
|
|
||||||
def label_outer(self):
|
|
||||||
"""Only show "outer" labels and tick labels.
|
|
||||||
|
|
||||||
x-labels are only kept for subplots on the last row; y-labels only for
|
|
||||||
subplots on the first column.
|
|
||||||
"""
|
|
||||||
lastrow = self.is_last_row()
|
|
||||||
firstcol = self.is_first_col()
|
|
||||||
if not lastrow:
|
|
||||||
for label in self.get_xticklabels(which="both"):
|
|
||||||
label.set_visible(False)
|
|
||||||
self.get_xaxis().get_offset_text().set_visible(False)
|
|
||||||
self.set_xlabel("")
|
|
||||||
if not firstcol:
|
|
||||||
for label in self.get_yticklabels(which="both"):
|
|
||||||
label.set_visible(False)
|
|
||||||
self.get_yaxis().get_offset_text().set_visible(False)
|
|
||||||
self.set_ylabel("")
|
|
||||||
|
|
||||||
def _make_twin_axes(self, *kl, **kwargs):
|
|
||||||
"""
|
|
||||||
Make a twinx axes of self. This is used for twinx and twiny.
|
|
||||||
"""
|
|
||||||
from matplotlib.projections import process_projection_requirements
|
|
||||||
if 'sharex' in kwargs and 'sharey' in kwargs:
|
|
||||||
# The following line is added in v2.2 to avoid breaking Seaborn,
|
|
||||||
# which currently uses this internal API.
|
|
||||||
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
|
|
||||||
raise ValueError("Twinned Axes may share only one axis.")
|
|
||||||
kl = (self.get_subplotspec(),) + kl
|
|
||||||
projection_class, kwargs, key = process_projection_requirements(
|
|
||||||
self.figure, *kl, **kwargs)
|
|
||||||
|
|
||||||
ax2 = subplot_class_factory(projection_class)(self.figure,
|
|
||||||
*kl, **kwargs)
|
|
||||||
self.figure.add_subplot(ax2)
|
|
||||||
self.set_adjustable('datalim')
|
|
||||||
ax2.set_adjustable('datalim')
|
|
||||||
|
|
||||||
if self._layoutbox is not None and ax2._layoutbox is not None:
|
|
||||||
# make the layout boxes be explicitly the same
|
|
||||||
ax2._layoutbox.constrain_same(self._layoutbox)
|
|
||||||
ax2._poslayoutbox.constrain_same(self._poslayoutbox)
|
|
||||||
|
|
||||||
self._twinned_axes.join(self, ax2)
|
|
||||||
return ax2
|
|
||||||
|
|
||||||
|
|
||||||
# this here to support cartopy which was using a private part of the
|
|
||||||
# API to register their Axes subclasses.
|
|
||||||
|
|
||||||
# In 3.1 this should be changed to a dict subclass that warns on use
|
|
||||||
# In 3.3 to a dict subclass that raises a useful exception on use
|
|
||||||
# In 3.4 should be removed
|
|
||||||
|
|
||||||
# The slow timeline is to give cartopy enough time to get several
|
|
||||||
# release out before we break them.
|
|
||||||
_subplot_classes = {}
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(None)
|
|
||||||
def subplot_class_factory(axes_class=None):
|
|
||||||
"""
|
|
||||||
This makes a new class that inherits from `.SubplotBase` and the
|
|
||||||
given axes_class (which is assumed to be a subclass of `.axes.Axes`).
|
|
||||||
This is perhaps a little bit roundabout to make a new class on
|
|
||||||
the fly like this, but it means that a new Subplot class does
|
|
||||||
not have to be created for every type of Axes.
|
|
||||||
"""
|
|
||||||
if axes_class is None:
|
|
||||||
axes_class = Axes
|
|
||||||
try:
|
|
||||||
# Avoid creating two different instances of GeoAxesSubplot...
|
|
||||||
# Only a temporary backcompat fix. This should be removed in
|
|
||||||
# 3.4
|
|
||||||
return next(cls for cls in SubplotBase.__subclasses__()
|
|
||||||
if cls.__bases__ == (SubplotBase, axes_class))
|
|
||||||
except StopIteration:
|
|
||||||
return type("%sSubplot" % axes_class.__name__,
|
|
||||||
(SubplotBase, axes_class),
|
|
||||||
{'_axes_class': axes_class})
|
|
||||||
|
|
||||||
|
|
||||||
# This is provided for backward compatibility
|
|
||||||
Subplot = subplot_class_factory()
|
|
||||||
|
|
||||||
|
|
||||||
def _picklable_subplot_class_constructor(axes_class):
|
|
||||||
"""
|
|
||||||
This stub class exists to return the appropriate subplot class when called
|
|
||||||
with an axes class. This is purely to allow pickling of Axes and Subplots.
|
|
||||||
"""
|
|
||||||
subplot_class = subplot_class_factory(axes_class)
|
|
||||||
return subplot_class.__new__(subplot_class)
|
|
||||||
|
|
||||||
|
|
||||||
docstring.interpd.update(Axes=martist.kwdoc(Axes))
|
|
||||||
docstring.dedent_interpd(Axes.__init__)
|
|
||||||
|
|
||||||
docstring.interpd.update(Subplot=martist.kwdoc(Axes))
|
|
||||||
@@ -1,433 +0,0 @@
|
|||||||
"""
|
|
||||||
`ToolManager`
|
|
||||||
Class that makes the bridge between user interaction (key press,
|
|
||||||
toolbar clicks, ..) and the actions in response to the user inputs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
import matplotlib.cbook as cbook
|
|
||||||
import matplotlib.widgets as widgets
|
|
||||||
from matplotlib.rcsetup import validate_stringlist
|
|
||||||
import matplotlib.backend_tools as tools
|
|
||||||
|
|
||||||
|
|
||||||
class ToolEvent(object):
|
|
||||||
"""Event for tool manipulation (add/remove)"""
|
|
||||||
def __init__(self, name, sender, tool, data=None):
|
|
||||||
self.name = name
|
|
||||||
self.sender = sender
|
|
||||||
self.tool = tool
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
|
|
||||||
class ToolTriggerEvent(ToolEvent):
|
|
||||||
"""Event to inform that a tool has been triggered"""
|
|
||||||
def __init__(self, name, sender, tool, canvasevent=None, data=None):
|
|
||||||
ToolEvent.__init__(self, name, sender, tool, data)
|
|
||||||
self.canvasevent = canvasevent
|
|
||||||
|
|
||||||
|
|
||||||
class ToolManagerMessageEvent(object):
|
|
||||||
"""
|
|
||||||
Event carrying messages from toolmanager
|
|
||||||
|
|
||||||
Messages usually get displayed to the user by the toolbar
|
|
||||||
"""
|
|
||||||
def __init__(self, name, sender, message):
|
|
||||||
self.name = name
|
|
||||||
self.sender = sender
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
|
|
||||||
class ToolManager(object):
|
|
||||||
"""
|
|
||||||
Helper class that groups all the user interactions for a Figure.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
figure: `Figure`
|
|
||||||
keypresslock: `widgets.LockDraw`
|
|
||||||
`LockDraw` object to know if the `canvas` key_press_event is locked
|
|
||||||
messagelock: `widgets.LockDraw`
|
|
||||||
`LockDraw` object to know if the message is available to write
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, figure=None):
|
|
||||||
warnings.warn('Treat the new Tool classes introduced in v1.5 as ' +
|
|
||||||
'experimental for now, the API will likely change in ' +
|
|
||||||
'version 2.1 and perhaps the rcParam as well')
|
|
||||||
|
|
||||||
self._key_press_handler_id = None
|
|
||||||
|
|
||||||
self._tools = {}
|
|
||||||
self._keys = {}
|
|
||||||
self._toggled = {}
|
|
||||||
self._callbacks = cbook.CallbackRegistry()
|
|
||||||
|
|
||||||
# to process keypress event
|
|
||||||
self.keypresslock = widgets.LockDraw()
|
|
||||||
self.messagelock = widgets.LockDraw()
|
|
||||||
|
|
||||||
self._figure = None
|
|
||||||
self.set_figure(figure)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def canvas(self):
|
|
||||||
"""Canvas managed by FigureManager"""
|
|
||||||
if not self._figure:
|
|
||||||
return None
|
|
||||||
return self._figure.canvas
|
|
||||||
|
|
||||||
@property
|
|
||||||
def figure(self):
|
|
||||||
"""Figure that holds the canvas"""
|
|
||||||
return self._figure
|
|
||||||
|
|
||||||
@figure.setter
|
|
||||||
def figure(self, figure):
|
|
||||||
self.set_figure(figure)
|
|
||||||
|
|
||||||
def set_figure(self, figure, update_tools=True):
|
|
||||||
"""
|
|
||||||
Bind the given figure to the tools.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
figure : `.Figure`
|
|
||||||
update_tools : bool
|
|
||||||
Force tools to update figure
|
|
||||||
"""
|
|
||||||
if self._key_press_handler_id:
|
|
||||||
self.canvas.mpl_disconnect(self._key_press_handler_id)
|
|
||||||
self._figure = figure
|
|
||||||
if figure:
|
|
||||||
self._key_press_handler_id = self.canvas.mpl_connect(
|
|
||||||
'key_press_event', self._key_press)
|
|
||||||
if update_tools:
|
|
||||||
for tool in self._tools.values():
|
|
||||||
tool.figure = figure
|
|
||||||
|
|
||||||
def toolmanager_connect(self, s, func):
|
|
||||||
"""
|
|
||||||
Connect event with string *s* to *func*.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
s : String
|
|
||||||
Name of the event
|
|
||||||
|
|
||||||
The following events are recognized
|
|
||||||
|
|
||||||
- 'tool_message_event'
|
|
||||||
- 'tool_removed_event'
|
|
||||||
- 'tool_added_event'
|
|
||||||
|
|
||||||
For every tool added a new event is created
|
|
||||||
|
|
||||||
- 'tool_trigger_TOOLNAME`
|
|
||||||
Where TOOLNAME is the id of the tool.
|
|
||||||
|
|
||||||
func : function
|
|
||||||
Function to be called with signature
|
|
||||||
def func(event)
|
|
||||||
"""
|
|
||||||
return self._callbacks.connect(s, func)
|
|
||||||
|
|
||||||
def toolmanager_disconnect(self, cid):
|
|
||||||
"""
|
|
||||||
Disconnect callback id *cid*
|
|
||||||
|
|
||||||
Example usage::
|
|
||||||
|
|
||||||
cid = toolmanager.toolmanager_connect('tool_trigger_zoom',
|
|
||||||
on_press)
|
|
||||||
#...later
|
|
||||||
toolmanager.toolmanager_disconnect(cid)
|
|
||||||
"""
|
|
||||||
return self._callbacks.disconnect(cid)
|
|
||||||
|
|
||||||
def message_event(self, message, sender=None):
|
|
||||||
""" Emit a `ToolManagerMessageEvent`"""
|
|
||||||
if sender is None:
|
|
||||||
sender = self
|
|
||||||
|
|
||||||
s = 'tool_message_event'
|
|
||||||
event = ToolManagerMessageEvent(s, sender, message)
|
|
||||||
self._callbacks.process(s, event)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def active_toggle(self):
|
|
||||||
"""Currently toggled tools"""
|
|
||||||
|
|
||||||
return self._toggled
|
|
||||||
|
|
||||||
def get_tool_keymap(self, name):
|
|
||||||
"""
|
|
||||||
Get the keymap associated with the specified tool
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : string
|
|
||||||
Name of the Tool
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
list : list of keys associated with the Tool
|
|
||||||
"""
|
|
||||||
|
|
||||||
keys = [k for k, i in self._keys.items() if i == name]
|
|
||||||
return keys
|
|
||||||
|
|
||||||
def _remove_keys(self, name):
|
|
||||||
for k in self.get_tool_keymap(name):
|
|
||||||
del self._keys[k]
|
|
||||||
|
|
||||||
def update_keymap(self, name, *keys):
|
|
||||||
"""
|
|
||||||
Set the keymap to associate with the specified tool
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : string
|
|
||||||
Name of the Tool
|
|
||||||
keys : keys to associate with the Tool
|
|
||||||
"""
|
|
||||||
|
|
||||||
if name not in self._tools:
|
|
||||||
raise KeyError('%s not in Tools' % name)
|
|
||||||
|
|
||||||
self._remove_keys(name)
|
|
||||||
|
|
||||||
for key in keys:
|
|
||||||
for k in validate_stringlist(key):
|
|
||||||
if k in self._keys:
|
|
||||||
warnings.warn('Key %s changed from %s to %s' %
|
|
||||||
(k, self._keys[k], name))
|
|
||||||
self._keys[k] = name
|
|
||||||
|
|
||||||
def remove_tool(self, name):
|
|
||||||
"""
|
|
||||||
Remove tool from `ToolManager`
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : string
|
|
||||||
Name of the Tool
|
|
||||||
"""
|
|
||||||
|
|
||||||
tool = self.get_tool(name)
|
|
||||||
tool.destroy()
|
|
||||||
|
|
||||||
# If is a toggle tool and toggled, untoggle
|
|
||||||
if getattr(tool, 'toggled', False):
|
|
||||||
self.trigger_tool(tool, 'toolmanager')
|
|
||||||
|
|
||||||
self._remove_keys(name)
|
|
||||||
|
|
||||||
s = 'tool_removed_event'
|
|
||||||
event = ToolEvent(s, self, tool)
|
|
||||||
self._callbacks.process(s, event)
|
|
||||||
|
|
||||||
del self._tools[name]
|
|
||||||
|
|
||||||
def add_tool(self, name, tool, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Add *tool* to `ToolManager`
|
|
||||||
|
|
||||||
If successful adds a new event `tool_trigger_name` where **name** is
|
|
||||||
the **name** of the tool, this event is fired everytime
|
|
||||||
the tool is triggered.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : str
|
|
||||||
Name of the tool, treated as the ID, has to be unique
|
|
||||||
tool : class_like, i.e. str or type
|
|
||||||
Reference to find the class of the Tool to added.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
args and kwargs get passed directly to the tools constructor.
|
|
||||||
|
|
||||||
See Also
|
|
||||||
--------
|
|
||||||
matplotlib.backend_tools.ToolBase : The base class for tools.
|
|
||||||
"""
|
|
||||||
|
|
||||||
tool_cls = self._get_cls_to_instantiate(tool)
|
|
||||||
if not tool_cls:
|
|
||||||
raise ValueError('Impossible to find class for %s' % str(tool))
|
|
||||||
|
|
||||||
if name in self._tools:
|
|
||||||
warnings.warn('A "Tool class" with the same name already exists, '
|
|
||||||
'not added')
|
|
||||||
return self._tools[name]
|
|
||||||
|
|
||||||
tool_obj = tool_cls(self, name, *args, **kwargs)
|
|
||||||
self._tools[name] = tool_obj
|
|
||||||
|
|
||||||
if tool_cls.default_keymap is not None:
|
|
||||||
self.update_keymap(name, tool_cls.default_keymap)
|
|
||||||
|
|
||||||
# For toggle tools init the radio_group in self._toggled
|
|
||||||
if isinstance(tool_obj, tools.ToolToggleBase):
|
|
||||||
# None group is not mutually exclusive, a set is used to keep track
|
|
||||||
# of all toggled tools in this group
|
|
||||||
if tool_obj.radio_group is None:
|
|
||||||
self._toggled.setdefault(None, set())
|
|
||||||
else:
|
|
||||||
self._toggled.setdefault(tool_obj.radio_group, None)
|
|
||||||
|
|
||||||
# If initially toggled
|
|
||||||
if tool_obj.toggled:
|
|
||||||
self._handle_toggle(tool_obj, None, None, None)
|
|
||||||
tool_obj.set_figure(self.figure)
|
|
||||||
|
|
||||||
self._tool_added_event(tool_obj)
|
|
||||||
return tool_obj
|
|
||||||
|
|
||||||
def _tool_added_event(self, tool):
|
|
||||||
s = 'tool_added_event'
|
|
||||||
event = ToolEvent(s, self, tool)
|
|
||||||
self._callbacks.process(s, event)
|
|
||||||
|
|
||||||
def _handle_toggle(self, tool, sender, canvasevent, data):
|
|
||||||
"""
|
|
||||||
Toggle tools, need to untoggle prior to using other Toggle tool
|
|
||||||
Called from trigger_tool
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
tool: Tool object
|
|
||||||
sender: object
|
|
||||||
Object that wishes to trigger the tool
|
|
||||||
canvasevent : Event
|
|
||||||
Original Canvas event or None
|
|
||||||
data : Object
|
|
||||||
Extra data to pass to the tool when triggering
|
|
||||||
"""
|
|
||||||
|
|
||||||
radio_group = tool.radio_group
|
|
||||||
# radio_group None is not mutually exclusive
|
|
||||||
# just keep track of toggled tools in this group
|
|
||||||
if radio_group is None:
|
|
||||||
if tool.name in self._toggled[None]:
|
|
||||||
self._toggled[None].remove(tool.name)
|
|
||||||
else:
|
|
||||||
self._toggled[None].add(tool.name)
|
|
||||||
return
|
|
||||||
|
|
||||||
# If the tool already has a toggled state, untoggle it
|
|
||||||
if self._toggled[radio_group] == tool.name:
|
|
||||||
toggled = None
|
|
||||||
# If no tool was toggled in the radio_group
|
|
||||||
# toggle it
|
|
||||||
elif self._toggled[radio_group] is None:
|
|
||||||
toggled = tool.name
|
|
||||||
# Other tool in the radio_group is toggled
|
|
||||||
else:
|
|
||||||
# Untoggle previously toggled tool
|
|
||||||
self.trigger_tool(self._toggled[radio_group],
|
|
||||||
self,
|
|
||||||
canvasevent,
|
|
||||||
data)
|
|
||||||
toggled = tool.name
|
|
||||||
|
|
||||||
# Keep track of the toggled tool in the radio_group
|
|
||||||
self._toggled[radio_group] = toggled
|
|
||||||
|
|
||||||
def _get_cls_to_instantiate(self, callback_class):
|
|
||||||
# Find the class that corresponds to the tool
|
|
||||||
if isinstance(callback_class, str):
|
|
||||||
# FIXME: make more complete searching structure
|
|
||||||
if callback_class in globals():
|
|
||||||
callback_class = globals()[callback_class]
|
|
||||||
else:
|
|
||||||
mod = 'backend_tools'
|
|
||||||
current_module = __import__(mod,
|
|
||||||
globals(), locals(), [mod], 1)
|
|
||||||
|
|
||||||
callback_class = getattr(current_module, callback_class, False)
|
|
||||||
if callable(callback_class):
|
|
||||||
return callback_class
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def trigger_tool(self, name, sender=None, canvasevent=None,
|
|
||||||
data=None):
|
|
||||||
"""
|
|
||||||
Trigger a tool and emit the tool_trigger_[name] event
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : string
|
|
||||||
Name of the tool
|
|
||||||
sender: object
|
|
||||||
Object that wishes to trigger the tool
|
|
||||||
canvasevent : Event
|
|
||||||
Original Canvas event or None
|
|
||||||
data : Object
|
|
||||||
Extra data to pass to the tool when triggering
|
|
||||||
"""
|
|
||||||
tool = self.get_tool(name)
|
|
||||||
if tool is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
if sender is None:
|
|
||||||
sender = self
|
|
||||||
|
|
||||||
self._trigger_tool(name, sender, canvasevent, data)
|
|
||||||
|
|
||||||
s = 'tool_trigger_%s' % name
|
|
||||||
event = ToolTriggerEvent(s, sender, tool, canvasevent, data)
|
|
||||||
self._callbacks.process(s, event)
|
|
||||||
|
|
||||||
def _trigger_tool(self, name, sender=None, canvasevent=None, data=None):
|
|
||||||
"""
|
|
||||||
Trigger on a tool
|
|
||||||
|
|
||||||
Method to actually trigger the tool
|
|
||||||
"""
|
|
||||||
tool = self.get_tool(name)
|
|
||||||
|
|
||||||
if isinstance(tool, tools.ToolToggleBase):
|
|
||||||
self._handle_toggle(tool, sender, canvasevent, data)
|
|
||||||
|
|
||||||
# Important!!!
|
|
||||||
# This is where the Tool object gets triggered
|
|
||||||
tool.trigger(sender, canvasevent, data)
|
|
||||||
|
|
||||||
def _key_press(self, event):
|
|
||||||
if event.key is None or self.keypresslock.locked():
|
|
||||||
return
|
|
||||||
|
|
||||||
name = self._keys.get(event.key, None)
|
|
||||||
if name is None:
|
|
||||||
return
|
|
||||||
self.trigger_tool(name, canvasevent=event)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def tools(self):
|
|
||||||
"""Return the tools controlled by `ToolManager`"""
|
|
||||||
|
|
||||||
return self._tools
|
|
||||||
|
|
||||||
def get_tool(self, name, warn=True):
|
|
||||||
"""
|
|
||||||
Return the tool object, also accepts the actual tool for convenience
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : str, ToolBase
|
|
||||||
Name of the tool, or the tool itself
|
|
||||||
warn : bool, optional
|
|
||||||
If this method should give warnings.
|
|
||||||
"""
|
|
||||||
if isinstance(name, tools.ToolBase) and name.name in self._tools:
|
|
||||||
return name
|
|
||||||
if name not in self._tools:
|
|
||||||
if warn:
|
|
||||||
warnings.warn("ToolManager does not control tool %s" % name)
|
|
||||||
return None
|
|
||||||
return self._tools[name]
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import importlib
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
from matplotlib import cbook
|
|
||||||
from matplotlib.backend_bases import _Backend
|
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE: plt.switch_backend() (called at import time) will add a "backend"
|
|
||||||
# attribute here for backcompat.
|
|
||||||
|
|
||||||
|
|
||||||
def _get_running_interactive_framework():
|
|
||||||
"""
|
|
||||||
Return the interactive framework whose event loop is currently running, if
|
|
||||||
any, or "headless" if no event loop can be started, or None.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
Optional[str]
|
|
||||||
One of the following values: "qt5", "qt4", "gtk3", "wx", "tk",
|
|
||||||
"macosx", "headless", ``None``.
|
|
||||||
"""
|
|
||||||
QtWidgets = (sys.modules.get("PyQt5.QtWidgets")
|
|
||||||
or sys.modules.get("PySide2.QtWidgets"))
|
|
||||||
if QtWidgets and QtWidgets.QApplication.instance():
|
|
||||||
return "qt5"
|
|
||||||
QtGui = (sys.modules.get("PyQt4.QtGui")
|
|
||||||
or sys.modules.get("PySide.QtGui"))
|
|
||||||
if QtGui and QtGui.QApplication.instance():
|
|
||||||
return "qt4"
|
|
||||||
Gtk = (sys.modules.get("gi.repository.Gtk")
|
|
||||||
or sys.modules.get("pgi.repository.Gtk"))
|
|
||||||
if Gtk and Gtk.main_level():
|
|
||||||
return "gtk3"
|
|
||||||
wx = sys.modules.get("wx")
|
|
||||||
if wx and wx.GetApp():
|
|
||||||
return "wx"
|
|
||||||
tkinter = sys.modules.get("tkinter")
|
|
||||||
if tkinter:
|
|
||||||
for frame in sys._current_frames().values():
|
|
||||||
while frame:
|
|
||||||
if frame.f_code == tkinter.mainloop.__code__:
|
|
||||||
return "tk"
|
|
||||||
frame = frame.f_back
|
|
||||||
if 'matplotlib.backends._macosx' in sys.modules:
|
|
||||||
if sys.modules["matplotlib.backends._macosx"].event_loop_is_running():
|
|
||||||
return "macosx"
|
|
||||||
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
|
|
||||||
return "headless"
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@cbook.deprecated("3.0")
|
|
||||||
def pylab_setup(name=None):
|
|
||||||
"""
|
|
||||||
Return new_figure_manager, draw_if_interactive and show for pyplot.
|
|
||||||
|
|
||||||
This provides the backend-specific functions that are used by pyplot to
|
|
||||||
abstract away the difference between backends.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
name : str, optional
|
|
||||||
The name of the backend to use. If `None`, falls back to
|
|
||||||
``matplotlib.get_backend()`` (which return :rc:`backend`).
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
backend_mod : module
|
|
||||||
The module which contains the backend of choice
|
|
||||||
|
|
||||||
new_figure_manager : function
|
|
||||||
Create a new figure manager (roughly maps to GUI window)
|
|
||||||
|
|
||||||
draw_if_interactive : function
|
|
||||||
Redraw the current figure if pyplot is interactive
|
|
||||||
|
|
||||||
show : function
|
|
||||||
Show (and possibly block) any unshown figures.
|
|
||||||
"""
|
|
||||||
# Import the requested backend into a generic module object.
|
|
||||||
if name is None:
|
|
||||||
name = matplotlib.get_backend()
|
|
||||||
backend_name = (name[9:] if name.startswith("module://")
|
|
||||||
else "matplotlib.backends.backend_{}".format(name.lower()))
|
|
||||||
backend_mod = importlib.import_module(backend_name)
|
|
||||||
# Create a local Backend class whose body corresponds to the contents of
|
|
||||||
# the backend module. This allows the Backend class to fill in the missing
|
|
||||||
# methods through inheritance.
|
|
||||||
Backend = type("Backend", (_Backend,), vars(backend_mod))
|
|
||||||
|
|
||||||
# Need to keep a global reference to the backend for compatibility reasons.
|
|
||||||
# See https://github.com/matplotlib/matplotlib/issues/6092
|
|
||||||
global backend
|
|
||||||
backend = name
|
|
||||||
|
|
||||||
_log.debug('backend %s version %s', name, Backend.backend_version)
|
|
||||||
return (backend_mod,
|
|
||||||
Backend.new_figure_manager,
|
|
||||||
Backend.draw_if_interactive,
|
|
||||||
Backend.show)
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
"""
|
|
||||||
GObject compatibility loader; supports ``gi`` and ``pgi``.
|
|
||||||
|
|
||||||
The binding selection rules are as follows:
|
|
||||||
- if ``gi`` has already been imported, use it; else
|
|
||||||
- if ``pgi`` has already been imported, use it; else
|
|
||||||
- if ``gi`` can be imported, use it; else
|
|
||||||
- if ``pgi`` can be imported, use it; else
|
|
||||||
- error out.
|
|
||||||
|
|
||||||
Thus, to force usage of PGI when both bindings are installed, import it first.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import importlib
|
|
||||||
import sys
|
|
||||||
|
|
||||||
if "gi" in sys.modules:
|
|
||||||
import gi
|
|
||||||
elif "pgi" in sys.modules:
|
|
||||||
import pgi as gi
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
import gi
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import pgi as gi
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError("The GTK3 backends require PyGObject or pgi")
|
|
||||||
|
|
||||||
from .backend_cairo import cairo # noqa
|
|
||||||
# The following combinations are allowed:
|
|
||||||
# gi + pycairo
|
|
||||||
# gi + cairocffi
|
|
||||||
# pgi + cairocffi
|
|
||||||
# (pgi doesn't work with pycairo)
|
|
||||||
# We always try to import cairocffi first so if a check below fails it means
|
|
||||||
# that cairocffi was unavailable to start with.
|
|
||||||
if gi.__name__ == "pgi" and cairo.__name__ == "cairo":
|
|
||||||
raise ImportError("pgi and pycairo are not compatible")
|
|
||||||
|
|
||||||
if gi.__name__ == "pgi" and gi.version_info < (0, 0, 11, 2):
|
|
||||||
raise ImportError("The GTK3 backends are incompatible with pgi<0.0.11.2")
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
globals().update(
|
|
||||||
{name:
|
|
||||||
importlib.import_module("{}.repository.{}".format(gi.__name__, name))
|
|
||||||
for name in ["GLib", "GObject", "Gtk", "Gdk"]})
|
|
||||||
@@ -1,595 +0,0 @@
|
|||||||
"""
|
|
||||||
An agg http://antigrain.com/ backend
|
|
||||||
|
|
||||||
Features that are implemented
|
|
||||||
|
|
||||||
* capstyles and join styles
|
|
||||||
* dashes
|
|
||||||
* linewidth
|
|
||||||
* lines, rectangles, ellipses
|
|
||||||
* clipping to a rectangle
|
|
||||||
* output to RGBA and PNG, optionally JPEG and TIFF
|
|
||||||
* alpha blending
|
|
||||||
* DPI scaling properly - everything scales properly (dashes, linewidths, etc)
|
|
||||||
* draw polygon
|
|
||||||
* freetype2 w/ ft2font
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
|
|
||||||
* integrate screen dpi w/ ppi and text
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
import threading
|
|
||||||
except ImportError:
|
|
||||||
import dummy_threading as threading
|
|
||||||
import numpy as np
|
|
||||||
from collections import OrderedDict
|
|
||||||
from math import radians, cos, sin
|
|
||||||
from matplotlib import cbook, rcParams, __version__
|
|
||||||
from matplotlib.backend_bases import (
|
|
||||||
_Backend, FigureCanvasBase, FigureManagerBase, RendererBase)
|
|
||||||
from matplotlib.font_manager import findfont, get_font
|
|
||||||
from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING,
|
|
||||||
LOAD_DEFAULT, LOAD_NO_AUTOHINT)
|
|
||||||
from matplotlib.mathtext import MathTextParser
|
|
||||||
from matplotlib.path import Path
|
|
||||||
from matplotlib.transforms import Bbox, BboxBase
|
|
||||||
from matplotlib import colors as mcolors
|
|
||||||
|
|
||||||
from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg
|
|
||||||
from matplotlib import _png
|
|
||||||
|
|
||||||
from matplotlib.backend_bases import _has_pil
|
|
||||||
|
|
||||||
if _has_pil:
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
backend_version = 'v2.2'
|
|
||||||
|
|
||||||
def get_hinting_flag():
|
|
||||||
mapping = {
|
|
||||||
True: LOAD_FORCE_AUTOHINT,
|
|
||||||
False: LOAD_NO_HINTING,
|
|
||||||
'either': LOAD_DEFAULT,
|
|
||||||
'native': LOAD_NO_AUTOHINT,
|
|
||||||
'auto': LOAD_FORCE_AUTOHINT,
|
|
||||||
'none': LOAD_NO_HINTING
|
|
||||||
}
|
|
||||||
return mapping[rcParams['text.hinting']]
|
|
||||||
|
|
||||||
|
|
||||||
class RendererAgg(RendererBase):
|
|
||||||
"""
|
|
||||||
The renderer handles all the drawing primitives using a graphics
|
|
||||||
context instance that controls the colors/styles
|
|
||||||
"""
|
|
||||||
|
|
||||||
# we want to cache the fonts at the class level so that when
|
|
||||||
# multiple figures are created we can reuse them. This helps with
|
|
||||||
# a bug on windows where the creation of too many figures leads to
|
|
||||||
# too many open file handles. However, storing them at the class
|
|
||||||
# level is not thread safe. The solution here is to let the
|
|
||||||
# FigureCanvas acquire a lock on the fontd at the start of the
|
|
||||||
# draw, and release it when it is done. This allows multiple
|
|
||||||
# renderers to share the cached fonts, but only one figure can
|
|
||||||
# draw at time and so the font cache is used by only one
|
|
||||||
# renderer at a time.
|
|
||||||
|
|
||||||
lock = threading.RLock()
|
|
||||||
|
|
||||||
def __init__(self, width, height, dpi):
|
|
||||||
RendererBase.__init__(self)
|
|
||||||
|
|
||||||
self.dpi = dpi
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self._renderer = _RendererAgg(int(width), int(height), dpi)
|
|
||||||
self._filter_renderers = []
|
|
||||||
|
|
||||||
self._update_methods()
|
|
||||||
self.mathtext_parser = MathTextParser('Agg')
|
|
||||||
|
|
||||||
self.bbox = Bbox.from_bounds(0, 0, self.width, self.height)
|
|
||||||
|
|
||||||
def __getstate__(self):
|
|
||||||
# We only want to preserve the init keywords of the Renderer.
|
|
||||||
# Anything else can be re-created.
|
|
||||||
return {'width': self.width, 'height': self.height, 'dpi': self.dpi}
|
|
||||||
|
|
||||||
def __setstate__(self, state):
|
|
||||||
self.__init__(state['width'], state['height'], state['dpi'])
|
|
||||||
|
|
||||||
def _update_methods(self):
|
|
||||||
self.draw_gouraud_triangle = self._renderer.draw_gouraud_triangle
|
|
||||||
self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles
|
|
||||||
self.draw_image = self._renderer.draw_image
|
|
||||||
self.draw_markers = self._renderer.draw_markers
|
|
||||||
self.draw_path_collection = self._renderer.draw_path_collection
|
|
||||||
self.draw_quad_mesh = self._renderer.draw_quad_mesh
|
|
||||||
self.copy_from_bbox = self._renderer.copy_from_bbox
|
|
||||||
self.get_content_extents = self._renderer.get_content_extents
|
|
||||||
|
|
||||||
def tostring_rgba_minimized(self):
|
|
||||||
extents = self.get_content_extents()
|
|
||||||
bbox = [[extents[0], self.height - (extents[1] + extents[3])],
|
|
||||||
[extents[0] + extents[2], self.height - extents[1]]]
|
|
||||||
region = self.copy_from_bbox(bbox)
|
|
||||||
return np.array(region), extents
|
|
||||||
|
|
||||||
def draw_path(self, gc, path, transform, rgbFace=None):
|
|
||||||
"""
|
|
||||||
Draw the path
|
|
||||||
"""
|
|
||||||
nmax = rcParams['agg.path.chunksize'] # here at least for testing
|
|
||||||
npts = path.vertices.shape[0]
|
|
||||||
|
|
||||||
if (nmax > 100 and npts > nmax and path.should_simplify and
|
|
||||||
rgbFace is None and gc.get_hatch() is None):
|
|
||||||
nch = np.ceil(npts / nmax)
|
|
||||||
chsize = int(np.ceil(npts / nch))
|
|
||||||
i0 = np.arange(0, npts, chsize)
|
|
||||||
i1 = np.zeros_like(i0)
|
|
||||||
i1[:-1] = i0[1:] - 1
|
|
||||||
i1[-1] = npts
|
|
||||||
for ii0, ii1 in zip(i0, i1):
|
|
||||||
v = path.vertices[ii0:ii1, :]
|
|
||||||
c = path.codes
|
|
||||||
if c is not None:
|
|
||||||
c = c[ii0:ii1]
|
|
||||||
c[0] = Path.MOVETO # move to end of last chunk
|
|
||||||
p = Path(v, c)
|
|
||||||
try:
|
|
||||||
self._renderer.draw_path(gc, p, transform, rgbFace)
|
|
||||||
except OverflowError:
|
|
||||||
raise OverflowError("Exceeded cell block limit (set "
|
|
||||||
"'agg.path.chunksize' rcparam)")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
self._renderer.draw_path(gc, path, transform, rgbFace)
|
|
||||||
except OverflowError:
|
|
||||||
raise OverflowError("Exceeded cell block limit (set "
|
|
||||||
"'agg.path.chunksize' rcparam)")
|
|
||||||
|
|
||||||
def draw_mathtext(self, gc, x, y, s, prop, angle):
|
|
||||||
"""
|
|
||||||
Draw the math text using matplotlib.mathtext
|
|
||||||
"""
|
|
||||||
ox, oy, width, height, descent, font_image, used_characters = \
|
|
||||||
self.mathtext_parser.parse(s, self.dpi, prop)
|
|
||||||
|
|
||||||
xd = descent * sin(radians(angle))
|
|
||||||
yd = descent * cos(radians(angle))
|
|
||||||
x = np.round(x + ox + xd)
|
|
||||||
y = np.round(y - oy + yd)
|
|
||||||
self._renderer.draw_text_image(font_image, x, y + 1, angle, gc)
|
|
||||||
|
|
||||||
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
|
|
||||||
"""
|
|
||||||
Render the text
|
|
||||||
"""
|
|
||||||
if ismath:
|
|
||||||
return self.draw_mathtext(gc, x, y, s, prop, angle)
|
|
||||||
|
|
||||||
flags = get_hinting_flag()
|
|
||||||
font = self._get_agg_font(prop)
|
|
||||||
|
|
||||||
if font is None:
|
|
||||||
return None
|
|
||||||
if len(s) == 1 and ord(s) > 127:
|
|
||||||
font.load_char(ord(s), flags=flags)
|
|
||||||
else:
|
|
||||||
# We pass '0' for angle here, since it will be rotated (in raster
|
|
||||||
# space) in the following call to draw_text_image).
|
|
||||||
font.set_text(s, 0, flags=flags)
|
|
||||||
font.draw_glyphs_to_bitmap(antialiased=rcParams['text.antialiased'])
|
|
||||||
d = font.get_descent() / 64.0
|
|
||||||
# The descent needs to be adjusted for the angle.
|
|
||||||
xo, yo = font.get_bitmap_offset()
|
|
||||||
xo /= 64.0
|
|
||||||
yo /= 64.0
|
|
||||||
xd = -d * sin(radians(angle))
|
|
||||||
yd = d * cos(radians(angle))
|
|
||||||
|
|
||||||
self._renderer.draw_text_image(
|
|
||||||
font, np.round(x - xd + xo), np.round(y + yd + yo) + 1, angle, gc)
|
|
||||||
|
|
||||||
def get_text_width_height_descent(self, s, prop, ismath):
|
|
||||||
"""
|
|
||||||
Get the width, height, and descent (offset from the bottom
|
|
||||||
to the baseline), in display coords, of the string *s* with
|
|
||||||
:class:`~matplotlib.font_manager.FontProperties` *prop*
|
|
||||||
"""
|
|
||||||
if ismath in ["TeX", "TeX!"]:
|
|
||||||
# todo: handle props
|
|
||||||
size = prop.get_size_in_points()
|
|
||||||
texmanager = self.get_texmanager()
|
|
||||||
fontsize = prop.get_size_in_points()
|
|
||||||
w, h, d = texmanager.get_text_width_height_descent(
|
|
||||||
s, fontsize, renderer=self)
|
|
||||||
return w, h, d
|
|
||||||
|
|
||||||
if ismath:
|
|
||||||
ox, oy, width, height, descent, fonts, used_characters = \
|
|
||||||
self.mathtext_parser.parse(s, self.dpi, prop)
|
|
||||||
return width, height, descent
|
|
||||||
|
|
||||||
flags = get_hinting_flag()
|
|
||||||
font = self._get_agg_font(prop)
|
|
||||||
font.set_text(s, 0.0, flags=flags)
|
|
||||||
w, h = font.get_width_height() # width and height of unrotated string
|
|
||||||
d = font.get_descent()
|
|
||||||
w /= 64.0 # convert from subpixels
|
|
||||||
h /= 64.0
|
|
||||||
d /= 64.0
|
|
||||||
return w, h, d
|
|
||||||
|
|
||||||
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
|
|
||||||
# todo, handle props, angle, origins
|
|
||||||
size = prop.get_size_in_points()
|
|
||||||
|
|
||||||
texmanager = self.get_texmanager()
|
|
||||||
|
|
||||||
Z = texmanager.get_grey(s, size, self.dpi)
|
|
||||||
Z = np.array(Z * 255.0, np.uint8)
|
|
||||||
|
|
||||||
w, h, d = self.get_text_width_height_descent(s, prop, ismath)
|
|
||||||
xd = d * sin(radians(angle))
|
|
||||||
yd = d * cos(radians(angle))
|
|
||||||
x = np.round(x + xd)
|
|
||||||
y = np.round(y + yd)
|
|
||||||
|
|
||||||
self._renderer.draw_text_image(Z, x, y, angle, gc)
|
|
||||||
|
|
||||||
def get_canvas_width_height(self):
|
|
||||||
'return the canvas width and height in display coords'
|
|
||||||
return self.width, self.height
|
|
||||||
|
|
||||||
def _get_agg_font(self, prop):
|
|
||||||
"""
|
|
||||||
Get the font for text instance t, caching for efficiency
|
|
||||||
"""
|
|
||||||
fname = findfont(prop)
|
|
||||||
font = get_font(fname)
|
|
||||||
|
|
||||||
font.clear()
|
|
||||||
size = prop.get_size_in_points()
|
|
||||||
font.set_size(size, self.dpi)
|
|
||||||
|
|
||||||
return font
|
|
||||||
|
|
||||||
def points_to_pixels(self, points):
|
|
||||||
"""
|
|
||||||
convert point measures to pixes using dpi and the pixels per
|
|
||||||
inch of the display
|
|
||||||
"""
|
|
||||||
return points * self.dpi / 72
|
|
||||||
|
|
||||||
def tostring_rgb(self):
|
|
||||||
return self._renderer.tostring_rgb()
|
|
||||||
|
|
||||||
def tostring_argb(self):
|
|
||||||
return self._renderer.tostring_argb()
|
|
||||||
|
|
||||||
def buffer_rgba(self):
|
|
||||||
return self._renderer.buffer_rgba()
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
self._renderer.clear()
|
|
||||||
|
|
||||||
def option_image_nocomposite(self):
|
|
||||||
# It is generally faster to composite each image directly to
|
|
||||||
# the Figure, and there's no file size benefit to compositing
|
|
||||||
# with the Agg backend
|
|
||||||
return True
|
|
||||||
|
|
||||||
def option_scale_image(self):
|
|
||||||
"""
|
|
||||||
agg backend doesn't support arbitrary scaling of image.
|
|
||||||
"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def restore_region(self, region, bbox=None, xy=None):
|
|
||||||
"""
|
|
||||||
Restore the saved region. If bbox (instance of BboxBase, or
|
|
||||||
its extents) is given, only the region specified by the bbox
|
|
||||||
will be restored. *xy* (a tuple of two floasts) optionally
|
|
||||||
specifies the new position (the LLC of the original region,
|
|
||||||
not the LLC of the bbox) where the region will be restored.
|
|
||||||
|
|
||||||
>>> region = renderer.copy_from_bbox()
|
|
||||||
>>> x1, y1, x2, y2 = region.get_extents()
|
|
||||||
>>> renderer.restore_region(region, bbox=(x1+dx, y1, x2, y2),
|
|
||||||
... xy=(x1-dx, y1))
|
|
||||||
|
|
||||||
"""
|
|
||||||
if bbox is not None or xy is not None:
|
|
||||||
if bbox is None:
|
|
||||||
x1, y1, x2, y2 = region.get_extents()
|
|
||||||
elif isinstance(bbox, BboxBase):
|
|
||||||
x1, y1, x2, y2 = bbox.extents
|
|
||||||
else:
|
|
||||||
x1, y1, x2, y2 = bbox
|
|
||||||
|
|
||||||
if xy is None:
|
|
||||||
ox, oy = x1, y1
|
|
||||||
else:
|
|
||||||
ox, oy = xy
|
|
||||||
|
|
||||||
# The incoming data is float, but the _renderer type-checking wants
|
|
||||||
# to see integers.
|
|
||||||
self._renderer.restore_region(region, int(x1), int(y1),
|
|
||||||
int(x2), int(y2), int(ox), int(oy))
|
|
||||||
|
|
||||||
else:
|
|
||||||
self._renderer.restore_region(region)
|
|
||||||
|
|
||||||
def start_filter(self):
|
|
||||||
"""
|
|
||||||
Start filtering. It simply create a new canvas (the old one is saved).
|
|
||||||
"""
|
|
||||||
self._filter_renderers.append(self._renderer)
|
|
||||||
self._renderer = _RendererAgg(int(self.width), int(self.height),
|
|
||||||
self.dpi)
|
|
||||||
self._update_methods()
|
|
||||||
|
|
||||||
def stop_filter(self, post_processing):
|
|
||||||
"""
|
|
||||||
Save the plot in the current canvas as a image and apply
|
|
||||||
the *post_processing* function.
|
|
||||||
|
|
||||||
def post_processing(image, dpi):
|
|
||||||
# ny, nx, depth = image.shape
|
|
||||||
# image (numpy array) has RGBA channels and has a depth of 4.
|
|
||||||
...
|
|
||||||
# create a new_image (numpy array of 4 channels, size can be
|
|
||||||
# different). The resulting image may have offsets from
|
|
||||||
# lower-left corner of the original image
|
|
||||||
return new_image, offset_x, offset_y
|
|
||||||
|
|
||||||
The saved renderer is restored and the returned image from
|
|
||||||
post_processing is plotted (using draw_image) on it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
width, height = int(self.width), int(self.height)
|
|
||||||
|
|
||||||
buffer, (l, b, w, h) = self.tostring_rgba_minimized()
|
|
||||||
|
|
||||||
self._renderer = self._filter_renderers.pop()
|
|
||||||
self._update_methods()
|
|
||||||
|
|
||||||
if w > 0 and h > 0:
|
|
||||||
img = np.fromstring(buffer, np.uint8)
|
|
||||||
img, ox, oy = post_processing(img.reshape((h, w, 4)) / 255.,
|
|
||||||
self.dpi)
|
|
||||||
gc = self.new_gc()
|
|
||||||
if img.dtype.kind == 'f':
|
|
||||||
img = np.asarray(img * 255., np.uint8)
|
|
||||||
img = img[::-1]
|
|
||||||
self._renderer.draw_image(gc, l + ox, height - b - h + oy, img)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasAgg(FigureCanvasBase):
|
|
||||||
"""
|
|
||||||
The canvas the figure renders into. Calls the draw and print fig
|
|
||||||
methods, creates the renderers, etc...
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
figure : `matplotlib.figure.Figure`
|
|
||||||
A high-level Figure instance
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def copy_from_bbox(self, bbox):
|
|
||||||
renderer = self.get_renderer()
|
|
||||||
return renderer.copy_from_bbox(bbox)
|
|
||||||
|
|
||||||
def restore_region(self, region, bbox=None, xy=None):
|
|
||||||
renderer = self.get_renderer()
|
|
||||||
return renderer.restore_region(region, bbox, xy)
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
"""
|
|
||||||
Draw the figure using the renderer.
|
|
||||||
"""
|
|
||||||
self.renderer = self.get_renderer(cleared=True)
|
|
||||||
# acquire a lock on the shared font cache
|
|
||||||
RendererAgg.lock.acquire()
|
|
||||||
|
|
||||||
toolbar = self.toolbar
|
|
||||||
try:
|
|
||||||
self.figure.draw(self.renderer)
|
|
||||||
# A GUI class may be need to update a window using this draw, so
|
|
||||||
# don't forget to call the superclass.
|
|
||||||
super().draw()
|
|
||||||
finally:
|
|
||||||
RendererAgg.lock.release()
|
|
||||||
|
|
||||||
def get_renderer(self, cleared=False):
|
|
||||||
l, b, w, h = self.figure.bbox.bounds
|
|
||||||
key = w, h, self.figure.dpi
|
|
||||||
try: self._lastKey, self.renderer
|
|
||||||
except AttributeError: need_new_renderer = True
|
|
||||||
else: need_new_renderer = (self._lastKey != key)
|
|
||||||
|
|
||||||
if need_new_renderer:
|
|
||||||
self.renderer = RendererAgg(w, h, self.figure.dpi)
|
|
||||||
self._lastKey = key
|
|
||||||
elif cleared:
|
|
||||||
self.renderer.clear()
|
|
||||||
return self.renderer
|
|
||||||
|
|
||||||
def tostring_rgb(self):
|
|
||||||
'''Get the image as an RGB byte string.
|
|
||||||
|
|
||||||
`draw` must be called at least once before this function will work and
|
|
||||||
to update the renderer for any subsequent changes to the Figure.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
bytes
|
|
||||||
'''
|
|
||||||
return self.renderer.tostring_rgb()
|
|
||||||
|
|
||||||
def tostring_argb(self):
|
|
||||||
'''Get the image as an ARGB byte string
|
|
||||||
|
|
||||||
`draw` must be called at least once before this function will work and
|
|
||||||
to update the renderer for any subsequent changes to the Figure.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
bytes
|
|
||||||
|
|
||||||
'''
|
|
||||||
return self.renderer.tostring_argb()
|
|
||||||
|
|
||||||
def buffer_rgba(self):
|
|
||||||
'''Get the image as an RGBA byte string.
|
|
||||||
|
|
||||||
`draw` must be called at least once before this function will work and
|
|
||||||
to update the renderer for any subsequent changes to the Figure.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
bytes
|
|
||||||
'''
|
|
||||||
return self.renderer.buffer_rgba()
|
|
||||||
|
|
||||||
def print_raw(self, filename_or_obj, *args, **kwargs):
|
|
||||||
FigureCanvasAgg.draw(self)
|
|
||||||
renderer = self.get_renderer()
|
|
||||||
with cbook._setattr_cm(renderer, dpi=self.figure.dpi), \
|
|
||||||
cbook.open_file_cm(filename_or_obj, "wb") as fh:
|
|
||||||
fh.write(renderer._renderer.buffer_rgba())
|
|
||||||
print_rgba = print_raw
|
|
||||||
|
|
||||||
def print_png(self, filename_or_obj, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Write the figure to a PNG file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
filename_or_obj : str or PathLike or file-like object
|
|
||||||
The file to write to.
|
|
||||||
|
|
||||||
metadata : dict, optional
|
|
||||||
Metadata in the PNG file as key-value pairs of bytes or latin-1
|
|
||||||
encodable strings.
|
|
||||||
According to the PNG specification, keys must be shorter than 79
|
|
||||||
chars.
|
|
||||||
|
|
||||||
The `PNG specification`_ defines some common keywords that may be
|
|
||||||
used as appropriate:
|
|
||||||
|
|
||||||
- Title: Short (one line) title or caption for image.
|
|
||||||
- Author: Name of image's creator.
|
|
||||||
- Description: Description of image (possibly long).
|
|
||||||
- Copyright: Copyright notice.
|
|
||||||
- Creation Time: Time of original image creation
|
|
||||||
(usually RFC 1123 format).
|
|
||||||
- Software: Software used to create the image.
|
|
||||||
- Disclaimer: Legal disclaimer.
|
|
||||||
- Warning: Warning of nature of content.
|
|
||||||
- Source: Device used to create the image.
|
|
||||||
- Comment: Miscellaneous comment;
|
|
||||||
conversion from other image format.
|
|
||||||
|
|
||||||
Other keywords may be invented for other purposes.
|
|
||||||
|
|
||||||
If 'Software' is not given, an autogenerated value for matplotlib
|
|
||||||
will be used.
|
|
||||||
|
|
||||||
For more details see the `PNG specification`_.
|
|
||||||
|
|
||||||
.. _PNG specification: \
|
|
||||||
https://www.w3.org/TR/2003/REC-PNG-20031110/#11keywords
|
|
||||||
|
|
||||||
"""
|
|
||||||
FigureCanvasAgg.draw(self)
|
|
||||||
renderer = self.get_renderer()
|
|
||||||
|
|
||||||
version_str = (
|
|
||||||
'matplotlib version ' + __version__ + ', http://matplotlib.org/')
|
|
||||||
metadata = OrderedDict({'Software': version_str})
|
|
||||||
user_metadata = kwargs.pop("metadata", None)
|
|
||||||
if user_metadata is not None:
|
|
||||||
metadata.update(user_metadata)
|
|
||||||
|
|
||||||
with cbook._setattr_cm(renderer, dpi=self.figure.dpi), \
|
|
||||||
cbook.open_file_cm(filename_or_obj, "wb") as fh:
|
|
||||||
_png.write_png(renderer._renderer, fh,
|
|
||||||
self.figure.dpi, metadata=metadata)
|
|
||||||
|
|
||||||
def print_to_buffer(self):
|
|
||||||
FigureCanvasAgg.draw(self)
|
|
||||||
renderer = self.get_renderer()
|
|
||||||
with cbook._setattr_cm(renderer, dpi=self.figure.dpi):
|
|
||||||
return (renderer._renderer.buffer_rgba(),
|
|
||||||
(int(renderer.width), int(renderer.height)))
|
|
||||||
|
|
||||||
if _has_pil:
|
|
||||||
# add JPEG support
|
|
||||||
def print_jpg(self, filename_or_obj, *args, dryrun=False, **kwargs):
|
|
||||||
"""
|
|
||||||
Write the figure to a JPEG file.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
filename_or_obj : str or PathLike or file-like object
|
|
||||||
The file to write to.
|
|
||||||
|
|
||||||
Other Parameters
|
|
||||||
----------------
|
|
||||||
quality : int
|
|
||||||
The image quality, on a scale from 1 (worst) to 100 (best).
|
|
||||||
The default is :rc:`savefig.jpeg_quality`. Values above
|
|
||||||
95 should be avoided; 100 completely disables the JPEG
|
|
||||||
quantization stage.
|
|
||||||
|
|
||||||
optimize : bool
|
|
||||||
If present, indicates that the encoder should
|
|
||||||
make an extra pass over the image in order to select
|
|
||||||
optimal encoder settings.
|
|
||||||
|
|
||||||
progressive : bool
|
|
||||||
If present, indicates that this image
|
|
||||||
should be stored as a progressive JPEG file.
|
|
||||||
"""
|
|
||||||
buf, size = self.print_to_buffer()
|
|
||||||
if dryrun:
|
|
||||||
return
|
|
||||||
# The image is "pasted" onto a white background image to safely
|
|
||||||
# handle any transparency
|
|
||||||
image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1)
|
|
||||||
rgba = mcolors.to_rgba(rcParams['savefig.facecolor'])
|
|
||||||
color = tuple([int(x * 255) for x in rgba[:3]])
|
|
||||||
background = Image.new('RGB', size, color)
|
|
||||||
background.paste(image, image)
|
|
||||||
options = {k: kwargs[k]
|
|
||||||
for k in ['quality', 'optimize', 'progressive', 'dpi']
|
|
||||||
if k in kwargs}
|
|
||||||
options.setdefault('quality', rcParams['savefig.jpeg_quality'])
|
|
||||||
if 'dpi' in options:
|
|
||||||
# Set the same dpi in both x and y directions
|
|
||||||
options['dpi'] = (options['dpi'], options['dpi'])
|
|
||||||
|
|
||||||
return background.save(filename_or_obj, format='jpeg', **options)
|
|
||||||
print_jpeg = print_jpg
|
|
||||||
|
|
||||||
# add TIFF support
|
|
||||||
def print_tif(self, filename_or_obj, *args, dryrun=False, **kwargs):
|
|
||||||
buf, size = self.print_to_buffer()
|
|
||||||
if dryrun:
|
|
||||||
return
|
|
||||||
image = Image.frombuffer('RGBA', size, buf, 'raw', 'RGBA', 0, 1)
|
|
||||||
dpi = (self.figure.dpi, self.figure.dpi)
|
|
||||||
return image.save(filename_or_obj, format='tiff', dpi=dpi)
|
|
||||||
print_tiff = print_tif
|
|
||||||
|
|
||||||
|
|
||||||
@_Backend.export
|
|
||||||
class _BackendAgg(_Backend):
|
|
||||||
FigureCanvas = FigureCanvasAgg
|
|
||||||
FigureManager = FigureManagerBase
|
|
||||||
@@ -1,633 +0,0 @@
|
|||||||
"""
|
|
||||||
A Cairo backend for matplotlib
|
|
||||||
==============================
|
|
||||||
:Author: Steve Chaplin and others
|
|
||||||
|
|
||||||
This backend depends on cairocffi or pycairo.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import gzip
|
|
||||||
import sys
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
# cairocffi is more widely compatible than pycairo (in particular pgi only
|
|
||||||
# works with cairocffi) so try it first.
|
|
||||||
try:
|
|
||||||
import cairocffi as cairo
|
|
||||||
except ImportError:
|
|
||||||
try:
|
|
||||||
import cairo
|
|
||||||
except ImportError:
|
|
||||||
raise ImportError("cairo backend requires that cairocffi or pycairo "
|
|
||||||
"is installed")
|
|
||||||
else:
|
|
||||||
if cairo.version_info < (1, 11, 0):
|
|
||||||
# Introduced create_for_data for Py3.
|
|
||||||
raise ImportError(
|
|
||||||
"cairo {} is installed; cairo>=1.11.0 is required"
|
|
||||||
.format(cairo.version))
|
|
||||||
|
|
||||||
backend_version = cairo.version
|
|
||||||
|
|
||||||
from .. import cbook
|
|
||||||
from matplotlib.backend_bases import (
|
|
||||||
_Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase,
|
|
||||||
RendererBase)
|
|
||||||
from matplotlib.font_manager import ttfFontProperty
|
|
||||||
from matplotlib.mathtext import MathTextParser
|
|
||||||
from matplotlib.path import Path
|
|
||||||
from matplotlib.transforms import Affine2D
|
|
||||||
|
|
||||||
|
|
||||||
if cairo.__name__ == "cairocffi":
|
|
||||||
# Convert a pycairo context to a cairocffi one.
|
|
||||||
def _to_context(ctx):
|
|
||||||
if not isinstance(ctx, cairo.Context):
|
|
||||||
ctx = cairo.Context._from_pointer(
|
|
||||||
cairo.ffi.cast(
|
|
||||||
'cairo_t **',
|
|
||||||
id(ctx) + object.__basicsize__)[0],
|
|
||||||
incref=True)
|
|
||||||
return ctx
|
|
||||||
else:
|
|
||||||
# Pass-through a pycairo context.
|
|
||||||
def _to_context(ctx):
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
@cbook.deprecated("3.0")
|
|
||||||
class ArrayWrapper:
|
|
||||||
"""Thin wrapper around numpy ndarray to expose the interface
|
|
||||||
expected by cairocffi. Basically replicates the
|
|
||||||
array.array interface.
|
|
||||||
"""
|
|
||||||
def __init__(self, myarray):
|
|
||||||
self.__array = myarray
|
|
||||||
self.__data = myarray.ctypes.data
|
|
||||||
self.__size = len(myarray.flatten())
|
|
||||||
self.itemsize = myarray.itemsize
|
|
||||||
|
|
||||||
def buffer_info(self):
|
|
||||||
return (self.__data, self.__size)
|
|
||||||
|
|
||||||
|
|
||||||
# Mapping from Matplotlib Path codes to cairo path codes.
|
|
||||||
_MPL_TO_CAIRO_PATH_TYPE = np.zeros(80, dtype=int) # CLOSEPOLY = 79.
|
|
||||||
_MPL_TO_CAIRO_PATH_TYPE[Path.MOVETO] = cairo.PATH_MOVE_TO
|
|
||||||
_MPL_TO_CAIRO_PATH_TYPE[Path.LINETO] = cairo.PATH_LINE_TO
|
|
||||||
_MPL_TO_CAIRO_PATH_TYPE[Path.CURVE4] = cairo.PATH_CURVE_TO
|
|
||||||
_MPL_TO_CAIRO_PATH_TYPE[Path.CLOSEPOLY] = cairo.PATH_CLOSE_PATH
|
|
||||||
# Sizes in cairo_path_data_t of each cairo path element.
|
|
||||||
_CAIRO_PATH_TYPE_SIZES = np.zeros(4, dtype=int)
|
|
||||||
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_MOVE_TO] = 2
|
|
||||||
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_LINE_TO] = 2
|
|
||||||
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CURVE_TO] = 4
|
|
||||||
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CLOSE_PATH] = 1
|
|
||||||
|
|
||||||
|
|
||||||
def _append_paths_slow(ctx, paths, transforms, clip=None):
|
|
||||||
for path, transform in zip(paths, transforms):
|
|
||||||
for points, code in path.iter_segments(
|
|
||||||
transform, remove_nans=True, clip=clip):
|
|
||||||
if code == Path.MOVETO:
|
|
||||||
ctx.move_to(*points)
|
|
||||||
elif code == Path.CLOSEPOLY:
|
|
||||||
ctx.close_path()
|
|
||||||
elif code == Path.LINETO:
|
|
||||||
ctx.line_to(*points)
|
|
||||||
elif code == Path.CURVE3:
|
|
||||||
cur = ctx.get_current_point()
|
|
||||||
ctx.curve_to(
|
|
||||||
*np.concatenate([cur / 3 + points[:2] * 2 / 3,
|
|
||||||
points[:2] * 2 / 3 + points[-2:] / 3]))
|
|
||||||
elif code == Path.CURVE4:
|
|
||||||
ctx.curve_to(*points)
|
|
||||||
|
|
||||||
|
|
||||||
def _append_paths_fast(ctx, paths, transforms, clip=None):
|
|
||||||
# We directly convert to the internal representation used by cairo, for
|
|
||||||
# which ABI compatibility is guaranteed. The layout for each item is
|
|
||||||
# --CODE(4)-- -LENGTH(4)- ---------PAD(8)---------
|
|
||||||
# ----------X(8)---------- ----------Y(8)----------
|
|
||||||
# with the size in bytes in parentheses, and (X, Y) repeated as many times
|
|
||||||
# as there are points for the current code.
|
|
||||||
ffi = cairo.ffi
|
|
||||||
|
|
||||||
# Convert curves to segment, so that 1. we don't have to handle
|
|
||||||
# variable-sized CURVE-n codes, and 2. we don't have to implement degree
|
|
||||||
# elevation for quadratic Beziers.
|
|
||||||
cleaneds = [path.cleaned(transform, remove_nans=True, clip=clip)
|
|
||||||
for path, transform in zip(paths, transforms)]
|
|
||||||
vertices = np.concatenate([cleaned.vertices for cleaned in cleaneds])
|
|
||||||
codes = np.concatenate([cleaned.codes for cleaned in cleaneds])
|
|
||||||
|
|
||||||
# Remove unused vertices and convert to cairo codes. Note that unlike
|
|
||||||
# cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after
|
|
||||||
# CLOSE_PATH, so our resulting buffer may be smaller.
|
|
||||||
vertices = vertices[(codes != Path.STOP) & (codes != Path.CLOSEPOLY)]
|
|
||||||
codes = codes[codes != Path.STOP]
|
|
||||||
codes = _MPL_TO_CAIRO_PATH_TYPE[codes]
|
|
||||||
|
|
||||||
# Where are the headers of each cairo portions?
|
|
||||||
cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes]
|
|
||||||
cairo_type_positions = np.insert(np.cumsum(cairo_type_sizes), 0, 0)
|
|
||||||
cairo_num_data = cairo_type_positions[-1]
|
|
||||||
cairo_type_positions = cairo_type_positions[:-1]
|
|
||||||
|
|
||||||
# Fill the buffer.
|
|
||||||
buf = np.empty(cairo_num_data * 16, np.uint8)
|
|
||||||
as_int = np.frombuffer(buf.data, np.int32)
|
|
||||||
as_int[::4][cairo_type_positions] = codes
|
|
||||||
as_int[1::4][cairo_type_positions] = cairo_type_sizes
|
|
||||||
as_float = np.frombuffer(buf.data, np.float64)
|
|
||||||
mask = np.ones_like(as_float, bool)
|
|
||||||
mask[::2][cairo_type_positions] = mask[1::2][cairo_type_positions] = False
|
|
||||||
as_float[mask] = vertices.ravel()
|
|
||||||
|
|
||||||
# Construct the cairo_path_t, and pass it to the context.
|
|
||||||
ptr = ffi.new("cairo_path_t *")
|
|
||||||
ptr.status = cairo.STATUS_SUCCESS
|
|
||||||
ptr.data = ffi.cast("cairo_path_data_t *", ffi.from_buffer(buf))
|
|
||||||
ptr.num_data = cairo_num_data
|
|
||||||
cairo.cairo.cairo_append_path(ctx._pointer, ptr)
|
|
||||||
|
|
||||||
|
|
||||||
_append_paths = (_append_paths_fast if cairo.__name__ == "cairocffi"
|
|
||||||
else _append_paths_slow)
|
|
||||||
|
|
||||||
|
|
||||||
def _append_path(ctx, path, transform, clip=None):
|
|
||||||
return _append_paths(ctx, [path], [transform], clip)
|
|
||||||
|
|
||||||
|
|
||||||
class RendererCairo(RendererBase):
|
|
||||||
fontweights = {
|
|
||||||
100 : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
200 : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
300 : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
400 : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
500 : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
600 : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
700 : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
800 : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
900 : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
'ultralight' : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
'light' : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
'normal' : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
'medium' : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
'regular' : cairo.FONT_WEIGHT_NORMAL,
|
|
||||||
'semibold' : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
'bold' : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
'heavy' : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
'ultrabold' : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
'black' : cairo.FONT_WEIGHT_BOLD,
|
|
||||||
}
|
|
||||||
fontangles = {
|
|
||||||
'italic' : cairo.FONT_SLANT_ITALIC,
|
|
||||||
'normal' : cairo.FONT_SLANT_NORMAL,
|
|
||||||
'oblique' : cairo.FONT_SLANT_OBLIQUE,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, dpi):
|
|
||||||
self.dpi = dpi
|
|
||||||
self.gc = GraphicsContextCairo(renderer=self)
|
|
||||||
self.text_ctx = cairo.Context(
|
|
||||||
cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1))
|
|
||||||
self.mathtext_parser = MathTextParser('Cairo')
|
|
||||||
RendererBase.__init__(self)
|
|
||||||
|
|
||||||
def set_ctx_from_surface(self, surface):
|
|
||||||
self.gc.ctx = cairo.Context(surface)
|
|
||||||
# Although it may appear natural to automatically call
|
|
||||||
# `self.set_width_height(surface.get_width(), surface.get_height())`
|
|
||||||
# here (instead of having the caller do so separately), this would fail
|
|
||||||
# for PDF/PS/SVG surfaces, which have no way to report their extents.
|
|
||||||
|
|
||||||
def set_width_height(self, width, height):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
|
|
||||||
def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
|
|
||||||
if fill_c is not None:
|
|
||||||
ctx.save()
|
|
||||||
if len(fill_c) == 3 or alpha_overrides:
|
|
||||||
ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], alpha)
|
|
||||||
else:
|
|
||||||
ctx.set_source_rgba(fill_c[0], fill_c[1], fill_c[2], fill_c[3])
|
|
||||||
ctx.fill_preserve()
|
|
||||||
ctx.restore()
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@cbook.deprecated("3.0")
|
|
||||||
def convert_path(ctx, path, transform, clip=None):
|
|
||||||
_append_path(ctx, path, transform, clip)
|
|
||||||
|
|
||||||
def draw_path(self, gc, path, transform, rgbFace=None):
|
|
||||||
ctx = gc.ctx
|
|
||||||
# Clip the path to the actual rendering extents if it isn't filled.
|
|
||||||
clip = (ctx.clip_extents()
|
|
||||||
if rgbFace is None and gc.get_hatch() is None
|
|
||||||
else None)
|
|
||||||
transform = (transform
|
|
||||||
+ Affine2D().scale(1, -1).translate(0, self.height))
|
|
||||||
ctx.new_path()
|
|
||||||
_append_path(ctx, path, transform, clip)
|
|
||||||
self._fill_and_stroke(
|
|
||||||
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
|
|
||||||
|
|
||||||
def draw_markers(self, gc, marker_path, marker_trans, path, transform,
|
|
||||||
rgbFace=None):
|
|
||||||
ctx = gc.ctx
|
|
||||||
|
|
||||||
ctx.new_path()
|
|
||||||
# Create the path for the marker; it needs to be flipped here already!
|
|
||||||
_append_path(ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
|
|
||||||
marker_path = ctx.copy_path_flat()
|
|
||||||
|
|
||||||
# Figure out whether the path has a fill
|
|
||||||
x1, y1, x2, y2 = ctx.fill_extents()
|
|
||||||
if x1 == 0 and y1 == 0 and x2 == 0 and y2 == 0:
|
|
||||||
filled = False
|
|
||||||
# No fill, just unset this (so we don't try to fill it later on)
|
|
||||||
rgbFace = None
|
|
||||||
else:
|
|
||||||
filled = True
|
|
||||||
|
|
||||||
transform = (transform
|
|
||||||
+ Affine2D().scale(1, -1).translate(0, self.height))
|
|
||||||
|
|
||||||
ctx.new_path()
|
|
||||||
for i, (vertices, codes) in enumerate(
|
|
||||||
path.iter_segments(transform, simplify=False)):
|
|
||||||
if len(vertices):
|
|
||||||
x, y = vertices[-2:]
|
|
||||||
ctx.save()
|
|
||||||
|
|
||||||
# Translate and apply path
|
|
||||||
ctx.translate(x, y)
|
|
||||||
ctx.append_path(marker_path)
|
|
||||||
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
# Slower code path if there is a fill; we need to draw
|
|
||||||
# the fill and stroke for each marker at the same time.
|
|
||||||
# Also flush out the drawing every once in a while to
|
|
||||||
# prevent the paths from getting way too long.
|
|
||||||
if filled or i % 1000 == 0:
|
|
||||||
self._fill_and_stroke(
|
|
||||||
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
|
|
||||||
|
|
||||||
# Fast path, if there is no fill, draw everything in one step
|
|
||||||
if not filled:
|
|
||||||
self._fill_and_stroke(
|
|
||||||
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
|
|
||||||
|
|
||||||
def draw_path_collection(
|
|
||||||
self, gc, master_transform, paths, all_transforms, offsets,
|
|
||||||
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
|
|
||||||
antialiaseds, urls, offset_position):
|
|
||||||
|
|
||||||
path_ids = []
|
|
||||||
for path, transform in self._iter_collection_raw_paths(
|
|
||||||
master_transform, paths, all_transforms):
|
|
||||||
path_ids.append((path, Affine2D(transform)))
|
|
||||||
|
|
||||||
reuse_key = None
|
|
||||||
grouped_draw = []
|
|
||||||
|
|
||||||
def _draw_paths():
|
|
||||||
if not grouped_draw:
|
|
||||||
return
|
|
||||||
gc_vars, rgb_fc = reuse_key
|
|
||||||
gc = copy.copy(gc0)
|
|
||||||
# We actually need to call the setters to reset the internal state.
|
|
||||||
vars(gc).update(gc_vars)
|
|
||||||
for k, v in gc_vars.items():
|
|
||||||
if k == "_linestyle": # Deprecated, no effect.
|
|
||||||
continue
|
|
||||||
try:
|
|
||||||
getattr(gc, "set" + k)(v)
|
|
||||||
except (AttributeError, TypeError) as e:
|
|
||||||
pass
|
|
||||||
gc.ctx.new_path()
|
|
||||||
paths, transforms = zip(*grouped_draw)
|
|
||||||
grouped_draw.clear()
|
|
||||||
_append_paths(gc.ctx, paths, transforms)
|
|
||||||
self._fill_and_stroke(
|
|
||||||
gc.ctx, rgb_fc, gc.get_alpha(), gc.get_forced_alpha())
|
|
||||||
|
|
||||||
for xo, yo, path_id, gc0, rgb_fc in self._iter_collection(
|
|
||||||
gc, master_transform, all_transforms, path_ids, offsets,
|
|
||||||
offsetTrans, facecolors, edgecolors, linewidths, linestyles,
|
|
||||||
antialiaseds, urls, offset_position):
|
|
||||||
path, transform = path_id
|
|
||||||
transform = (Affine2D(transform.get_matrix())
|
|
||||||
.translate(xo, yo - self.height).scale(1, -1))
|
|
||||||
# rgb_fc could be a ndarray, for which equality is elementwise.
|
|
||||||
new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None
|
|
||||||
if new_key == reuse_key:
|
|
||||||
grouped_draw.append((path, transform))
|
|
||||||
else:
|
|
||||||
_draw_paths()
|
|
||||||
grouped_draw.append((path, transform))
|
|
||||||
reuse_key = new_key
|
|
||||||
_draw_paths()
|
|
||||||
|
|
||||||
def draw_image(self, gc, x, y, im):
|
|
||||||
im = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(im[::-1])
|
|
||||||
surface = cairo.ImageSurface.create_for_data(
|
|
||||||
im.ravel().data, cairo.FORMAT_ARGB32,
|
|
||||||
im.shape[1], im.shape[0], im.shape[1] * 4)
|
|
||||||
ctx = gc.ctx
|
|
||||||
y = self.height - y - im.shape[0]
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
ctx.set_source_surface(surface, float(x), float(y))
|
|
||||||
ctx.paint()
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
|
|
||||||
# Note: x,y are device/display coords, not user-coords, unlike other
|
|
||||||
# draw_* methods
|
|
||||||
if ismath:
|
|
||||||
self._draw_mathtext(gc, x, y, s, prop, angle)
|
|
||||||
|
|
||||||
else:
|
|
||||||
ctx = gc.ctx
|
|
||||||
ctx.new_path()
|
|
||||||
ctx.move_to(x, y)
|
|
||||||
ctx.select_font_face(prop.get_name(),
|
|
||||||
self.fontangles[prop.get_style()],
|
|
||||||
self.fontweights[prop.get_weight()])
|
|
||||||
|
|
||||||
size = prop.get_size_in_points() * self.dpi / 72.0
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
if angle:
|
|
||||||
ctx.rotate(np.deg2rad(-angle))
|
|
||||||
ctx.set_font_size(size)
|
|
||||||
|
|
||||||
ctx.show_text(s)
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
def _draw_mathtext(self, gc, x, y, s, prop, angle):
|
|
||||||
ctx = gc.ctx
|
|
||||||
width, height, descent, glyphs, rects = self.mathtext_parser.parse(
|
|
||||||
s, self.dpi, prop)
|
|
||||||
|
|
||||||
ctx.save()
|
|
||||||
ctx.translate(x, y)
|
|
||||||
if angle:
|
|
||||||
ctx.rotate(np.deg2rad(-angle))
|
|
||||||
|
|
||||||
for font, fontsize, s, ox, oy in glyphs:
|
|
||||||
ctx.new_path()
|
|
||||||
ctx.move_to(ox, oy)
|
|
||||||
|
|
||||||
fontProp = ttfFontProperty(font)
|
|
||||||
ctx.select_font_face(fontProp.name,
|
|
||||||
self.fontangles[fontProp.style],
|
|
||||||
self.fontweights[fontProp.weight])
|
|
||||||
|
|
||||||
size = fontsize * self.dpi / 72.0
|
|
||||||
ctx.set_font_size(size)
|
|
||||||
ctx.show_text(s)
|
|
||||||
|
|
||||||
for ox, oy, w, h in rects:
|
|
||||||
ctx.new_path()
|
|
||||||
ctx.rectangle(ox, oy, w, h)
|
|
||||||
ctx.set_source_rgb(0, 0, 0)
|
|
||||||
ctx.fill_preserve()
|
|
||||||
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
def get_canvas_width_height(self):
|
|
||||||
return self.width, self.height
|
|
||||||
|
|
||||||
def get_text_width_height_descent(self, s, prop, ismath):
|
|
||||||
if ismath:
|
|
||||||
width, height, descent, fonts, used_characters = \
|
|
||||||
self.mathtext_parser.parse(s, self.dpi, prop)
|
|
||||||
return width, height, descent
|
|
||||||
|
|
||||||
ctx = self.text_ctx
|
|
||||||
ctx.save()
|
|
||||||
ctx.select_font_face(prop.get_name(),
|
|
||||||
self.fontangles[prop.get_style()],
|
|
||||||
self.fontweights[prop.get_weight()])
|
|
||||||
|
|
||||||
# Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c
|
|
||||||
# but if /96.0 is used the font is too small
|
|
||||||
size = prop.get_size_in_points() * self.dpi / 72
|
|
||||||
|
|
||||||
# problem - scale remembers last setting and font can become
|
|
||||||
# enormous causing program to crash
|
|
||||||
# save/restore prevents the problem
|
|
||||||
ctx.set_font_size(size)
|
|
||||||
|
|
||||||
y_bearing, w, h = ctx.text_extents(s)[1:4]
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
return w, h, h + y_bearing
|
|
||||||
|
|
||||||
def new_gc(self):
|
|
||||||
self.gc.ctx.save()
|
|
||||||
self.gc._alpha = 1
|
|
||||||
self.gc._forced_alpha = False # if True, _alpha overrides A from RGBA
|
|
||||||
return self.gc
|
|
||||||
|
|
||||||
def points_to_pixels(self, points):
|
|
||||||
return points / 72 * self.dpi
|
|
||||||
|
|
||||||
|
|
||||||
class GraphicsContextCairo(GraphicsContextBase):
|
|
||||||
_joind = {
|
|
||||||
'bevel' : cairo.LINE_JOIN_BEVEL,
|
|
||||||
'miter' : cairo.LINE_JOIN_MITER,
|
|
||||||
'round' : cairo.LINE_JOIN_ROUND,
|
|
||||||
}
|
|
||||||
|
|
||||||
_capd = {
|
|
||||||
'butt' : cairo.LINE_CAP_BUTT,
|
|
||||||
'projecting' : cairo.LINE_CAP_SQUARE,
|
|
||||||
'round' : cairo.LINE_CAP_ROUND,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, renderer):
|
|
||||||
GraphicsContextBase.__init__(self)
|
|
||||||
self.renderer = renderer
|
|
||||||
|
|
||||||
def restore(self):
|
|
||||||
self.ctx.restore()
|
|
||||||
|
|
||||||
def set_alpha(self, alpha):
|
|
||||||
GraphicsContextBase.set_alpha(self, alpha)
|
|
||||||
_alpha = self.get_alpha()
|
|
||||||
rgb = self._rgb
|
|
||||||
if self.get_forced_alpha():
|
|
||||||
self.ctx.set_source_rgba(rgb[0], rgb[1], rgb[2], _alpha)
|
|
||||||
else:
|
|
||||||
self.ctx.set_source_rgba(rgb[0], rgb[1], rgb[2], rgb[3])
|
|
||||||
|
|
||||||
# def set_antialiased(self, b):
|
|
||||||
# cairo has many antialiasing modes, we need to pick one for True and
|
|
||||||
# one for False.
|
|
||||||
|
|
||||||
def set_capstyle(self, cs):
|
|
||||||
if cs in ('butt', 'round', 'projecting'):
|
|
||||||
self._capstyle = cs
|
|
||||||
self.ctx.set_line_cap(self._capd[cs])
|
|
||||||
else:
|
|
||||||
raise ValueError('Unrecognized cap style. Found %s' % cs)
|
|
||||||
|
|
||||||
def set_clip_rectangle(self, rectangle):
|
|
||||||
if not rectangle:
|
|
||||||
return
|
|
||||||
x, y, w, h = np.round(rectangle.bounds)
|
|
||||||
ctx = self.ctx
|
|
||||||
ctx.new_path()
|
|
||||||
ctx.rectangle(x, self.renderer.height - h - y, w, h)
|
|
||||||
ctx.clip()
|
|
||||||
|
|
||||||
def set_clip_path(self, path):
|
|
||||||
if not path:
|
|
||||||
return
|
|
||||||
tpath, affine = path.get_transformed_path_and_affine()
|
|
||||||
ctx = self.ctx
|
|
||||||
ctx.new_path()
|
|
||||||
affine = (affine
|
|
||||||
+ Affine2D().scale(1, -1).translate(0, self.renderer.height))
|
|
||||||
_append_path(ctx, tpath, affine)
|
|
||||||
ctx.clip()
|
|
||||||
|
|
||||||
def set_dashes(self, offset, dashes):
|
|
||||||
self._dashes = offset, dashes
|
|
||||||
if dashes is None:
|
|
||||||
self.ctx.set_dash([], 0) # switch dashes off
|
|
||||||
else:
|
|
||||||
self.ctx.set_dash(
|
|
||||||
list(self.renderer.points_to_pixels(np.asarray(dashes))),
|
|
||||||
offset)
|
|
||||||
|
|
||||||
def set_foreground(self, fg, isRGBA=None):
|
|
||||||
GraphicsContextBase.set_foreground(self, fg, isRGBA)
|
|
||||||
if len(self._rgb) == 3:
|
|
||||||
self.ctx.set_source_rgb(*self._rgb)
|
|
||||||
else:
|
|
||||||
self.ctx.set_source_rgba(*self._rgb)
|
|
||||||
|
|
||||||
def get_rgb(self):
|
|
||||||
return self.ctx.get_source().get_rgba()[:3]
|
|
||||||
|
|
||||||
def set_joinstyle(self, js):
|
|
||||||
if js in ('miter', 'round', 'bevel'):
|
|
||||||
self._joinstyle = js
|
|
||||||
self.ctx.set_line_join(self._joind[js])
|
|
||||||
else:
|
|
||||||
raise ValueError('Unrecognized join style. Found %s' % js)
|
|
||||||
|
|
||||||
def set_linewidth(self, w):
|
|
||||||
self._linewidth = float(w)
|
|
||||||
self.ctx.set_line_width(self.renderer.points_to_pixels(w))
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasCairo(FigureCanvasBase):
|
|
||||||
supports_blit = False
|
|
||||||
|
|
||||||
def print_png(self, fobj, *args, **kwargs):
|
|
||||||
self._get_printed_image_surface().write_to_png(fobj)
|
|
||||||
|
|
||||||
def print_rgba(self, fobj, *args, **kwargs):
|
|
||||||
width, height = self.get_width_height()
|
|
||||||
buf = self._get_printed_image_surface().get_data()
|
|
||||||
fobj.write(cbook._premultiplied_argb32_to_unmultiplied_rgba8888(
|
|
||||||
np.asarray(buf).reshape((width, height, 4))))
|
|
||||||
|
|
||||||
print_raw = print_rgba
|
|
||||||
|
|
||||||
def _get_printed_image_surface(self):
|
|
||||||
width, height = self.get_width_height()
|
|
||||||
renderer = RendererCairo(self.figure.dpi)
|
|
||||||
renderer.set_width_height(width, height)
|
|
||||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
|
||||||
renderer.set_ctx_from_surface(surface)
|
|
||||||
self.figure.draw(renderer)
|
|
||||||
return surface
|
|
||||||
|
|
||||||
def print_pdf(self, fobj, *args, **kwargs):
|
|
||||||
return self._save(fobj, 'pdf', *args, **kwargs)
|
|
||||||
|
|
||||||
def print_ps(self, fobj, *args, **kwargs):
|
|
||||||
return self._save(fobj, 'ps', *args, **kwargs)
|
|
||||||
|
|
||||||
def print_svg(self, fobj, *args, **kwargs):
|
|
||||||
return self._save(fobj, 'svg', *args, **kwargs)
|
|
||||||
|
|
||||||
def print_svgz(self, fobj, *args, **kwargs):
|
|
||||||
return self._save(fobj, 'svgz', *args, **kwargs)
|
|
||||||
|
|
||||||
def _save(self, fo, fmt, **kwargs):
|
|
||||||
# save PDF/PS/SVG
|
|
||||||
orientation = kwargs.get('orientation', 'portrait')
|
|
||||||
|
|
||||||
dpi = 72
|
|
||||||
self.figure.dpi = dpi
|
|
||||||
w_in, h_in = self.figure.get_size_inches()
|
|
||||||
width_in_points, height_in_points = w_in * dpi, h_in * dpi
|
|
||||||
|
|
||||||
if orientation == 'landscape':
|
|
||||||
width_in_points, height_in_points = (
|
|
||||||
height_in_points, width_in_points)
|
|
||||||
|
|
||||||
if fmt == 'ps':
|
|
||||||
if not hasattr(cairo, 'PSSurface'):
|
|
||||||
raise RuntimeError('cairo has not been compiled with PS '
|
|
||||||
'support enabled')
|
|
||||||
surface = cairo.PSSurface(fo, width_in_points, height_in_points)
|
|
||||||
elif fmt == 'pdf':
|
|
||||||
if not hasattr(cairo, 'PDFSurface'):
|
|
||||||
raise RuntimeError('cairo has not been compiled with PDF '
|
|
||||||
'support enabled')
|
|
||||||
surface = cairo.PDFSurface(fo, width_in_points, height_in_points)
|
|
||||||
elif fmt in ('svg', 'svgz'):
|
|
||||||
if not hasattr(cairo, 'SVGSurface'):
|
|
||||||
raise RuntimeError('cairo has not been compiled with SVG '
|
|
||||||
'support enabled')
|
|
||||||
if fmt == 'svgz':
|
|
||||||
if isinstance(fo, str):
|
|
||||||
fo = gzip.GzipFile(fo, 'wb')
|
|
||||||
else:
|
|
||||||
fo = gzip.GzipFile(None, 'wb', fileobj=fo)
|
|
||||||
surface = cairo.SVGSurface(fo, width_in_points, height_in_points)
|
|
||||||
else:
|
|
||||||
warnings.warn("unknown format: %s" % fmt, stacklevel=2)
|
|
||||||
return
|
|
||||||
|
|
||||||
# surface.set_dpi() can be used
|
|
||||||
renderer = RendererCairo(self.figure.dpi)
|
|
||||||
renderer.set_width_height(width_in_points, height_in_points)
|
|
||||||
renderer.set_ctx_from_surface(surface)
|
|
||||||
ctx = renderer.gc.ctx
|
|
||||||
|
|
||||||
if orientation == 'landscape':
|
|
||||||
ctx.rotate(np.pi / 2)
|
|
||||||
ctx.translate(0, -height_in_points)
|
|
||||||
# Perhaps add an '%%Orientation: Landscape' comment?
|
|
||||||
|
|
||||||
self.figure.draw(renderer)
|
|
||||||
|
|
||||||
ctx.show_page()
|
|
||||||
surface.finish()
|
|
||||||
if fmt == 'svgz':
|
|
||||||
fo.close()
|
|
||||||
|
|
||||||
|
|
||||||
@_Backend.export
|
|
||||||
class _BackendCairo(_Backend):
|
|
||||||
FigureCanvas = FigureCanvasCairo
|
|
||||||
FigureManager = FigureManagerBase
|
|
||||||
@@ -1,990 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
from matplotlib import backend_tools, cbook, rcParams
|
|
||||||
from matplotlib._pylab_helpers import Gcf
|
|
||||||
from matplotlib.backend_bases import (
|
|
||||||
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
|
|
||||||
StatusbarBase, TimerBase, ToolContainerBase, cursors)
|
|
||||||
from matplotlib.backend_managers import ToolManager
|
|
||||||
from matplotlib.figure import Figure
|
|
||||||
from matplotlib.widgets import SubplotTool
|
|
||||||
from ._gtk3_compat import GLib, GObject, Gtk, Gdk
|
|
||||||
|
|
||||||
|
|
||||||
_log = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
backend_version = "%s.%s.%s" % (
|
|
||||||
Gtk.get_major_version(), Gtk.get_micro_version(), Gtk.get_minor_version())
|
|
||||||
|
|
||||||
# the true dots per inch on the screen; should be display dependent
|
|
||||||
# see http://groups.google.com/groups?q=screen+dpi+x11&hl=en&lr=&ie=UTF-8&oe=UTF-8&safe=off&selm=7077.26e81ad5%40swift.cs.tcd.ie&rnum=5 for some info about screen dpi
|
|
||||||
PIXELS_PER_INCH = 96
|
|
||||||
|
|
||||||
try:
|
|
||||||
cursord = {
|
|
||||||
cursors.MOVE : Gdk.Cursor.new(Gdk.CursorType.FLEUR),
|
|
||||||
cursors.HAND : Gdk.Cursor.new(Gdk.CursorType.HAND2),
|
|
||||||
cursors.POINTER : Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR),
|
|
||||||
cursors.SELECT_REGION : Gdk.Cursor.new(Gdk.CursorType.TCROSS),
|
|
||||||
cursors.WAIT : Gdk.Cursor.new(Gdk.CursorType.WATCH),
|
|
||||||
}
|
|
||||||
except TypeError as exc:
|
|
||||||
# Happens when running headless. Convert to ImportError to cooperate with
|
|
||||||
# backend switching.
|
|
||||||
raise ImportError(exc)
|
|
||||||
|
|
||||||
|
|
||||||
class TimerGTK3(TimerBase):
|
|
||||||
'''
|
|
||||||
Subclass of :class:`backend_bases.TimerBase` using GTK3 for timer events.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
interval : int
|
|
||||||
The time between timer events in milliseconds. Default is 1000 ms.
|
|
||||||
single_shot : bool
|
|
||||||
Boolean flag indicating whether this timer should operate as single
|
|
||||||
shot (run once and then stop). Defaults to False.
|
|
||||||
callbacks : list
|
|
||||||
Stores list of (func, args) tuples that will be called upon timer
|
|
||||||
events. This list can be manipulated directly, or the functions
|
|
||||||
`add_callback` and `remove_callback` can be used.
|
|
||||||
|
|
||||||
'''
|
|
||||||
def _timer_start(self):
|
|
||||||
# Need to stop it, otherwise we potentially leak a timer id that will
|
|
||||||
# never be stopped.
|
|
||||||
self._timer_stop()
|
|
||||||
self._timer = GLib.timeout_add(self._interval, self._on_timer)
|
|
||||||
|
|
||||||
def _timer_stop(self):
|
|
||||||
if self._timer is not None:
|
|
||||||
GLib.source_remove(self._timer)
|
|
||||||
self._timer = None
|
|
||||||
|
|
||||||
def _timer_set_interval(self):
|
|
||||||
# Only stop and restart it if the timer has already been started
|
|
||||||
if self._timer is not None:
|
|
||||||
self._timer_stop()
|
|
||||||
self._timer_start()
|
|
||||||
|
|
||||||
def _on_timer(self):
|
|
||||||
TimerBase._on_timer(self)
|
|
||||||
|
|
||||||
# Gtk timeout_add() requires that the callback returns True if it
|
|
||||||
# is to be called again.
|
|
||||||
if self.callbacks and not self._single:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
self._timer = None
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
|
|
||||||
keyvald = {65507: 'control',
|
|
||||||
65505: 'shift',
|
|
||||||
65513: 'alt',
|
|
||||||
65508: 'control',
|
|
||||||
65506: 'shift',
|
|
||||||
65514: 'alt',
|
|
||||||
65361: 'left',
|
|
||||||
65362: 'up',
|
|
||||||
65363: 'right',
|
|
||||||
65364: 'down',
|
|
||||||
65307: 'escape',
|
|
||||||
65470: 'f1',
|
|
||||||
65471: 'f2',
|
|
||||||
65472: 'f3',
|
|
||||||
65473: 'f4',
|
|
||||||
65474: 'f5',
|
|
||||||
65475: 'f6',
|
|
||||||
65476: 'f7',
|
|
||||||
65477: 'f8',
|
|
||||||
65478: 'f9',
|
|
||||||
65479: 'f10',
|
|
||||||
65480: 'f11',
|
|
||||||
65481: 'f12',
|
|
||||||
65300: 'scroll_lock',
|
|
||||||
65299: 'break',
|
|
||||||
65288: 'backspace',
|
|
||||||
65293: 'enter',
|
|
||||||
65379: 'insert',
|
|
||||||
65535: 'delete',
|
|
||||||
65360: 'home',
|
|
||||||
65367: 'end',
|
|
||||||
65365: 'pageup',
|
|
||||||
65366: 'pagedown',
|
|
||||||
65438: '0',
|
|
||||||
65436: '1',
|
|
||||||
65433: '2',
|
|
||||||
65435: '3',
|
|
||||||
65430: '4',
|
|
||||||
65437: '5',
|
|
||||||
65432: '6',
|
|
||||||
65429: '7',
|
|
||||||
65431: '8',
|
|
||||||
65434: '9',
|
|
||||||
65451: '+',
|
|
||||||
65453: '-',
|
|
||||||
65450: '*',
|
|
||||||
65455: '/',
|
|
||||||
65439: 'dec',
|
|
||||||
65421: 'enter',
|
|
||||||
}
|
|
||||||
|
|
||||||
# Setting this as a static constant prevents
|
|
||||||
# this resulting expression from leaking
|
|
||||||
event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK |
|
|
||||||
Gdk.EventMask.BUTTON_RELEASE_MASK |
|
|
||||||
Gdk.EventMask.EXPOSURE_MASK |
|
|
||||||
Gdk.EventMask.KEY_PRESS_MASK |
|
|
||||||
Gdk.EventMask.KEY_RELEASE_MASK |
|
|
||||||
Gdk.EventMask.ENTER_NOTIFY_MASK |
|
|
||||||
Gdk.EventMask.LEAVE_NOTIFY_MASK |
|
|
||||||
Gdk.EventMask.POINTER_MOTION_MASK |
|
|
||||||
Gdk.EventMask.POINTER_MOTION_HINT_MASK|
|
|
||||||
Gdk.EventMask.SCROLL_MASK)
|
|
||||||
|
|
||||||
def __init__(self, figure):
|
|
||||||
FigureCanvasBase.__init__(self, figure)
|
|
||||||
GObject.GObject.__init__(self)
|
|
||||||
|
|
||||||
self._idle_draw_id = 0
|
|
||||||
self._lastCursor = None
|
|
||||||
|
|
||||||
self.connect('scroll_event', self.scroll_event)
|
|
||||||
self.connect('button_press_event', self.button_press_event)
|
|
||||||
self.connect('button_release_event', self.button_release_event)
|
|
||||||
self.connect('configure_event', self.configure_event)
|
|
||||||
self.connect('draw', self.on_draw_event)
|
|
||||||
self.connect('key_press_event', self.key_press_event)
|
|
||||||
self.connect('key_release_event', self.key_release_event)
|
|
||||||
self.connect('motion_notify_event', self.motion_notify_event)
|
|
||||||
self.connect('leave_notify_event', self.leave_notify_event)
|
|
||||||
self.connect('enter_notify_event', self.enter_notify_event)
|
|
||||||
self.connect('size_allocate', self.size_allocate)
|
|
||||||
|
|
||||||
self.set_events(self.__class__.event_mask)
|
|
||||||
|
|
||||||
self.set_double_buffered(True)
|
|
||||||
self.set_can_focus(True)
|
|
||||||
self._renderer_init()
|
|
||||||
default_context = GLib.main_context_get_thread_default() or GLib.main_context_default()
|
|
||||||
|
|
||||||
def destroy(self):
|
|
||||||
#Gtk.DrawingArea.destroy(self)
|
|
||||||
self.close_event()
|
|
||||||
if self._idle_draw_id != 0:
|
|
||||||
GLib.source_remove(self._idle_draw_id)
|
|
||||||
|
|
||||||
def scroll_event(self, widget, event):
|
|
||||||
x = event.x
|
|
||||||
# flipy so y=0 is bottom of canvas
|
|
||||||
y = self.get_allocation().height - event.y
|
|
||||||
if event.direction==Gdk.ScrollDirection.UP:
|
|
||||||
step = 1
|
|
||||||
else:
|
|
||||||
step = -1
|
|
||||||
FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
|
|
||||||
return False # finish event propagation?
|
|
||||||
|
|
||||||
def button_press_event(self, widget, event):
|
|
||||||
x = event.x
|
|
||||||
# flipy so y=0 is bottom of canvas
|
|
||||||
y = self.get_allocation().height - event.y
|
|
||||||
FigureCanvasBase.button_press_event(self, x, y, event.button, guiEvent=event)
|
|
||||||
return False # finish event propagation?
|
|
||||||
|
|
||||||
def button_release_event(self, widget, event):
|
|
||||||
x = event.x
|
|
||||||
# flipy so y=0 is bottom of canvas
|
|
||||||
y = self.get_allocation().height - event.y
|
|
||||||
FigureCanvasBase.button_release_event(self, x, y, event.button, guiEvent=event)
|
|
||||||
return False # finish event propagation?
|
|
||||||
|
|
||||||
def key_press_event(self, widget, event):
|
|
||||||
key = self._get_key(event)
|
|
||||||
FigureCanvasBase.key_press_event(self, key, guiEvent=event)
|
|
||||||
return True # stop event propagation
|
|
||||||
|
|
||||||
def key_release_event(self, widget, event):
|
|
||||||
key = self._get_key(event)
|
|
||||||
FigureCanvasBase.key_release_event(self, key, guiEvent=event)
|
|
||||||
return True # stop event propagation
|
|
||||||
|
|
||||||
def motion_notify_event(self, widget, event):
|
|
||||||
if event.is_hint:
|
|
||||||
t, x, y, state = event.window.get_pointer()
|
|
||||||
else:
|
|
||||||
x, y, state = event.x, event.y, event.get_state()
|
|
||||||
|
|
||||||
# flipy so y=0 is bottom of canvas
|
|
||||||
y = self.get_allocation().height - y
|
|
||||||
FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
|
|
||||||
return False # finish event propagation?
|
|
||||||
|
|
||||||
def leave_notify_event(self, widget, event):
|
|
||||||
FigureCanvasBase.leave_notify_event(self, event)
|
|
||||||
|
|
||||||
def enter_notify_event(self, widget, event):
|
|
||||||
x = event.x
|
|
||||||
# flipy so y=0 is bottom of canvas
|
|
||||||
y = self.get_allocation().height - event.y
|
|
||||||
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
|
|
||||||
|
|
||||||
def size_allocate(self, widget, allocation):
|
|
||||||
dpival = self.figure.dpi
|
|
||||||
winch = allocation.width / dpival
|
|
||||||
hinch = allocation.height / dpival
|
|
||||||
self.figure.set_size_inches(winch, hinch, forward=False)
|
|
||||||
FigureCanvasBase.resize_event(self)
|
|
||||||
self.draw_idle()
|
|
||||||
|
|
||||||
def _get_key(self, event):
|
|
||||||
if event.keyval in self.keyvald:
|
|
||||||
key = self.keyvald[event.keyval]
|
|
||||||
elif event.keyval < 256:
|
|
||||||
key = chr(event.keyval)
|
|
||||||
else:
|
|
||||||
key = None
|
|
||||||
|
|
||||||
modifiers = [
|
|
||||||
(Gdk.ModifierType.MOD4_MASK, 'super'),
|
|
||||||
(Gdk.ModifierType.MOD1_MASK, 'alt'),
|
|
||||||
(Gdk.ModifierType.CONTROL_MASK, 'ctrl'),
|
|
||||||
]
|
|
||||||
for key_mask, prefix in modifiers:
|
|
||||||
if event.state & key_mask:
|
|
||||||
key = '{0}+{1}'.format(prefix, key)
|
|
||||||
|
|
||||||
return key
|
|
||||||
|
|
||||||
def configure_event(self, widget, event):
|
|
||||||
if widget.get_property("window") is None:
|
|
||||||
return
|
|
||||||
w, h = event.width, event.height
|
|
||||||
if w < 3 or h < 3:
|
|
||||||
return # empty fig
|
|
||||||
# resize the figure (in inches)
|
|
||||||
dpi = self.figure.dpi
|
|
||||||
self.figure.set_size_inches(w / dpi, h / dpi, forward=False)
|
|
||||||
return False # finish event propagation?
|
|
||||||
|
|
||||||
def on_draw_event(self, widget, ctx):
|
|
||||||
# to be overwritten by GTK3Agg or GTK3Cairo
|
|
||||||
pass
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
if self.get_visible() and self.get_mapped():
|
|
||||||
self.queue_draw()
|
|
||||||
# do a synchronous draw (its less efficient than an async draw,
|
|
||||||
# but is required if/when animation is used)
|
|
||||||
self.get_property("window").process_updates(False)
|
|
||||||
|
|
||||||
def draw_idle(self):
|
|
||||||
if self._idle_draw_id != 0:
|
|
||||||
return
|
|
||||||
def idle_draw(*args):
|
|
||||||
try:
|
|
||||||
self.draw()
|
|
||||||
finally:
|
|
||||||
self._idle_draw_id = 0
|
|
||||||
return False
|
|
||||||
self._idle_draw_id = GLib.idle_add(idle_draw)
|
|
||||||
|
|
||||||
def new_timer(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Creates a new backend-specific subclass of :class:`backend_bases.Timer`.
|
|
||||||
This is useful for getting periodic events through the backend's native
|
|
||||||
event loop. Implemented only for backends with GUIs.
|
|
||||||
|
|
||||||
Other Parameters
|
|
||||||
----------------
|
|
||||||
interval : scalar
|
|
||||||
Timer interval in milliseconds
|
|
||||||
callbacks : list
|
|
||||||
Sequence of (func, args, kwargs) where ``func(*args, **kwargs)``
|
|
||||||
will be executed by the timer every *interval*.
|
|
||||||
"""
|
|
||||||
return TimerGTK3(*args, **kwargs)
|
|
||||||
|
|
||||||
def flush_events(self):
|
|
||||||
Gdk.threads_enter()
|
|
||||||
while Gtk.events_pending():
|
|
||||||
Gtk.main_iteration()
|
|
||||||
Gdk.flush()
|
|
||||||
Gdk.threads_leave()
|
|
||||||
|
|
||||||
|
|
||||||
class FigureManagerGTK3(FigureManagerBase):
|
|
||||||
"""
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
canvas : `FigureCanvas`
|
|
||||||
The FigureCanvas instance
|
|
||||||
num : int or str
|
|
||||||
The Figure number
|
|
||||||
toolbar : Gtk.Toolbar
|
|
||||||
The Gtk.Toolbar
|
|
||||||
vbox : Gtk.VBox
|
|
||||||
The Gtk.VBox containing the canvas and toolbar
|
|
||||||
window : Gtk.Window
|
|
||||||
The Gtk.Window
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, canvas, num):
|
|
||||||
FigureManagerBase.__init__(self, canvas, num)
|
|
||||||
|
|
||||||
self.window = Gtk.Window()
|
|
||||||
self.window.set_wmclass("matplotlib", "Matplotlib")
|
|
||||||
self.set_window_title("Figure %d" % num)
|
|
||||||
try:
|
|
||||||
self.window.set_icon_from_file(window_icon)
|
|
||||||
except Exception:
|
|
||||||
# Some versions of gtk throw a glib.GError but not all, so I am not
|
|
||||||
# sure how to catch it. I am unhappy doing a blanket catch here,
|
|
||||||
# but am not sure what a better way is - JDH
|
|
||||||
_log.info('Could not load matplotlib icon: %s', sys.exc_info()[1])
|
|
||||||
|
|
||||||
self.vbox = Gtk.Box()
|
|
||||||
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
|
|
||||||
self.window.add(self.vbox)
|
|
||||||
self.vbox.show()
|
|
||||||
|
|
||||||
self.canvas.show()
|
|
||||||
|
|
||||||
self.vbox.pack_start(self.canvas, True, True, 0)
|
|
||||||
# calculate size for window
|
|
||||||
w = int(self.canvas.figure.bbox.width)
|
|
||||||
h = int(self.canvas.figure.bbox.height)
|
|
||||||
|
|
||||||
self.toolmanager = self._get_toolmanager()
|
|
||||||
self.toolbar = self._get_toolbar()
|
|
||||||
self.statusbar = None
|
|
||||||
|
|
||||||
def add_widget(child, expand, fill, padding):
|
|
||||||
child.show()
|
|
||||||
self.vbox.pack_end(child, False, False, 0)
|
|
||||||
size_request = child.size_request()
|
|
||||||
return size_request.height
|
|
||||||
|
|
||||||
if self.toolmanager:
|
|
||||||
backend_tools.add_tools_to_manager(self.toolmanager)
|
|
||||||
if self.toolbar:
|
|
||||||
backend_tools.add_tools_to_container(self.toolbar)
|
|
||||||
self.statusbar = StatusbarGTK3(self.toolmanager)
|
|
||||||
h += add_widget(self.statusbar, False, False, 0)
|
|
||||||
h += add_widget(Gtk.HSeparator(), False, False, 0)
|
|
||||||
|
|
||||||
if self.toolbar is not None:
|
|
||||||
self.toolbar.show()
|
|
||||||
h += add_widget(self.toolbar, False, False, 0)
|
|
||||||
|
|
||||||
self.window.set_default_size(w, h)
|
|
||||||
|
|
||||||
def destroy(*args):
|
|
||||||
Gcf.destroy(num)
|
|
||||||
self.window.connect("destroy", destroy)
|
|
||||||
self.window.connect("delete_event", destroy)
|
|
||||||
if matplotlib.is_interactive():
|
|
||||||
self.window.show()
|
|
||||||
self.canvas.draw_idle()
|
|
||||||
|
|
||||||
self.canvas.grab_focus()
|
|
||||||
|
|
||||||
def destroy(self, *args):
|
|
||||||
self.vbox.destroy()
|
|
||||||
self.window.destroy()
|
|
||||||
self.canvas.destroy()
|
|
||||||
if self.toolbar:
|
|
||||||
self.toolbar.destroy()
|
|
||||||
|
|
||||||
if (Gcf.get_num_fig_managers() == 0 and
|
|
||||||
not matplotlib.is_interactive() and
|
|
||||||
Gtk.main_level() >= 1):
|
|
||||||
Gtk.main_quit()
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
# show the figure window
|
|
||||||
self.window.show()
|
|
||||||
self.window.present()
|
|
||||||
|
|
||||||
def full_screen_toggle(self):
|
|
||||||
self._full_screen_flag = not self._full_screen_flag
|
|
||||||
if self._full_screen_flag:
|
|
||||||
self.window.fullscreen()
|
|
||||||
else:
|
|
||||||
self.window.unfullscreen()
|
|
||||||
_full_screen_flag = False
|
|
||||||
|
|
||||||
def _get_toolbar(self):
|
|
||||||
# must be inited after the window, drawingArea and figure
|
|
||||||
# attrs are set
|
|
||||||
if rcParams['toolbar'] == 'toolbar2':
|
|
||||||
toolbar = NavigationToolbar2GTK3(self.canvas, self.window)
|
|
||||||
elif rcParams['toolbar'] == 'toolmanager':
|
|
||||||
toolbar = ToolbarGTK3(self.toolmanager)
|
|
||||||
else:
|
|
||||||
toolbar = None
|
|
||||||
return toolbar
|
|
||||||
|
|
||||||
def _get_toolmanager(self):
|
|
||||||
# must be initialised after toolbar has been set
|
|
||||||
if rcParams['toolbar'] == 'toolmanager':
|
|
||||||
toolmanager = ToolManager(self.canvas.figure)
|
|
||||||
else:
|
|
||||||
toolmanager = None
|
|
||||||
return toolmanager
|
|
||||||
|
|
||||||
def get_window_title(self):
|
|
||||||
return self.window.get_title()
|
|
||||||
|
|
||||||
def set_window_title(self, title):
|
|
||||||
self.window.set_title(title)
|
|
||||||
|
|
||||||
def resize(self, width, height):
|
|
||||||
'set the canvas size in pixels'
|
|
||||||
#_, _, cw, ch = self.canvas.allocation
|
|
||||||
#_, _, ww, wh = self.window.allocation
|
|
||||||
#self.window.resize (width-cw+ww, height-ch+wh)
|
|
||||||
self.window.resize(width, height)
|
|
||||||
|
|
||||||
|
|
||||||
class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar):
|
|
||||||
def __init__(self, canvas, window):
|
|
||||||
self.win = window
|
|
||||||
GObject.GObject.__init__(self)
|
|
||||||
NavigationToolbar2.__init__(self, canvas)
|
|
||||||
self.ctx = None
|
|
||||||
|
|
||||||
def set_message(self, s):
|
|
||||||
self.message.set_label(s)
|
|
||||||
|
|
||||||
def set_cursor(self, cursor):
|
|
||||||
self.canvas.get_property("window").set_cursor(cursord[cursor])
|
|
||||||
Gtk.main_iteration()
|
|
||||||
|
|
||||||
def draw_rubberband(self, event, x0, y0, x1, y1):
|
|
||||||
'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/189744'
|
|
||||||
self.ctx = self.canvas.get_property("window").cairo_create()
|
|
||||||
|
|
||||||
# todo: instead of redrawing the entire figure, copy the part of
|
|
||||||
# the figure that was covered by the previous rubberband rectangle
|
|
||||||
self.canvas.draw()
|
|
||||||
|
|
||||||
height = self.canvas.figure.bbox.height
|
|
||||||
y1 = height - y1
|
|
||||||
y0 = height - y0
|
|
||||||
w = abs(x1 - x0)
|
|
||||||
h = abs(y1 - y0)
|
|
||||||
rect = [int(val) for val in (min(x0, x1), min(y0, y1), w, h)]
|
|
||||||
|
|
||||||
self.ctx.new_path()
|
|
||||||
self.ctx.set_line_width(0.5)
|
|
||||||
self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3])
|
|
||||||
self.ctx.set_source_rgb(0, 0, 0)
|
|
||||||
self.ctx.stroke()
|
|
||||||
|
|
||||||
def _init_toolbar(self):
|
|
||||||
self.set_style(Gtk.ToolbarStyle.ICONS)
|
|
||||||
basedir = os.path.join(rcParams['datapath'], 'images')
|
|
||||||
|
|
||||||
for text, tooltip_text, image_file, callback in self.toolitems:
|
|
||||||
if text is None:
|
|
||||||
self.insert(Gtk.SeparatorToolItem(), -1)
|
|
||||||
continue
|
|
||||||
fname = os.path.join(basedir, image_file + '.png')
|
|
||||||
image = Gtk.Image()
|
|
||||||
image.set_from_file(fname)
|
|
||||||
tbutton = Gtk.ToolButton()
|
|
||||||
tbutton.set_label(text)
|
|
||||||
tbutton.set_icon_widget(image)
|
|
||||||
self.insert(tbutton, -1)
|
|
||||||
tbutton.connect('clicked', getattr(self, callback))
|
|
||||||
tbutton.set_tooltip_text(tooltip_text)
|
|
||||||
|
|
||||||
toolitem = Gtk.SeparatorToolItem()
|
|
||||||
self.insert(toolitem, -1)
|
|
||||||
toolitem.set_draw(False)
|
|
||||||
toolitem.set_expand(True)
|
|
||||||
|
|
||||||
toolitem = Gtk.ToolItem()
|
|
||||||
self.insert(toolitem, -1)
|
|
||||||
self.message = Gtk.Label()
|
|
||||||
toolitem.add(self.message)
|
|
||||||
|
|
||||||
self.show_all()
|
|
||||||
|
|
||||||
def get_filechooser(self):
|
|
||||||
fc = FileChooserDialog(
|
|
||||||
title='Save the figure',
|
|
||||||
parent=self.win,
|
|
||||||
path=os.path.expanduser(rcParams['savefig.directory']),
|
|
||||||
filetypes=self.canvas.get_supported_filetypes(),
|
|
||||||
default_filetype=self.canvas.get_default_filetype())
|
|
||||||
fc.set_current_name(self.canvas.get_default_filename())
|
|
||||||
return fc
|
|
||||||
|
|
||||||
def save_figure(self, *args):
|
|
||||||
chooser = self.get_filechooser()
|
|
||||||
fname, format = chooser.get_filename_from_user()
|
|
||||||
chooser.destroy()
|
|
||||||
if fname:
|
|
||||||
startpath = os.path.expanduser(rcParams['savefig.directory'])
|
|
||||||
# Save dir for next time, unless empty str (i.e., use cwd).
|
|
||||||
if startpath != "":
|
|
||||||
rcParams['savefig.directory'] = os.path.dirname(fname)
|
|
||||||
try:
|
|
||||||
self.canvas.figure.savefig(fname, format=format)
|
|
||||||
except Exception as e:
|
|
||||||
error_msg_gtk(str(e), parent=self)
|
|
||||||
|
|
||||||
def configure_subplots(self, button):
|
|
||||||
toolfig = Figure(figsize=(6, 3))
|
|
||||||
canvas = self._get_canvas(toolfig)
|
|
||||||
toolfig.subplots_adjust(top=0.9)
|
|
||||||
tool = SubplotTool(self.canvas.figure, toolfig)
|
|
||||||
|
|
||||||
w = int(toolfig.bbox.width)
|
|
||||||
h = int(toolfig.bbox.height)
|
|
||||||
|
|
||||||
window = Gtk.Window()
|
|
||||||
try:
|
|
||||||
window.set_icon_from_file(window_icon)
|
|
||||||
except Exception:
|
|
||||||
# we presumably already logged a message on the
|
|
||||||
# failure of the main plot, don't keep reporting
|
|
||||||
pass
|
|
||||||
window.set_title("Subplot Configuration Tool")
|
|
||||||
window.set_default_size(w, h)
|
|
||||||
vbox = Gtk.Box()
|
|
||||||
vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
|
|
||||||
window.add(vbox)
|
|
||||||
vbox.show()
|
|
||||||
|
|
||||||
canvas.show()
|
|
||||||
vbox.pack_start(canvas, True, True, 0)
|
|
||||||
window.show()
|
|
||||||
|
|
||||||
def _get_canvas(self, fig):
|
|
||||||
return self.canvas.__class__(fig)
|
|
||||||
|
|
||||||
|
|
||||||
class FileChooserDialog(Gtk.FileChooserDialog):
|
|
||||||
"""GTK+ file selector which remembers the last file/directory
|
|
||||||
selected and presents the user with a menu of supported image formats
|
|
||||||
"""
|
|
||||||
def __init__(self,
|
|
||||||
title = 'Save file',
|
|
||||||
parent = None,
|
|
||||||
action = Gtk.FileChooserAction.SAVE,
|
|
||||||
buttons = (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
|
|
||||||
Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
|
|
||||||
path = None,
|
|
||||||
filetypes = [],
|
|
||||||
default_filetype = None
|
|
||||||
):
|
|
||||||
super().__init__(title, parent, action, buttons)
|
|
||||||
self.set_default_response(Gtk.ResponseType.OK)
|
|
||||||
self.set_do_overwrite_confirmation(True)
|
|
||||||
|
|
||||||
if not path:
|
|
||||||
path = os.getcwd()
|
|
||||||
|
|
||||||
# create an extra widget to list supported image formats
|
|
||||||
self.set_current_folder(path)
|
|
||||||
self.set_current_name('image.' + default_filetype)
|
|
||||||
|
|
||||||
hbox = Gtk.Box(spacing=10)
|
|
||||||
hbox.pack_start(Gtk.Label(label="File Format:"), False, False, 0)
|
|
||||||
|
|
||||||
liststore = Gtk.ListStore(GObject.TYPE_STRING)
|
|
||||||
cbox = Gtk.ComboBox()
|
|
||||||
cbox.set_model(liststore)
|
|
||||||
cell = Gtk.CellRendererText()
|
|
||||||
cbox.pack_start(cell, True)
|
|
||||||
cbox.add_attribute(cell, 'text', 0)
|
|
||||||
hbox.pack_start(cbox, False, False, 0)
|
|
||||||
|
|
||||||
self.filetypes = filetypes
|
|
||||||
sorted_filetypes = sorted(filetypes.items())
|
|
||||||
default = 0
|
|
||||||
for i, (ext, name) in enumerate(sorted_filetypes):
|
|
||||||
liststore.append(["%s (*.%s)" % (name, ext)])
|
|
||||||
if ext == default_filetype:
|
|
||||||
default = i
|
|
||||||
cbox.set_active(default)
|
|
||||||
self.ext = default_filetype
|
|
||||||
|
|
||||||
def cb_cbox_changed(cbox, data=None):
|
|
||||||
"""File extension changed"""
|
|
||||||
head, filename = os.path.split(self.get_filename())
|
|
||||||
root, ext = os.path.splitext(filename)
|
|
||||||
ext = ext[1:]
|
|
||||||
new_ext = sorted_filetypes[cbox.get_active()][0]
|
|
||||||
self.ext = new_ext
|
|
||||||
|
|
||||||
if ext in self.filetypes:
|
|
||||||
filename = root + '.' + new_ext
|
|
||||||
elif ext == '':
|
|
||||||
filename = filename.rstrip('.') + '.' + new_ext
|
|
||||||
|
|
||||||
self.set_current_name(filename)
|
|
||||||
cbox.connect("changed", cb_cbox_changed)
|
|
||||||
|
|
||||||
hbox.show_all()
|
|
||||||
self.set_extra_widget(hbox)
|
|
||||||
|
|
||||||
@cbook.deprecated("3.0", alternative="sorted(self.filetypes.items())")
|
|
||||||
def sorted_filetypes(self):
|
|
||||||
return sorted(self.filetypes.items())
|
|
||||||
|
|
||||||
def get_filename_from_user(self):
|
|
||||||
if self.run() == int(Gtk.ResponseType.OK):
|
|
||||||
return self.get_filename(), self.ext
|
|
||||||
else:
|
|
||||||
return None, self.ext
|
|
||||||
|
|
||||||
|
|
||||||
class RubberbandGTK3(backend_tools.RubberbandBase):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
backend_tools.RubberbandBase.__init__(self, *args, **kwargs)
|
|
||||||
self.ctx = None
|
|
||||||
|
|
||||||
def draw_rubberband(self, x0, y0, x1, y1):
|
|
||||||
# 'adapted from http://aspn.activestate.com/ASPN/Cookbook/Python/
|
|
||||||
# Recipe/189744'
|
|
||||||
self.ctx = self.figure.canvas.get_property("window").cairo_create()
|
|
||||||
|
|
||||||
# todo: instead of redrawing the entire figure, copy the part of
|
|
||||||
# the figure that was covered by the previous rubberband rectangle
|
|
||||||
self.figure.canvas.draw()
|
|
||||||
|
|
||||||
height = self.figure.bbox.height
|
|
||||||
y1 = height - y1
|
|
||||||
y0 = height - y0
|
|
||||||
w = abs(x1 - x0)
|
|
||||||
h = abs(y1 - y0)
|
|
||||||
rect = [int(val) for val in (min(x0, x1), min(y0, y1), w, h)]
|
|
||||||
|
|
||||||
self.ctx.new_path()
|
|
||||||
self.ctx.set_line_width(0.5)
|
|
||||||
self.ctx.rectangle(rect[0], rect[1], rect[2], rect[3])
|
|
||||||
self.ctx.set_source_rgb(0, 0, 0)
|
|
||||||
self.ctx.stroke()
|
|
||||||
|
|
||||||
|
|
||||||
class ToolbarGTK3(ToolContainerBase, Gtk.Box):
|
|
||||||
_icon_extension = '.png'
|
|
||||||
|
|
||||||
def __init__(self, toolmanager):
|
|
||||||
ToolContainerBase.__init__(self, toolmanager)
|
|
||||||
Gtk.Box.__init__(self)
|
|
||||||
self.set_property("orientation", Gtk.Orientation.VERTICAL)
|
|
||||||
|
|
||||||
self._toolarea = Gtk.Box()
|
|
||||||
self._toolarea.set_property('orientation', Gtk.Orientation.HORIZONTAL)
|
|
||||||
self.pack_start(self._toolarea, False, False, 0)
|
|
||||||
self._toolarea.show_all()
|
|
||||||
self._groups = {}
|
|
||||||
self._toolitems = {}
|
|
||||||
|
|
||||||
def add_toolitem(self, name, group, position, image_file, description,
|
|
||||||
toggle):
|
|
||||||
if toggle:
|
|
||||||
tbutton = Gtk.ToggleToolButton()
|
|
||||||
else:
|
|
||||||
tbutton = Gtk.ToolButton()
|
|
||||||
tbutton.set_label(name)
|
|
||||||
|
|
||||||
if image_file is not None:
|
|
||||||
image = Gtk.Image()
|
|
||||||
image.set_from_file(image_file)
|
|
||||||
tbutton.set_icon_widget(image)
|
|
||||||
|
|
||||||
if position is None:
|
|
||||||
position = -1
|
|
||||||
|
|
||||||
self._add_button(tbutton, group, position)
|
|
||||||
signal = tbutton.connect('clicked', self._call_tool, name)
|
|
||||||
tbutton.set_tooltip_text(description)
|
|
||||||
tbutton.show_all()
|
|
||||||
self._toolitems.setdefault(name, [])
|
|
||||||
self._toolitems[name].append((tbutton, signal))
|
|
||||||
|
|
||||||
def _add_button(self, button, group, position):
|
|
||||||
if group not in self._groups:
|
|
||||||
if self._groups:
|
|
||||||
self._add_separator()
|
|
||||||
toolbar = Gtk.Toolbar()
|
|
||||||
toolbar.set_style(Gtk.ToolbarStyle.ICONS)
|
|
||||||
self._toolarea.pack_start(toolbar, False, False, 0)
|
|
||||||
toolbar.show_all()
|
|
||||||
self._groups[group] = toolbar
|
|
||||||
self._groups[group].insert(button, position)
|
|
||||||
|
|
||||||
def _call_tool(self, btn, name):
|
|
||||||
self.trigger_tool(name)
|
|
||||||
|
|
||||||
def toggle_toolitem(self, name, toggled):
|
|
||||||
if name not in self._toolitems:
|
|
||||||
return
|
|
||||||
for toolitem, signal in self._toolitems[name]:
|
|
||||||
toolitem.handler_block(signal)
|
|
||||||
toolitem.set_active(toggled)
|
|
||||||
toolitem.handler_unblock(signal)
|
|
||||||
|
|
||||||
def remove_toolitem(self, name):
|
|
||||||
if name not in self._toolitems:
|
|
||||||
self.toolmanager.message_event('%s Not in toolbar' % name, self)
|
|
||||||
return
|
|
||||||
|
|
||||||
for group in self._groups:
|
|
||||||
for toolitem, _signal in self._toolitems[name]:
|
|
||||||
if toolitem in self._groups[group]:
|
|
||||||
self._groups[group].remove(toolitem)
|
|
||||||
del self._toolitems[name]
|
|
||||||
|
|
||||||
def _add_separator(self):
|
|
||||||
sep = Gtk.Separator()
|
|
||||||
sep.set_property("orientation", Gtk.Orientation.VERTICAL)
|
|
||||||
self._toolarea.pack_start(sep, False, True, 0)
|
|
||||||
sep.show_all()
|
|
||||||
|
|
||||||
|
|
||||||
class StatusbarGTK3(StatusbarBase, Gtk.Statusbar):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
StatusbarBase.__init__(self, *args, **kwargs)
|
|
||||||
Gtk.Statusbar.__init__(self)
|
|
||||||
self._context = self.get_context_id('message')
|
|
||||||
|
|
||||||
def set_message(self, s):
|
|
||||||
self.pop(self._context)
|
|
||||||
self.push(self._context, s)
|
|
||||||
|
|
||||||
|
|
||||||
class SaveFigureGTK3(backend_tools.SaveFigureBase):
|
|
||||||
|
|
||||||
def get_filechooser(self):
|
|
||||||
fc = FileChooserDialog(
|
|
||||||
title='Save the figure',
|
|
||||||
parent=self.figure.canvas.manager.window,
|
|
||||||
path=os.path.expanduser(rcParams['savefig.directory']),
|
|
||||||
filetypes=self.figure.canvas.get_supported_filetypes(),
|
|
||||||
default_filetype=self.figure.canvas.get_default_filetype())
|
|
||||||
fc.set_current_name(self.figure.canvas.get_default_filename())
|
|
||||||
return fc
|
|
||||||
|
|
||||||
def trigger(self, *args, **kwargs):
|
|
||||||
chooser = self.get_filechooser()
|
|
||||||
fname, format_ = chooser.get_filename_from_user()
|
|
||||||
chooser.destroy()
|
|
||||||
if fname:
|
|
||||||
startpath = os.path.expanduser(rcParams['savefig.directory'])
|
|
||||||
if startpath == '':
|
|
||||||
# explicitly missing key or empty str signals to use cwd
|
|
||||||
rcParams['savefig.directory'] = startpath
|
|
||||||
else:
|
|
||||||
# save dir for next time
|
|
||||||
rcParams['savefig.directory'] = os.path.dirname(fname)
|
|
||||||
try:
|
|
||||||
self.figure.canvas.print_figure(fname, format=format_)
|
|
||||||
except Exception as e:
|
|
||||||
error_msg_gtk(str(e), parent=self)
|
|
||||||
|
|
||||||
|
|
||||||
class SetCursorGTK3(backend_tools.SetCursorBase):
|
|
||||||
def set_cursor(self, cursor):
|
|
||||||
self.figure.canvas.get_property("window").set_cursor(cursord[cursor])
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs)
|
|
||||||
self.window = None
|
|
||||||
|
|
||||||
def init_window(self):
|
|
||||||
if self.window:
|
|
||||||
return
|
|
||||||
self.window = Gtk.Window(title="Subplot Configuration Tool")
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.window.window.set_icon_from_file(window_icon)
|
|
||||||
except Exception:
|
|
||||||
# we presumably already logged a message on the
|
|
||||||
# failure of the main plot, don't keep reporting
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.vbox = Gtk.Box()
|
|
||||||
self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL)
|
|
||||||
self.window.add(self.vbox)
|
|
||||||
self.vbox.show()
|
|
||||||
self.window.connect('destroy', self.destroy)
|
|
||||||
|
|
||||||
toolfig = Figure(figsize=(6, 3))
|
|
||||||
canvas = self.figure.canvas.__class__(toolfig)
|
|
||||||
|
|
||||||
toolfig.subplots_adjust(top=0.9)
|
|
||||||
SubplotTool(self.figure, toolfig)
|
|
||||||
|
|
||||||
w = int(toolfig.bbox.width)
|
|
||||||
h = int(toolfig.bbox.height)
|
|
||||||
|
|
||||||
self.window.set_default_size(w, h)
|
|
||||||
|
|
||||||
canvas.show()
|
|
||||||
self.vbox.pack_start(canvas, True, True, 0)
|
|
||||||
self.window.show()
|
|
||||||
|
|
||||||
def destroy(self, *args):
|
|
||||||
self.window.destroy()
|
|
||||||
self.window = None
|
|
||||||
|
|
||||||
def _get_canvas(self, fig):
|
|
||||||
return self.canvas.__class__(fig)
|
|
||||||
|
|
||||||
def trigger(self, sender, event, data=None):
|
|
||||||
self.init_window()
|
|
||||||
self.window.present()
|
|
||||||
|
|
||||||
|
|
||||||
class HelpGTK3(backend_tools.ToolHelpBase):
|
|
||||||
def _normalize_shortcut(self, key):
|
|
||||||
"""
|
|
||||||
Convert Matplotlib key presses to GTK+ accelerator identifiers.
|
|
||||||
|
|
||||||
Related to `FigureCanvasGTK3._get_key`.
|
|
||||||
"""
|
|
||||||
special = {
|
|
||||||
'backspace': 'BackSpace',
|
|
||||||
'pagedown': 'Page_Down',
|
|
||||||
'pageup': 'Page_Up',
|
|
||||||
'scroll_lock': 'Scroll_Lock',
|
|
||||||
}
|
|
||||||
|
|
||||||
parts = key.split('+')
|
|
||||||
mods = ['<' + mod + '>' for mod in parts[:-1]]
|
|
||||||
key = parts[-1]
|
|
||||||
|
|
||||||
if key in special:
|
|
||||||
key = special[key]
|
|
||||||
elif len(key) > 1:
|
|
||||||
key = key.capitalize()
|
|
||||||
elif key.isupper():
|
|
||||||
mods += ['<shift>']
|
|
||||||
|
|
||||||
return ''.join(mods) + key
|
|
||||||
|
|
||||||
def _show_shortcuts_window(self):
|
|
||||||
section = Gtk.ShortcutsSection()
|
|
||||||
|
|
||||||
for name, tool in sorted(self.toolmanager.tools.items()):
|
|
||||||
if not tool.description:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Putting everything in a separate group allows GTK to
|
|
||||||
# automatically split them into separate columns/pages, which is
|
|
||||||
# useful because we have lots of shortcuts, some with many keys
|
|
||||||
# that are very wide.
|
|
||||||
group = Gtk.ShortcutsGroup()
|
|
||||||
section.add(group)
|
|
||||||
# A hack to remove the title since we have no group naming.
|
|
||||||
group.forall(lambda widget, data: widget.set_visible(False), None)
|
|
||||||
|
|
||||||
shortcut = Gtk.ShortcutsShortcut(
|
|
||||||
accelerator=' '.join(
|
|
||||||
self._normalize_shortcut(key)
|
|
||||||
for key in self.toolmanager.get_tool_keymap(name)
|
|
||||||
# Will never be sent:
|
|
||||||
if 'cmd+' not in key),
|
|
||||||
title=tool.name,
|
|
||||||
subtitle=tool.description)
|
|
||||||
group.add(shortcut)
|
|
||||||
|
|
||||||
window = Gtk.ShortcutsWindow(
|
|
||||||
title='Help',
|
|
||||||
modal=True,
|
|
||||||
transient_for=self._figure.canvas.get_toplevel())
|
|
||||||
section.show() # Must be done explicitly before add!
|
|
||||||
window.add(section)
|
|
||||||
|
|
||||||
window.show_all()
|
|
||||||
|
|
||||||
def _show_shortcuts_dialog(self):
|
|
||||||
dialog = Gtk.MessageDialog(
|
|
||||||
self._figure.canvas.get_toplevel(),
|
|
||||||
0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, self._get_help_text(),
|
|
||||||
title="Help")
|
|
||||||
dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
def trigger(self, *args):
|
|
||||||
if Gtk.check_version(3, 20, 0) is None:
|
|
||||||
self._show_shortcuts_window()
|
|
||||||
else:
|
|
||||||
self._show_shortcuts_dialog()
|
|
||||||
|
|
||||||
|
|
||||||
class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase):
|
|
||||||
def trigger(self, *args, **kwargs):
|
|
||||||
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
|
||||||
window = self.canvas.get_window()
|
|
||||||
x, y, width, height = window.get_geometry()
|
|
||||||
pb = Gdk.pixbuf_get_from_window(window, x, y, width, height)
|
|
||||||
clipboard.set_image(pb)
|
|
||||||
|
|
||||||
|
|
||||||
# Define the file to use as the GTk icon
|
|
||||||
if sys.platform == 'win32':
|
|
||||||
icon_filename = 'matplotlib.png'
|
|
||||||
else:
|
|
||||||
icon_filename = 'matplotlib.svg'
|
|
||||||
window_icon = os.path.join(
|
|
||||||
matplotlib.rcParams['datapath'], 'images', icon_filename)
|
|
||||||
|
|
||||||
|
|
||||||
def error_msg_gtk(msg, parent=None):
|
|
||||||
if parent is not None: # find the toplevel Gtk.Window
|
|
||||||
parent = parent.get_toplevel()
|
|
||||||
if not parent.is_toplevel():
|
|
||||||
parent = None
|
|
||||||
|
|
||||||
if not isinstance(msg, str):
|
|
||||||
msg = ','.join(map(str, msg))
|
|
||||||
|
|
||||||
dialog = Gtk.MessageDialog(
|
|
||||||
parent = parent,
|
|
||||||
type = Gtk.MessageType.ERROR,
|
|
||||||
buttons = Gtk.ButtonsType.OK,
|
|
||||||
message_format = msg)
|
|
||||||
dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
|
|
||||||
backend_tools.ToolSaveFigure = SaveFigureGTK3
|
|
||||||
backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3
|
|
||||||
backend_tools.ToolSetCursor = SetCursorGTK3
|
|
||||||
backend_tools.ToolRubberband = RubberbandGTK3
|
|
||||||
backend_tools.ToolHelp = HelpGTK3
|
|
||||||
backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK3
|
|
||||||
|
|
||||||
Toolbar = ToolbarGTK3
|
|
||||||
|
|
||||||
|
|
||||||
@_Backend.export
|
|
||||||
class _BackendGTK3(_Backend):
|
|
||||||
required_interactive_framework = "gtk3"
|
|
||||||
FigureCanvas = FigureCanvasGTK3
|
|
||||||
FigureManager = FigureManagerGTK3
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def trigger_manager_draw(manager):
|
|
||||||
manager.canvas.draw_idle()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mainloop():
|
|
||||||
if Gtk.main_level() == 0:
|
|
||||||
Gtk.main()
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from .. import cbook
|
|
||||||
from . import backend_agg, backend_cairo, backend_gtk3
|
|
||||||
from ._gtk3_compat import gi
|
|
||||||
from .backend_cairo import cairo
|
|
||||||
from .backend_gtk3 import Gtk, _BackendGTK3
|
|
||||||
from matplotlib import transforms
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasGTK3Agg(backend_gtk3.FigureCanvasGTK3,
|
|
||||||
backend_agg.FigureCanvasAgg):
|
|
||||||
def __init__(self, figure):
|
|
||||||
backend_gtk3.FigureCanvasGTK3.__init__(self, figure)
|
|
||||||
self._bbox_queue = []
|
|
||||||
|
|
||||||
def _renderer_init(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _render_figure(self, width, height):
|
|
||||||
backend_agg.FigureCanvasAgg.draw(self)
|
|
||||||
|
|
||||||
def on_draw_event(self, widget, ctx):
|
|
||||||
"""GtkDrawable draw event, like expose_event in GTK 2.X.
|
|
||||||
"""
|
|
||||||
allocation = self.get_allocation()
|
|
||||||
w, h = allocation.width, allocation.height
|
|
||||||
|
|
||||||
if not len(self._bbox_queue):
|
|
||||||
self._render_figure(w, h)
|
|
||||||
Gtk.render_background(
|
|
||||||
self.get_style_context(), ctx,
|
|
||||||
allocation.x, allocation.y,
|
|
||||||
allocation.width, allocation.height)
|
|
||||||
bbox_queue = [transforms.Bbox([[0, 0], [w, h]])]
|
|
||||||
else:
|
|
||||||
bbox_queue = self._bbox_queue
|
|
||||||
|
|
||||||
ctx = backend_cairo._to_context(ctx)
|
|
||||||
|
|
||||||
for bbox in bbox_queue:
|
|
||||||
x = int(bbox.x0)
|
|
||||||
y = h - int(bbox.y1)
|
|
||||||
width = int(bbox.x1) - int(bbox.x0)
|
|
||||||
height = int(bbox.y1) - int(bbox.y0)
|
|
||||||
|
|
||||||
buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(
|
|
||||||
np.asarray(self.copy_from_bbox(bbox)))
|
|
||||||
image = cairo.ImageSurface.create_for_data(
|
|
||||||
buf.ravel().data, cairo.FORMAT_ARGB32, width, height)
|
|
||||||
ctx.set_source_surface(image, x, y)
|
|
||||||
ctx.paint()
|
|
||||||
|
|
||||||
if len(self._bbox_queue):
|
|
||||||
self._bbox_queue = []
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def blit(self, bbox=None):
|
|
||||||
# If bbox is None, blit the entire canvas to gtk. Otherwise
|
|
||||||
# blit only the area defined by the bbox.
|
|
||||||
if bbox is None:
|
|
||||||
bbox = self.figure.bbox
|
|
||||||
|
|
||||||
allocation = self.get_allocation()
|
|
||||||
w, h = allocation.width, allocation.height
|
|
||||||
x = int(bbox.x0)
|
|
||||||
y = h - int(bbox.y1)
|
|
||||||
width = int(bbox.x1) - int(bbox.x0)
|
|
||||||
height = int(bbox.y1) - int(bbox.y0)
|
|
||||||
|
|
||||||
self._bbox_queue.append(bbox)
|
|
||||||
self.queue_draw_area(x, y, width, height)
|
|
||||||
|
|
||||||
def print_png(self, filename, *args, **kwargs):
|
|
||||||
# Do this so we can save the resolution of figure in the PNG file
|
|
||||||
agg = self.switch_backends(backend_agg.FigureCanvasAgg)
|
|
||||||
return agg.print_png(filename, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureManagerGTK3Agg(backend_gtk3.FigureManagerGTK3):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendGTK3.export
|
|
||||||
class _BackendGTK3Cairo(_BackendGTK3):
|
|
||||||
FigureCanvas = FigureCanvasGTK3Agg
|
|
||||||
FigureManager = FigureManagerGTK3Agg
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
from . import backend_cairo, backend_gtk3
|
|
||||||
from ._gtk3_compat import gi
|
|
||||||
from .backend_gtk3 import Gtk, _BackendGTK3
|
|
||||||
from matplotlib.backend_bases import cursors
|
|
||||||
|
|
||||||
|
|
||||||
class RendererGTK3Cairo(backend_cairo.RendererCairo):
|
|
||||||
def set_context(self, ctx):
|
|
||||||
self.gc.ctx = backend_cairo._to_context(ctx)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasGTK3Cairo(backend_gtk3.FigureCanvasGTK3,
|
|
||||||
backend_cairo.FigureCanvasCairo):
|
|
||||||
|
|
||||||
def _renderer_init(self):
|
|
||||||
"""Use cairo renderer."""
|
|
||||||
self._renderer = RendererGTK3Cairo(self.figure.dpi)
|
|
||||||
|
|
||||||
def _render_figure(self, width, height):
|
|
||||||
self._renderer.set_width_height(width, height)
|
|
||||||
self.figure.draw(self._renderer)
|
|
||||||
|
|
||||||
def on_draw_event(self, widget, ctx):
|
|
||||||
"""GtkDrawable draw event."""
|
|
||||||
toolbar = self.toolbar
|
|
||||||
# if toolbar:
|
|
||||||
# toolbar.set_cursor(cursors.WAIT)
|
|
||||||
self._renderer.set_context(ctx)
|
|
||||||
allocation = self.get_allocation()
|
|
||||||
Gtk.render_background(
|
|
||||||
self.get_style_context(), ctx,
|
|
||||||
allocation.x, allocation.y, allocation.width, allocation.height)
|
|
||||||
self._render_figure(allocation.width, allocation.height)
|
|
||||||
# if toolbar:
|
|
||||||
# toolbar.set_cursor(toolbar._lastCursor)
|
|
||||||
return False # finish event propagation?
|
|
||||||
|
|
||||||
|
|
||||||
class FigureManagerGTK3Cairo(backend_gtk3.FigureManagerGTK3):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendGTK3.export
|
|
||||||
class _BackendGTK3Cairo(_BackendGTK3):
|
|
||||||
FigureCanvas = FigureCanvasGTK3Cairo
|
|
||||||
FigureManager = FigureManagerGTK3Cairo
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from matplotlib._pylab_helpers import Gcf
|
|
||||||
from matplotlib.backend_bases import (
|
|
||||||
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
|
|
||||||
TimerBase)
|
|
||||||
|
|
||||||
from matplotlib.figure import Figure
|
|
||||||
from matplotlib import rcParams
|
|
||||||
|
|
||||||
from matplotlib.widgets import SubplotTool
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
from matplotlib.backends import _macosx
|
|
||||||
|
|
||||||
from .backend_agg import FigureCanvasAgg
|
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
#
|
|
||||||
# The following functions and classes are for pylab and implement
|
|
||||||
# window/figure managers, etc...
|
|
||||||
#
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
|
|
||||||
class TimerMac(_macosx.Timer, TimerBase):
|
|
||||||
'''
|
|
||||||
Subclass of :class:`backend_bases.TimerBase` that uses CoreFoundation
|
|
||||||
run loops for timer events.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
interval : int
|
|
||||||
The time between timer events in milliseconds. Default is 1000 ms.
|
|
||||||
single_shot : bool
|
|
||||||
Boolean flag indicating whether this timer should operate as single
|
|
||||||
shot (run once and then stop). Defaults to False.
|
|
||||||
callbacks : list
|
|
||||||
Stores list of (func, args) tuples that will be called upon timer
|
|
||||||
events. This list can be manipulated directly, or the functions
|
|
||||||
`add_callback` and `remove_callback` can be used.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# completely implemented at the C-level (in _macosx.Timer)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasMac(_macosx.FigureCanvas, FigureCanvasAgg):
|
|
||||||
"""
|
|
||||||
The canvas the figure renders into. Calls the draw and print fig
|
|
||||||
methods, creates the renderers, etc...
|
|
||||||
|
|
||||||
Events such as button presses, mouse movements, and key presses
|
|
||||||
are handled in the C code and the base class methods
|
|
||||||
button_press_event, button_release_event, motion_notify_event,
|
|
||||||
key_press_event, and key_release_event are called from there.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
figure : `matplotlib.figure.Figure`
|
|
||||||
A high-level Figure instance
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, figure):
|
|
||||||
FigureCanvasBase.__init__(self, figure)
|
|
||||||
width, height = self.get_width_height()
|
|
||||||
_macosx.FigureCanvas.__init__(self, width, height)
|
|
||||||
self._device_scale = 1.0
|
|
||||||
|
|
||||||
def _set_device_scale(self, value):
|
|
||||||
if self._device_scale != value:
|
|
||||||
self.figure.dpi = self.figure.dpi / self._device_scale * value
|
|
||||||
self._device_scale = value
|
|
||||||
|
|
||||||
def _draw(self):
|
|
||||||
renderer = self.get_renderer(cleared=self.figure.stale)
|
|
||||||
|
|
||||||
if self.figure.stale:
|
|
||||||
self.figure.draw(renderer)
|
|
||||||
|
|
||||||
return renderer
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
self.invalidate()
|
|
||||||
self.flush_events()
|
|
||||||
|
|
||||||
def draw_idle(self, *args, **kwargs):
|
|
||||||
self.invalidate()
|
|
||||||
|
|
||||||
def blit(self, bbox=None):
|
|
||||||
self.invalidate()
|
|
||||||
|
|
||||||
def resize(self, width, height):
|
|
||||||
dpi = self.figure.dpi
|
|
||||||
width /= dpi
|
|
||||||
height /= dpi
|
|
||||||
self.figure.set_size_inches(width * self._device_scale,
|
|
||||||
height * self._device_scale,
|
|
||||||
forward=False)
|
|
||||||
FigureCanvasBase.resize_event(self)
|
|
||||||
self.draw_idle()
|
|
||||||
|
|
||||||
def new_timer(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Creates a new backend-specific subclass of `backend_bases.Timer`.
|
|
||||||
This is useful for getting periodic events through the backend's native
|
|
||||||
event loop. Implemented only for backends with GUIs.
|
|
||||||
|
|
||||||
Other Parameters
|
|
||||||
----------------
|
|
||||||
interval : scalar
|
|
||||||
Timer interval in milliseconds
|
|
||||||
callbacks : list
|
|
||||||
Sequence of (func, args, kwargs) where ``func(*args, **kwargs)``
|
|
||||||
will be executed by the timer every *interval*.
|
|
||||||
"""
|
|
||||||
return TimerMac(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureManagerMac(_macosx.FigureManager, FigureManagerBase):
|
|
||||||
"""
|
|
||||||
Wrap everything up into a window for the pylab interface
|
|
||||||
"""
|
|
||||||
def __init__(self, canvas, num):
|
|
||||||
FigureManagerBase.__init__(self, canvas, num)
|
|
||||||
title = "Figure %d" % num
|
|
||||||
_macosx.FigureManager.__init__(self, canvas, title)
|
|
||||||
if rcParams['toolbar'] == 'toolbar2':
|
|
||||||
self.toolbar = NavigationToolbar2Mac(canvas)
|
|
||||||
else:
|
|
||||||
self.toolbar = None
|
|
||||||
if self.toolbar is not None:
|
|
||||||
self.toolbar.update()
|
|
||||||
|
|
||||||
if matplotlib.is_interactive():
|
|
||||||
self.show()
|
|
||||||
self.canvas.draw_idle()
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
Gcf.destroy(self.num)
|
|
||||||
|
|
||||||
|
|
||||||
class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):
|
|
||||||
|
|
||||||
def __init__(self, canvas):
|
|
||||||
NavigationToolbar2.__init__(self, canvas)
|
|
||||||
|
|
||||||
def _init_toolbar(self):
|
|
||||||
basedir = os.path.join(rcParams['datapath'], "images")
|
|
||||||
_macosx.NavigationToolbar2.__init__(self, basedir)
|
|
||||||
|
|
||||||
def draw_rubberband(self, event, x0, y0, x1, y1):
|
|
||||||
self.canvas.set_rubberband(int(x0), int(y0), int(x1), int(y1))
|
|
||||||
|
|
||||||
def release(self, event):
|
|
||||||
self.canvas.remove_rubberband()
|
|
||||||
|
|
||||||
def set_cursor(self, cursor):
|
|
||||||
_macosx.set_cursor(cursor)
|
|
||||||
|
|
||||||
def save_figure(self, *args):
|
|
||||||
filename = _macosx.choose_save_file('Save the figure',
|
|
||||||
self.canvas.get_default_filename())
|
|
||||||
if filename is None: # Cancel
|
|
||||||
return
|
|
||||||
self.canvas.figure.savefig(filename)
|
|
||||||
|
|
||||||
def prepare_configure_subplots(self):
|
|
||||||
toolfig = Figure(figsize=(6,3))
|
|
||||||
canvas = FigureCanvasMac(toolfig)
|
|
||||||
toolfig.subplots_adjust(top=0.9)
|
|
||||||
tool = SubplotTool(self.canvas.figure, toolfig)
|
|
||||||
return canvas
|
|
||||||
|
|
||||||
def set_message(self, message):
|
|
||||||
_macosx.NavigationToolbar2.set_message(self, message.encode('utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
#
|
|
||||||
# Now just provide the standard names that backend.__init__ is expecting
|
|
||||||
#
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
@_Backend.export
|
|
||||||
class _BackendMac(_Backend):
|
|
||||||
required_interactive_framework = "macosx"
|
|
||||||
FigureCanvas = FigureCanvasMac
|
|
||||||
FigureManager = FigureManagerMac
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def trigger_manager_draw(manager):
|
|
||||||
# For performance reasons, we don't want to redraw the figure after
|
|
||||||
# each draw command. Instead, we mark the figure as invalid, so that it
|
|
||||||
# will be redrawn as soon as the event loop resumes via PyOS_InputHook.
|
|
||||||
# This function should be called after each draw event, even if
|
|
||||||
# matplotlib is not running interactively.
|
|
||||||
manager.canvas.invalidate()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def mainloop():
|
|
||||||
_macosx.show()
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import numpy as np
|
|
||||||
|
|
||||||
from matplotlib.backends.backend_agg import RendererAgg
|
|
||||||
from matplotlib.tight_bbox import process_figure_for_rasterizing
|
|
||||||
|
|
||||||
|
|
||||||
class MixedModeRenderer(object):
|
|
||||||
"""
|
|
||||||
A helper class to implement a renderer that switches between
|
|
||||||
vector and raster drawing. An example may be a PDF writer, where
|
|
||||||
most things are drawn with PDF vector commands, but some very
|
|
||||||
complex objects, such as quad meshes, are rasterised and then
|
|
||||||
output as images.
|
|
||||||
"""
|
|
||||||
def __init__(self, figure, width, height, dpi, vector_renderer,
|
|
||||||
raster_renderer_class=None,
|
|
||||||
bbox_inches_restore=None):
|
|
||||||
"""
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
figure : `matplotlib.figure.Figure`
|
|
||||||
The figure instance.
|
|
||||||
|
|
||||||
width : scalar
|
|
||||||
The width of the canvas in logical units
|
|
||||||
|
|
||||||
height : scalar
|
|
||||||
The height of the canvas in logical units
|
|
||||||
|
|
||||||
dpi : scalar
|
|
||||||
The dpi of the canvas
|
|
||||||
|
|
||||||
vector_renderer : `matplotlib.backend_bases.RendererBase`
|
|
||||||
An instance of a subclass of
|
|
||||||
`~matplotlib.backend_bases.RendererBase` that will be used for the
|
|
||||||
vector drawing.
|
|
||||||
|
|
||||||
raster_renderer_class : `matplotlib.backend_bases.RendererBase`
|
|
||||||
The renderer class to use for the raster drawing. If not provided,
|
|
||||||
this will use the Agg backend (which is currently the only viable
|
|
||||||
option anyway.)
|
|
||||||
|
|
||||||
"""
|
|
||||||
if raster_renderer_class is None:
|
|
||||||
raster_renderer_class = RendererAgg
|
|
||||||
|
|
||||||
self._raster_renderer_class = raster_renderer_class
|
|
||||||
self._width = width
|
|
||||||
self._height = height
|
|
||||||
self.dpi = dpi
|
|
||||||
|
|
||||||
self._vector_renderer = vector_renderer
|
|
||||||
|
|
||||||
self._raster_renderer = None
|
|
||||||
self._rasterizing = 0
|
|
||||||
|
|
||||||
# A reference to the figure is needed as we need to change
|
|
||||||
# the figure dpi before and after the rasterization. Although
|
|
||||||
# this looks ugly, I couldn't find a better solution. -JJL
|
|
||||||
self.figure = figure
|
|
||||||
self._figdpi = figure.get_dpi()
|
|
||||||
|
|
||||||
self._bbox_inches_restore = bbox_inches_restore
|
|
||||||
|
|
||||||
self._set_current_renderer(vector_renderer)
|
|
||||||
|
|
||||||
_methods = """
|
|
||||||
close_group draw_image draw_markers draw_path
|
|
||||||
draw_path_collection draw_quad_mesh draw_tex draw_text
|
|
||||||
finalize flipy get_canvas_width_height get_image_magnification
|
|
||||||
get_texmanager get_text_width_height_descent new_gc open_group
|
|
||||||
option_image_nocomposite points_to_pixels strip_math
|
|
||||||
start_filter stop_filter draw_gouraud_triangle
|
|
||||||
draw_gouraud_triangles option_scale_image
|
|
||||||
_text2path _get_text_path_transform height width
|
|
||||||
""".split()
|
|
||||||
|
|
||||||
def _set_current_renderer(self, renderer):
|
|
||||||
self._renderer = renderer
|
|
||||||
|
|
||||||
for method in self._methods:
|
|
||||||
if hasattr(renderer, method):
|
|
||||||
setattr(self, method, getattr(renderer, method))
|
|
||||||
renderer.start_rasterizing = self.start_rasterizing
|
|
||||||
renderer.stop_rasterizing = self.stop_rasterizing
|
|
||||||
|
|
||||||
def start_rasterizing(self):
|
|
||||||
"""
|
|
||||||
Enter "raster" mode. All subsequent drawing commands (until
|
|
||||||
stop_rasterizing is called) will be drawn with the raster
|
|
||||||
backend.
|
|
||||||
|
|
||||||
If start_rasterizing is called multiple times before
|
|
||||||
stop_rasterizing is called, this method has no effect.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# change the dpi of the figure temporarily.
|
|
||||||
self.figure.set_dpi(self.dpi)
|
|
||||||
|
|
||||||
if self._bbox_inches_restore: # when tight bbox is used
|
|
||||||
r = process_figure_for_rasterizing(self.figure,
|
|
||||||
self._bbox_inches_restore)
|
|
||||||
self._bbox_inches_restore = r
|
|
||||||
|
|
||||||
if self._rasterizing == 0:
|
|
||||||
self._raster_renderer = self._raster_renderer_class(
|
|
||||||
self._width*self.dpi, self._height*self.dpi, self.dpi)
|
|
||||||
self._set_current_renderer(self._raster_renderer)
|
|
||||||
self._rasterizing += 1
|
|
||||||
|
|
||||||
def stop_rasterizing(self):
|
|
||||||
"""
|
|
||||||
Exit "raster" mode. All of the drawing that was done since
|
|
||||||
the last start_rasterizing command will be copied to the
|
|
||||||
vector backend by calling draw_image.
|
|
||||||
|
|
||||||
If stop_rasterizing is called multiple times before
|
|
||||||
start_rasterizing is called, this method has no effect.
|
|
||||||
"""
|
|
||||||
self._rasterizing -= 1
|
|
||||||
if self._rasterizing == 0:
|
|
||||||
self._set_current_renderer(self._vector_renderer)
|
|
||||||
|
|
||||||
height = self._height * self.dpi
|
|
||||||
buffer, bounds = self._raster_renderer.tostring_rgba_minimized()
|
|
||||||
l, b, w, h = bounds
|
|
||||||
if w > 0 and h > 0:
|
|
||||||
image = np.frombuffer(buffer, dtype=np.uint8)
|
|
||||||
image = image.reshape((h, w, 4))
|
|
||||||
image = image[::-1]
|
|
||||||
gc = self._renderer.new_gc()
|
|
||||||
# TODO: If the mixedmode resolution differs from the figure's
|
|
||||||
# dpi, the image must be scaled (dpi->_figdpi). Not all
|
|
||||||
# backends support this.
|
|
||||||
self._renderer.draw_image(
|
|
||||||
gc,
|
|
||||||
l * self._figdpi / self.dpi,
|
|
||||||
(height-b-h) * self._figdpi / self.dpi,
|
|
||||||
image)
|
|
||||||
self._raster_renderer = None
|
|
||||||
self._rasterizing = False
|
|
||||||
|
|
||||||
# restore the figure dpi.
|
|
||||||
self.figure.set_dpi(self._figdpi)
|
|
||||||
|
|
||||||
if self._bbox_inches_restore: # when tight bbox is used
|
|
||||||
r = process_figure_for_rasterizing(self.figure,
|
|
||||||
self._bbox_inches_restore,
|
|
||||||
self._figdpi)
|
|
||||||
self._bbox_inches_restore = r
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
"""Interactive figures in the IPython notebook"""
|
|
||||||
# Note: There is a notebook in
|
|
||||||
# lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify
|
|
||||||
# that changes made maintain expected behaviour.
|
|
||||||
|
|
||||||
from base64 import b64encode
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import pathlib
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
from IPython.display import display, Javascript, HTML
|
|
||||||
try:
|
|
||||||
# Jupyter/IPython 4.x or later
|
|
||||||
from ipykernel.comm import Comm
|
|
||||||
except ImportError:
|
|
||||||
# Jupyter/IPython 3.x or earlier
|
|
||||||
from IPython.kernel.comm import Comm
|
|
||||||
|
|
||||||
from matplotlib import rcParams, is_interactive
|
|
||||||
from matplotlib._pylab_helpers import Gcf
|
|
||||||
from matplotlib.backend_bases import (
|
|
||||||
_Backend, FigureCanvasBase, NavigationToolbar2)
|
|
||||||
from matplotlib.backends.backend_webagg_core import (
|
|
||||||
FigureCanvasWebAggCore, FigureManagerWebAgg, NavigationToolbar2WebAgg,
|
|
||||||
TimerTornado)
|
|
||||||
|
|
||||||
|
|
||||||
def connection_info():
|
|
||||||
"""
|
|
||||||
Return a string showing the figure and connection status for
|
|
||||||
the backend. This is intended as a diagnostic tool, and not for general
|
|
||||||
use.
|
|
||||||
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
for manager in Gcf.get_all_fig_managers():
|
|
||||||
fig = manager.canvas.figure
|
|
||||||
result.append('{0} - {0}'.format((fig.get_label() or
|
|
||||||
"Figure {0}".format(manager.num)),
|
|
||||||
manager.web_sockets))
|
|
||||||
if not is_interactive():
|
|
||||||
result.append('Figures pending show: {0}'.format(len(Gcf._activeQue)))
|
|
||||||
return '\n'.join(result)
|
|
||||||
|
|
||||||
|
|
||||||
# Note: Version 3.2 and 4.x icons
|
|
||||||
# http://fontawesome.io/3.2.1/icons/
|
|
||||||
# http://fontawesome.io/
|
|
||||||
# the `fa fa-xxx` part targets font-awesome 4, (IPython 3.x)
|
|
||||||
# the icon-xxx targets font awesome 3.21 (IPython 2.x)
|
|
||||||
_FONT_AWESOME_CLASSES = {
|
|
||||||
'home': 'fa fa-home icon-home',
|
|
||||||
'back': 'fa fa-arrow-left icon-arrow-left',
|
|
||||||
'forward': 'fa fa-arrow-right icon-arrow-right',
|
|
||||||
'zoom_to_rect': 'fa fa-square-o icon-check-empty',
|
|
||||||
'move': 'fa fa-arrows icon-move',
|
|
||||||
'download': 'fa fa-floppy-o icon-save',
|
|
||||||
None: None
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NavigationIPy(NavigationToolbar2WebAgg):
|
|
||||||
|
|
||||||
# Use the standard toolbar items + download button
|
|
||||||
toolitems = [(text, tooltip_text,
|
|
||||||
_FONT_AWESOME_CLASSES[image_file], name_of_method)
|
|
||||||
for text, tooltip_text, image_file, name_of_method
|
|
||||||
in (NavigationToolbar2.toolitems +
|
|
||||||
(('Download', 'Download plot', 'download', 'download'),))
|
|
||||||
if image_file in _FONT_AWESOME_CLASSES]
|
|
||||||
|
|
||||||
|
|
||||||
class FigureManagerNbAgg(FigureManagerWebAgg):
|
|
||||||
ToolbarCls = NavigationIPy
|
|
||||||
|
|
||||||
def __init__(self, canvas, num):
|
|
||||||
self._shown = False
|
|
||||||
FigureManagerWebAgg.__init__(self, canvas, num)
|
|
||||||
|
|
||||||
def display_js(self):
|
|
||||||
# XXX How to do this just once? It has to deal with multiple
|
|
||||||
# browser instances using the same kernel (require.js - but the
|
|
||||||
# file isn't static?).
|
|
||||||
display(Javascript(FigureManagerNbAgg.get_javascript()))
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
if not self._shown:
|
|
||||||
self.display_js()
|
|
||||||
self._create_comm()
|
|
||||||
else:
|
|
||||||
self.canvas.draw_idle()
|
|
||||||
self._shown = True
|
|
||||||
|
|
||||||
def reshow(self):
|
|
||||||
"""
|
|
||||||
A special method to re-show the figure in the notebook.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._shown = False
|
|
||||||
self.show()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connected(self):
|
|
||||||
return bool(self.web_sockets)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_javascript(cls, stream=None):
|
|
||||||
if stream is None:
|
|
||||||
output = io.StringIO()
|
|
||||||
else:
|
|
||||||
output = stream
|
|
||||||
super().get_javascript(stream=output)
|
|
||||||
output.write((pathlib.Path(__file__).parent
|
|
||||||
/ "web_backend/js/nbagg_mpl.js")
|
|
||||||
.read_text(encoding="utf-8"))
|
|
||||||
if stream is None:
|
|
||||||
return output.getvalue()
|
|
||||||
|
|
||||||
def _create_comm(self):
|
|
||||||
comm = CommSocket(self)
|
|
||||||
self.add_web_socket(comm)
|
|
||||||
return comm
|
|
||||||
|
|
||||||
def destroy(self):
|
|
||||||
self._send_event('close')
|
|
||||||
# need to copy comms as callbacks will modify this list
|
|
||||||
for comm in list(self.web_sockets):
|
|
||||||
comm.on_close()
|
|
||||||
self.clearup_closed()
|
|
||||||
|
|
||||||
def clearup_closed(self):
|
|
||||||
"""Clear up any closed Comms."""
|
|
||||||
self.web_sockets = {socket for socket in self.web_sockets
|
|
||||||
if socket.is_open()}
|
|
||||||
|
|
||||||
if len(self.web_sockets) == 0:
|
|
||||||
self.canvas.close_event()
|
|
||||||
|
|
||||||
def remove_comm(self, comm_id):
|
|
||||||
self.web_sockets = {socket for socket in self.web_sockets
|
|
||||||
if not socket.comm.comm_id == comm_id}
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
|
|
||||||
def new_timer(self, *args, **kwargs):
|
|
||||||
return TimerTornado(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class CommSocket(object):
|
|
||||||
"""
|
|
||||||
Manages the Comm connection between IPython and the browser (client).
|
|
||||||
|
|
||||||
Comms are 2 way, with the CommSocket being able to publish a message
|
|
||||||
via the send_json method, and handle a message with on_message. On the
|
|
||||||
JS side figure.send_message and figure.ws.onmessage do the sending and
|
|
||||||
receiving respectively.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, manager):
|
|
||||||
self.supports_binary = None
|
|
||||||
self.manager = manager
|
|
||||||
self.uuid = str(uuid.uuid4())
|
|
||||||
# Publish an output area with a unique ID. The javascript can then
|
|
||||||
# hook into this area.
|
|
||||||
display(HTML("<div id=%r></div>" % self.uuid))
|
|
||||||
try:
|
|
||||||
self.comm = Comm('matplotlib', data={'id': self.uuid})
|
|
||||||
except AttributeError:
|
|
||||||
raise RuntimeError('Unable to create an IPython notebook Comm '
|
|
||||||
'instance. Are you in the IPython notebook?')
|
|
||||||
self.comm.on_msg(self.on_message)
|
|
||||||
|
|
||||||
manager = self.manager
|
|
||||||
self._ext_close = False
|
|
||||||
|
|
||||||
def _on_close(close_message):
|
|
||||||
self._ext_close = True
|
|
||||||
manager.remove_comm(close_message['content']['comm_id'])
|
|
||||||
manager.clearup_closed()
|
|
||||||
|
|
||||||
self.comm.on_close(_on_close)
|
|
||||||
|
|
||||||
def is_open(self):
|
|
||||||
return not (self._ext_close or self.comm._closed)
|
|
||||||
|
|
||||||
def on_close(self):
|
|
||||||
# When the socket is closed, deregister the websocket with
|
|
||||||
# the FigureManager.
|
|
||||||
if self.is_open():
|
|
||||||
try:
|
|
||||||
self.comm.close()
|
|
||||||
except KeyError:
|
|
||||||
# apparently already cleaned it up?
|
|
||||||
pass
|
|
||||||
|
|
||||||
def send_json(self, content):
|
|
||||||
self.comm.send({'data': json.dumps(content)})
|
|
||||||
|
|
||||||
def send_binary(self, blob):
|
|
||||||
# The comm is ascii, so we always send the image in base64
|
|
||||||
# encoded data URL form.
|
|
||||||
data = b64encode(blob).decode('ascii')
|
|
||||||
data_uri = "data:image/png;base64,{0}".format(data)
|
|
||||||
self.comm.send({'data': data_uri})
|
|
||||||
|
|
||||||
def on_message(self, message):
|
|
||||||
# The 'supports_binary' message is relevant to the
|
|
||||||
# websocket itself. The other messages get passed along
|
|
||||||
# to matplotlib as-is.
|
|
||||||
|
|
||||||
# Every message has a "type" and a "figure_id".
|
|
||||||
message = json.loads(message['content']['data'])
|
|
||||||
if message['type'] == 'closing':
|
|
||||||
self.on_close()
|
|
||||||
self.manager.clearup_closed()
|
|
||||||
elif message['type'] == 'supports_binary':
|
|
||||||
self.supports_binary = message['value']
|
|
||||||
else:
|
|
||||||
self.manager.handle_json(message)
|
|
||||||
|
|
||||||
|
|
||||||
@_Backend.export
|
|
||||||
class _BackendNbAgg(_Backend):
|
|
||||||
FigureCanvas = FigureCanvasNbAgg
|
|
||||||
FigureManager = FigureManagerNbAgg
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def new_figure_manager_given_figure(num, figure):
|
|
||||||
canvas = FigureCanvasNbAgg(figure)
|
|
||||||
manager = FigureManagerNbAgg(canvas, num)
|
|
||||||
if is_interactive():
|
|
||||||
manager.show()
|
|
||||||
figure.canvas.draw_idle()
|
|
||||||
canvas.mpl_connect('close_event', lambda event: Gcf.destroy(num))
|
|
||||||
return manager
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def trigger_manager_draw(manager):
|
|
||||||
manager.show()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def show(*args, **kwargs):
|
|
||||||
## TODO: something to do when keyword block==False ?
|
|
||||||
from matplotlib._pylab_helpers import Gcf
|
|
||||||
|
|
||||||
managers = Gcf.get_all_fig_managers()
|
|
||||||
if not managers:
|
|
||||||
return
|
|
||||||
|
|
||||||
interactive = is_interactive()
|
|
||||||
|
|
||||||
for manager in managers:
|
|
||||||
manager.show()
|
|
||||||
|
|
||||||
# plt.figure adds an event which puts the figure in focus
|
|
||||||
# in the activeQue. Disable this behaviour, as it results in
|
|
||||||
# figures being put as the active figure after they have been
|
|
||||||
# shown, even in non-interactive mode.
|
|
||||||
if hasattr(manager, '_cidgcf'):
|
|
||||||
manager.canvas.mpl_disconnect(manager._cidgcf)
|
|
||||||
|
|
||||||
if not interactive and manager in Gcf._activeQue:
|
|
||||||
Gcf._activeQue.remove(manager)
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from .backend_qt5 import (
|
|
||||||
backend_version, SPECIAL_KEYS, SUPER, ALT, CTRL, SHIFT, MODIFIER_KEYS,
|
|
||||||
cursord, _create_qApp, _BackendQT5, TimerQT, MainWindow, FigureManagerQT,
|
|
||||||
NavigationToolbar2QT, SubplotToolQt, error_msg_qt, exception_handler)
|
|
||||||
from .backend_qt5 import FigureCanvasQT as FigureCanvasQT5
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendQT5.export
|
|
||||||
class _BackendQT4(_BackendQT5):
|
|
||||||
required_interactive_framework = "qt4"
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
"""
|
|
||||||
Render to qt from agg
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .backend_qt5agg import (
|
|
||||||
_BackendQT5Agg, FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT)
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendQT5Agg.export
|
|
||||||
class _BackendQT4Agg(_BackendQT5Agg):
|
|
||||||
required_interactive_framework = "qt4"
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from .backend_qt5cairo import _BackendQT5Cairo
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendQT5Cairo.export
|
|
||||||
class _BackendQT4Cairo(_BackendQT5Cairo):
|
|
||||||
required_interactive_framework = "qt4"
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
"""
|
|
||||||
Render to qt from agg
|
|
||||||
"""
|
|
||||||
|
|
||||||
import ctypes
|
|
||||||
|
|
||||||
from matplotlib.transforms import Bbox
|
|
||||||
|
|
||||||
from .. import cbook
|
|
||||||
from .backend_agg import FigureCanvasAgg
|
|
||||||
from .backend_qt5 import (
|
|
||||||
QtCore, QtGui, QtWidgets, _BackendQT5, FigureCanvasQT, FigureManagerQT,
|
|
||||||
NavigationToolbar2QT, backend_version)
|
|
||||||
from .qt_compat import QT_API
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT):
|
|
||||||
|
|
||||||
def __init__(self, figure):
|
|
||||||
# Must pass 'figure' as kwarg to Qt base class.
|
|
||||||
super().__init__(figure=figure)
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
"""Copy the image from the Agg canvas to the qt.drawable.
|
|
||||||
|
|
||||||
In Qt, all drawing should be done inside of here when a widget is
|
|
||||||
shown onscreen.
|
|
||||||
"""
|
|
||||||
if self._update_dpi():
|
|
||||||
# The dpi update triggered its own paintEvent.
|
|
||||||
return
|
|
||||||
self._draw_idle() # Only does something if a draw is pending.
|
|
||||||
|
|
||||||
# if the canvas does not have a renderer, then give up and wait for
|
|
||||||
# FigureCanvasAgg.draw(self) to be called
|
|
||||||
if not hasattr(self, 'renderer'):
|
|
||||||
return
|
|
||||||
|
|
||||||
painter = QtGui.QPainter(self)
|
|
||||||
|
|
||||||
if self._erase_before_paint:
|
|
||||||
painter.eraseRect(self.rect())
|
|
||||||
self._erase_before_paint = False
|
|
||||||
|
|
||||||
rect = event.rect()
|
|
||||||
left = rect.left()
|
|
||||||
top = rect.top()
|
|
||||||
width = rect.width()
|
|
||||||
height = rect.height()
|
|
||||||
# See documentation of QRect: bottom() and right() are off by 1, so use
|
|
||||||
# left() + width() and top() + height().
|
|
||||||
bbox = Bbox(
|
|
||||||
[[left, self.renderer.height - (top + height * self._dpi_ratio)],
|
|
||||||
[left + width * self._dpi_ratio, self.renderer.height - top]])
|
|
||||||
reg = self.copy_from_bbox(bbox)
|
|
||||||
buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(
|
|
||||||
memoryview(reg))
|
|
||||||
qimage = QtGui.QImage(buf, buf.shape[1], buf.shape[0],
|
|
||||||
QtGui.QImage.Format_ARGB32_Premultiplied)
|
|
||||||
if hasattr(qimage, 'setDevicePixelRatio'):
|
|
||||||
# Not available on Qt4 or some older Qt5.
|
|
||||||
qimage.setDevicePixelRatio(self._dpi_ratio)
|
|
||||||
origin = QtCore.QPoint(left, top)
|
|
||||||
painter.drawImage(origin / self._dpi_ratio, qimage)
|
|
||||||
# Adjust the buf reference count to work around a memory
|
|
||||||
# leak bug in QImage under PySide on Python 3.
|
|
||||||
if QT_API in ('PySide', 'PySide2'):
|
|
||||||
ctypes.c_long.from_address(id(buf)).value = 1
|
|
||||||
|
|
||||||
self._draw_rect_callback(painter)
|
|
||||||
|
|
||||||
painter.end()
|
|
||||||
|
|
||||||
def blit(self, bbox=None):
|
|
||||||
"""Blit the region in bbox.
|
|
||||||
"""
|
|
||||||
# If bbox is None, blit the entire canvas. Otherwise
|
|
||||||
# blit only the area defined by the bbox.
|
|
||||||
if bbox is None and self.figure:
|
|
||||||
bbox = self.figure.bbox
|
|
||||||
|
|
||||||
# repaint uses logical pixels, not physical pixels like the renderer.
|
|
||||||
l, b, w, h = [pt / self._dpi_ratio for pt in bbox.bounds]
|
|
||||||
t = b + h
|
|
||||||
self.repaint(l, self.renderer.height / self._dpi_ratio - t, w, h)
|
|
||||||
|
|
||||||
def print_figure(self, *args, **kwargs):
|
|
||||||
super().print_figure(*args, **kwargs)
|
|
||||||
self.draw()
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendQT5.export
|
|
||||||
class _BackendQT5Agg(_BackendQT5):
|
|
||||||
FigureCanvas = FigureCanvasQTAgg
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import ctypes
|
|
||||||
|
|
||||||
from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo
|
|
||||||
from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT
|
|
||||||
from .qt_compat import QT_API
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasQTCairo(FigureCanvasQT, FigureCanvasCairo):
|
|
||||||
def __init__(self, figure):
|
|
||||||
super().__init__(figure=figure)
|
|
||||||
self._renderer = RendererCairo(self.figure.dpi)
|
|
||||||
self._renderer.set_width_height(-1, -1) # Invalid values.
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
if hasattr(self._renderer.gc, "ctx"):
|
|
||||||
self.figure.draw(self._renderer)
|
|
||||||
super().draw()
|
|
||||||
|
|
||||||
def paintEvent(self, event):
|
|
||||||
self._update_dpi()
|
|
||||||
dpi_ratio = self._dpi_ratio
|
|
||||||
width = dpi_ratio * self.width()
|
|
||||||
height = dpi_ratio * self.height()
|
|
||||||
if (width, height) != self._renderer.get_canvas_width_height():
|
|
||||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
|
||||||
self._renderer.set_ctx_from_surface(surface)
|
|
||||||
self._renderer.set_width_height(width, height)
|
|
||||||
self.figure.draw(self._renderer)
|
|
||||||
buf = self._renderer.gc.ctx.get_target().get_data()
|
|
||||||
qimage = QtGui.QImage(buf, width, height,
|
|
||||||
QtGui.QImage.Format_ARGB32_Premultiplied)
|
|
||||||
# Adjust the buf reference count to work around a memory leak bug in
|
|
||||||
# QImage under PySide on Python 3.
|
|
||||||
if QT_API == 'PySide':
|
|
||||||
ctypes.c_long.from_address(id(buf)).value = 1
|
|
||||||
if hasattr(qimage, 'setDevicePixelRatio'):
|
|
||||||
# Not available on Qt4 or some older Qt5.
|
|
||||||
qimage.setDevicePixelRatio(dpi_ratio)
|
|
||||||
painter = QtGui.QPainter(self)
|
|
||||||
if self._erase_before_paint:
|
|
||||||
painter.eraseRect(self.rect())
|
|
||||||
self._erase_before_paint = False
|
|
||||||
painter.drawImage(0, 0, qimage)
|
|
||||||
self._draw_rect_callback(painter)
|
|
||||||
painter.end()
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendQT5.export
|
|
||||||
class _BackendQT5Cairo(_BackendQT5):
|
|
||||||
FigureCanvas = FigureCanvasQTCairo
|
|
||||||
@@ -1,266 +0,0 @@
|
|||||||
"""
|
|
||||||
This is a fully functional do nothing backend to provide a template to
|
|
||||||
backend writers. It is fully functional in that you can select it as
|
|
||||||
a backend with
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
matplotlib.use('Template')
|
|
||||||
|
|
||||||
and your matplotlib scripts will (should!) run without error, though
|
|
||||||
no output is produced. This provides a nice starting point for
|
|
||||||
backend writers because you can selectively implement methods
|
|
||||||
(draw_rectangle, draw_lines, etc...) and slowly see your figure come
|
|
||||||
to life w/o having to have a full blown implementation before getting
|
|
||||||
any results.
|
|
||||||
|
|
||||||
Copy this to backend_xxx.py and replace all instances of 'template'
|
|
||||||
with 'xxx'. Then implement the class methods and functions below, and
|
|
||||||
add 'xxx' to the switchyard in matplotlib/backends/__init__.py and
|
|
||||||
'xxx' to the backends list in the validate_backend methon in
|
|
||||||
matplotlib/__init__.py and you're off. You can use your backend with::
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
matplotlib.use('xxx')
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
plt.plot([1,2,3])
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
matplotlib also supports external backends, so you can place you can
|
|
||||||
use any module in your PYTHONPATH with the syntax::
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
matplotlib.use('module://my_backend')
|
|
||||||
|
|
||||||
where my_backend.py is your module name. This syntax is also
|
|
||||||
recognized in the rc file and in the -d argument in pylab, e.g.,::
|
|
||||||
|
|
||||||
python simple_plot.py -dmodule://my_backend
|
|
||||||
|
|
||||||
If your backend implements support for saving figures (i.e. has a print_xyz()
|
|
||||||
method) you can register it as the default handler for a given file type
|
|
||||||
|
|
||||||
from matplotlib.backend_bases import register_backend
|
|
||||||
register_backend('xyz', 'my_backend', 'XYZ File Format')
|
|
||||||
...
|
|
||||||
plt.savefig("figure.xyz")
|
|
||||||
|
|
||||||
The files that are most relevant to backend_writers are
|
|
||||||
|
|
||||||
matplotlib/backends/backend_your_backend.py
|
|
||||||
matplotlib/backend_bases.py
|
|
||||||
matplotlib/backends/__init__.py
|
|
||||||
matplotlib/__init__.py
|
|
||||||
matplotlib/_pylab_helpers.py
|
|
||||||
|
|
||||||
Naming Conventions
|
|
||||||
|
|
||||||
* classes Upper or MixedUpperCase
|
|
||||||
|
|
||||||
* variables lower or lowerUpper
|
|
||||||
|
|
||||||
* functions lower or underscore_separated
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
from matplotlib._pylab_helpers import Gcf
|
|
||||||
from matplotlib.backend_bases import (
|
|
||||||
FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase)
|
|
||||||
from matplotlib.figure import Figure
|
|
||||||
|
|
||||||
|
|
||||||
class RendererTemplate(RendererBase):
|
|
||||||
"""
|
|
||||||
The renderer handles drawing/rendering operations.
|
|
||||||
|
|
||||||
This is a minimal do-nothing class that can be used to get started when
|
|
||||||
writing a new backend. Refer to backend_bases.RendererBase for
|
|
||||||
documentation of the classes methods.
|
|
||||||
"""
|
|
||||||
def __init__(self, dpi):
|
|
||||||
self.dpi = dpi
|
|
||||||
|
|
||||||
def draw_path(self, gc, path, transform, rgbFace=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# draw_markers is optional, and we get more correct relative
|
|
||||||
# timings by leaving it out. backend implementers concerned with
|
|
||||||
# performance will probably want to implement it
|
|
||||||
# def draw_markers(self, gc, marker_path, marker_trans, path, trans,
|
|
||||||
# rgbFace=None):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# draw_path_collection is optional, and we get more correct
|
|
||||||
# relative timings by leaving it out. backend implementers concerned with
|
|
||||||
# performance will probably want to implement it
|
|
||||||
# def draw_path_collection(self, gc, master_transform, paths,
|
|
||||||
# all_transforms, offsets, offsetTrans,
|
|
||||||
# facecolors, edgecolors, linewidths, linestyles,
|
|
||||||
# antialiaseds):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
# draw_quad_mesh is optional, and we get more correct
|
|
||||||
# relative timings by leaving it out. backend implementers concerned with
|
|
||||||
# performance will probably want to implement it
|
|
||||||
# def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
|
|
||||||
# coordinates, offsets, offsetTrans, facecolors,
|
|
||||||
# antialiased, edgecolors):
|
|
||||||
# pass
|
|
||||||
|
|
||||||
def draw_image(self, gc, x, y, im):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def flipy(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_canvas_width_height(self):
|
|
||||||
return 100, 100
|
|
||||||
|
|
||||||
def get_text_width_height_descent(self, s, prop, ismath):
|
|
||||||
return 1, 1, 1
|
|
||||||
|
|
||||||
def new_gc(self):
|
|
||||||
return GraphicsContextTemplate()
|
|
||||||
|
|
||||||
def points_to_pixels(self, points):
|
|
||||||
# if backend doesn't have dpi, e.g., postscript or svg
|
|
||||||
return points
|
|
||||||
# elif backend assumes a value for pixels_per_inch
|
|
||||||
#return points/72.0 * self.dpi.get() * pixels_per_inch/72.0
|
|
||||||
# else
|
|
||||||
#return points/72.0 * self.dpi.get()
|
|
||||||
|
|
||||||
|
|
||||||
class GraphicsContextTemplate(GraphicsContextBase):
|
|
||||||
"""
|
|
||||||
The graphics context provides the color, line styles, etc... See the cairo
|
|
||||||
and postscript backends for examples of mapping the graphics context
|
|
||||||
attributes (cap styles, join styles, line widths, colors) to a particular
|
|
||||||
backend. In cairo this is done by wrapping a cairo.Context object and
|
|
||||||
forwarding the appropriate calls to it using a dictionary mapping styles
|
|
||||||
to gdk constants. In Postscript, all the work is done by the renderer,
|
|
||||||
mapping line styles to postscript calls.
|
|
||||||
|
|
||||||
If it's more appropriate to do the mapping at the renderer level (as in
|
|
||||||
the postscript backend), you don't need to override any of the GC methods.
|
|
||||||
If it's more appropriate to wrap an instance (as in the cairo backend) and
|
|
||||||
do the mapping here, you'll need to override several of the setter
|
|
||||||
methods.
|
|
||||||
|
|
||||||
The base GraphicsContext stores colors as a RGB tuple on the unit
|
|
||||||
interval, e.g., (0.5, 0.0, 1.0). You may need to map this to colors
|
|
||||||
appropriate for your backend.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
#
|
|
||||||
# The following functions and classes are for pylab and implement
|
|
||||||
# window/figure managers, etc...
|
|
||||||
#
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
def draw_if_interactive():
|
|
||||||
"""
|
|
||||||
For image backends - is not required.
|
|
||||||
For GUI backends - this should be overridden if drawing should be done in
|
|
||||||
interactive python mode.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def show(block=None):
|
|
||||||
"""
|
|
||||||
For image backends - is not required.
|
|
||||||
For GUI backends - show() is usually the last line of a pyplot script and
|
|
||||||
tells the backend that it is time to draw. In interactive mode, this
|
|
||||||
should do nothing.
|
|
||||||
"""
|
|
||||||
for manager in Gcf.get_all_fig_managers():
|
|
||||||
# do something to display the GUI
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def new_figure_manager(num, *args, FigureClass=Figure, **kwargs):
|
|
||||||
"""
|
|
||||||
Create a new figure manager instance
|
|
||||||
"""
|
|
||||||
# If a main-level app must be created, this (and
|
|
||||||
# new_figure_manager_given_figure) is the usual place to do it -- see
|
|
||||||
# backend_wx, backend_wxagg and backend_tkagg for examples. Not all GUIs
|
|
||||||
# require explicit instantiation of a main-level app (e.g., backend_gtk3)
|
|
||||||
# for pylab.
|
|
||||||
thisFig = FigureClass(*args, **kwargs)
|
|
||||||
return new_figure_manager_given_figure(num, thisFig)
|
|
||||||
|
|
||||||
|
|
||||||
def new_figure_manager_given_figure(num, figure):
|
|
||||||
"""
|
|
||||||
Create a new figure manager instance for the given figure.
|
|
||||||
"""
|
|
||||||
canvas = FigureCanvasTemplate(figure)
|
|
||||||
manager = FigureManagerTemplate(canvas, num)
|
|
||||||
return manager
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasTemplate(FigureCanvasBase):
|
|
||||||
"""
|
|
||||||
The canvas the figure renders into. Calls the draw and print fig
|
|
||||||
methods, creates the renderers, etc.
|
|
||||||
|
|
||||||
Note: GUI templates will want to connect events for button presses,
|
|
||||||
mouse movements and key presses to functions that call the base
|
|
||||||
class methods button_press_event, button_release_event,
|
|
||||||
motion_notify_event, key_press_event, and key_release_event. See the
|
|
||||||
implementations of the interactive backends for examples.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
figure : `matplotlib.figure.Figure`
|
|
||||||
A high-level Figure instance
|
|
||||||
"""
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
"""
|
|
||||||
Draw the figure using the renderer
|
|
||||||
"""
|
|
||||||
renderer = RendererTemplate(self.figure.dpi)
|
|
||||||
self.figure.draw(renderer)
|
|
||||||
|
|
||||||
# You should provide a print_xxx function for every file format
|
|
||||||
# you can write.
|
|
||||||
|
|
||||||
# If the file type is not in the base set of filetypes,
|
|
||||||
# you should add it to the class-scope filetypes dictionary as follows:
|
|
||||||
filetypes = FigureCanvasBase.filetypes.copy()
|
|
||||||
filetypes['foo'] = 'My magic Foo format'
|
|
||||||
|
|
||||||
def print_foo(self, filename, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Write out format foo. The dpi, facecolor and edgecolor are restored
|
|
||||||
to their original values after this call, so you don't need to
|
|
||||||
save and restore them.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_default_filetype(self):
|
|
||||||
return 'foo'
|
|
||||||
|
|
||||||
|
|
||||||
class FigureManagerTemplate(FigureManagerBase):
|
|
||||||
"""
|
|
||||||
Wrap everything up into a window for the pylab interface
|
|
||||||
|
|
||||||
For non interactive backends, the base class does all the work
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
########################################################################
|
|
||||||
#
|
|
||||||
# Now just provide the standard names that backend.__init__ is expecting
|
|
||||||
#
|
|
||||||
########################################################################
|
|
||||||
|
|
||||||
FigureCanvas = FigureCanvasTemplate
|
|
||||||
FigureManager = FigureManagerTemplate
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from . import _backend_tk
|
|
||||||
from .backend_agg import FigureCanvasAgg
|
|
||||||
from ._backend_tk import (
|
|
||||||
_BackendTk, FigureCanvasTk, FigureManagerTk, NavigationToolbar2Tk)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasTkAgg(FigureCanvasAgg, FigureCanvasTk):
|
|
||||||
def draw(self):
|
|
||||||
super(FigureCanvasTkAgg, self).draw()
|
|
||||||
_backend_tk.blit(self._tkphoto, self.renderer._renderer, (0, 1, 2, 3))
|
|
||||||
self._master.update_idletasks()
|
|
||||||
|
|
||||||
def blit(self, bbox=None):
|
|
||||||
_backend_tk.blit(
|
|
||||||
self._tkphoto, self.renderer._renderer, (0, 1, 2, 3), bbox=bbox)
|
|
||||||
self._master.update_idletasks()
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendTk.export
|
|
||||||
class _BackendTkAgg(_BackendTk):
|
|
||||||
FigureCanvas = FigureCanvasTkAgg
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import sys
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from . import _backend_tk
|
|
||||||
from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo
|
|
||||||
from ._backend_tk import _BackendTk, FigureCanvasTk
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasTkCairo(FigureCanvasCairo, FigureCanvasTk):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(FigureCanvasTkCairo, self).__init__(*args, **kwargs)
|
|
||||||
self._renderer = RendererCairo(self.figure.dpi)
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
width = int(self.figure.bbox.width)
|
|
||||||
height = int(self.figure.bbox.height)
|
|
||||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
|
||||||
self._renderer.set_ctx_from_surface(surface)
|
|
||||||
self._renderer.set_width_height(width, height)
|
|
||||||
self.figure.draw(self._renderer)
|
|
||||||
buf = np.reshape(surface.get_data(), (height, width, 4))
|
|
||||||
_backend_tk.blit(
|
|
||||||
self._tkphoto, buf,
|
|
||||||
(2, 1, 0, 3) if sys.byteorder == "little" else (1, 2, 3, 0))
|
|
||||||
self._master.update_idletasks()
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendTk.export
|
|
||||||
class _BackendTkCairo(_BackendTk):
|
|
||||||
FigureCanvas = FigureCanvasTkCairo
|
|
||||||
@@ -1,335 +0,0 @@
|
|||||||
"""
|
|
||||||
Displays Agg images in the browser, with interactivity
|
|
||||||
"""
|
|
||||||
|
|
||||||
# The WebAgg backend is divided into two modules:
|
|
||||||
#
|
|
||||||
# - `backend_webagg_core.py` contains code necessary to embed a WebAgg
|
|
||||||
# plot inside of a web application, and communicate in an abstract
|
|
||||||
# way over a web socket.
|
|
||||||
#
|
|
||||||
# - `backend_webagg.py` contains a concrete implementation of a basic
|
|
||||||
# application, implemented with tornado.
|
|
||||||
|
|
||||||
from contextlib import contextmanager
|
|
||||||
import errno
|
|
||||||
from io import BytesIO
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import random
|
|
||||||
import sys
|
|
||||||
import signal
|
|
||||||
import socket
|
|
||||||
import threading
|
|
||||||
|
|
||||||
try:
|
|
||||||
import tornado
|
|
||||||
except ImportError:
|
|
||||||
raise RuntimeError("The WebAgg backend requires Tornado.")
|
|
||||||
|
|
||||||
import tornado.web
|
|
||||||
import tornado.ioloop
|
|
||||||
import tornado.websocket
|
|
||||||
|
|
||||||
from matplotlib import rcParams
|
|
||||||
from matplotlib.backend_bases import _Backend
|
|
||||||
from matplotlib._pylab_helpers import Gcf
|
|
||||||
from . import backend_webagg_core as core
|
|
||||||
from .backend_webagg_core import TimerTornado
|
|
||||||
|
|
||||||
|
|
||||||
class ServerThread(threading.Thread):
|
|
||||||
def run(self):
|
|
||||||
tornado.ioloop.IOLoop.instance().start()
|
|
||||||
|
|
||||||
|
|
||||||
webagg_server_thread = ServerThread()
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
|
|
||||||
def show(self):
|
|
||||||
# show the figure window
|
|
||||||
global show # placates pyflakes: created by @_Backend.export below
|
|
||||||
show()
|
|
||||||
|
|
||||||
def new_timer(self, *args, **kwargs):
|
|
||||||
return TimerTornado(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class WebAggApplication(tornado.web.Application):
|
|
||||||
initialized = False
|
|
||||||
started = False
|
|
||||||
|
|
||||||
class FavIcon(tornado.web.RequestHandler):
|
|
||||||
def get(self):
|
|
||||||
self.set_header('Content-Type', 'image/png')
|
|
||||||
image_path = Path(rcParams["datapath"], "images", "matplotlib.png")
|
|
||||||
self.write(image_path.read_bytes())
|
|
||||||
|
|
||||||
class SingleFigurePage(tornado.web.RequestHandler):
|
|
||||||
def __init__(self, application, request, *, url_prefix='', **kwargs):
|
|
||||||
self.url_prefix = url_prefix
|
|
||||||
super().__init__(application, request, **kwargs)
|
|
||||||
|
|
||||||
def get(self, fignum):
|
|
||||||
fignum = int(fignum)
|
|
||||||
manager = Gcf.get_fig_manager(fignum)
|
|
||||||
|
|
||||||
ws_uri = 'ws://{req.host}{prefix}/'.format(req=self.request,
|
|
||||||
prefix=self.url_prefix)
|
|
||||||
self.render(
|
|
||||||
"single_figure.html",
|
|
||||||
prefix=self.url_prefix,
|
|
||||||
ws_uri=ws_uri,
|
|
||||||
fig_id=fignum,
|
|
||||||
toolitems=core.NavigationToolbar2WebAgg.toolitems,
|
|
||||||
canvas=manager.canvas)
|
|
||||||
|
|
||||||
class AllFiguresPage(tornado.web.RequestHandler):
|
|
||||||
def __init__(self, application, request, *, url_prefix='', **kwargs):
|
|
||||||
self.url_prefix = url_prefix
|
|
||||||
super().__init__(application, request, **kwargs)
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
ws_uri = 'ws://{req.host}{prefix}/'.format(req=self.request,
|
|
||||||
prefix=self.url_prefix)
|
|
||||||
self.render(
|
|
||||||
"all_figures.html",
|
|
||||||
prefix=self.url_prefix,
|
|
||||||
ws_uri=ws_uri,
|
|
||||||
figures=sorted(Gcf.figs.items()),
|
|
||||||
toolitems=core.NavigationToolbar2WebAgg.toolitems)
|
|
||||||
|
|
||||||
class MplJs(tornado.web.RequestHandler):
|
|
||||||
def get(self):
|
|
||||||
self.set_header('Content-Type', 'application/javascript')
|
|
||||||
|
|
||||||
js_content = core.FigureManagerWebAgg.get_javascript()
|
|
||||||
|
|
||||||
self.write(js_content)
|
|
||||||
|
|
||||||
class Download(tornado.web.RequestHandler):
|
|
||||||
def get(self, fignum, fmt):
|
|
||||||
fignum = int(fignum)
|
|
||||||
manager = Gcf.get_fig_manager(fignum)
|
|
||||||
|
|
||||||
# TODO: Move this to a central location
|
|
||||||
mimetypes = {
|
|
||||||
'ps': 'application/postscript',
|
|
||||||
'eps': 'application/postscript',
|
|
||||||
'pdf': 'application/pdf',
|
|
||||||
'svg': 'image/svg+xml',
|
|
||||||
'png': 'image/png',
|
|
||||||
'jpeg': 'image/jpeg',
|
|
||||||
'tif': 'image/tiff',
|
|
||||||
'emf': 'application/emf'
|
|
||||||
}
|
|
||||||
|
|
||||||
self.set_header('Content-Type', mimetypes.get(fmt, 'binary'))
|
|
||||||
|
|
||||||
buff = BytesIO()
|
|
||||||
manager.canvas.figure.savefig(buff, format=fmt)
|
|
||||||
self.write(buff.getvalue())
|
|
||||||
|
|
||||||
class WebSocket(tornado.websocket.WebSocketHandler):
|
|
||||||
supports_binary = True
|
|
||||||
|
|
||||||
def open(self, fignum):
|
|
||||||
self.fignum = int(fignum)
|
|
||||||
self.manager = Gcf.get_fig_manager(self.fignum)
|
|
||||||
self.manager.add_web_socket(self)
|
|
||||||
if hasattr(self, 'set_nodelay'):
|
|
||||||
self.set_nodelay(True)
|
|
||||||
|
|
||||||
def on_close(self):
|
|
||||||
self.manager.remove_web_socket(self)
|
|
||||||
|
|
||||||
def on_message(self, message):
|
|
||||||
message = json.loads(message)
|
|
||||||
# The 'supports_binary' message is on a client-by-client
|
|
||||||
# basis. The others affect the (shared) canvas as a
|
|
||||||
# whole.
|
|
||||||
if message['type'] == 'supports_binary':
|
|
||||||
self.supports_binary = message['value']
|
|
||||||
else:
|
|
||||||
manager = Gcf.get_fig_manager(self.fignum)
|
|
||||||
# It is possible for a figure to be closed,
|
|
||||||
# but a stale figure UI is still sending messages
|
|
||||||
# from the browser.
|
|
||||||
if manager is not None:
|
|
||||||
manager.handle_json(message)
|
|
||||||
|
|
||||||
def send_json(self, content):
|
|
||||||
self.write_message(json.dumps(content))
|
|
||||||
|
|
||||||
def send_binary(self, blob):
|
|
||||||
if self.supports_binary:
|
|
||||||
self.write_message(blob, binary=True)
|
|
||||||
else:
|
|
||||||
data_uri = "data:image/png;base64,{0}".format(
|
|
||||||
blob.encode('base64').replace('\n', ''))
|
|
||||||
self.write_message(data_uri)
|
|
||||||
|
|
||||||
def __init__(self, url_prefix=''):
|
|
||||||
if url_prefix:
|
|
||||||
assert url_prefix[0] == '/' and url_prefix[-1] != '/', \
|
|
||||||
'url_prefix must start with a "/" and not end with one.'
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
[
|
|
||||||
# Static files for the CSS and JS
|
|
||||||
(url_prefix + r'/_static/(.*)',
|
|
||||||
tornado.web.StaticFileHandler,
|
|
||||||
{'path': core.FigureManagerWebAgg.get_static_file_path()}),
|
|
||||||
|
|
||||||
# An MPL favicon
|
|
||||||
(url_prefix + r'/favicon.ico', self.FavIcon),
|
|
||||||
|
|
||||||
# The page that contains all of the pieces
|
|
||||||
(url_prefix + r'/([0-9]+)', self.SingleFigurePage,
|
|
||||||
{'url_prefix': url_prefix}),
|
|
||||||
|
|
||||||
# The page that contains all of the figures
|
|
||||||
(url_prefix + r'/?', self.AllFiguresPage,
|
|
||||||
{'url_prefix': url_prefix}),
|
|
||||||
|
|
||||||
(url_prefix + r'/js/mpl.js', self.MplJs),
|
|
||||||
|
|
||||||
# Sends images and events to the browser, and receives
|
|
||||||
# events from the browser
|
|
||||||
(url_prefix + r'/([0-9]+)/ws', self.WebSocket),
|
|
||||||
|
|
||||||
# Handles the downloading (i.e., saving) of static images
|
|
||||||
(url_prefix + r'/([0-9]+)/download.([a-z0-9.]+)',
|
|
||||||
self.Download),
|
|
||||||
],
|
|
||||||
template_path=core.FigureManagerWebAgg.get_static_file_path())
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def initialize(cls, url_prefix='', port=None, address=None):
|
|
||||||
if cls.initialized:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create the class instance
|
|
||||||
app = cls(url_prefix=url_prefix)
|
|
||||||
|
|
||||||
cls.url_prefix = url_prefix
|
|
||||||
|
|
||||||
# This port selection algorithm is borrowed, more or less
|
|
||||||
# verbatim, from IPython.
|
|
||||||
def random_ports(port, n):
|
|
||||||
"""
|
|
||||||
Generate a list of n random ports near the given port.
|
|
||||||
|
|
||||||
The first 5 ports will be sequential, and the remaining n-5 will be
|
|
||||||
randomly selected in the range [port-2*n, port+2*n].
|
|
||||||
"""
|
|
||||||
for i in range(min(5, n)):
|
|
||||||
yield port + i
|
|
||||||
for i in range(n - 5):
|
|
||||||
yield port + random.randint(-2 * n, 2 * n)
|
|
||||||
|
|
||||||
if address is None:
|
|
||||||
cls.address = rcParams['webagg.address']
|
|
||||||
else:
|
|
||||||
cls.address = address
|
|
||||||
cls.port = rcParams['webagg.port']
|
|
||||||
for port in random_ports(cls.port, rcParams['webagg.port_retries']):
|
|
||||||
try:
|
|
||||||
app.listen(port, cls.address)
|
|
||||||
except socket.error as e:
|
|
||||||
if e.errno != errno.EADDRINUSE:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
cls.port = port
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise SystemExit(
|
|
||||||
"The webagg server could not be started because an available "
|
|
||||||
"port could not be found")
|
|
||||||
|
|
||||||
cls.initialized = True
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def start(cls):
|
|
||||||
if cls.started:
|
|
||||||
return
|
|
||||||
|
|
||||||
"""
|
|
||||||
IOLoop.running() was removed as of Tornado 2.4; see for example
|
|
||||||
https://groups.google.com/forum/#!topic/python-tornado/QLMzkpQBGOY
|
|
||||||
Thus there is no correct way to check if the loop has already been
|
|
||||||
launched. We may end up with two concurrently running loops in that
|
|
||||||
unlucky case with all the expected consequences.
|
|
||||||
"""
|
|
||||||
ioloop = tornado.ioloop.IOLoop.instance()
|
|
||||||
|
|
||||||
def shutdown():
|
|
||||||
ioloop.stop()
|
|
||||||
print("Server is stopped")
|
|
||||||
sys.stdout.flush()
|
|
||||||
cls.started = False
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def catch_sigint():
|
|
||||||
old_handler = signal.signal(
|
|
||||||
signal.SIGINT,
|
|
||||||
lambda sig, frame: ioloop.add_callback_from_signal(shutdown))
|
|
||||||
try:
|
|
||||||
yield
|
|
||||||
finally:
|
|
||||||
signal.signal(signal.SIGINT, old_handler)
|
|
||||||
|
|
||||||
# Set the flag to True *before* blocking on ioloop.start()
|
|
||||||
cls.started = True
|
|
||||||
|
|
||||||
print("Press Ctrl+C to stop WebAgg server")
|
|
||||||
sys.stdout.flush()
|
|
||||||
with catch_sigint():
|
|
||||||
ioloop.start()
|
|
||||||
|
|
||||||
|
|
||||||
def ipython_inline_display(figure):
|
|
||||||
import tornado.template
|
|
||||||
|
|
||||||
WebAggApplication.initialize()
|
|
||||||
if not webagg_server_thread.is_alive():
|
|
||||||
webagg_server_thread.start()
|
|
||||||
|
|
||||||
fignum = figure.number
|
|
||||||
tpl = Path(core.FigureManagerWebAgg.get_static_file_path(),
|
|
||||||
"ipython_inline_figure.html").read_text()
|
|
||||||
t = tornado.template.Template(tpl)
|
|
||||||
return t.generate(
|
|
||||||
prefix=WebAggApplication.url_prefix,
|
|
||||||
fig_id=fignum,
|
|
||||||
toolitems=core.NavigationToolbar2WebAgg.toolitems,
|
|
||||||
canvas=figure.canvas,
|
|
||||||
port=WebAggApplication.port).decode('utf-8')
|
|
||||||
|
|
||||||
|
|
||||||
@_Backend.export
|
|
||||||
class _BackendWebAgg(_Backend):
|
|
||||||
FigureCanvas = FigureCanvasWebAgg
|
|
||||||
FigureManager = core.FigureManagerWebAgg
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def trigger_manager_draw(manager):
|
|
||||||
manager.canvas.draw_idle()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def show():
|
|
||||||
WebAggApplication.initialize()
|
|
||||||
|
|
||||||
url = "http://127.0.0.1:{port}{prefix}".format(
|
|
||||||
port=WebAggApplication.port,
|
|
||||||
prefix=WebAggApplication.url_prefix)
|
|
||||||
|
|
||||||
if rcParams['webagg.open_in_browser']:
|
|
||||||
import webbrowser
|
|
||||||
webbrowser.open(url)
|
|
||||||
else:
|
|
||||||
print("To view figure, visit {0}".format(url))
|
|
||||||
|
|
||||||
WebAggApplication.start()
|
|
||||||
@@ -1,528 +0,0 @@
|
|||||||
"""
|
|
||||||
Displays Agg images in the browser, with interactivity
|
|
||||||
"""
|
|
||||||
# The WebAgg backend is divided into two modules:
|
|
||||||
#
|
|
||||||
# - `backend_webagg_core.py` contains code necessary to embed a WebAgg
|
|
||||||
# plot inside of a web application, and communicate in an abstract
|
|
||||||
# way over a web socket.
|
|
||||||
#
|
|
||||||
# - `backend_webagg.py` contains a concrete implementation of a basic
|
|
||||||
# application, implemented with tornado.
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
from io import StringIO
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
import tornado
|
|
||||||
|
|
||||||
from matplotlib.backends import backend_agg
|
|
||||||
from matplotlib.backend_bases import _Backend
|
|
||||||
from matplotlib import backend_bases, _png
|
|
||||||
|
|
||||||
|
|
||||||
# http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
|
|
||||||
_SHIFT_LUT = {59: ':',
|
|
||||||
61: '+',
|
|
||||||
173: '_',
|
|
||||||
186: ':',
|
|
||||||
187: '+',
|
|
||||||
188: '<',
|
|
||||||
189: '_',
|
|
||||||
190: '>',
|
|
||||||
191: '?',
|
|
||||||
192: '~',
|
|
||||||
219: '{',
|
|
||||||
220: '|',
|
|
||||||
221: '}',
|
|
||||||
222: '"'}
|
|
||||||
|
|
||||||
_LUT = {8: 'backspace',
|
|
||||||
9: 'tab',
|
|
||||||
13: 'enter',
|
|
||||||
16: 'shift',
|
|
||||||
17: 'control',
|
|
||||||
18: 'alt',
|
|
||||||
19: 'pause',
|
|
||||||
20: 'caps',
|
|
||||||
27: 'escape',
|
|
||||||
32: ' ',
|
|
||||||
33: 'pageup',
|
|
||||||
34: 'pagedown',
|
|
||||||
35: 'end',
|
|
||||||
36: 'home',
|
|
||||||
37: 'left',
|
|
||||||
38: 'up',
|
|
||||||
39: 'right',
|
|
||||||
40: 'down',
|
|
||||||
45: 'insert',
|
|
||||||
46: 'delete',
|
|
||||||
91: 'super',
|
|
||||||
92: 'super',
|
|
||||||
93: 'select',
|
|
||||||
106: '*',
|
|
||||||
107: '+',
|
|
||||||
109: '-',
|
|
||||||
110: '.',
|
|
||||||
111: '/',
|
|
||||||
144: 'num_lock',
|
|
||||||
145: 'scroll_lock',
|
|
||||||
186: ':',
|
|
||||||
187: '=',
|
|
||||||
188: ',',
|
|
||||||
189: '-',
|
|
||||||
190: '.',
|
|
||||||
191: '/',
|
|
||||||
192: '`',
|
|
||||||
219: '[',
|
|
||||||
220: '\\',
|
|
||||||
221: ']',
|
|
||||||
222: "'"}
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_key(key):
|
|
||||||
"""Handle key codes"""
|
|
||||||
code = int(key[key.index('k') + 1:])
|
|
||||||
value = chr(code)
|
|
||||||
# letter keys
|
|
||||||
if 65 <= code <= 90:
|
|
||||||
if 'shift+' in key:
|
|
||||||
key = key.replace('shift+', '')
|
|
||||||
else:
|
|
||||||
value = value.lower()
|
|
||||||
# number keys
|
|
||||||
elif 48 <= code <= 57:
|
|
||||||
if 'shift+' in key:
|
|
||||||
value = ')!@#$%^&*('[int(value)]
|
|
||||||
key = key.replace('shift+', '')
|
|
||||||
# function keys
|
|
||||||
elif 112 <= code <= 123:
|
|
||||||
value = 'f%s' % (code - 111)
|
|
||||||
# number pad keys
|
|
||||||
elif 96 <= code <= 105:
|
|
||||||
value = '%s' % (code - 96)
|
|
||||||
# keys with shift alternatives
|
|
||||||
elif code in _SHIFT_LUT and 'shift+' in key:
|
|
||||||
key = key.replace('shift+', '')
|
|
||||||
value = _SHIFT_LUT[code]
|
|
||||||
elif code in _LUT:
|
|
||||||
value = _LUT[code]
|
|
||||||
key = key[:key.index('k')] + value
|
|
||||||
return key
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasWebAggCore(backend_agg.FigureCanvasAgg):
|
|
||||||
supports_blit = False
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
backend_agg.FigureCanvasAgg.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
# Set to True when the renderer contains data that is newer
|
|
||||||
# than the PNG buffer.
|
|
||||||
self._png_is_old = True
|
|
||||||
|
|
||||||
# Set to True by the `refresh` message so that the next frame
|
|
||||||
# sent to the clients will be a full frame.
|
|
||||||
self._force_full = True
|
|
||||||
|
|
||||||
# Store the current image mode so that at any point, clients can
|
|
||||||
# request the information. This should be changed by calling
|
|
||||||
# self.set_image_mode(mode) so that the notification can be given
|
|
||||||
# to the connected clients.
|
|
||||||
self._current_image_mode = 'full'
|
|
||||||
|
|
||||||
# Store the DPI ratio of the browser. This is the scaling that
|
|
||||||
# occurs automatically for all images on a HiDPI display.
|
|
||||||
self._dpi_ratio = 1
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
# show the figure window
|
|
||||||
from matplotlib.pyplot import show
|
|
||||||
show()
|
|
||||||
|
|
||||||
def draw(self):
|
|
||||||
self._png_is_old = True
|
|
||||||
try:
|
|
||||||
super().draw()
|
|
||||||
finally:
|
|
||||||
self.manager.refresh_all() # Swap the frames.
|
|
||||||
|
|
||||||
def draw_idle(self):
|
|
||||||
self.send_event("draw")
|
|
||||||
|
|
||||||
def set_image_mode(self, mode):
|
|
||||||
"""
|
|
||||||
Set the image mode for any subsequent images which will be sent
|
|
||||||
to the clients. The modes may currently be either 'full' or 'diff'.
|
|
||||||
|
|
||||||
Note: diff images may not contain transparency, therefore upon
|
|
||||||
draw this mode may be changed if the resulting image has any
|
|
||||||
transparent component.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if mode not in ['full', 'diff']:
|
|
||||||
raise ValueError('image mode must be either full or diff.')
|
|
||||||
if self._current_image_mode != mode:
|
|
||||||
self._current_image_mode = mode
|
|
||||||
self.handle_send_image_mode(None)
|
|
||||||
|
|
||||||
def get_diff_image(self):
|
|
||||||
if self._png_is_old:
|
|
||||||
renderer = self.get_renderer()
|
|
||||||
|
|
||||||
# The buffer is created as type uint32 so that entire
|
|
||||||
# pixels can be compared in one numpy call, rather than
|
|
||||||
# needing to compare each plane separately.
|
|
||||||
buff = (np.frombuffer(renderer.buffer_rgba(), dtype=np.uint32)
|
|
||||||
.reshape((renderer.height, renderer.width)))
|
|
||||||
|
|
||||||
# If any pixels have transparency, we need to force a full
|
|
||||||
# draw as we cannot overlay new on top of old.
|
|
||||||
pixels = buff.view(dtype=np.uint8).reshape(buff.shape + (4,))
|
|
||||||
|
|
||||||
if self._force_full or np.any(pixels[:, :, 3] != 255):
|
|
||||||
self.set_image_mode('full')
|
|
||||||
output = buff
|
|
||||||
else:
|
|
||||||
self.set_image_mode('diff')
|
|
||||||
last_buffer = (np.frombuffer(self._last_renderer.buffer_rgba(),
|
|
||||||
dtype=np.uint32)
|
|
||||||
.reshape((renderer.height, renderer.width)))
|
|
||||||
diff = buff != last_buffer
|
|
||||||
output = np.where(diff, buff, 0)
|
|
||||||
|
|
||||||
# TODO: We should write a new version of write_png that
|
|
||||||
# handles the differencing inline
|
|
||||||
buff = _png.write_png(
|
|
||||||
output.view(dtype=np.uint8).reshape(output.shape + (4,)),
|
|
||||||
None, compression=6, filter=_png.PNG_FILTER_NONE)
|
|
||||||
|
|
||||||
# Swap the renderer frames
|
|
||||||
self._renderer, self._last_renderer = (
|
|
||||||
self._last_renderer, renderer)
|
|
||||||
self._force_full = False
|
|
||||||
self._png_is_old = False
|
|
||||||
return buff
|
|
||||||
|
|
||||||
def get_renderer(self, cleared=None):
|
|
||||||
# Mirrors super.get_renderer, but caches the old one
|
|
||||||
# so that we can do things such as produce a diff image
|
|
||||||
# in get_diff_image
|
|
||||||
_, _, w, h = self.figure.bbox.bounds
|
|
||||||
w, h = int(w), int(h)
|
|
||||||
key = w, h, self.figure.dpi
|
|
||||||
try:
|
|
||||||
self._lastKey, self._renderer
|
|
||||||
except AttributeError:
|
|
||||||
need_new_renderer = True
|
|
||||||
else:
|
|
||||||
need_new_renderer = (self._lastKey != key)
|
|
||||||
|
|
||||||
if need_new_renderer:
|
|
||||||
self._renderer = backend_agg.RendererAgg(
|
|
||||||
w, h, self.figure.dpi)
|
|
||||||
self._last_renderer = backend_agg.RendererAgg(
|
|
||||||
w, h, self.figure.dpi)
|
|
||||||
self._lastKey = key
|
|
||||||
|
|
||||||
elif cleared:
|
|
||||||
self._renderer.clear()
|
|
||||||
|
|
||||||
return self._renderer
|
|
||||||
|
|
||||||
def handle_event(self, event):
|
|
||||||
e_type = event['type']
|
|
||||||
handler = getattr(self, 'handle_{0}'.format(e_type),
|
|
||||||
self.handle_unknown_event)
|
|
||||||
return handler(event)
|
|
||||||
|
|
||||||
def handle_unknown_event(self, event):
|
|
||||||
warnings.warn('Unhandled message type {0}. {1}'.format(
|
|
||||||
event['type'], event), stacklevel=2)
|
|
||||||
|
|
||||||
def handle_ack(self, event):
|
|
||||||
# Network latency tends to decrease if traffic is flowing
|
|
||||||
# in both directions. Therefore, the browser sends back
|
|
||||||
# an "ack" message after each image frame is received.
|
|
||||||
# This could also be used as a simple sanity check in the
|
|
||||||
# future, but for now the performance increase is enough
|
|
||||||
# to justify it, even if the server does nothing with it.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def handle_draw(self, event):
|
|
||||||
self.draw()
|
|
||||||
|
|
||||||
def _handle_mouse(self, event):
|
|
||||||
x = event['x']
|
|
||||||
y = event['y']
|
|
||||||
y = self.get_renderer().height - y
|
|
||||||
|
|
||||||
# Javascript button numbers and matplotlib button numbers are
|
|
||||||
# off by 1
|
|
||||||
button = event['button'] + 1
|
|
||||||
|
|
||||||
# The right mouse button pops up a context menu, which
|
|
||||||
# doesn't work very well, so use the middle mouse button
|
|
||||||
# instead. It doesn't seem that it's possible to disable
|
|
||||||
# the context menu in recent versions of Chrome. If this
|
|
||||||
# is resolved, please also adjust the docstring in MouseEvent.
|
|
||||||
if button == 2:
|
|
||||||
button = 3
|
|
||||||
|
|
||||||
e_type = event['type']
|
|
||||||
guiEvent = event.get('guiEvent', None)
|
|
||||||
if e_type == 'button_press':
|
|
||||||
self.button_press_event(x, y, button, guiEvent=guiEvent)
|
|
||||||
elif e_type == 'button_release':
|
|
||||||
self.button_release_event(x, y, button, guiEvent=guiEvent)
|
|
||||||
elif e_type == 'motion_notify':
|
|
||||||
self.motion_notify_event(x, y, guiEvent=guiEvent)
|
|
||||||
elif e_type == 'figure_enter':
|
|
||||||
self.enter_notify_event(xy=(x, y), guiEvent=guiEvent)
|
|
||||||
elif e_type == 'figure_leave':
|
|
||||||
self.leave_notify_event()
|
|
||||||
elif e_type == 'scroll':
|
|
||||||
self.scroll_event(x, y, event['step'], guiEvent=guiEvent)
|
|
||||||
handle_button_press = handle_button_release = handle_motion_notify = \
|
|
||||||
handle_figure_enter = handle_figure_leave = handle_scroll = \
|
|
||||||
_handle_mouse
|
|
||||||
|
|
||||||
def _handle_key(self, event):
|
|
||||||
key = _handle_key(event['key'])
|
|
||||||
e_type = event['type']
|
|
||||||
guiEvent = event.get('guiEvent', None)
|
|
||||||
if e_type == 'key_press':
|
|
||||||
self.key_press_event(key, guiEvent=guiEvent)
|
|
||||||
elif e_type == 'key_release':
|
|
||||||
self.key_release_event(key, guiEvent=guiEvent)
|
|
||||||
handle_key_press = handle_key_release = _handle_key
|
|
||||||
|
|
||||||
def handle_toolbar_button(self, event):
|
|
||||||
# TODO: Be more suspicious of the input
|
|
||||||
getattr(self.toolbar, event['name'])()
|
|
||||||
|
|
||||||
def handle_refresh(self, event):
|
|
||||||
figure_label = self.figure.get_label()
|
|
||||||
if not figure_label:
|
|
||||||
figure_label = "Figure {0}".format(self.manager.num)
|
|
||||||
self.send_event('figure_label', label=figure_label)
|
|
||||||
self._force_full = True
|
|
||||||
self.draw_idle()
|
|
||||||
|
|
||||||
def handle_resize(self, event):
|
|
||||||
x, y = event.get('width', 800), event.get('height', 800)
|
|
||||||
x, y = int(x) * self._dpi_ratio, int(y) * self._dpi_ratio
|
|
||||||
fig = self.figure
|
|
||||||
# An attempt at approximating the figure size in pixels.
|
|
||||||
fig.set_size_inches(x / fig.dpi, y / fig.dpi, forward=False)
|
|
||||||
|
|
||||||
_, _, w, h = self.figure.bbox.bounds
|
|
||||||
# Acknowledge the resize, and force the viewer to update the
|
|
||||||
# canvas size to the figure's new size (which is hopefully
|
|
||||||
# identical or within a pixel or so).
|
|
||||||
self._png_is_old = True
|
|
||||||
self.manager.resize(w, h)
|
|
||||||
self.resize_event()
|
|
||||||
|
|
||||||
def handle_send_image_mode(self, event):
|
|
||||||
# The client requests notification of what the current image mode is.
|
|
||||||
self.send_event('image_mode', mode=self._current_image_mode)
|
|
||||||
|
|
||||||
def handle_set_dpi_ratio(self, event):
|
|
||||||
dpi_ratio = event.get('dpi_ratio', 1)
|
|
||||||
if dpi_ratio != self._dpi_ratio:
|
|
||||||
# We don't want to scale up the figure dpi more than once.
|
|
||||||
if not hasattr(self.figure, '_original_dpi'):
|
|
||||||
self.figure._original_dpi = self.figure.dpi
|
|
||||||
self.figure.dpi = dpi_ratio * self.figure._original_dpi
|
|
||||||
self._dpi_ratio = dpi_ratio
|
|
||||||
self._force_full = True
|
|
||||||
self.draw_idle()
|
|
||||||
|
|
||||||
def send_event(self, event_type, **kwargs):
|
|
||||||
self.manager._send_event(event_type, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
_JQUERY_ICON_CLASSES = {
|
|
||||||
'home': 'ui-icon ui-icon-home',
|
|
||||||
'back': 'ui-icon ui-icon-circle-arrow-w',
|
|
||||||
'forward': 'ui-icon ui-icon-circle-arrow-e',
|
|
||||||
'zoom_to_rect': 'ui-icon ui-icon-search',
|
|
||||||
'move': 'ui-icon ui-icon-arrow-4',
|
|
||||||
'download': 'ui-icon ui-icon-disk',
|
|
||||||
None: None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
|
|
||||||
|
|
||||||
# Use the standard toolbar items + download button
|
|
||||||
toolitems = [(text, tooltip_text, _JQUERY_ICON_CLASSES[image_file],
|
|
||||||
name_of_method)
|
|
||||||
for text, tooltip_text, image_file, name_of_method
|
|
||||||
in (backend_bases.NavigationToolbar2.toolitems +
|
|
||||||
(('Download', 'Download plot', 'download', 'download'),))
|
|
||||||
if image_file in _JQUERY_ICON_CLASSES]
|
|
||||||
|
|
||||||
def _init_toolbar(self):
|
|
||||||
self.message = ''
|
|
||||||
self.cursor = 0
|
|
||||||
|
|
||||||
def set_message(self, message):
|
|
||||||
if message != self.message:
|
|
||||||
self.canvas.send_event("message", message=message)
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def set_cursor(self, cursor):
|
|
||||||
if cursor != self.cursor:
|
|
||||||
self.canvas.send_event("cursor", cursor=cursor)
|
|
||||||
self.cursor = cursor
|
|
||||||
|
|
||||||
def draw_rubberband(self, event, x0, y0, x1, y1):
|
|
||||||
self.canvas.send_event(
|
|
||||||
"rubberband", x0=x0, y0=y0, x1=x1, y1=y1)
|
|
||||||
|
|
||||||
def release_zoom(self, event):
|
|
||||||
backend_bases.NavigationToolbar2.release_zoom(self, event)
|
|
||||||
self.canvas.send_event(
|
|
||||||
"rubberband", x0=-1, y0=-1, x1=-1, y1=-1)
|
|
||||||
|
|
||||||
def save_figure(self, *args):
|
|
||||||
"""Save the current figure"""
|
|
||||||
self.canvas.send_event('save')
|
|
||||||
|
|
||||||
|
|
||||||
class FigureManagerWebAgg(backend_bases.FigureManagerBase):
|
|
||||||
ToolbarCls = NavigationToolbar2WebAgg
|
|
||||||
|
|
||||||
def __init__(self, canvas, num):
|
|
||||||
backend_bases.FigureManagerBase.__init__(self, canvas, num)
|
|
||||||
|
|
||||||
self.web_sockets = set()
|
|
||||||
|
|
||||||
self.toolbar = self._get_toolbar(canvas)
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _get_toolbar(self, canvas):
|
|
||||||
toolbar = self.ToolbarCls(canvas)
|
|
||||||
return toolbar
|
|
||||||
|
|
||||||
def resize(self, w, h):
|
|
||||||
self._send_event(
|
|
||||||
'resize',
|
|
||||||
size=(w / self.canvas._dpi_ratio, h / self.canvas._dpi_ratio))
|
|
||||||
|
|
||||||
def set_window_title(self, title):
|
|
||||||
self._send_event('figure_label', label=title)
|
|
||||||
|
|
||||||
# The following methods are specific to FigureManagerWebAgg
|
|
||||||
|
|
||||||
def add_web_socket(self, web_socket):
|
|
||||||
assert hasattr(web_socket, 'send_binary')
|
|
||||||
assert hasattr(web_socket, 'send_json')
|
|
||||||
|
|
||||||
self.web_sockets.add(web_socket)
|
|
||||||
|
|
||||||
_, _, w, h = self.canvas.figure.bbox.bounds
|
|
||||||
self.resize(w, h)
|
|
||||||
self._send_event('refresh')
|
|
||||||
|
|
||||||
def remove_web_socket(self, web_socket):
|
|
||||||
self.web_sockets.remove(web_socket)
|
|
||||||
|
|
||||||
def handle_json(self, content):
|
|
||||||
self.canvas.handle_event(content)
|
|
||||||
|
|
||||||
def refresh_all(self):
|
|
||||||
if self.web_sockets:
|
|
||||||
diff = self.canvas.get_diff_image()
|
|
||||||
if diff is not None:
|
|
||||||
for s in self.web_sockets:
|
|
||||||
s.send_binary(diff)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_javascript(cls, stream=None):
|
|
||||||
if stream is None:
|
|
||||||
output = StringIO()
|
|
||||||
else:
|
|
||||||
output = stream
|
|
||||||
|
|
||||||
output.write((Path(__file__).parent / "web_backend/js/mpl.js")
|
|
||||||
.read_text(encoding="utf-8"))
|
|
||||||
|
|
||||||
toolitems = []
|
|
||||||
for name, tooltip, image, method in cls.ToolbarCls.toolitems:
|
|
||||||
if name is None:
|
|
||||||
toolitems.append(['', '', '', ''])
|
|
||||||
else:
|
|
||||||
toolitems.append([name, tooltip, image, method])
|
|
||||||
output.write("mpl.toolbar_items = {0};\n\n".format(
|
|
||||||
json.dumps(toolitems)))
|
|
||||||
|
|
||||||
extensions = []
|
|
||||||
for filetype, ext in sorted(FigureCanvasWebAggCore.
|
|
||||||
get_supported_filetypes_grouped().
|
|
||||||
items()):
|
|
||||||
if not ext[0] == 'pgf': # pgf does not support BytesIO
|
|
||||||
extensions.append(ext[0])
|
|
||||||
output.write("mpl.extensions = {0};\n\n".format(
|
|
||||||
json.dumps(extensions)))
|
|
||||||
|
|
||||||
output.write("mpl.default_extension = {0};".format(
|
|
||||||
json.dumps(FigureCanvasWebAggCore.get_default_filetype())))
|
|
||||||
|
|
||||||
if stream is None:
|
|
||||||
return output.getvalue()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_static_file_path(cls):
|
|
||||||
return os.path.join(os.path.dirname(__file__), 'web_backend')
|
|
||||||
|
|
||||||
def _send_event(self, event_type, **kwargs):
|
|
||||||
payload = {'type': event_type, **kwargs}
|
|
||||||
for s in self.web_sockets:
|
|
||||||
s.send_json(payload)
|
|
||||||
|
|
||||||
|
|
||||||
class TimerTornado(backend_bases.TimerBase):
|
|
||||||
def _timer_start(self):
|
|
||||||
self._timer_stop()
|
|
||||||
if self._single:
|
|
||||||
ioloop = tornado.ioloop.IOLoop.instance()
|
|
||||||
self._timer = ioloop.add_timeout(
|
|
||||||
datetime.timedelta(milliseconds=self.interval),
|
|
||||||
self._on_timer)
|
|
||||||
else:
|
|
||||||
self._timer = tornado.ioloop.PeriodicCallback(
|
|
||||||
self._on_timer,
|
|
||||||
self.interval)
|
|
||||||
self._timer.start()
|
|
||||||
|
|
||||||
def _timer_stop(self):
|
|
||||||
if self._timer is None:
|
|
||||||
return
|
|
||||||
elif self._single:
|
|
||||||
ioloop = tornado.ioloop.IOLoop.instance()
|
|
||||||
ioloop.remove_timeout(self._timer)
|
|
||||||
else:
|
|
||||||
self._timer.stop()
|
|
||||||
|
|
||||||
self._timer = None
|
|
||||||
|
|
||||||
def _timer_set_interval(self):
|
|
||||||
# Only stop and restart it if the timer has already been started
|
|
||||||
if self._timer is not None:
|
|
||||||
self._timer_stop()
|
|
||||||
self._timer_start()
|
|
||||||
|
|
||||||
|
|
||||||
@_Backend.export
|
|
||||||
class _BackendWebAggCoreAgg(_Backend):
|
|
||||||
FigureCanvas = FigureCanvasWebAggCore
|
|
||||||
FigureManager = FigureManagerWebAgg
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import wx
|
|
||||||
|
|
||||||
from .backend_agg import FigureCanvasAgg
|
|
||||||
from .backend_wx import (
|
|
||||||
_BackendWx, _FigureCanvasWxBase, FigureFrameWx,
|
|
||||||
NavigationToolbar2Wx as NavigationToolbar2WxAgg)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureFrameWxAgg(FigureFrameWx):
|
|
||||||
def get_canvas(self, fig):
|
|
||||||
return FigureCanvasWxAgg(self, -1, fig)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasWxAgg(FigureCanvasAgg, _FigureCanvasWxBase):
|
|
||||||
"""
|
|
||||||
The FigureCanvas contains the figure and does event handling.
|
|
||||||
|
|
||||||
In the wxPython backend, it is derived from wxPanel, and (usually)
|
|
||||||
lives inside a frame instantiated by a FigureManagerWx. The parent
|
|
||||||
window probably implements a wxSizer to control the displayed
|
|
||||||
control size - but we give a hint as to our preferred minimum
|
|
||||||
size.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def draw(self, drawDC=None):
|
|
||||||
"""
|
|
||||||
Render the figure using agg.
|
|
||||||
"""
|
|
||||||
FigureCanvasAgg.draw(self)
|
|
||||||
|
|
||||||
self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None)
|
|
||||||
self._isDrawn = True
|
|
||||||
self.gui_repaint(drawDC=drawDC, origin='WXAgg')
|
|
||||||
|
|
||||||
def blit(self, bbox=None):
|
|
||||||
"""
|
|
||||||
Transfer the region of the agg buffer defined by bbox to the display.
|
|
||||||
If bbox is None, the entire buffer is transferred.
|
|
||||||
"""
|
|
||||||
if bbox is None:
|
|
||||||
self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None)
|
|
||||||
self.gui_repaint()
|
|
||||||
return
|
|
||||||
|
|
||||||
l, b, w, h = bbox.bounds
|
|
||||||
r = l + w
|
|
||||||
t = b + h
|
|
||||||
x = int(l)
|
|
||||||
y = int(self.bitmap.GetHeight() - t)
|
|
||||||
|
|
||||||
srcBmp = _convert_agg_to_wx_bitmap(self.get_renderer(), None)
|
|
||||||
srcDC = wx.MemoryDC()
|
|
||||||
srcDC.SelectObject(srcBmp)
|
|
||||||
|
|
||||||
destDC = wx.MemoryDC()
|
|
||||||
destDC.SelectObject(self.bitmap)
|
|
||||||
|
|
||||||
destDC.Blit(x, y, int(w), int(h), srcDC, x, y)
|
|
||||||
|
|
||||||
destDC.SelectObject(wx.NullBitmap)
|
|
||||||
srcDC.SelectObject(wx.NullBitmap)
|
|
||||||
self.gui_repaint()
|
|
||||||
|
|
||||||
filetypes = FigureCanvasAgg.filetypes
|
|
||||||
|
|
||||||
|
|
||||||
# agg/wxPython image conversion functions (wxPython >= 2.8)
|
|
||||||
|
|
||||||
def _convert_agg_to_wx_image(agg, bbox):
|
|
||||||
"""
|
|
||||||
Convert the region of the agg buffer bounded by bbox to a wx.Image. If
|
|
||||||
bbox is None, the entire buffer is converted.
|
|
||||||
|
|
||||||
Note: agg must be a backend_agg.RendererAgg instance.
|
|
||||||
"""
|
|
||||||
if bbox is None:
|
|
||||||
# agg => rgb -> image
|
|
||||||
image = wx.Image(int(agg.width), int(agg.height))
|
|
||||||
image.SetData(agg.tostring_rgb())
|
|
||||||
return image
|
|
||||||
else:
|
|
||||||
# agg => rgba buffer -> bitmap => clipped bitmap => image
|
|
||||||
return wx.ImageFromBitmap(_WX28_clipped_agg_as_bitmap(agg, bbox))
|
|
||||||
|
|
||||||
|
|
||||||
def _convert_agg_to_wx_bitmap(agg, bbox):
|
|
||||||
"""
|
|
||||||
Convert the region of the agg buffer bounded by bbox to a wx.Bitmap. If
|
|
||||||
bbox is None, the entire buffer is converted.
|
|
||||||
|
|
||||||
Note: agg must be a backend_agg.RendererAgg instance.
|
|
||||||
"""
|
|
||||||
if bbox is None:
|
|
||||||
# agg => rgba buffer -> bitmap
|
|
||||||
return wx.Bitmap.FromBufferRGBA(int(agg.width), int(agg.height),
|
|
||||||
agg.buffer_rgba())
|
|
||||||
else:
|
|
||||||
# agg => rgba buffer -> bitmap => clipped bitmap
|
|
||||||
return _WX28_clipped_agg_as_bitmap(agg, bbox)
|
|
||||||
|
|
||||||
|
|
||||||
def _WX28_clipped_agg_as_bitmap(agg, bbox):
|
|
||||||
"""
|
|
||||||
Convert the region of a the agg buffer bounded by bbox to a wx.Bitmap.
|
|
||||||
|
|
||||||
Note: agg must be a backend_agg.RendererAgg instance.
|
|
||||||
"""
|
|
||||||
l, b, width, height = bbox.bounds
|
|
||||||
r = l + width
|
|
||||||
t = b + height
|
|
||||||
|
|
||||||
srcBmp = wx.Bitmap.FromBufferRGBA(int(agg.width), int(agg.height),
|
|
||||||
agg.buffer_rgba())
|
|
||||||
srcDC = wx.MemoryDC()
|
|
||||||
srcDC.SelectObject(srcBmp)
|
|
||||||
|
|
||||||
destBmp = wx.Bitmap(int(width), int(height))
|
|
||||||
destDC = wx.MemoryDC()
|
|
||||||
destDC.SelectObject(destBmp)
|
|
||||||
|
|
||||||
x = int(l)
|
|
||||||
y = int(int(agg.height) - t)
|
|
||||||
destDC.Blit(0, 0, int(width), int(height), srcDC, x, y)
|
|
||||||
|
|
||||||
srcDC.SelectObject(wx.NullBitmap)
|
|
||||||
destDC.SelectObject(wx.NullBitmap)
|
|
||||||
|
|
||||||
return destBmp
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendWx.export
|
|
||||||
class _BackendWxAgg(_BackendWx):
|
|
||||||
FigureCanvas = FigureCanvasWxAgg
|
|
||||||
_frame_class = FigureFrameWxAgg
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import wx
|
|
||||||
|
|
||||||
from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo
|
|
||||||
from .backend_wx import (
|
|
||||||
_BackendWx, _FigureCanvasWxBase, FigureFrameWx,
|
|
||||||
NavigationToolbar2Wx as NavigationToolbar2WxCairo)
|
|
||||||
import wx.lib.wxcairo as wxcairo
|
|
||||||
|
|
||||||
|
|
||||||
class FigureFrameWxCairo(FigureFrameWx):
|
|
||||||
def get_canvas(self, fig):
|
|
||||||
return FigureCanvasWxCairo(self, -1, fig)
|
|
||||||
|
|
||||||
|
|
||||||
class FigureCanvasWxCairo(_FigureCanvasWxBase, FigureCanvasCairo):
|
|
||||||
"""
|
|
||||||
The FigureCanvas contains the figure and does event handling.
|
|
||||||
|
|
||||||
In the wxPython backend, it is derived from wxPanel, and (usually) lives
|
|
||||||
inside a frame instantiated by a FigureManagerWx. The parent window
|
|
||||||
probably implements a wxSizer to control the displayed control size - but
|
|
||||||
we give a hint as to our preferred minimum size.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, parent, id, figure):
|
|
||||||
# _FigureCanvasWxBase should be fixed to have the same signature as
|
|
||||||
# every other FigureCanvas and use cooperative inheritance, but in the
|
|
||||||
# meantime the following will make do.
|
|
||||||
_FigureCanvasWxBase.__init__(self, parent, id, figure)
|
|
||||||
FigureCanvasCairo.__init__(self, figure)
|
|
||||||
self._renderer = RendererCairo(self.figure.dpi)
|
|
||||||
|
|
||||||
def draw(self, drawDC=None):
|
|
||||||
width = int(self.figure.bbox.width)
|
|
||||||
height = int(self.figure.bbox.height)
|
|
||||||
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
|
|
||||||
self._renderer.set_ctx_from_surface(surface)
|
|
||||||
self._renderer.set_width_height(width, height)
|
|
||||||
self.figure.draw(self._renderer)
|
|
||||||
self.bitmap = wxcairo.BitmapFromImageSurface(surface)
|
|
||||||
self._isDrawn = True
|
|
||||||
self.gui_repaint(drawDC=drawDC, origin='WXCairo')
|
|
||||||
|
|
||||||
|
|
||||||
@_BackendWx.export
|
|
||||||
class _BackendWxCairo(_BackendWx):
|
|
||||||
FigureCanvas = FigureCanvasWxCairo
|
|
||||||
_frame_class = FigureFrameWxCairo
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
"""
|
|
||||||
Qt binding and backend selector.
|
|
||||||
|
|
||||||
The selection logic is as follows:
|
|
||||||
- if any of PyQt5, PySide2, PyQt4 or PySide have already been imported
|
|
||||||
(checked in that order), use it;
|
|
||||||
- otherwise, if the QT_API environment variable (used by Enthought) is
|
|
||||||
set, use it to determine which binding to use (but do not change the
|
|
||||||
backend based on it; i.e. if the Qt4Agg backend is requested but QT_API
|
|
||||||
is set to "pyqt5", then actually use Qt4 with the binding specified by
|
|
||||||
``rcParams["backend.qt4"]``;
|
|
||||||
- otherwise, use whatever the rcParams indicate.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from distutils.version import LooseVersion
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from matplotlib import rcParams
|
|
||||||
|
|
||||||
|
|
||||||
QT_API_PYQT5 = "PyQt5"
|
|
||||||
QT_API_PYSIDE2 = "PySide2"
|
|
||||||
QT_API_PYQTv2 = "PyQt4v2"
|
|
||||||
QT_API_PYSIDE = "PySide"
|
|
||||||
QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2).
|
|
||||||
QT_API_ENV = os.environ.get("QT_API")
|
|
||||||
# Mapping of QT_API_ENV to requested binding. ETS does not support PyQt4v1.
|
|
||||||
# (https://github.com/enthought/pyface/blob/master/pyface/qt/__init__.py)
|
|
||||||
_ETS = {"pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2,
|
|
||||||
"pyqt": QT_API_PYQTv2, "pyside": QT_API_PYSIDE,
|
|
||||||
None: None}
|
|
||||||
# First, check if anything is already imported.
|
|
||||||
if "PyQt5" in sys.modules:
|
|
||||||
QT_API = QT_API_PYQT5
|
|
||||||
dict.__setitem__(rcParams, "backend.qt5", QT_API)
|
|
||||||
elif "PySide2" in sys.modules:
|
|
||||||
QT_API = QT_API_PYSIDE2
|
|
||||||
dict.__setitem__(rcParams, "backend.qt5", QT_API)
|
|
||||||
elif "PyQt4" in sys.modules:
|
|
||||||
QT_API = QT_API_PYQTv2
|
|
||||||
dict.__setitem__(rcParams, "backend.qt4", QT_API)
|
|
||||||
elif "PySide" in sys.modules:
|
|
||||||
QT_API = QT_API_PYSIDE
|
|
||||||
dict.__setitem__(rcParams, "backend.qt4", QT_API)
|
|
||||||
# Otherwise, check the QT_API environment variable (from Enthought). This can
|
|
||||||
# only override the binding, not the backend (in other words, we check that the
|
|
||||||
# requested backend actually matches).
|
|
||||||
elif rcParams["backend"] in ["Qt5Agg", "Qt5Cairo"]:
|
|
||||||
if QT_API_ENV == "pyqt5":
|
|
||||||
dict.__setitem__(rcParams, "backend.qt5", QT_API_PYQT5)
|
|
||||||
elif QT_API_ENV == "pyside2":
|
|
||||||
dict.__setitem__(rcParams, "backend.qt5", QT_API_PYSIDE2)
|
|
||||||
QT_API = dict.__getitem__(rcParams, "backend.qt5")
|
|
||||||
elif rcParams["backend"] in ["Qt4Agg", "Qt4Cairo"]:
|
|
||||||
if QT_API_ENV == "pyqt4":
|
|
||||||
dict.__setitem__(rcParams, "backend.qt4", QT_API_PYQTv2)
|
|
||||||
elif QT_API_ENV == "pyside":
|
|
||||||
dict.__setitem__(rcParams, "backend.qt4", QT_API_PYSIDE)
|
|
||||||
QT_API = dict.__getitem__(rcParams, "backend.qt4")
|
|
||||||
# A non-Qt backend was selected but we still got there (possible, e.g., when
|
|
||||||
# fully manually embedding Matplotlib in a Qt app without using pyplot).
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
QT_API = _ETS[QT_API_ENV]
|
|
||||||
except KeyError:
|
|
||||||
raise RuntimeError(
|
|
||||||
"The environment variable QT_API has the unrecognized value {!r};"
|
|
||||||
"valid values are 'pyqt5', 'pyside2', 'pyqt', and 'pyside'")
|
|
||||||
|
|
||||||
|
|
||||||
def _setup_pyqt5():
|
|
||||||
global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, _getSaveFileName
|
|
||||||
|
|
||||||
if QT_API == QT_API_PYQT5:
|
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
||||||
__version__ = QtCore.PYQT_VERSION_STR
|
|
||||||
QtCore.Signal = QtCore.pyqtSignal
|
|
||||||
QtCore.Slot = QtCore.pyqtSlot
|
|
||||||
QtCore.Property = QtCore.pyqtProperty
|
|
||||||
elif QT_API == QT_API_PYSIDE2:
|
|
||||||
from PySide2 import QtCore, QtGui, QtWidgets, __version__
|
|
||||||
else:
|
|
||||||
raise ValueError("Unexpected value for the 'backend.qt5' rcparam")
|
|
||||||
_getSaveFileName = QtWidgets.QFileDialog.getSaveFileName
|
|
||||||
|
|
||||||
def is_pyqt5():
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def _setup_pyqt4():
|
|
||||||
global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, _getSaveFileName
|
|
||||||
|
|
||||||
def _setup_pyqt4_internal(api):
|
|
||||||
global QtCore, QtGui, QtWidgets, \
|
|
||||||
__version__, is_pyqt5, _getSaveFileName
|
|
||||||
# List of incompatible APIs:
|
|
||||||
# http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html
|
|
||||||
_sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime",
|
|
||||||
"QUrl", "QVariant"]
|
|
||||||
try:
|
|
||||||
import sip
|
|
||||||
except ImportError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
for _sip_api in _sip_apis:
|
|
||||||
try:
|
|
||||||
sip.setapi(_sip_api, api)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
from PyQt4 import QtCore, QtGui
|
|
||||||
__version__ = QtCore.PYQT_VERSION_STR
|
|
||||||
# PyQt 4.6 introduced getSaveFileNameAndFilter:
|
|
||||||
# https://riverbankcomputing.com/news/pyqt-46
|
|
||||||
if __version__ < LooseVersion("4.6"):
|
|
||||||
raise ImportError("PyQt<4.6 is not supported")
|
|
||||||
QtCore.Signal = QtCore.pyqtSignal
|
|
||||||
QtCore.Slot = QtCore.pyqtSlot
|
|
||||||
QtCore.Property = QtCore.pyqtProperty
|
|
||||||
_getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter
|
|
||||||
|
|
||||||
if QT_API == QT_API_PYQTv2:
|
|
||||||
_setup_pyqt4_internal(api=2)
|
|
||||||
elif QT_API == QT_API_PYSIDE:
|
|
||||||
from PySide import QtCore, QtGui, __version__, __version_info__
|
|
||||||
# PySide 1.0.3 fixed the following:
|
|
||||||
# https://srinikom.github.io/pyside-bz-archive/809.html
|
|
||||||
if __version_info__ < (1, 0, 3):
|
|
||||||
raise ImportError("PySide<1.0.3 is not supported")
|
|
||||||
_getSaveFileName = QtGui.QFileDialog.getSaveFileName
|
|
||||||
elif QT_API == QT_API_PYQT:
|
|
||||||
_setup_pyqt4_internal(api=1)
|
|
||||||
else:
|
|
||||||
raise ValueError("Unexpected value for the 'backend.qt4' rcparam")
|
|
||||||
QtWidgets = QtGui
|
|
||||||
|
|
||||||
def is_pyqt5():
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]:
|
|
||||||
_setup_pyqt5()
|
|
||||||
elif QT_API in [QT_API_PYQTv2, QT_API_PYSIDE, QT_API_PYQT]:
|
|
||||||
_setup_pyqt4()
|
|
||||||
elif QT_API is None:
|
|
||||||
if rcParams["backend"] == "Qt4Agg":
|
|
||||||
_candidates = [(_setup_pyqt4, QT_API_PYQTv2),
|
|
||||||
(_setup_pyqt4, QT_API_PYSIDE),
|
|
||||||
(_setup_pyqt4, QT_API_PYQT),
|
|
||||||
(_setup_pyqt5, QT_API_PYQT5),
|
|
||||||
(_setup_pyqt5, QT_API_PYSIDE2)]
|
|
||||||
else:
|
|
||||||
_candidates = [(_setup_pyqt5, QT_API_PYQT5),
|
|
||||||
(_setup_pyqt5, QT_API_PYSIDE2),
|
|
||||||
(_setup_pyqt4, QT_API_PYQTv2),
|
|
||||||
(_setup_pyqt4, QT_API_PYSIDE),
|
|
||||||
(_setup_pyqt4, QT_API_PYQT)]
|
|
||||||
for _setup, QT_API in _candidates:
|
|
||||||
try:
|
|
||||||
_setup()
|
|
||||||
except ImportError:
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ImportError("Failed to import any qt binding")
|
|
||||||
else: # We should not get there.
|
|
||||||
raise AssertionError("Unexpected QT_API: {}".format(QT_API))
|
|
||||||
|
|
||||||
|
|
||||||
# These globals are only defined for backcompatibilty purposes.
|
|
||||||
ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4),
|
|
||||||
pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5))
|
|
||||||
QT_RC_MAJOR_VERSION = 5 if is_pyqt5() else 4
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
# Copyright © 2009 Pierre Raybaut
|
|
||||||
# Licensed under the terms of the MIT License
|
|
||||||
# see the mpl licenses directory for a copy of the license
|
|
||||||
|
|
||||||
|
|
||||||
"""Module that provides a GUI-based editor for matplotlib's figure options."""
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
import re
|
|
||||||
|
|
||||||
import matplotlib
|
|
||||||
from matplotlib import cm, colors as mcolors, markers, image as mimage
|
|
||||||
import matplotlib.backends.qt_editor.formlayout as formlayout
|
|
||||||
from matplotlib.backends.qt_compat import QtGui
|
|
||||||
|
|
||||||
|
|
||||||
def get_icon(name):
|
|
||||||
basedir = os.path.join(matplotlib.rcParams['datapath'], 'images')
|
|
||||||
return QtGui.QIcon(os.path.join(basedir, name))
|
|
||||||
|
|
||||||
|
|
||||||
LINESTYLES = {'-': 'Solid',
|
|
||||||
'--': 'Dashed',
|
|
||||||
'-.': 'DashDot',
|
|
||||||
':': 'Dotted',
|
|
||||||
'None': 'None',
|
|
||||||
}
|
|
||||||
|
|
||||||
DRAWSTYLES = {
|
|
||||||
'default': 'Default',
|
|
||||||
'steps-pre': 'Steps (Pre)', 'steps': 'Steps (Pre)',
|
|
||||||
'steps-mid': 'Steps (Mid)',
|
|
||||||
'steps-post': 'Steps (Post)'}
|
|
||||||
|
|
||||||
MARKERS = markers.MarkerStyle.markers
|
|
||||||
|
|
||||||
|
|
||||||
def figure_edit(axes, parent=None):
|
|
||||||
"""Edit matplotlib figure options"""
|
|
||||||
sep = (None, None) # separator
|
|
||||||
|
|
||||||
# Get / General
|
|
||||||
# Cast to builtin floats as they have nicer reprs.
|
|
||||||
xmin, xmax = map(float, axes.get_xlim())
|
|
||||||
ymin, ymax = map(float, axes.get_ylim())
|
|
||||||
general = [('Title', axes.get_title()),
|
|
||||||
sep,
|
|
||||||
(None, "<b>X-Axis</b>"),
|
|
||||||
('Left', xmin), ('Right', xmax),
|
|
||||||
('Label', axes.get_xlabel()),
|
|
||||||
('Scale', [axes.get_xscale(), 'linear', 'log', 'logit']),
|
|
||||||
sep,
|
|
||||||
(None, "<b>Y-Axis</b>"),
|
|
||||||
('Bottom', ymin), ('Top', ymax),
|
|
||||||
('Label', axes.get_ylabel()),
|
|
||||||
('Scale', [axes.get_yscale(), 'linear', 'log', 'logit']),
|
|
||||||
sep,
|
|
||||||
('(Re-)Generate automatic legend', False),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Save the unit data
|
|
||||||
xconverter = axes.xaxis.converter
|
|
||||||
yconverter = axes.yaxis.converter
|
|
||||||
xunits = axes.xaxis.get_units()
|
|
||||||
yunits = axes.yaxis.get_units()
|
|
||||||
|
|
||||||
# Sorting for default labels (_lineXXX, _imageXXX).
|
|
||||||
def cmp_key(label):
|
|
||||||
match = re.match(r"(_line|_image)(\d+)", label)
|
|
||||||
if match:
|
|
||||||
return match.group(1), int(match.group(2))
|
|
||||||
else:
|
|
||||||
return label, 0
|
|
||||||
|
|
||||||
# Get / Curves
|
|
||||||
linedict = {}
|
|
||||||
for line in axes.get_lines():
|
|
||||||
label = line.get_label()
|
|
||||||
if label == '_nolegend_':
|
|
||||||
continue
|
|
||||||
linedict[label] = line
|
|
||||||
curves = []
|
|
||||||
|
|
||||||
def prepare_data(d, init):
|
|
||||||
"""Prepare entry for FormLayout.
|
|
||||||
|
|
||||||
`d` is a mapping of shorthands to style names (a single style may
|
|
||||||
have multiple shorthands, in particular the shorthands `None`,
|
|
||||||
`"None"`, `"none"` and `""` are synonyms); `init` is one shorthand
|
|
||||||
of the initial style.
|
|
||||||
|
|
||||||
This function returns an list suitable for initializing a
|
|
||||||
FormLayout combobox, namely `[initial_name, (shorthand,
|
|
||||||
style_name), (shorthand, style_name), ...]`.
|
|
||||||
"""
|
|
||||||
if init not in d:
|
|
||||||
d = {**d, init: str(init)}
|
|
||||||
# Drop duplicate shorthands from dict (by overwriting them during
|
|
||||||
# the dict comprehension).
|
|
||||||
name2short = {name: short for short, name in d.items()}
|
|
||||||
# Convert back to {shorthand: name}.
|
|
||||||
short2name = {short: name for name, short in name2short.items()}
|
|
||||||
# Find the kept shorthand for the style specified by init.
|
|
||||||
canonical_init = name2short[d[init]]
|
|
||||||
# Sort by representation and prepend the initial value.
|
|
||||||
return ([canonical_init] +
|
|
||||||
sorted(short2name.items(),
|
|
||||||
key=lambda short_and_name: short_and_name[1]))
|
|
||||||
|
|
||||||
curvelabels = sorted(linedict, key=cmp_key)
|
|
||||||
for label in curvelabels:
|
|
||||||
line = linedict[label]
|
|
||||||
color = mcolors.to_hex(
|
|
||||||
mcolors.to_rgba(line.get_color(), line.get_alpha()),
|
|
||||||
keep_alpha=True)
|
|
||||||
ec = mcolors.to_hex(
|
|
||||||
mcolors.to_rgba(line.get_markeredgecolor(), line.get_alpha()),
|
|
||||||
keep_alpha=True)
|
|
||||||
fc = mcolors.to_hex(
|
|
||||||
mcolors.to_rgba(line.get_markerfacecolor(), line.get_alpha()),
|
|
||||||
keep_alpha=True)
|
|
||||||
curvedata = [
|
|
||||||
('Label', label),
|
|
||||||
sep,
|
|
||||||
(None, '<b>Line</b>'),
|
|
||||||
('Line style', prepare_data(LINESTYLES, line.get_linestyle())),
|
|
||||||
('Draw style', prepare_data(DRAWSTYLES, line.get_drawstyle())),
|
|
||||||
('Width', line.get_linewidth()),
|
|
||||||
('Color (RGBA)', color),
|
|
||||||
sep,
|
|
||||||
(None, '<b>Marker</b>'),
|
|
||||||
('Style', prepare_data(MARKERS, line.get_marker())),
|
|
||||||
('Size', line.get_markersize()),
|
|
||||||
('Face color (RGBA)', fc),
|
|
||||||
('Edge color (RGBA)', ec)]
|
|
||||||
curves.append([curvedata, label, ""])
|
|
||||||
# Is there a curve displayed?
|
|
||||||
has_curve = bool(curves)
|
|
||||||
|
|
||||||
# Get / Images
|
|
||||||
imagedict = {}
|
|
||||||
for image in axes.get_images():
|
|
||||||
label = image.get_label()
|
|
||||||
if label == '_nolegend_':
|
|
||||||
continue
|
|
||||||
imagedict[label] = image
|
|
||||||
imagelabels = sorted(imagedict, key=cmp_key)
|
|
||||||
images = []
|
|
||||||
cmaps = [(cmap, name) for name, cmap in sorted(cm.cmap_d.items())]
|
|
||||||
for label in imagelabels:
|
|
||||||
image = imagedict[label]
|
|
||||||
cmap = image.get_cmap()
|
|
||||||
if cmap not in cm.cmap_d.values():
|
|
||||||
cmaps = [(cmap, cmap.name)] + cmaps
|
|
||||||
low, high = image.get_clim()
|
|
||||||
imagedata = [
|
|
||||||
('Label', label),
|
|
||||||
('Colormap', [cmap.name] + cmaps),
|
|
||||||
('Min. value', low),
|
|
||||||
('Max. value', high),
|
|
||||||
('Interpolation',
|
|
||||||
[image.get_interpolation()]
|
|
||||||
+ [(name, name) for name in sorted(mimage.interpolations_names)])]
|
|
||||||
images.append([imagedata, label, ""])
|
|
||||||
# Is there an image displayed?
|
|
||||||
has_image = bool(images)
|
|
||||||
|
|
||||||
datalist = [(general, "Axes", "")]
|
|
||||||
if curves:
|
|
||||||
datalist.append((curves, "Curves", ""))
|
|
||||||
if images:
|
|
||||||
datalist.append((images, "Images", ""))
|
|
||||||
|
|
||||||
def apply_callback(data):
|
|
||||||
"""This function will be called to apply changes"""
|
|
||||||
orig_xlim = axes.get_xlim()
|
|
||||||
orig_ylim = axes.get_ylim()
|
|
||||||
|
|
||||||
general = data.pop(0)
|
|
||||||
curves = data.pop(0) if has_curve else []
|
|
||||||
images = data.pop(0) if has_image else []
|
|
||||||
if data:
|
|
||||||
raise ValueError("Unexpected field")
|
|
||||||
|
|
||||||
# Set / General
|
|
||||||
(title, xmin, xmax, xlabel, xscale, ymin, ymax, ylabel, yscale,
|
|
||||||
generate_legend) = general
|
|
||||||
|
|
||||||
if axes.get_xscale() != xscale:
|
|
||||||
axes.set_xscale(xscale)
|
|
||||||
if axes.get_yscale() != yscale:
|
|
||||||
axes.set_yscale(yscale)
|
|
||||||
|
|
||||||
axes.set_title(title)
|
|
||||||
axes.set_xlim(xmin, xmax)
|
|
||||||
axes.set_xlabel(xlabel)
|
|
||||||
axes.set_ylim(ymin, ymax)
|
|
||||||
axes.set_ylabel(ylabel)
|
|
||||||
|
|
||||||
# Restore the unit data
|
|
||||||
axes.xaxis.converter = xconverter
|
|
||||||
axes.yaxis.converter = yconverter
|
|
||||||
axes.xaxis.set_units(xunits)
|
|
||||||
axes.yaxis.set_units(yunits)
|
|
||||||
axes.xaxis._update_axisinfo()
|
|
||||||
axes.yaxis._update_axisinfo()
|
|
||||||
|
|
||||||
# Set / Curves
|
|
||||||
for index, curve in enumerate(curves):
|
|
||||||
line = linedict[curvelabels[index]]
|
|
||||||
(label, linestyle, drawstyle, linewidth, color, marker, markersize,
|
|
||||||
markerfacecolor, markeredgecolor) = curve
|
|
||||||
line.set_label(label)
|
|
||||||
line.set_linestyle(linestyle)
|
|
||||||
line.set_drawstyle(drawstyle)
|
|
||||||
line.set_linewidth(linewidth)
|
|
||||||
rgba = mcolors.to_rgba(color)
|
|
||||||
line.set_alpha(None)
|
|
||||||
line.set_color(rgba)
|
|
||||||
if marker is not 'none':
|
|
||||||
line.set_marker(marker)
|
|
||||||
line.set_markersize(markersize)
|
|
||||||
line.set_markerfacecolor(markerfacecolor)
|
|
||||||
line.set_markeredgecolor(markeredgecolor)
|
|
||||||
|
|
||||||
# Set / Images
|
|
||||||
for index, image_settings in enumerate(images):
|
|
||||||
image = imagedict[imagelabels[index]]
|
|
||||||
label, cmap, low, high, interpolation = image_settings
|
|
||||||
image.set_label(label)
|
|
||||||
image.set_cmap(cm.get_cmap(cmap))
|
|
||||||
image.set_clim(*sorted([low, high]))
|
|
||||||
image.set_interpolation(interpolation)
|
|
||||||
|
|
||||||
# re-generate legend, if checkbox is checked
|
|
||||||
if generate_legend:
|
|
||||||
draggable = None
|
|
||||||
ncol = 1
|
|
||||||
if axes.legend_ is not None:
|
|
||||||
old_legend = axes.get_legend()
|
|
||||||
draggable = old_legend._draggable is not None
|
|
||||||
ncol = old_legend._ncol
|
|
||||||
new_legend = axes.legend(ncol=ncol)
|
|
||||||
if new_legend:
|
|
||||||
new_legend.set_draggable(draggable)
|
|
||||||
|
|
||||||
# Redraw
|
|
||||||
figure = axes.get_figure()
|
|
||||||
figure.canvas.draw()
|
|
||||||
if not (axes.get_xlim() == orig_xlim and axes.get_ylim() == orig_ylim):
|
|
||||||
figure.canvas.toolbar.push_current()
|
|
||||||
|
|
||||||
data = formlayout.fedit(datalist, title="Figure options", parent=parent,
|
|
||||||
icon=get_icon('qt4_editor_options.svg'),
|
|
||||||
apply=apply_callback)
|
|
||||||
if data is not None:
|
|
||||||
apply_callback(data)
|
|
||||||
@@ -1,542 +0,0 @@
|
|||||||
"""
|
|
||||||
formlayout
|
|
||||||
==========
|
|
||||||
|
|
||||||
Module creating Qt form dialogs/layouts to edit various type of parameters
|
|
||||||
|
|
||||||
|
|
||||||
formlayout License Agreement (MIT License)
|
|
||||||
------------------------------------------
|
|
||||||
|
|
||||||
Copyright (c) 2009 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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# History:
|
|
||||||
# 1.0.10: added float validator (disable "Ok" and "Apply" button when not valid)
|
|
||||||
# 1.0.7: added support for "Apply" button
|
|
||||||
# 1.0.6: code cleaning
|
|
||||||
|
|
||||||
__version__ = '1.0.10'
|
|
||||||
__license__ = __doc__
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import datetime
|
|
||||||
from numbers import Integral, Real
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
from matplotlib import colors as mcolors
|
|
||||||
from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore
|
|
||||||
|
|
||||||
|
|
||||||
BLACKLIST = {"title", "label"}
|
|
||||||
|
|
||||||
|
|
||||||
class ColorButton(QtWidgets.QPushButton):
|
|
||||||
"""
|
|
||||||
Color choosing push button
|
|
||||||
"""
|
|
||||||
colorChanged = QtCore.Signal(QtGui.QColor)
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
QtWidgets.QPushButton.__init__(self, parent)
|
|
||||||
self.setFixedSize(20, 20)
|
|
||||||
self.setIconSize(QtCore.QSize(12, 12))
|
|
||||||
self.clicked.connect(self.choose_color)
|
|
||||||
self._color = QtGui.QColor()
|
|
||||||
|
|
||||||
def choose_color(self):
|
|
||||||
color = QtWidgets.QColorDialog.getColor(
|
|
||||||
self._color, self.parentWidget(), "",
|
|
||||||
QtWidgets.QColorDialog.ShowAlphaChannel)
|
|
||||||
if color.isValid():
|
|
||||||
self.set_color(color)
|
|
||||||
|
|
||||||
def get_color(self):
|
|
||||||
return self._color
|
|
||||||
|
|
||||||
@QtCore.Slot(QtGui.QColor)
|
|
||||||
def set_color(self, color):
|
|
||||||
if color != self._color:
|
|
||||||
self._color = color
|
|
||||||
self.colorChanged.emit(self._color)
|
|
||||||
pixmap = QtGui.QPixmap(self.iconSize())
|
|
||||||
pixmap.fill(color)
|
|
||||||
self.setIcon(QtGui.QIcon(pixmap))
|
|
||||||
|
|
||||||
color = QtCore.Property(QtGui.QColor, get_color, set_color)
|
|
||||||
|
|
||||||
|
|
||||||
def to_qcolor(color):
|
|
||||||
"""Create a QColor from a matplotlib color"""
|
|
||||||
qcolor = QtGui.QColor()
|
|
||||||
try:
|
|
||||||
rgba = mcolors.to_rgba(color)
|
|
||||||
except ValueError:
|
|
||||||
warnings.warn('Ignoring invalid color %r' % color, stacklevel=2)
|
|
||||||
return qcolor # return invalid QColor
|
|
||||||
qcolor.setRgbF(*rgba)
|
|
||||||
return qcolor
|
|
||||||
|
|
||||||
|
|
||||||
class ColorLayout(QtWidgets.QHBoxLayout):
|
|
||||||
"""Color-specialized QLineEdit layout"""
|
|
||||||
def __init__(self, color, parent=None):
|
|
||||||
QtWidgets.QHBoxLayout.__init__(self)
|
|
||||||
assert isinstance(color, QtGui.QColor)
|
|
||||||
self.lineedit = QtWidgets.QLineEdit(
|
|
||||||
mcolors.to_hex(color.getRgbF(), keep_alpha=True), parent)
|
|
||||||
self.lineedit.editingFinished.connect(self.update_color)
|
|
||||||
self.addWidget(self.lineedit)
|
|
||||||
self.colorbtn = ColorButton(parent)
|
|
||||||
self.colorbtn.color = color
|
|
||||||
self.colorbtn.colorChanged.connect(self.update_text)
|
|
||||||
self.addWidget(self.colorbtn)
|
|
||||||
|
|
||||||
def update_color(self):
|
|
||||||
color = self.text()
|
|
||||||
qcolor = to_qcolor(color)
|
|
||||||
self.colorbtn.color = qcolor # defaults to black if not qcolor.isValid()
|
|
||||||
|
|
||||||
def update_text(self, color):
|
|
||||||
self.lineedit.setText(mcolors.to_hex(color.getRgbF(), keep_alpha=True))
|
|
||||||
|
|
||||||
def text(self):
|
|
||||||
return self.lineedit.text()
|
|
||||||
|
|
||||||
|
|
||||||
def font_is_installed(font):
|
|
||||||
"""Check if font is installed"""
|
|
||||||
return [fam for fam in QtGui.QFontDatabase().families()
|
|
||||||
if str(fam) == font]
|
|
||||||
|
|
||||||
|
|
||||||
def tuple_to_qfont(tup):
|
|
||||||
"""
|
|
||||||
Create a QFont from tuple:
|
|
||||||
(family [string], size [int], italic [bool], bold [bool])
|
|
||||||
"""
|
|
||||||
if not (isinstance(tup, tuple) and len(tup) == 4
|
|
||||||
and font_is_installed(tup[0])
|
|
||||||
and isinstance(tup[1], Integral)
|
|
||||||
and isinstance(tup[2], bool)
|
|
||||||
and isinstance(tup[3], bool)):
|
|
||||||
return None
|
|
||||||
font = QtGui.QFont()
|
|
||||||
family, size, italic, bold = tup
|
|
||||||
font.setFamily(family)
|
|
||||||
font.setPointSize(size)
|
|
||||||
font.setItalic(italic)
|
|
||||||
font.setBold(bold)
|
|
||||||
return font
|
|
||||||
|
|
||||||
|
|
||||||
def qfont_to_tuple(font):
|
|
||||||
return (str(font.family()), int(font.pointSize()),
|
|
||||||
font.italic(), font.bold())
|
|
||||||
|
|
||||||
|
|
||||||
class FontLayout(QtWidgets.QGridLayout):
|
|
||||||
"""Font selection"""
|
|
||||||
def __init__(self, value, parent=None):
|
|
||||||
QtWidgets.QGridLayout.__init__(self)
|
|
||||||
font = tuple_to_qfont(value)
|
|
||||||
assert font is not None
|
|
||||||
|
|
||||||
# Font family
|
|
||||||
self.family = QtWidgets.QFontComboBox(parent)
|
|
||||||
self.family.setCurrentFont(font)
|
|
||||||
self.addWidget(self.family, 0, 0, 1, -1)
|
|
||||||
|
|
||||||
# Font size
|
|
||||||
self.size = QtWidgets.QComboBox(parent)
|
|
||||||
self.size.setEditable(True)
|
|
||||||
sizelist = [*range(6, 12), *range(12, 30, 2), 36, 48, 72]
|
|
||||||
size = font.pointSize()
|
|
||||||
if size not in sizelist:
|
|
||||||
sizelist.append(size)
|
|
||||||
sizelist.sort()
|
|
||||||
self.size.addItems([str(s) for s in sizelist])
|
|
||||||
self.size.setCurrentIndex(sizelist.index(size))
|
|
||||||
self.addWidget(self.size, 1, 0)
|
|
||||||
|
|
||||||
# Italic or not
|
|
||||||
self.italic = QtWidgets.QCheckBox(self.tr("Italic"), parent)
|
|
||||||
self.italic.setChecked(font.italic())
|
|
||||||
self.addWidget(self.italic, 1, 1)
|
|
||||||
|
|
||||||
# Bold or not
|
|
||||||
self.bold = QtWidgets.QCheckBox(self.tr("Bold"), parent)
|
|
||||||
self.bold.setChecked(font.bold())
|
|
||||||
self.addWidget(self.bold, 1, 2)
|
|
||||||
|
|
||||||
def get_font(self):
|
|
||||||
font = self.family.currentFont()
|
|
||||||
font.setItalic(self.italic.isChecked())
|
|
||||||
font.setBold(self.bold.isChecked())
|
|
||||||
font.setPointSize(int(self.size.currentText()))
|
|
||||||
return qfont_to_tuple(font)
|
|
||||||
|
|
||||||
|
|
||||||
def is_edit_valid(edit):
|
|
||||||
text = edit.text()
|
|
||||||
state = edit.validator().validate(text, 0)[0]
|
|
||||||
|
|
||||||
return state == QtGui.QDoubleValidator.Acceptable
|
|
||||||
|
|
||||||
|
|
||||||
class FormWidget(QtWidgets.QWidget):
|
|
||||||
update_buttons = QtCore.Signal()
|
|
||||||
def __init__(self, data, comment="", parent=None):
|
|
||||||
QtWidgets.QWidget.__init__(self, parent)
|
|
||||||
self.data = copy.deepcopy(data)
|
|
||||||
self.widgets = []
|
|
||||||
self.formlayout = QtWidgets.QFormLayout(self)
|
|
||||||
if comment:
|
|
||||||
self.formlayout.addRow(QtWidgets.QLabel(comment))
|
|
||||||
self.formlayout.addRow(QtWidgets.QLabel(" "))
|
|
||||||
|
|
||||||
def get_dialog(self):
|
|
||||||
"""Return FormDialog instance"""
|
|
||||||
dialog = self.parent()
|
|
||||||
while not isinstance(dialog, QtWidgets.QDialog):
|
|
||||||
dialog = dialog.parent()
|
|
||||||
return dialog
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
for label, value in self.data:
|
|
||||||
if label is None and value is None:
|
|
||||||
# Separator: (None, None)
|
|
||||||
self.formlayout.addRow(QtWidgets.QLabel(" "), QtWidgets.QLabel(" "))
|
|
||||||
self.widgets.append(None)
|
|
||||||
continue
|
|
||||||
elif label is None:
|
|
||||||
# Comment
|
|
||||||
self.formlayout.addRow(QtWidgets.QLabel(value))
|
|
||||||
self.widgets.append(None)
|
|
||||||
continue
|
|
||||||
elif tuple_to_qfont(value) is not None:
|
|
||||||
field = FontLayout(value, self)
|
|
||||||
elif (label.lower() not in BLACKLIST
|
|
||||||
and mcolors.is_color_like(value)):
|
|
||||||
field = ColorLayout(to_qcolor(value), self)
|
|
||||||
elif isinstance(value, str):
|
|
||||||
field = QtWidgets.QLineEdit(value, self)
|
|
||||||
elif isinstance(value, (list, tuple)):
|
|
||||||
if isinstance(value, tuple):
|
|
||||||
value = list(value)
|
|
||||||
# Note: get() below checks the type of value[0] in self.data so
|
|
||||||
# it is essential that value gets modified in-place.
|
|
||||||
# This means that the code is actually broken in the case where
|
|
||||||
# value is a tuple, but fortunately we always pass a list...
|
|
||||||
selindex = value.pop(0)
|
|
||||||
field = QtWidgets.QComboBox(self)
|
|
||||||
if isinstance(value[0], (list, tuple)):
|
|
||||||
keys = [key for key, _val in value]
|
|
||||||
value = [val for _key, val in value]
|
|
||||||
else:
|
|
||||||
keys = value
|
|
||||||
field.addItems(value)
|
|
||||||
if selindex in value:
|
|
||||||
selindex = value.index(selindex)
|
|
||||||
elif selindex in keys:
|
|
||||||
selindex = keys.index(selindex)
|
|
||||||
elif not isinstance(selindex, Integral):
|
|
||||||
warnings.warn(
|
|
||||||
"index '%s' is invalid (label: %s, value: %s)" %
|
|
||||||
(selindex, label, value), stacklevel=2)
|
|
||||||
selindex = 0
|
|
||||||
field.setCurrentIndex(selindex)
|
|
||||||
elif isinstance(value, bool):
|
|
||||||
field = QtWidgets.QCheckBox(self)
|
|
||||||
if value:
|
|
||||||
field.setCheckState(QtCore.Qt.Checked)
|
|
||||||
else:
|
|
||||||
field.setCheckState(QtCore.Qt.Unchecked)
|
|
||||||
elif isinstance(value, Integral):
|
|
||||||
field = QtWidgets.QSpinBox(self)
|
|
||||||
field.setRange(-1e9, 1e9)
|
|
||||||
field.setValue(value)
|
|
||||||
elif isinstance(value, Real):
|
|
||||||
field = QtWidgets.QLineEdit(repr(value), self)
|
|
||||||
field.setCursorPosition(0)
|
|
||||||
field.setValidator(QtGui.QDoubleValidator(field))
|
|
||||||
field.validator().setLocale(QtCore.QLocale("C"))
|
|
||||||
dialog = self.get_dialog()
|
|
||||||
dialog.register_float_field(field)
|
|
||||||
field.textChanged.connect(lambda text: dialog.update_buttons())
|
|
||||||
elif isinstance(value, datetime.datetime):
|
|
||||||
field = QtWidgets.QDateTimeEdit(self)
|
|
||||||
field.setDateTime(value)
|
|
||||||
elif isinstance(value, datetime.date):
|
|
||||||
field = QtWidgets.QDateEdit(self)
|
|
||||||
field.setDate(value)
|
|
||||||
else:
|
|
||||||
field = QtWidgets.QLineEdit(repr(value), self)
|
|
||||||
self.formlayout.addRow(label, field)
|
|
||||||
self.widgets.append(field)
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
valuelist = []
|
|
||||||
for index, (label, value) in enumerate(self.data):
|
|
||||||
field = self.widgets[index]
|
|
||||||
if label is None:
|
|
||||||
# Separator / Comment
|
|
||||||
continue
|
|
||||||
elif tuple_to_qfont(value) is not None:
|
|
||||||
value = field.get_font()
|
|
||||||
elif isinstance(value, str) or mcolors.is_color_like(value):
|
|
||||||
value = str(field.text())
|
|
||||||
elif isinstance(value, (list, tuple)):
|
|
||||||
index = int(field.currentIndex())
|
|
||||||
if isinstance(value[0], (list, tuple)):
|
|
||||||
value = value[index][0]
|
|
||||||
else:
|
|
||||||
value = value[index]
|
|
||||||
elif isinstance(value, bool):
|
|
||||||
value = field.checkState() == QtCore.Qt.Checked
|
|
||||||
elif isinstance(value, Integral):
|
|
||||||
value = int(field.value())
|
|
||||||
elif isinstance(value, Real):
|
|
||||||
value = float(str(field.text()))
|
|
||||||
elif isinstance(value, datetime.datetime):
|
|
||||||
value = field.dateTime().toPyDateTime()
|
|
||||||
elif isinstance(value, datetime.date):
|
|
||||||
value = field.date().toPyDate()
|
|
||||||
else:
|
|
||||||
value = eval(str(field.text()))
|
|
||||||
valuelist.append(value)
|
|
||||||
return valuelist
|
|
||||||
|
|
||||||
|
|
||||||
class FormComboWidget(QtWidgets.QWidget):
|
|
||||||
update_buttons = QtCore.Signal()
|
|
||||||
|
|
||||||
def __init__(self, datalist, comment="", parent=None):
|
|
||||||
QtWidgets.QWidget.__init__(self, parent)
|
|
||||||
layout = QtWidgets.QVBoxLayout()
|
|
||||||
self.setLayout(layout)
|
|
||||||
self.combobox = QtWidgets.QComboBox()
|
|
||||||
layout.addWidget(self.combobox)
|
|
||||||
|
|
||||||
self.stackwidget = QtWidgets.QStackedWidget(self)
|
|
||||||
layout.addWidget(self.stackwidget)
|
|
||||||
self.combobox.currentIndexChanged.connect(self.stackwidget.setCurrentIndex)
|
|
||||||
|
|
||||||
self.widgetlist = []
|
|
||||||
for data, title, comment in datalist:
|
|
||||||
self.combobox.addItem(title)
|
|
||||||
widget = FormWidget(data, comment=comment, parent=self)
|
|
||||||
self.stackwidget.addWidget(widget)
|
|
||||||
self.widgetlist.append(widget)
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
for widget in self.widgetlist:
|
|
||||||
widget.setup()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return [widget.get() for widget in self.widgetlist]
|
|
||||||
|
|
||||||
|
|
||||||
class FormTabWidget(QtWidgets.QWidget):
|
|
||||||
update_buttons = QtCore.Signal()
|
|
||||||
|
|
||||||
def __init__(self, datalist, comment="", parent=None):
|
|
||||||
QtWidgets.QWidget.__init__(self, parent)
|
|
||||||
layout = QtWidgets.QVBoxLayout()
|
|
||||||
self.tabwidget = QtWidgets.QTabWidget()
|
|
||||||
layout.addWidget(self.tabwidget)
|
|
||||||
self.setLayout(layout)
|
|
||||||
self.widgetlist = []
|
|
||||||
for data, title, comment in datalist:
|
|
||||||
if len(data[0]) == 3:
|
|
||||||
widget = FormComboWidget(data, comment=comment, parent=self)
|
|
||||||
else:
|
|
||||||
widget = FormWidget(data, comment=comment, parent=self)
|
|
||||||
index = self.tabwidget.addTab(widget, title)
|
|
||||||
self.tabwidget.setTabToolTip(index, comment)
|
|
||||||
self.widgetlist.append(widget)
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
for widget in self.widgetlist:
|
|
||||||
widget.setup()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return [widget.get() for widget in self.widgetlist]
|
|
||||||
|
|
||||||
|
|
||||||
class FormDialog(QtWidgets.QDialog):
|
|
||||||
"""Form Dialog"""
|
|
||||||
def __init__(self, data, title="", comment="",
|
|
||||||
icon=None, parent=None, apply=None):
|
|
||||||
QtWidgets.QDialog.__init__(self, parent)
|
|
||||||
|
|
||||||
self.apply_callback = apply
|
|
||||||
|
|
||||||
# Form
|
|
||||||
if isinstance(data[0][0], (list, tuple)):
|
|
||||||
self.formwidget = FormTabWidget(data, comment=comment,
|
|
||||||
parent=self)
|
|
||||||
elif len(data[0]) == 3:
|
|
||||||
self.formwidget = FormComboWidget(data, comment=comment,
|
|
||||||
parent=self)
|
|
||||||
else:
|
|
||||||
self.formwidget = FormWidget(data, comment=comment,
|
|
||||||
parent=self)
|
|
||||||
layout = QtWidgets.QVBoxLayout()
|
|
||||||
layout.addWidget(self.formwidget)
|
|
||||||
|
|
||||||
self.float_fields = []
|
|
||||||
self.formwidget.setup()
|
|
||||||
|
|
||||||
# Button box
|
|
||||||
self.bbox = bbox = QtWidgets.QDialogButtonBox(
|
|
||||||
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
|
|
||||||
self.formwidget.update_buttons.connect(self.update_buttons)
|
|
||||||
if self.apply_callback is not None:
|
|
||||||
apply_btn = bbox.addButton(QtWidgets.QDialogButtonBox.Apply)
|
|
||||||
apply_btn.clicked.connect(self.apply)
|
|
||||||
|
|
||||||
bbox.accepted.connect(self.accept)
|
|
||||||
bbox.rejected.connect(self.reject)
|
|
||||||
layout.addWidget(bbox)
|
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.setWindowTitle(title)
|
|
||||||
if not isinstance(icon, QtGui.QIcon):
|
|
||||||
icon = QtWidgets.QWidget().style().standardIcon(QtWidgets.QStyle.SP_MessageBoxQuestion)
|
|
||||||
self.setWindowIcon(icon)
|
|
||||||
|
|
||||||
def register_float_field(self, field):
|
|
||||||
self.float_fields.append(field)
|
|
||||||
|
|
||||||
def update_buttons(self):
|
|
||||||
valid = True
|
|
||||||
for field in self.float_fields:
|
|
||||||
if not is_edit_valid(field):
|
|
||||||
valid = False
|
|
||||||
for btn_type in (QtWidgets.QDialogButtonBox.Ok,
|
|
||||||
QtWidgets.QDialogButtonBox.Apply):
|
|
||||||
btn = self.bbox.button(btn_type)
|
|
||||||
if btn is not None:
|
|
||||||
btn.setEnabled(valid)
|
|
||||||
|
|
||||||
def accept(self):
|
|
||||||
self.data = self.formwidget.get()
|
|
||||||
QtWidgets.QDialog.accept(self)
|
|
||||||
|
|
||||||
def reject(self):
|
|
||||||
self.data = None
|
|
||||||
QtWidgets.QDialog.reject(self)
|
|
||||||
|
|
||||||
def apply(self):
|
|
||||||
self.apply_callback(self.formwidget.get())
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""Return form result"""
|
|
||||||
return self.data
|
|
||||||
|
|
||||||
|
|
||||||
def fedit(data, title="", comment="", icon=None, parent=None, apply=None):
|
|
||||||
"""
|
|
||||||
Create form dialog and return result
|
|
||||||
(if Cancel button is pressed, return None)
|
|
||||||
|
|
||||||
data: datalist, datagroup
|
|
||||||
title: string
|
|
||||||
comment: string
|
|
||||||
icon: QIcon instance
|
|
||||||
parent: parent QWidget
|
|
||||||
apply: apply callback (function)
|
|
||||||
|
|
||||||
datalist: list/tuple of (field_name, field_value)
|
|
||||||
datagroup: list/tuple of (datalist *or* datagroup, title, comment)
|
|
||||||
|
|
||||||
-> one field for each member of a datalist
|
|
||||||
-> one tab for each member of a top-level datagroup
|
|
||||||
-> one page (of a multipage widget, each page can be selected with a combo
|
|
||||||
box) for each member of a datagroup inside a datagroup
|
|
||||||
|
|
||||||
Supported types for field_value:
|
|
||||||
- int, float, str, unicode, bool
|
|
||||||
- colors: in Qt-compatible text form, i.e. in hex format or name (red,...)
|
|
||||||
(automatically detected from a string)
|
|
||||||
- list/tuple:
|
|
||||||
* the first element will be the selected index (or value)
|
|
||||||
* the other elements can be couples (key, value) or only values
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Create a QApplication instance if no instance currently exists
|
|
||||||
# (e.g., if the module is used directly from the interpreter)
|
|
||||||
if QtWidgets.QApplication.startingUp():
|
|
||||||
_app = QtWidgets.QApplication([])
|
|
||||||
dialog = FormDialog(data, title, comment, icon, parent, apply)
|
|
||||||
if dialog.exec_():
|
|
||||||
return dialog.get()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
|
|
||||||
def create_datalist_example():
|
|
||||||
return [('str', 'this is a string'),
|
|
||||||
('list', [0, '1', '3', '4']),
|
|
||||||
('list2', ['--', ('none', 'None'), ('--', 'Dashed'),
|
|
||||||
('-.', 'DashDot'), ('-', 'Solid'),
|
|
||||||
('steps', 'Steps'), (':', 'Dotted')]),
|
|
||||||
('float', 1.2),
|
|
||||||
(None, 'Other:'),
|
|
||||||
('int', 12),
|
|
||||||
('font', ('Arial', 10, False, True)),
|
|
||||||
('color', '#123409'),
|
|
||||||
('bool', True),
|
|
||||||
('date', datetime.date(2010, 10, 10)),
|
|
||||||
('datetime', datetime.datetime(2010, 10, 10)),
|
|
||||||
]
|
|
||||||
|
|
||||||
def create_datagroup_example():
|
|
||||||
datalist = create_datalist_example()
|
|
||||||
return ((datalist, "Category 1", "Category 1 comment"),
|
|
||||||
(datalist, "Category 2", "Category 2 comment"),
|
|
||||||
(datalist, "Category 3", "Category 3 comment"))
|
|
||||||
|
|
||||||
#--------- datalist example
|
|
||||||
datalist = create_datalist_example()
|
|
||||||
|
|
||||||
def apply_test(data):
|
|
||||||
print("data:", data)
|
|
||||||
print("result:", fedit(datalist, title="Example",
|
|
||||||
comment="This is just an <b>example</b>.",
|
|
||||||
apply=apply_test))
|
|
||||||
|
|
||||||
#--------- datagroup example
|
|
||||||
datagroup = create_datagroup_example()
|
|
||||||
print("result:", fedit(datagroup, "Global title"))
|
|
||||||
|
|
||||||
#--------- datagroup inside a datagroup example
|
|
||||||
datalist = create_datalist_example()
|
|
||||||
datagroup = create_datagroup_example()
|
|
||||||
print("result:", fedit(((datagroup, "Title 1", "Tab 1 comment"),
|
|
||||||
(datalist, "Title 2", "Tab 2 comment"),
|
|
||||||
(datalist, "Title 3", "Tab 3 comment")),
|
|
||||||
"Global title"))
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
from matplotlib.backends.qt_compat import QtWidgets
|
|
||||||
|
|
||||||
|
|
||||||
class UiSubplotTool(QtWidgets.QDialog):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.setObjectName("SubplotTool")
|
|
||||||
self._widgets = {}
|
|
||||||
|
|
||||||
layout = QtWidgets.QHBoxLayout()
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
left = QtWidgets.QVBoxLayout()
|
|
||||||
layout.addLayout(left)
|
|
||||||
right = QtWidgets.QVBoxLayout()
|
|
||||||
layout.addLayout(right)
|
|
||||||
|
|
||||||
box = QtWidgets.QGroupBox("Borders")
|
|
||||||
left.addWidget(box)
|
|
||||||
inner = QtWidgets.QFormLayout(box)
|
|
||||||
for side in ["top", "bottom", "left", "right"]:
|
|
||||||
self._widgets[side] = widget = QtWidgets.QDoubleSpinBox()
|
|
||||||
widget.setMinimum(0)
|
|
||||||
widget.setMaximum(1)
|
|
||||||
widget.setDecimals(3)
|
|
||||||
widget.setSingleStep(.005)
|
|
||||||
widget.setKeyboardTracking(False)
|
|
||||||
inner.addRow(side, widget)
|
|
||||||
left.addStretch(1)
|
|
||||||
|
|
||||||
box = QtWidgets.QGroupBox("Spacings")
|
|
||||||
right.addWidget(box)
|
|
||||||
inner = QtWidgets.QFormLayout(box)
|
|
||||||
for side in ["hspace", "wspace"]:
|
|
||||||
self._widgets[side] = widget = QtWidgets.QDoubleSpinBox()
|
|
||||||
widget.setMinimum(0)
|
|
||||||
widget.setMaximum(1)
|
|
||||||
widget.setDecimals(3)
|
|
||||||
widget.setSingleStep(.005)
|
|
||||||
widget.setKeyboardTracking(False)
|
|
||||||
inner.addRow(side, widget)
|
|
||||||
right.addStretch(1)
|
|
||||||
|
|
||||||
widget = QtWidgets.QPushButton("Export values")
|
|
||||||
self._widgets["Export values"] = widget
|
|
||||||
# Don't trigger on <enter>, which is used to input values.
|
|
||||||
widget.setAutoDefault(False)
|
|
||||||
left.addWidget(widget)
|
|
||||||
|
|
||||||
for action in ["Tight layout", "Reset", "Close"]:
|
|
||||||
self._widgets[action] = widget = QtWidgets.QPushButton(action)
|
|
||||||
widget.setAutoDefault(False)
|
|
||||||
right.addWidget(widget)
|
|
||||||
|
|
||||||
self._widgets["Close"].setFocus()
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import tkinter as Tk
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from matplotlib import cbook
|
|
||||||
from matplotlib.backends import _tkagg
|
|
||||||
|
|
||||||
|
|
||||||
cbook.warn_deprecated(
|
|
||||||
"3.0", "The matplotlib.backends.tkagg module is deprecated.")
|
|
||||||
|
|
||||||
|
|
||||||
def blit(photoimage, aggimage, bbox=None, colormode=1):
|
|
||||||
tk = photoimage.tk
|
|
||||||
|
|
||||||
if bbox is not None:
|
|
||||||
bbox_array = bbox.__array__()
|
|
||||||
# x1, x2, y1, y2
|
|
||||||
bboxptr = (bbox_array[0, 0], bbox_array[1, 0],
|
|
||||||
bbox_array[0, 1], bbox_array[1, 1])
|
|
||||||
else:
|
|
||||||
bboxptr = 0
|
|
||||||
data = np.asarray(aggimage)
|
|
||||||
dataptr = (data.shape[0], data.shape[1], data.ctypes.data)
|
|
||||||
try:
|
|
||||||
tk.call(
|
|
||||||
"PyAggImagePhoto", photoimage,
|
|
||||||
dataptr, colormode, bboxptr)
|
|
||||||
except Tk.TclError:
|
|
||||||
if hasattr(tk, 'interpaddr'):
|
|
||||||
_tkagg.tkinit(tk.interpaddr(), 1)
|
|
||||||
else:
|
|
||||||
# very old python?
|
|
||||||
_tkagg.tkinit(tk, 0)
|
|
||||||
tk.call("PyAggImagePhoto", photoimage,
|
|
||||||
dataptr, colormode, bboxptr)
|
|
||||||
|
|
||||||
def test(aggimage):
|
|
||||||
r = Tk.Tk()
|
|
||||||
c = Tk.Canvas(r, width=aggimage.width, height=aggimage.height)
|
|
||||||
c.pack()
|
|
||||||
p = Tk.PhotoImage(width=aggimage.width, height=aggimage.height)
|
|
||||||
blit(p, aggimage)
|
|
||||||
c.create_image(aggimage.width,aggimage.height,image=p)
|
|
||||||
blit(p, aggimage)
|
|
||||||
while True: r.update_idletasks()
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="stylesheet" href="{{ prefix }}/_static/css/page.css" type="text/css">
|
|
||||||
<link rel="stylesheet" href="{{ prefix }}/_static/css/boilerplate.css" type="text/css" />
|
|
||||||
<link rel="stylesheet" href="{{ prefix }}/_static/css/fbm.css" type="text/css" />
|
|
||||||
<link rel="stylesheet" href="{{ prefix }}/_static/jquery/css/themes/base/jquery-ui.min.css" >
|
|
||||||
<script src="{{ prefix }}/_static/jquery/js/jquery-1.11.3.min.js"></script>
|
|
||||||
<script src="{{ prefix }}/_static/jquery/js/jquery-ui.min.js"></script>
|
|
||||||
<script src="{{ prefix }}/_static/js/mpl_tornado.js"></script>
|
|
||||||
<script src="{{ prefix }}/js/mpl.js"></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
{% for (fig_id, fig_manager) in figures %}
|
|
||||||
$(document).ready(
|
|
||||||
function() {
|
|
||||||
var main_div = $('div#figures');
|
|
||||||
var figure_div = $('<div id="figure-div"/>')
|
|
||||||
main_div.append(figure_div);
|
|
||||||
var websocket_type = mpl.get_websocket_type();
|
|
||||||
var websocket = new websocket_type(
|
|
||||||
"{{ ws_uri }}" + "{{ fig_id }}" + "/ws");
|
|
||||||
var fig = new mpl.figure(
|
|
||||||
"{{ fig_id }}", websocket, mpl_ondownload, figure_div);
|
|
||||||
|
|
||||||
fig.focus_on_mouseover = true;
|
|
||||||
|
|
||||||
$(fig.canvas).attr('tabindex', {{ fig_id }});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
{% end %}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<title>MPL | WebAgg current figures</title>
|
|
||||||
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="mpl-warnings" class="mpl-warnings"></div>
|
|
||||||
|
|
||||||
<div id="figures" style="margin: 10px 10px;"></div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/**
|
|
||||||
* HTML5 ✰ Boilerplate
|
|
||||||
*
|
|
||||||
* style.css contains a reset, font normalization and some base styles.
|
|
||||||
*
|
|
||||||
* Credit is left where credit is due.
|
|
||||||
* Much inspiration was taken from these projects:
|
|
||||||
* - yui.yahooapis.com/2.8.1/build/base/base.css
|
|
||||||
* - camendesign.com/design/
|
|
||||||
* - praegnanz.de/weblog/htmlcssjs-kickstart
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline)
|
|
||||||
* v1.6.1 2010-09-17 | Authors: Eric Meyer & Richard Clark
|
|
||||||
* html5doctor.com/html-5-reset-stylesheet/
|
|
||||||
*/
|
|
||||||
|
|
||||||
html, body, div, span, object, iframe,
|
|
||||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
|
||||||
abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
|
|
||||||
small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
|
|
||||||
fieldset, form, label, legend,
|
|
||||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
|
||||||
article, aside, canvas, details, figcaption, figure,
|
|
||||||
footer, header, hgroup, menu, nav, section, summary,
|
|
||||||
time, mark, audio, video {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
font-size: 100%;
|
|
||||||
font: inherit;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
sup { vertical-align: super; }
|
|
||||||
sub { vertical-align: sub; }
|
|
||||||
|
|
||||||
article, aside, details, figcaption, figure,
|
|
||||||
footer, header, hgroup, menu, nav, section {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote, q { quotes: none; }
|
|
||||||
|
|
||||||
blockquote:before, blockquote:after,
|
|
||||||
q:before, q:after { content: ""; content: none; }
|
|
||||||
|
|
||||||
ins { background-color: #ff9; color: #000; text-decoration: none; }
|
|
||||||
|
|
||||||
mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; }
|
|
||||||
|
|
||||||
del { text-decoration: line-through; }
|
|
||||||
|
|
||||||
abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; }
|
|
||||||
|
|
||||||
table { border-collapse: collapse; border-spacing: 0; }
|
|
||||||
|
|
||||||
hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; }
|
|
||||||
|
|
||||||
input, select { vertical-align: middle; }
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Font normalization inspired by YUI Library's fonts.css: developer.yahoo.com/yui/
|
|
||||||
*/
|
|
||||||
|
|
||||||
body { font:13px/1.231 sans-serif; *font-size:small; } /* Hack retained to preserve specificity */
|
|
||||||
select, input, textarea, button { font:99% sans-serif; }
|
|
||||||
|
|
||||||
/* Normalize monospace sizing:
|
|
||||||
en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome */
|
|
||||||
pre, code, kbd, samp { font-family: monospace, sans-serif; }
|
|
||||||
|
|
||||||
em,i { font-style: italic; }
|
|
||||||
b,strong { font-weight: bold; }
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
|
|
||||||
/* Flexible box model classes */
|
|
||||||
/* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
|
|
||||||
|
|
||||||
.hbox {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: horizontal;
|
|
||||||
-webkit-box-align: stretch;
|
|
||||||
|
|
||||||
display: -moz-box;
|
|
||||||
-moz-box-orient: horizontal;
|
|
||||||
-moz-box-align: stretch;
|
|
||||||
|
|
||||||
display: box;
|
|
||||||
box-orient: horizontal;
|
|
||||||
box-align: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hbox > * {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-moz-box-flex: 0;
|
|
||||||
box-flex: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vbox {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-box-align: stretch;
|
|
||||||
|
|
||||||
display: -moz-box;
|
|
||||||
-moz-box-orient: vertical;
|
|
||||||
-moz-box-align: stretch;
|
|
||||||
|
|
||||||
display: box;
|
|
||||||
box-orient: vertical;
|
|
||||||
box-align: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vbox > * {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-moz-box-flex: 0;
|
|
||||||
box-flex: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reverse {
|
|
||||||
-webkit-box-direction: reverse;
|
|
||||||
-moz-box-direction: reverse;
|
|
||||||
box-direction: reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-flex0 {
|
|
||||||
-webkit-box-flex: 0;
|
|
||||||
-moz-box-flex: 0;
|
|
||||||
box-flex: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-flex1, .box-flex {
|
|
||||||
-webkit-box-flex: 1;
|
|
||||||
-moz-box-flex: 1;
|
|
||||||
box-flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-flex2 {
|
|
||||||
-webkit-box-flex: 2;
|
|
||||||
-moz-box-flex: 2;
|
|
||||||
box-flex: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-group1 {
|
|
||||||
-webkit-box-flex-group: 1;
|
|
||||||
-moz-box-flex-group: 1;
|
|
||||||
box-flex-group: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.box-group2 {
|
|
||||||
-webkit-box-flex-group: 2;
|
|
||||||
-moz-box-flex-group: 2;
|
|
||||||
box-flex-group: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start {
|
|
||||||
-webkit-box-pack: start;
|
|
||||||
-moz-box-pack: start;
|
|
||||||
box-pack: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.end {
|
|
||||||
-webkit-box-pack: end;
|
|
||||||
-moz-box-pack: end;
|
|
||||||
box-pack: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
|
||||||
-webkit-box-pack: center;
|
|
||||||
-moz-box-pack: center;
|
|
||||||
box-pack: center;
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/**
|
|
||||||
* Primary styles
|
|
||||||
*
|
|
||||||
* Author: IPython Development Team
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: white;
|
|
||||||
/* This makes sure that the body covers the entire window and needs to
|
|
||||||
be in a different element than the display: box in wrapper below */
|
|
||||||
position: absolute;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
top: 0px;
|
|
||||||
bottom: 0px;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
div#header {
|
|
||||||
/* Initially hidden to prevent FLOUC */
|
|
||||||
display: none;
|
|
||||||
position: relative;
|
|
||||||
height: 40px;
|
|
||||||
padding: 5px;
|
|
||||||
margin: 0px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
span#ipython_notebook {
|
|
||||||
position: absolute;
|
|
||||||
padding: 2px 2px 2px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
span#ipython_notebook img {
|
|
||||||
font-family: Verdana, "Helvetica Neue", Arial, Helvetica, Geneva, sans-serif;
|
|
||||||
height: 24px;
|
|
||||||
text-decoration:none;
|
|
||||||
display: inline;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
#site {
|
|
||||||
width: 100%;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* We set the fonts by hand here to override the values in the theme */
|
|
||||||
.ui-widget {
|
|
||||||
font-family: "Lucinda Grande", "Lucinda Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button {
|
|
||||||
font-family: "Lucinda Grande", "Lucinda Sans Unicode", Helvetica, Arial, Verdana, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smaller buttons */
|
|
||||||
.ui-button .ui-button-text {
|
|
||||||
padding: 0.2em 0.8em;
|
|
||||||
font-size: 77%;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.ui-button {
|
|
||||||
padding: 0.3em 0.9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
span#login_widget {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.border-box-sizing {
|
|
||||||
box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
#figure-div {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<!-- Within the kernel, we don't know the address of the matplotlib
|
|
||||||
websocket server, so we have to get in client-side and fetch our
|
|
||||||
resources that way. -->
|
|
||||||
<script>
|
|
||||||
// We can't proceed until these Javascript files are fetched, so
|
|
||||||
// we fetch them synchronously
|
|
||||||
$.ajaxSetup({async: false});
|
|
||||||
$.getScript("http://" + window.location.hostname + ":{{ port }}{{prefix}}/_static/js/mpl_tornado.js");
|
|
||||||
$.getScript("http://" + window.location.hostname + ":{{ port }}{{prefix}}/js/mpl.js");
|
|
||||||
$.ajaxSetup({async: true});
|
|
||||||
|
|
||||||
function init_figure{{ fig_id }}(e) {
|
|
||||||
$('div.output').off('resize');
|
|
||||||
|
|
||||||
var output_div = $(e.target).find('div.output_subarea');
|
|
||||||
var websocket_type = mpl.get_websocket_type();
|
|
||||||
var websocket = new websocket_type(
|
|
||||||
"ws://" + window.location.hostname + ":{{ port }}{{ prefix}}/" +
|
|
||||||
{{ repr(str(fig_id)) }} + "/ws");
|
|
||||||
|
|
||||||
var fig = new mpl.figure(
|
|
||||||
{{repr(str(fig_id))}}, websocket, mpl_ondownload, output_div);
|
|
||||||
|
|
||||||
// Fetch the first image
|
|
||||||
fig.context.drawImage(fig.imageObj, 0, 0);
|
|
||||||
|
|
||||||
fig.focus_on_mouseover = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can't initialize the figure contents until our content
|
|
||||||
// has been added to the DOM. This is a bit of hack to get an
|
|
||||||
// event for that.
|
|
||||||
$('div.output').resize(init_figure{{ fig_id }});
|
|
||||||
</script>
|
|
||||||
|
Before Width: | Height: | Size: 418 B |
|
Before Width: | Height: | Size: 312 B |
|
Before Width: | Height: | Size: 205 B |
|
Before Width: | Height: | Size: 262 B |
|
Before Width: | Height: | Size: 348 B |
|
Before Width: | Height: | Size: 207 B |
|
Before Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 278 B |
|
Before Width: | Height: | Size: 328 B |
|
Before Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |