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)
|
||||
# <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:
|
||||
+ [__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
|
||||
*Za slovensko različico se odpravite na <http://git.sckr-lab.tk/kristjank/TeraHz>*
|
||||
|
||||
## Why?
|
||||
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.
|
||||
TeraHz is a low-cost portable spectrometer based on Raspberry Pi and a few
|
||||
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
|
||||
Copyright 2018, 2019
|
||||
|
||||
- Kristjan "cls-02" Komloši (electronics, sensor drivers, backend)
|
||||
- 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.
|
||||
- Kristjan Komloši (cls-02) - Project leader and main programmer
|
||||
- Jakob Kosec (D3m1j4ck) - Frontend designer
|
||||
- Juš Dolžan (ANormalPerson) - Math double-checker
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
# run.sh - run the backend server
|
||||
cd `dirname $0`
|
||||
sudo gunicorn app:app -b 0.0.0.0:5000 &
|
||||
disown
|
||||
|
||||
@@ -1,169 +1,39 @@
|
||||
# TeraHz build guide
|
||||
This document describes the process of building/installing TeraHz from the Git
|
||||
repository.
|
||||
In its early development phase, TeraHz was hard and time-consuming to compile and install.
|
||||
This is not case now, as the more optimized DietPi Linux distribution allows
|
||||
better performance and simpler configuration than formerly used Raspbian.
|
||||
|
||||
## Warning
|
||||
The recommended way of getting TeraHz is the official Raspberry Pi SD card image
|
||||
provided under the releases tab in the GitHub repository. Installing TeraHz from
|
||||
source is a time consuming and painful process, even more so if you don't know
|
||||
what you're doing, and whatever you end up building __will not be officially
|
||||
supported__ (unless you're a core developer).
|
||||
## Downloading the complete image
|
||||
The easiest way to get TeraHz is to download the premade complete image and
|
||||
write it to an SD card. It already has TeraHz installed and **will work out of
|
||||
the box** with the correct hardware. The image is designed to run from a 16 GB
|
||||
micro SD card, class 10 or higher is recommended for snappy performance. The
|
||||
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
|
||||
The most reliable way to get working source code is by cloning the official
|
||||
GitHub repository and checking out the `development-stable` tag. This tag marks
|
||||
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.
|
||||
## Downloading the clean DietPi image and installing TeraHz manually
|
||||
This process is a bit more involved, but the version of TeraHz installed is
|
||||
guaranteed to be the latest one available from the repository.
|
||||
|
||||
Make sure that the repository is cloned into `/home/pi/TeraHz`, as Lighttpd
|
||||
expects to find frontend files inside this directory.
|
||||
The SD card image used in this case also contains some pre-configuration, but no
|
||||
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
|
||||
and the required version of python in the `docs/dependencies.md` file.
|
||||
|
||||
## 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.
|
||||
After connecting to the Raspberry Pi with username `root` and password `terahz`,
|
||||
TeraHz can be installed by cloning the Git repository and running the `etcs/install.sh`
|
||||
script.
|
||||
|
||||
```
|
||||
sudo apt update
|
||||
sudo apt full-upgrade
|
||||
sudo reboot
|
||||
git clone https://github.com/cls-02/TeraHz.git
|
||||
cd TeraHz/etcs
|
||||
./install.sh
|
||||
```
|
||||
|
||||
Install the required build tools, libraries and their headers.
|
||||
|
||||
```
|
||||
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.
|
||||
When the installation completes, reboot the Raspberry Pi and it will
|
||||
automatically boot into TeraHz.
|
||||
|
||||
@@ -1,26 +1,24 @@
|
||||
# Development-stable dependencies
|
||||
The current development version of TeraHz has been verified to work with:
|
||||
|
||||
- Raspbian Stretch (9)
|
||||
- Python 3.6.8 (built from source code and altinstall'd)
|
||||
- Module versions (direct `pip3.6 list` output):
|
||||
|
||||
# 1.0.0-rc1 Dependencies
|
||||
The 1.0.0-rc1 version of TeraHz is confirmed to work with:
|
||||
- DietPi 6.26.3
|
||||
- Python 3.7.3
|
||||
- Pip 18.1
|
||||
- Following versions of pip3 packages:
|
||||
```
|
||||
Package Version
|
||||
--------------- ---------
|
||||
Click 7.0
|
||||
Flask 1.0.3
|
||||
itsdangerous 1.1.0
|
||||
Jinja2 2.10.1
|
||||
MarkupSafe 1.1.1
|
||||
numpy 1.16.4
|
||||
pandas 0.24.2
|
||||
pip 18.1
|
||||
pyserial 3.4
|
||||
python-dateutil 2.8.0
|
||||
pytz 2019.1
|
||||
setuptools 40.6.2
|
||||
six 1.12.0
|
||||
smbus 1.1.post2
|
||||
Werkzeug 0.15.4
|
||||
Click 7.0
|
||||
Flask 1.1.1
|
||||
gunicorn 20.0.0
|
||||
itsdangerous 1.1.0
|
||||
Jinja2 2.10.3
|
||||
MarkupSafe 1.1.1
|
||||
numpy 1.17.4
|
||||
pandas 0.25.3
|
||||
pip 18.1
|
||||
pyserial 3.4
|
||||
python-dateutil 2.8.1
|
||||
pytz 2019.3
|
||||
setuptools 41.6.0
|
||||
six 1.13.0
|
||||
smbus2 0.3.0
|
||||
Werkzeug 0.16.0
|
||||
```
|
||||
|
||||
@@ -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
|
||||
`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
|
||||
external Wi-Fi adapter if Wi-Fi functionality is desired. The practicality of
|
||||
compiling Python on the first generation of Raspberry Pis is also very
|
||||
questionable.
|
||||
external Wi-Fi adapter if Wi-Fi functionality is desired.
|
||||
|
||||
Sensors required for operation are:
|
||||
+ AS7265x
|
||||
+ VEML6075
|
||||
+ APDS-9301
|
||||
|
||||
They provide the spectrometry data, UV data and illuminance data, respectively.
|
||||
They all support I2C, AS7265x supports UART in addition.
|
||||
TeraHz depends on three separate sensor boards:
|
||||
- AS7265x spectral chipset
|
||||
- VEML6075 UV sensor
|
||||
- APDS-9301 lux-meter
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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 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.
|
||||
|
||||
## UART sensor
|
||||
<<<<<<< HEAD
|
||||
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
|
||||
computer's Rx line and vice versa.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
# 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)}'`
|
||||
sed "/ssid=.*/s/ssid=.*/ssid=$ssid/" hostapd.conf > newconf
|
||||
mv newconf hostapd.conf
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
# install.sh - install TeraHz onto a Raspbian or DietPi installation
|
||||
cd `dirname $0`
|
||||
|
||||
apt -y update
|
||||
apt -y full-upgrade
|
||||
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 lighttpd/ /etc
|
||||
chmod +rx /etc/hostapd/edit_ssid.sh
|
||||
cp dnsmasq.conf /etc
|
||||
cp rc.local /etc
|
||||
cp interfaces-terahz /etc/network/interfaces.d/
|
||||
|
||||
cp -R ../frontend/* /var/www/html
|
||||
mkdir -p /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 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.
|
||||
#
|
||||
# By default this script does nothing.
|
||||
/etc/hostapd/edit_ssid.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:
|
||||
- Build guide: 'build.md'
|
||||
- Developer's guide: 'dev-guide.md'
|
||||
- Electrical connections: 'electrical.md'
|
||||
- Electrical guide: 'electrical.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 |