pruned venvs
@@ -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 +0,0 @@
|
||||
python3
|
||||
@@ -1 +0,0 @@
|
||||
/usr/bin/python3
|
||||
@@ -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 |
|
Before Width: | Height: | Size: 6.2 KiB |
@@ -1,554 +0,0 @@
|
||||
/* Put everything inside the global mpl namespace */
|
||||
window.mpl = {};
|
||||
|
||||
|
||||
mpl.get_websocket_type = function() {
|
||||
if (typeof(WebSocket) !== 'undefined') {
|
||||
return WebSocket;
|
||||
} else if (typeof(MozWebSocket) !== 'undefined') {
|
||||
return MozWebSocket;
|
||||
} else {
|
||||
alert('Your browser does not have WebSocket support.' +
|
||||
'Please try Chrome, Safari or Firefox ≥ 6. ' +
|
||||
'Firefox 4 and 5 are also supported but you ' +
|
||||
'have to enable WebSockets in about:config.');
|
||||
};
|
||||
}
|
||||
|
||||
mpl.figure = function(figure_id, websocket, ondownload, parent_element) {
|
||||
this.id = figure_id;
|
||||
|
||||
this.ws = websocket;
|
||||
|
||||
this.supports_binary = (this.ws.binaryType != undefined);
|
||||
|
||||
if (!this.supports_binary) {
|
||||
var warnings = document.getElementById("mpl-warnings");
|
||||
if (warnings) {
|
||||
warnings.style.display = 'block';
|
||||
warnings.textContent = (
|
||||
"This browser does not support binary websocket messages. " +
|
||||
"Performance may be slow.");
|
||||
}
|
||||
}
|
||||
|
||||
this.imageObj = new Image();
|
||||
|
||||
this.context = undefined;
|
||||
this.message = undefined;
|
||||
this.canvas = undefined;
|
||||
this.rubberband_canvas = undefined;
|
||||
this.rubberband_context = undefined;
|
||||
this.format_dropdown = undefined;
|
||||
|
||||
this.image_mode = 'full';
|
||||
|
||||
this.root = $('<div/>');
|
||||
this._root_extra_style(this.root)
|
||||
this.root.attr('style', 'display: inline-block');
|
||||
|
||||
$(parent_element).append(this.root);
|
||||
|
||||
this._init_header(this);
|
||||
this._init_canvas(this);
|
||||
this._init_toolbar(this);
|
||||
|
||||
var fig = this;
|
||||
|
||||
this.waiting = false;
|
||||
|
||||
this.ws.onopen = function () {
|
||||
fig.send_message("supports_binary", {value: fig.supports_binary});
|
||||
fig.send_message("send_image_mode", {});
|
||||
if (mpl.ratio != 1) {
|
||||
fig.send_message("set_dpi_ratio", {'dpi_ratio': mpl.ratio});
|
||||
}
|
||||
fig.send_message("refresh", {});
|
||||
}
|
||||
|
||||
this.imageObj.onload = function() {
|
||||
if (fig.image_mode == 'full') {
|
||||
// Full images could contain transparency (where diff images
|
||||
// almost always do), so we need to clear the canvas so that
|
||||
// there is no ghosting.
|
||||
fig.context.clearRect(0, 0, fig.canvas.width, fig.canvas.height);
|
||||
}
|
||||
fig.context.drawImage(fig.imageObj, 0, 0);
|
||||
};
|
||||
|
||||
this.imageObj.onunload = function() {
|
||||
fig.ws.close();
|
||||
}
|
||||
|
||||
this.ws.onmessage = this._make_on_message_function(this);
|
||||
|
||||
this.ondownload = ondownload;
|
||||
}
|
||||
|
||||
mpl.figure.prototype._init_header = function() {
|
||||
var titlebar = $(
|
||||
'<div class="ui-dialog-titlebar ui-widget-header ui-corner-all ' +
|
||||
'ui-helper-clearfix"/>');
|
||||
var titletext = $(
|
||||
'<div class="ui-dialog-title" style="width: 100%; ' +
|
||||
'text-align: center; padding: 3px;"/>');
|
||||
titlebar.append(titletext)
|
||||
this.root.append(titlebar);
|
||||
this.header = titletext[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
mpl.figure.prototype._canvas_extra_style = function(canvas_div) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
mpl.figure.prototype._root_extra_style = function(canvas_div) {
|
||||
|
||||
}
|
||||
|
||||
mpl.figure.prototype._init_canvas = function() {
|
||||
var fig = this;
|
||||
|
||||
var canvas_div = $('<div/>');
|
||||
|
||||
canvas_div.attr('style', 'position: relative; clear: both; outline: 0');
|
||||
|
||||
function canvas_keyboard_event(event) {
|
||||
return fig.key_event(event, event['data']);
|
||||
}
|
||||
|
||||
canvas_div.keydown('key_press', canvas_keyboard_event);
|
||||
canvas_div.keyup('key_release', canvas_keyboard_event);
|
||||
this.canvas_div = canvas_div
|
||||
this._canvas_extra_style(canvas_div)
|
||||
this.root.append(canvas_div);
|
||||
|
||||
var canvas = $('<canvas/>');
|
||||
canvas.addClass('mpl-canvas');
|
||||
canvas.attr('style', "left: 0; top: 0; z-index: 0; outline: 0")
|
||||
|
||||
this.canvas = canvas[0];
|
||||
this.context = canvas[0].getContext("2d");
|
||||
|
||||
var backingStore = this.context.backingStorePixelRatio ||
|
||||
this.context.webkitBackingStorePixelRatio ||
|
||||
this.context.mozBackingStorePixelRatio ||
|
||||
this.context.msBackingStorePixelRatio ||
|
||||
this.context.oBackingStorePixelRatio ||
|
||||
this.context.backingStorePixelRatio || 1;
|
||||
|
||||
mpl.ratio = (window.devicePixelRatio || 1) / backingStore;
|
||||
|
||||
var rubberband = $('<canvas/>');
|
||||
rubberband.attr('style', "position: absolute; left: 0; top: 0; z-index: 1;")
|
||||
|
||||
var pass_mouse_events = true;
|
||||
|
||||
canvas_div.resizable({
|
||||
start: function(event, ui) {
|
||||
pass_mouse_events = false;
|
||||
},
|
||||
resize: function(event, ui) {
|
||||
fig.request_resize(ui.size.width, ui.size.height);
|
||||
},
|
||||
stop: function(event, ui) {
|
||||
pass_mouse_events = true;
|
||||
fig.request_resize(ui.size.width, ui.size.height);
|
||||
},
|
||||
});
|
||||
|
||||
function mouse_event_fn(event) {
|
||||
if (pass_mouse_events)
|
||||
return fig.mouse_event(event, event['data']);
|
||||
}
|
||||
|
||||
rubberband.mousedown('button_press', mouse_event_fn);
|
||||
rubberband.mouseup('button_release', mouse_event_fn);
|
||||
// Throttle sequential mouse events to 1 every 20ms.
|
||||
rubberband.mousemove('motion_notify', mouse_event_fn);
|
||||
|
||||
rubberband.mouseenter('figure_enter', mouse_event_fn);
|
||||
rubberband.mouseleave('figure_leave', mouse_event_fn);
|
||||
|
||||
canvas_div.on("wheel", function (event) {
|
||||
event = event.originalEvent;
|
||||
event['data'] = 'scroll'
|
||||
if (event.deltaY < 0) {
|
||||
event.step = 1;
|
||||
} else {
|
||||
event.step = -1;
|
||||
}
|
||||
mouse_event_fn(event);
|
||||
});
|
||||
|
||||
canvas_div.append(canvas);
|
||||
canvas_div.append(rubberband);
|
||||
|
||||
this.rubberband = rubberband;
|
||||
this.rubberband_canvas = rubberband[0];
|
||||
this.rubberband_context = rubberband[0].getContext("2d");
|
||||
this.rubberband_context.strokeStyle = "#000000";
|
||||
|
||||
this._resize_canvas = function(width, height) {
|
||||
// Keep the size of the canvas, canvas container, and rubber band
|
||||
// canvas in synch.
|
||||
canvas_div.css('width', width)
|
||||
canvas_div.css('height', height)
|
||||
|
||||
canvas.attr('width', width * mpl.ratio);
|
||||
canvas.attr('height', height * mpl.ratio);
|
||||
canvas.attr('style', 'width: ' + width + 'px; height: ' + height + 'px;');
|
||||
|
||||
rubberband.attr('width', width);
|
||||
rubberband.attr('height', height);
|
||||
}
|
||||
|
||||
// Set the figure to an initial 600x600px, this will subsequently be updated
|
||||
// upon first draw.
|
||||
this._resize_canvas(600, 600);
|
||||
|
||||
// Disable right mouse context menu.
|
||||
$(this.rubberband_canvas).bind("contextmenu",function(e){
|
||||
return false;
|
||||
});
|
||||
|
||||
function set_focus () {
|
||||
canvas.focus();
|
||||
canvas_div.focus();
|
||||
}
|
||||
|
||||
window.setTimeout(set_focus, 100);
|
||||
}
|
||||
|
||||
mpl.figure.prototype._init_toolbar = function() {
|
||||
var fig = this;
|
||||
|
||||
var nav_element = $('<div/>')
|
||||
nav_element.attr('style', 'width: 100%');
|
||||
this.root.append(nav_element);
|
||||
|
||||
// Define a callback function for later on.
|
||||
function toolbar_event(event) {
|
||||
return fig.toolbar_button_onclick(event['data']);
|
||||
}
|
||||
function toolbar_mouse_event(event) {
|
||||
return fig.toolbar_button_onmouseover(event['data']);
|
||||
}
|
||||
|
||||
for(var toolbar_ind in mpl.toolbar_items) {
|
||||
var name = mpl.toolbar_items[toolbar_ind][0];
|
||||
var tooltip = mpl.toolbar_items[toolbar_ind][1];
|
||||
var image = mpl.toolbar_items[toolbar_ind][2];
|
||||
var method_name = mpl.toolbar_items[toolbar_ind][3];
|
||||
|
||||
if (!name) {
|
||||
// put a spacer in here.
|
||||
continue;
|
||||
}
|
||||
var button = $('<button/>');
|
||||
button.addClass('ui-button ui-widget ui-state-default ui-corner-all ' +
|
||||
'ui-button-icon-only');
|
||||
button.attr('role', 'button');
|
||||
button.attr('aria-disabled', 'false');
|
||||
button.click(method_name, toolbar_event);
|
||||
button.mouseover(tooltip, toolbar_mouse_event);
|
||||
|
||||
var icon_img = $('<span/>');
|
||||
icon_img.addClass('ui-button-icon-primary ui-icon');
|
||||
icon_img.addClass(image);
|
||||
icon_img.addClass('ui-corner-all');
|
||||
|
||||
var tooltip_span = $('<span/>');
|
||||
tooltip_span.addClass('ui-button-text');
|
||||
tooltip_span.html(tooltip);
|
||||
|
||||
button.append(icon_img);
|
||||
button.append(tooltip_span);
|
||||
|
||||
nav_element.append(button);
|
||||
}
|
||||
|
||||
var fmt_picker_span = $('<span/>');
|
||||
|
||||
var fmt_picker = $('<select/>');
|
||||
fmt_picker.addClass('mpl-toolbar-option ui-widget ui-widget-content');
|
||||
fmt_picker_span.append(fmt_picker);
|
||||
nav_element.append(fmt_picker_span);
|
||||
this.format_dropdown = fmt_picker[0];
|
||||
|
||||
for (var ind in mpl.extensions) {
|
||||
var fmt = mpl.extensions[ind];
|
||||
var option = $(
|
||||
'<option/>', {selected: fmt === mpl.default_extension}).html(fmt);
|
||||
fmt_picker.append(option)
|
||||
}
|
||||
|
||||
// Add hover states to the ui-buttons
|
||||
$( ".ui-button" ).hover(
|
||||
function() { $(this).addClass("ui-state-hover");},
|
||||
function() { $(this).removeClass("ui-state-hover");}
|
||||
);
|
||||
|
||||
var status_bar = $('<span class="mpl-message"/>');
|
||||
nav_element.append(status_bar);
|
||||
this.message = status_bar[0];
|
||||
}
|
||||
|
||||
mpl.figure.prototype.request_resize = function(x_pixels, y_pixels) {
|
||||
// Request matplotlib to resize the figure. Matplotlib will then trigger a resize in the client,
|
||||
// which will in turn request a refresh of the image.
|
||||
this.send_message('resize', {'width': x_pixels, 'height': y_pixels});
|
||||
}
|
||||
|
||||
mpl.figure.prototype.send_message = function(type, properties) {
|
||||
properties['type'] = type;
|
||||
properties['figure_id'] = this.id;
|
||||
this.ws.send(JSON.stringify(properties));
|
||||
}
|
||||
|
||||
mpl.figure.prototype.send_draw_message = function() {
|
||||
if (!this.waiting) {
|
||||
this.waiting = true;
|
||||
this.ws.send(JSON.stringify({type: "draw", figure_id: this.id}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
mpl.figure.prototype.handle_save = function(fig, msg) {
|
||||
var format_dropdown = fig.format_dropdown;
|
||||
var format = format_dropdown.options[format_dropdown.selectedIndex].value;
|
||||
fig.ondownload(fig, format);
|
||||
}
|
||||
|
||||
|
||||
mpl.figure.prototype.handle_resize = function(fig, msg) {
|
||||
var size = msg['size'];
|
||||
if (size[0] != fig.canvas.width || size[1] != fig.canvas.height) {
|
||||
fig._resize_canvas(size[0], size[1]);
|
||||
fig.send_message("refresh", {});
|
||||
};
|
||||
}
|
||||
|
||||
mpl.figure.prototype.handle_rubberband = function(fig, msg) {
|
||||
var x0 = msg['x0'] / mpl.ratio;
|
||||
var y0 = (fig.canvas.height - msg['y0']) / mpl.ratio;
|
||||
var x1 = msg['x1'] / mpl.ratio;
|
||||
var y1 = (fig.canvas.height - msg['y1']) / mpl.ratio;
|
||||
x0 = Math.floor(x0) + 0.5;
|
||||
y0 = Math.floor(y0) + 0.5;
|
||||
x1 = Math.floor(x1) + 0.5;
|
||||
y1 = Math.floor(y1) + 0.5;
|
||||
var min_x = Math.min(x0, x1);
|
||||
var min_y = Math.min(y0, y1);
|
||||
var width = Math.abs(x1 - x0);
|
||||
var height = Math.abs(y1 - y0);
|
||||
|
||||
fig.rubberband_context.clearRect(
|
||||
0, 0, fig.canvas.width, fig.canvas.height);
|
||||
|
||||
fig.rubberband_context.strokeRect(min_x, min_y, width, height);
|
||||
}
|
||||
|
||||
mpl.figure.prototype.handle_figure_label = function(fig, msg) {
|
||||
// Updates the figure title.
|
||||
fig.header.textContent = msg['label'];
|
||||
}
|
||||
|
||||
mpl.figure.prototype.handle_cursor = function(fig, msg) {
|
||||
var cursor = msg['cursor'];
|
||||
switch(cursor)
|
||||
{
|
||||
case 0:
|
||||
cursor = 'pointer';
|
||||
break;
|
||||
case 1:
|
||||
cursor = 'default';
|
||||
break;
|
||||
case 2:
|
||||
cursor = 'crosshair';
|
||||
break;
|
||||
case 3:
|
||||
cursor = 'move';
|
||||
break;
|
||||
}
|
||||
fig.rubberband_canvas.style.cursor = cursor;
|
||||
}
|
||||
|
||||
mpl.figure.prototype.handle_message = function(fig, msg) {
|
||||
fig.message.textContent = msg['message'];
|
||||
}
|
||||
|
||||
mpl.figure.prototype.handle_draw = function(fig, msg) {
|
||||
// Request the server to send over a new figure.
|
||||
fig.send_draw_message();
|
||||
}
|
||||
|
||||
mpl.figure.prototype.handle_image_mode = function(fig, msg) {
|
||||
fig.image_mode = msg['mode'];
|
||||
}
|
||||
|
||||
mpl.figure.prototype.updated_canvas_event = function() {
|
||||
// Called whenever the canvas gets updated.
|
||||
this.send_message("ack", {});
|
||||
}
|
||||
|
||||
// A function to construct a web socket function for onmessage handling.
|
||||
// Called in the figure constructor.
|
||||
mpl.figure.prototype._make_on_message_function = function(fig) {
|
||||
return function socket_on_message(evt) {
|
||||
if (evt.data instanceof Blob) {
|
||||
/* FIXME: We get "Resource interpreted as Image but
|
||||
* transferred with MIME type text/plain:" errors on
|
||||
* Chrome. But how to set the MIME type? It doesn't seem
|
||||
* to be part of the websocket stream */
|
||||
evt.data.type = "image/png";
|
||||
|
||||
/* Free the memory for the previous frames */
|
||||
if (fig.imageObj.src) {
|
||||
(window.URL || window.webkitURL).revokeObjectURL(
|
||||
fig.imageObj.src);
|
||||
}
|
||||
|
||||
fig.imageObj.src = (window.URL || window.webkitURL).createObjectURL(
|
||||
evt.data);
|
||||
fig.updated_canvas_event();
|
||||
fig.waiting = false;
|
||||
return;
|
||||
}
|
||||
else if (typeof evt.data === 'string' && evt.data.slice(0, 21) == "data:image/png;base64") {
|
||||
fig.imageObj.src = evt.data;
|
||||
fig.updated_canvas_event();
|
||||
fig.waiting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var msg = JSON.parse(evt.data);
|
||||
var msg_type = msg['type'];
|
||||
|
||||
// Call the "handle_{type}" callback, which takes
|
||||
// the figure and JSON message as its only arguments.
|
||||
try {
|
||||
var callback = fig["handle_" + msg_type];
|
||||
} catch (e) {
|
||||
console.log("No handler for the '" + msg_type + "' message type: ", msg);
|
||||
return;
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
try {
|
||||
// console.log("Handling '" + msg_type + "' message: ", msg);
|
||||
callback(fig, msg);
|
||||
} catch (e) {
|
||||
console.log("Exception inside the 'handler_" + msg_type + "' callback:", e, e.stack, msg);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas
|
||||
mpl.findpos = function(e) {
|
||||
//this section is from http://www.quirksmode.org/js/events_properties.html
|
||||
var targ;
|
||||
if (!e)
|
||||
e = window.event;
|
||||
if (e.target)
|
||||
targ = e.target;
|
||||
else if (e.srcElement)
|
||||
targ = e.srcElement;
|
||||
if (targ.nodeType == 3) // defeat Safari bug
|
||||
targ = targ.parentNode;
|
||||
|
||||
// jQuery normalizes the pageX and pageY
|
||||
// pageX,Y are the mouse positions relative to the document
|
||||
// offset() returns the position of the element relative to the document
|
||||
var x = e.pageX - $(targ).offset().left;
|
||||
var y = e.pageY - $(targ).offset().top;
|
||||
|
||||
return {"x": x, "y": y};
|
||||
};
|
||||
|
||||
/*
|
||||
* return a copy of an object with only non-object keys
|
||||
* we need this to avoid circular references
|
||||
* http://stackoverflow.com/a/24161582/3208463
|
||||
*/
|
||||
function simpleKeys (original) {
|
||||
return Object.keys(original).reduce(function (obj, key) {
|
||||
if (typeof original[key] !== 'object')
|
||||
obj[key] = original[key]
|
||||
return obj;
|
||||
}, {});
|
||||
}
|
||||
|
||||
mpl.figure.prototype.mouse_event = function(event, name) {
|
||||
var canvas_pos = mpl.findpos(event)
|
||||
|
||||
if (name === 'button_press')
|
||||
{
|
||||
this.canvas.focus();
|
||||
this.canvas_div.focus();
|
||||
}
|
||||
|
||||
var x = canvas_pos.x * mpl.ratio;
|
||||
var y = canvas_pos.y * mpl.ratio;
|
||||
|
||||
this.send_message(name, {x: x, y: y, button: event.button,
|
||||
step: event.step,
|
||||
guiEvent: simpleKeys(event)});
|
||||
|
||||
/* This prevents the web browser from automatically changing to
|
||||
* the text insertion cursor when the button is pressed. We want
|
||||
* to control all of the cursor setting manually through the
|
||||
* 'cursor' event from matplotlib */
|
||||
event.preventDefault();
|
||||
return false;
|
||||
}
|
||||
|
||||
mpl.figure.prototype._key_event_extra = function(event, name) {
|
||||
// Handle any extra behaviour associated with a key event
|
||||
}
|
||||
|
||||
mpl.figure.prototype.key_event = function(event, name) {
|
||||
|
||||
// Prevent repeat events
|
||||
if (name == 'key_press')
|
||||
{
|
||||
if (event.which === this._key)
|
||||
return;
|
||||
else
|
||||
this._key = event.which;
|
||||
}
|
||||
if (name == 'key_release')
|
||||
this._key = null;
|
||||
|
||||
var value = '';
|
||||
if (event.ctrlKey && event.which != 17)
|
||||
value += "ctrl+";
|
||||
if (event.altKey && event.which != 18)
|
||||
value += "alt+";
|
||||
if (event.shiftKey && event.which != 16)
|
||||
value += "shift+";
|
||||
|
||||
value += 'k';
|
||||
value += event.which.toString();
|
||||
|
||||
this._key_event_extra(event, name);
|
||||
|
||||
this.send_message(name, {key: value,
|
||||
guiEvent: simpleKeys(event)});
|
||||
return false;
|
||||
}
|
||||
|
||||
mpl.figure.prototype.toolbar_button_onclick = function(name) {
|
||||
if (name == 'download') {
|
||||
this.handle_save(this, null);
|
||||
} else {
|
||||
this.send_message("toolbar_button", {name: name});
|
||||
}
|
||||
};
|
||||
|
||||
mpl.figure.prototype.toolbar_button_onmouseover = function(tooltip) {
|
||||
this.message.textContent = tooltip;
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
/* This .js file contains functions for matplotlib's built-in
|
||||
tornado-based server, that are not relevant when embedding WebAgg
|
||||
in another web application. */
|
||||
|
||||
function mpl_ondownload(figure, format) {
|
||||
window.open(figure.id + '/download.' + format, '_blank');
|
||||
}
|
||||
@@ -1,211 +0,0 @@
|
||||
var comm_websocket_adapter = function(comm) {
|
||||
// Create a "websocket"-like object which calls the given IPython comm
|
||||
// object with the appropriate methods. Currently this is a non binary
|
||||
// socket, so there is still some room for performance tuning.
|
||||
var ws = {};
|
||||
|
||||
ws.close = function() {
|
||||
comm.close()
|
||||
};
|
||||
ws.send = function(m) {
|
||||
//console.log('sending', m);
|
||||
comm.send(m);
|
||||
};
|
||||
// Register the callback with on_msg.
|
||||
comm.on_msg(function(msg) {
|
||||
//console.log('receiving', msg['content']['data'], msg);
|
||||
// Pass the mpl event to the overridden (by mpl) onmessage function.
|
||||
ws.onmessage(msg['content']['data'])
|
||||
});
|
||||
return ws;
|
||||
}
|
||||
|
||||
mpl.mpl_figure_comm = function(comm, msg) {
|
||||
// This is the function which gets called when the mpl process
|
||||
// starts-up an IPython Comm through the "matplotlib" channel.
|
||||
|
||||
var id = msg.content.data.id;
|
||||
// Get hold of the div created by the display call when the Comm
|
||||
// socket was opened in Python.
|
||||
var element = $("#" + id);
|
||||
var ws_proxy = comm_websocket_adapter(comm)
|
||||
|
||||
function ondownload(figure, format) {
|
||||
window.open(figure.imageObj.src);
|
||||
}
|
||||
|
||||
var fig = new mpl.figure(id, ws_proxy,
|
||||
ondownload,
|
||||
element.get(0));
|
||||
|
||||
// Call onopen now - mpl needs it, as it is assuming we've passed it a real
|
||||
// web socket which is closed, not our websocket->open comm proxy.
|
||||
ws_proxy.onopen();
|
||||
|
||||
fig.parent_element = element.get(0);
|
||||
fig.cell_info = mpl.find_output_cell("<div id='" + id + "'></div>");
|
||||
if (!fig.cell_info) {
|
||||
console.error("Failed to find cell for figure", id, fig);
|
||||
return;
|
||||
}
|
||||
|
||||
var output_index = fig.cell_info[2]
|
||||
var cell = fig.cell_info[0];
|
||||
|
||||
};
|
||||
|
||||
mpl.figure.prototype.handle_close = function(fig, msg) {
|
||||
var width = fig.canvas.width/mpl.ratio
|
||||
fig.root.unbind('remove')
|
||||
|
||||
// Update the output cell to use the data from the current canvas.
|
||||
fig.push_to_output();
|
||||
var dataURL = fig.canvas.toDataURL();
|
||||
// Re-enable the keyboard manager in IPython - without this line, in FF,
|
||||
// the notebook keyboard shortcuts fail.
|
||||
IPython.keyboard_manager.enable()
|
||||
$(fig.parent_element).html('<img src="' + dataURL + '" width="' + width + '">');
|
||||
fig.close_ws(fig, msg);
|
||||
}
|
||||
|
||||
mpl.figure.prototype.close_ws = function(fig, msg){
|
||||
fig.send_message('closing', msg);
|
||||
// fig.ws.close()
|
||||
}
|
||||
|
||||
mpl.figure.prototype.push_to_output = function(remove_interactive) {
|
||||
// Turn the data on the canvas into data in the output cell.
|
||||
var width = this.canvas.width/mpl.ratio
|
||||
var dataURL = this.canvas.toDataURL();
|
||||
this.cell_info[1]['text/html'] = '<img src="' + dataURL + '" width="' + width + '">';
|
||||
}
|
||||
|
||||
mpl.figure.prototype.updated_canvas_event = function() {
|
||||
// Tell IPython that the notebook contents must change.
|
||||
IPython.notebook.set_dirty(true);
|
||||
this.send_message("ack", {});
|
||||
var fig = this;
|
||||
// Wait a second, then push the new image to the DOM so
|
||||
// that it is saved nicely (might be nice to debounce this).
|
||||
setTimeout(function () { fig.push_to_output() }, 1000);
|
||||
}
|
||||
|
||||
mpl.figure.prototype._init_toolbar = function() {
|
||||
var fig = this;
|
||||
|
||||
var nav_element = $('<div/>')
|
||||
nav_element.attr('style', 'width: 100%');
|
||||
this.root.append(nav_element);
|
||||
|
||||
// Define a callback function for later on.
|
||||
function toolbar_event(event) {
|
||||
return fig.toolbar_button_onclick(event['data']);
|
||||
}
|
||||
function toolbar_mouse_event(event) {
|
||||
return fig.toolbar_button_onmouseover(event['data']);
|
||||
}
|
||||
|
||||
for(var toolbar_ind in mpl.toolbar_items){
|
||||
var name = mpl.toolbar_items[toolbar_ind][0];
|
||||
var tooltip = mpl.toolbar_items[toolbar_ind][1];
|
||||
var image = mpl.toolbar_items[toolbar_ind][2];
|
||||
var method_name = mpl.toolbar_items[toolbar_ind][3];
|
||||
|
||||
if (!name) { continue; };
|
||||
|
||||
var button = $('<button class="btn btn-default" href="#" title="' + name + '"><i class="fa ' + image + ' fa-lg"></i></button>');
|
||||
button.click(method_name, toolbar_event);
|
||||
button.mouseover(tooltip, toolbar_mouse_event);
|
||||
nav_element.append(button);
|
||||
}
|
||||
|
||||
// Add the status bar.
|
||||
var status_bar = $('<span class="mpl-message" style="text-align:right; float: right;"/>');
|
||||
nav_element.append(status_bar);
|
||||
this.message = status_bar[0];
|
||||
|
||||
// Add the close button to the window.
|
||||
var buttongrp = $('<div class="btn-group inline pull-right"></div>');
|
||||
var button = $('<button class="btn btn-mini btn-primary" href="#" title="Stop Interaction"><i class="fa fa-power-off icon-remove icon-large"></i></button>');
|
||||
button.click(function (evt) { fig.handle_close(fig, {}); } );
|
||||
button.mouseover('Stop Interaction', toolbar_mouse_event);
|
||||
buttongrp.append(button);
|
||||
var titlebar = this.root.find($('.ui-dialog-titlebar'));
|
||||
titlebar.prepend(buttongrp);
|
||||
}
|
||||
|
||||
mpl.figure.prototype._root_extra_style = function(el){
|
||||
var fig = this
|
||||
el.on("remove", function(){
|
||||
fig.close_ws(fig, {});
|
||||
});
|
||||
}
|
||||
|
||||
mpl.figure.prototype._canvas_extra_style = function(el){
|
||||
// this is important to make the div 'focusable
|
||||
el.attr('tabindex', 0)
|
||||
// reach out to IPython and tell the keyboard manager to turn it's self
|
||||
// off when our div gets focus
|
||||
|
||||
// location in version 3
|
||||
if (IPython.notebook.keyboard_manager) {
|
||||
IPython.notebook.keyboard_manager.register_events(el);
|
||||
}
|
||||
else {
|
||||
// location in version 2
|
||||
IPython.keyboard_manager.register_events(el);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mpl.figure.prototype._key_event_extra = function(event, name) {
|
||||
var manager = IPython.notebook.keyboard_manager;
|
||||
if (!manager)
|
||||
manager = IPython.keyboard_manager;
|
||||
|
||||
// Check for shift+enter
|
||||
if (event.shiftKey && event.which == 13) {
|
||||
this.canvas_div.blur();
|
||||
event.shiftKey = false;
|
||||
// Send a "J" for go to next cell
|
||||
event.which = 74;
|
||||
event.keyCode = 74;
|
||||
manager.command_mode();
|
||||
manager.handle_keydown(event);
|
||||
}
|
||||
}
|
||||
|
||||
mpl.figure.prototype.handle_save = function(fig, msg) {
|
||||
fig.ondownload(fig, null);
|
||||
}
|
||||
|
||||
|
||||
mpl.find_output_cell = function(html_output) {
|
||||
// Return the cell and output element which can be found *uniquely* in the notebook.
|
||||
// Note - this is a bit hacky, but it is done because the "notebook_saving.Notebook"
|
||||
// IPython event is triggered only after the cells have been serialised, which for
|
||||
// our purposes (turning an active figure into a static one), is too late.
|
||||
var cells = IPython.notebook.get_cells();
|
||||
var ncells = cells.length;
|
||||
for (var i=0; i<ncells; i++) {
|
||||
var cell = cells[i];
|
||||
if (cell.cell_type === 'code'){
|
||||
for (var j=0; j<cell.output_area.outputs.length; j++) {
|
||||
var data = cell.output_area.outputs[j];
|
||||
if (data.data) {
|
||||
// IPython >= 3 moved mimebundle to data attribute of output
|
||||
data = data.data;
|
||||
}
|
||||
if (data['text/html'] == html_output) {
|
||||
return [cell, data, j];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the function which deals with the matplotlib target/channel.
|
||||
// The kernel may be null if the page has been refreshed.
|
||||
if (IPython.notebook.kernel != null) {
|
||||
IPython.notebook.kernel.comm_manager.register_target('matplotlib', mpl.mpl_figure_comm);
|
||||
}
|
||||
@@ -1,639 +0,0 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from imp import reload"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## UAT for NbAgg backend.\n",
|
||||
"\n",
|
||||
"The first line simply reloads matplotlib, uses the nbagg backend and then reloads the backend, just to ensure we have the latest modification to the backend code. Note: The underlying JavaScript will not be updated by this process, so a refresh of the browser after clearing the output and saving is necessary to clear everything fully."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import matplotlib\n",
|
||||
"reload(matplotlib)\n",
|
||||
"\n",
|
||||
"matplotlib.use('nbagg')\n",
|
||||
"\n",
|
||||
"import matplotlib.backends.backend_nbagg\n",
|
||||
"reload(matplotlib.backends.backend_nbagg)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 1 - Simple figure creation using pyplot\n",
|
||||
"\n",
|
||||
"Should produce a figure window which is interactive with the pan and zoom buttons. (Do not press the close button, but any others may be used)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import matplotlib.backends.backend_webagg_core\n",
|
||||
"reload(matplotlib.backends.backend_webagg_core)\n",
|
||||
"\n",
|
||||
"import matplotlib.pyplot as plt\n",
|
||||
"plt.interactive(False)\n",
|
||||
"\n",
|
||||
"fig1 = plt.figure()\n",
|
||||
"plt.plot(range(10))\n",
|
||||
"\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 2 - Creation of another figure, without the need to do plt.figure.\n",
|
||||
"\n",
|
||||
"As above, a new figure should be created."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.plot([3, 2, 1])\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 3 - Connection info\n",
|
||||
"\n",
|
||||
"The printout should show that there are two figures which have active CommSockets, and no figures pending show."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(matplotlib.backends.backend_nbagg.connection_info())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 4 - Closing figures\n",
|
||||
"\n",
|
||||
"Closing a specific figure instance should turn the figure into a plain image - the UI should have been removed. In this case, scroll back to the first figure and assert this is the case."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.close(fig1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 5 - No show without plt.show in non-interactive mode\n",
|
||||
"\n",
|
||||
"Simply doing a plt.plot should not show a new figure, nor indeed update an existing one (easily verified in UAT 6).\n",
|
||||
"The output should simply be a list of Line2D instances."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.plot(range(10))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 6 - Connection information\n",
|
||||
"\n",
|
||||
"We just created a new figure, but didn't show it. Connection info should no longer have \"Figure 1\" (as we closed it in UAT 4) and should have figure 2 and 3, with Figure 3 without any connections. There should be 1 figure pending."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(matplotlib.backends.backend_nbagg.connection_info())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 7 - Show of previously created figure\n",
|
||||
"\n",
|
||||
"We should be able to show a figure we've previously created. The following should produce two figure windows."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.show()\n",
|
||||
"plt.figure()\n",
|
||||
"plt.plot(range(5))\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 8 - Interactive mode\n",
|
||||
"\n",
|
||||
"In interactive mode, creating a line should result in a figure being shown."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.interactive(True)\n",
|
||||
"plt.figure()\n",
|
||||
"plt.plot([3, 2, 1])"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Subsequent lines should be added to the existing figure, rather than creating a new one."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.plot(range(3))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Calling connection_info in interactive mode should not show any pending figures."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"print(matplotlib.backends.backend_nbagg.connection_info())"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Disable interactive mode again."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.interactive(False)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 9 - Multiple shows\n",
|
||||
"\n",
|
||||
"Unlike most of the other matplotlib backends, we may want to see a figure multiple times (with or without synchronisation between the views, though the former is not yet implemented). Assert that plt.gcf().canvas.manager.reshow() results in another figure window which is synchronised upon pan & zoom."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"plt.gcf().canvas.manager.reshow()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 10 - Saving notebook\n",
|
||||
"\n",
|
||||
"Saving the notebook (with CTRL+S or File->Save) should result in the saved notebook having static versions of the figues embedded within. The image should be the last update from user interaction and interactive plotting. (check by converting with ``ipython nbconvert <notebook>``)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 11 - Creation of a new figure on second show\n",
|
||||
"\n",
|
||||
"Create a figure, show it, then create a new axes and show it. The result should be a new figure.\n",
|
||||
"\n",
|
||||
"**BUG: Sometimes this doesn't work - not sure why (@pelson).**"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fig = plt.figure()\n",
|
||||
"plt.axes()\n",
|
||||
"plt.show()\n",
|
||||
"\n",
|
||||
"plt.plot([1, 2, 3])\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 12 - OO interface\n",
|
||||
"\n",
|
||||
"Should produce a new figure and plot it."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from matplotlib.backends.backend_nbagg import new_figure_manager,show\n",
|
||||
"\n",
|
||||
"manager = new_figure_manager(1000)\n",
|
||||
"fig = manager.canvas.figure\n",
|
||||
"ax = fig.add_subplot(1,1,1)\n",
|
||||
"ax.plot([1,2,3])\n",
|
||||
"fig.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## UAT 13 - Animation\n",
|
||||
"\n",
|
||||
"The following should generate an animated line:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import matplotlib.animation as animation\n",
|
||||
"import numpy as np\n",
|
||||
"\n",
|
||||
"fig, ax = plt.subplots()\n",
|
||||
"\n",
|
||||
"x = np.arange(0, 2*np.pi, 0.01) # x-array\n",
|
||||
"line, = ax.plot(x, np.sin(x))\n",
|
||||
"\n",
|
||||
"def animate(i):\n",
|
||||
" line.set_ydata(np.sin(x+i/10.0)) # update the data\n",
|
||||
" return line,\n",
|
||||
"\n",
|
||||
"#Init only required for blitting to give a clean slate.\n",
|
||||
"def init():\n",
|
||||
" line.set_ydata(np.ma.array(x, mask=True))\n",
|
||||
" return line,\n",
|
||||
"\n",
|
||||
"ani = animation.FuncAnimation(fig, animate, np.arange(1, 200), init_func=init,\n",
|
||||
" interval=32., blit=True)\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 14 - Keyboard shortcuts in IPython after close of figure\n",
|
||||
"\n",
|
||||
"After closing the previous figure (with the close button above the figure) the IPython keyboard shortcuts should still function.\n",
|
||||
"\n",
|
||||
"### UAT 15 - Figure face colours\n",
|
||||
"\n",
|
||||
"The nbagg honours all colours apart from that of the figure.patch. The two plots below should produce a figure with a red background. There should be no yellow figure."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import matplotlib\n",
|
||||
"matplotlib.rcParams.update({'figure.facecolor': 'red',\n",
|
||||
" 'savefig.facecolor': 'yellow'})\n",
|
||||
"plt.figure()\n",
|
||||
"plt.plot([3, 2, 1])\n",
|
||||
"\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 16 - Events\n",
|
||||
"\n",
|
||||
"Pressing any keyboard key or mouse button (or scrolling) should cycle the line line while the figure has focus. The figure should have focus by default when it is created and re-gain it by clicking on the canvas. Clicking anywhere outside of the figure should release focus, but moving the mouse out of the figure should not release focus."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import itertools\n",
|
||||
"fig, ax = plt.subplots()\n",
|
||||
"x = np.linspace(0,10,10000)\n",
|
||||
"y = np.sin(x)\n",
|
||||
"ln, = ax.plot(x,y)\n",
|
||||
"evt = []\n",
|
||||
"colors = iter(itertools.cycle(['r', 'g', 'b', 'k', 'c']))\n",
|
||||
"def on_event(event):\n",
|
||||
" if event.name.startswith('key'):\n",
|
||||
" fig.suptitle('%s: %s' % (event.name, event.key))\n",
|
||||
" elif event.name == 'scroll_event':\n",
|
||||
" fig.suptitle('%s: %s' % (event.name, event.step))\n",
|
||||
" else:\n",
|
||||
" fig.suptitle('%s: %s' % (event.name, event.button))\n",
|
||||
" evt.append(event)\n",
|
||||
" ln.set_color(next(colors))\n",
|
||||
" fig.canvas.draw()\n",
|
||||
" fig.canvas.draw_idle()\n",
|
||||
"\n",
|
||||
"fig.canvas.mpl_connect('button_press_event', on_event)\n",
|
||||
"fig.canvas.mpl_connect('button_release_event', on_event)\n",
|
||||
"fig.canvas.mpl_connect('scroll_event', on_event)\n",
|
||||
"fig.canvas.mpl_connect('key_press_event', on_event)\n",
|
||||
"fig.canvas.mpl_connect('key_release_event', on_event)\n",
|
||||
"\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT 17 - Timers\n",
|
||||
"\n",
|
||||
"Single-shot timers follow a completely different code path in the nbagg backend than regular timers (such as those used in the animation example above.) The next set of tests ensures that both \"regular\" and \"single-shot\" timers work properly.\n",
|
||||
"\n",
|
||||
"The following should show a simple clock that updates twice a second:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import time\n",
|
||||
"\n",
|
||||
"fig, ax = plt.subplots()\n",
|
||||
"text = ax.text(0.5, 0.5, '', ha='center')\n",
|
||||
"\n",
|
||||
"def update(text):\n",
|
||||
" text.set(text=time.ctime())\n",
|
||||
" text.axes.figure.canvas.draw()\n",
|
||||
" \n",
|
||||
"timer = fig.canvas.new_timer(500, [(update, [text], {})])\n",
|
||||
"timer.start()\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"However, the following should only update once and then stop:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fig, ax = plt.subplots()\n",
|
||||
"text = ax.text(0.5, 0.5, '', ha='center') \n",
|
||||
"timer = fig.canvas.new_timer(500, [(update, [text], {})])\n",
|
||||
"\n",
|
||||
"timer.single_shot = True\n",
|
||||
"timer.start()\n",
|
||||
"\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"And the next two examples should never show any visible text at all:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fig, ax = plt.subplots()\n",
|
||||
"text = ax.text(0.5, 0.5, '', ha='center')\n",
|
||||
"timer = fig.canvas.new_timer(500, [(update, [text], {})])\n",
|
||||
"\n",
|
||||
"timer.start()\n",
|
||||
"timer.stop()\n",
|
||||
"\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fig, ax = plt.subplots()\n",
|
||||
"text = ax.text(0.5, 0.5, '', ha='center')\n",
|
||||
"timer = fig.canvas.new_timer(500, [(update, [text], {})])\n",
|
||||
"\n",
|
||||
"timer.single_shot = True\n",
|
||||
"timer.start()\n",
|
||||
"timer.stop()\n",
|
||||
"\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"### UAT17 - stopping figure when removed from DOM\n",
|
||||
"\n",
|
||||
"When the div that contains from the figure is removed from the DOM the figure should shut down it's comm, and if the python-side figure has no more active comms, it should destroy the figure. Repeatedly running the cell below should always have the same figure number"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fig, ax = plt.subplots()\n",
|
||||
"ax.plot(range(5))\n",
|
||||
"plt.show()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Running the cell below will re-show the figure. After this, re-running the cell above should result in a new figure number."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"fig.canvas.manager.reshow()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": true
|
||||
},
|
||||
"outputs": [],
|
||||
"source": []
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.4.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 0
|
||||
}
|
||||
@@ -1,30 +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>
|
||||
$(document).ready(
|
||||
function() {
|
||||
var websocket_type = mpl.get_websocket_type();
|
||||
var websocket = new websocket_type(
|
||||
"{{ ws_uri }}" + {{ repr(str(fig_id)) }} + "/ws");
|
||||
var fig = new mpl.figure(
|
||||
{{repr(str(fig_id))}}, websocket, mpl_ondownload, $('div#figure'));
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<title>matplotlib</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="mpl-warnings" class="mpl-warnings"></div>
|
||||
<div id="figure" style="margin: 10px 10px;"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,31 +0,0 @@
|
||||
"""
|
||||
MS Windows-specific helper for the TkAgg backend.
|
||||
|
||||
With rcParams['tk.window_focus'] default of False, it is
|
||||
effectively disabled.
|
||||
|
||||
It uses a tiny C++ extension module to access MS Win functions.
|
||||
|
||||
This module is deprecated and will be removed in version 3.2
|
||||
"""
|
||||
|
||||
from matplotlib import rcParams, cbook
|
||||
|
||||
cbook.warn_deprecated('3.0', obj_type='module', name='backends.windowing')
|
||||
|
||||
try:
|
||||
if not rcParams['tk.window_focus']:
|
||||
raise ImportError
|
||||
from matplotlib._windowing import GetForegroundWindow, SetForegroundWindow
|
||||
except ImportError:
|
||||
def GetForegroundWindow():
|
||||
return 0
|
||||
def SetForegroundWindow(hwnd):
|
||||
pass
|
||||
|
||||
class FocusManager(object):
|
||||
def __init__(self):
|
||||
self._shellWindow = GetForegroundWindow()
|
||||
|
||||
def __del__(self):
|
||||
SetForegroundWindow(self._shellWindow)
|
||||
@@ -1,36 +0,0 @@
|
||||
"""
|
||||
A wx API adapter to hide differences between wxPython classic and phoenix.
|
||||
|
||||
It is assumed that the user code is selecting what version it wants to use,
|
||||
here we just ensure that it meets the minimum required by matplotlib.
|
||||
|
||||
For an example see embedding_in_wx2.py
|
||||
"""
|
||||
import wx
|
||||
|
||||
from .. import cbook
|
||||
from .backend_wx import RendererWx
|
||||
|
||||
|
||||
cbook.warn_deprecated("3.0", "{} is deprecated.".format(__name__))
|
||||
|
||||
backend_version = wx.VERSION_STRING
|
||||
is_phoenix = 'phoenix' in wx.PlatformInfo
|
||||
|
||||
fontweights = RendererWx.fontweights
|
||||
fontangles = RendererWx.fontangles
|
||||
fontnames = RendererWx.fontnames
|
||||
|
||||
dashd_wx = {'solid': wx.PENSTYLE_SOLID,
|
||||
'dashed': wx.PENSTYLE_SHORT_DASH,
|
||||
'dashdot': wx.PENSTYLE_DOT_DASH,
|
||||
'dotted': wx.PENSTYLE_DOT}
|
||||
|
||||
# functions changes
|
||||
BitmapFromBuffer = wx.Bitmap.FromBufferRGBA
|
||||
EmptyBitmap = wx.Bitmap
|
||||
EmptyImage = wx.Image
|
||||
Cursor = wx.Cursor
|
||||
EventLoop = wx.GUIEventLoop
|
||||
NamedColour = wx.Colour
|
||||
StockCursor = wx.Cursor
|
||||
@@ -1,490 +0,0 @@
|
||||
"""
|
||||
A module providing some utility functions regarding bezier path manipulation.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
from matplotlib.path import Path
|
||||
|
||||
|
||||
class NonIntersectingPathException(ValueError):
|
||||
pass
|
||||
|
||||
# some functions
|
||||
|
||||
|
||||
def get_intersection(cx1, cy1, cos_t1, sin_t1,
|
||||
cx2, cy2, cos_t2, sin_t2):
|
||||
""" return a intersecting point between a line through (cx1, cy1)
|
||||
and having angle t1 and a line through (cx2, cy2) and angle t2.
|
||||
"""
|
||||
|
||||
# line1 => sin_t1 * (x - cx1) - cos_t1 * (y - cy1) = 0.
|
||||
# line1 => sin_t1 * x + cos_t1 * y = sin_t1*cx1 - cos_t1*cy1
|
||||
|
||||
line1_rhs = sin_t1 * cx1 - cos_t1 * cy1
|
||||
line2_rhs = sin_t2 * cx2 - cos_t2 * cy2
|
||||
|
||||
# rhs matrix
|
||||
a, b = sin_t1, -cos_t1
|
||||
c, d = sin_t2, -cos_t2
|
||||
|
||||
ad_bc = a * d - b * c
|
||||
if np.abs(ad_bc) < 1.0e-12:
|
||||
raise ValueError("Given lines do not intersect. Please verify that "
|
||||
"the angles are not equal or differ by 180 degrees.")
|
||||
|
||||
# rhs_inverse
|
||||
a_, b_ = d, -b
|
||||
c_, d_ = -c, a
|
||||
a_, b_, c_, d_ = [k / ad_bc for k in [a_, b_, c_, d_]]
|
||||
|
||||
x = a_ * line1_rhs + b_ * line2_rhs
|
||||
y = c_ * line1_rhs + d_ * line2_rhs
|
||||
|
||||
return x, y
|
||||
|
||||
|
||||
def get_normal_points(cx, cy, cos_t, sin_t, length):
|
||||
"""
|
||||
For a line passing through (*cx*, *cy*) and having a angle *t*, return
|
||||
locations of the two points located along its perpendicular line at the
|
||||
distance of *length*.
|
||||
"""
|
||||
|
||||
if length == 0.:
|
||||
return cx, cy, cx, cy
|
||||
|
||||
cos_t1, sin_t1 = sin_t, -cos_t
|
||||
cos_t2, sin_t2 = -sin_t, cos_t
|
||||
|
||||
x1, y1 = length * cos_t1 + cx, length * sin_t1 + cy
|
||||
x2, y2 = length * cos_t2 + cx, length * sin_t2 + cy
|
||||
|
||||
return x1, y1, x2, y2
|
||||
|
||||
|
||||
# BEZIER routines
|
||||
|
||||
# subdividing bezier curve
|
||||
# http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-sub.html
|
||||
|
||||
|
||||
def _de_casteljau1(beta, t):
|
||||
next_beta = beta[:-1] * (1 - t) + beta[1:] * t
|
||||
return next_beta
|
||||
|
||||
|
||||
def split_de_casteljau(beta, t):
|
||||
"""split a bezier segment defined by its controlpoints *beta*
|
||||
into two separate segment divided at *t* and return their control points.
|
||||
|
||||
"""
|
||||
beta = np.asarray(beta)
|
||||
beta_list = [beta]
|
||||
while True:
|
||||
beta = _de_casteljau1(beta, t)
|
||||
beta_list.append(beta)
|
||||
if len(beta) == 1:
|
||||
break
|
||||
left_beta = [beta[0] for beta in beta_list]
|
||||
right_beta = [beta[-1] for beta in reversed(beta_list)]
|
||||
|
||||
return left_beta, right_beta
|
||||
|
||||
|
||||
# FIXME spelling mistake in the name of the parameter ``tolerence``
|
||||
def find_bezier_t_intersecting_with_closedpath(bezier_point_at_t,
|
||||
inside_closedpath,
|
||||
t0=0., t1=1., tolerence=0.01):
|
||||
""" Find a parameter t0 and t1 of the given bezier path which
|
||||
bounds the intersecting points with a provided closed
|
||||
path(*inside_closedpath*). Search starts from *t0* and *t1* and it
|
||||
uses a simple bisecting algorithm therefore one of the end point
|
||||
must be inside the path while the orther doesn't. The search stop
|
||||
when |t0-t1| gets smaller than the given tolerence.
|
||||
value for
|
||||
|
||||
- bezier_point_at_t : a function which returns x, y coordinates at *t*
|
||||
|
||||
- inside_closedpath : return True if the point is inside the path
|
||||
|
||||
"""
|
||||
# inside_closedpath : function
|
||||
|
||||
start = bezier_point_at_t(t0)
|
||||
end = bezier_point_at_t(t1)
|
||||
|
||||
start_inside = inside_closedpath(start)
|
||||
end_inside = inside_closedpath(end)
|
||||
|
||||
if start_inside == end_inside and start != end:
|
||||
raise NonIntersectingPathException(
|
||||
"Both points are on the same side of the closed path")
|
||||
|
||||
while True:
|
||||
|
||||
# return if the distance is smaller than the tolerence
|
||||
if np.hypot(start[0] - end[0], start[1] - end[1]) < tolerence:
|
||||
return t0, t1
|
||||
|
||||
# calculate the middle point
|
||||
middle_t = 0.5 * (t0 + t1)
|
||||
middle = bezier_point_at_t(middle_t)
|
||||
middle_inside = inside_closedpath(middle)
|
||||
|
||||
if start_inside ^ middle_inside:
|
||||
t1 = middle_t
|
||||
end = middle
|
||||
end_inside = middle_inside
|
||||
else:
|
||||
t0 = middle_t
|
||||
start = middle
|
||||
start_inside = middle_inside
|
||||
|
||||
|
||||
class BezierSegment(object):
|
||||
"""
|
||||
A simple class of a 2-dimensional bezier segment
|
||||
"""
|
||||
|
||||
# Higher order bezier lines can be supported by simplying adding
|
||||
# corresponding values.
|
||||
_binom_coeff = {1: np.array([1., 1.]),
|
||||
2: np.array([1., 2., 1.]),
|
||||
3: np.array([1., 3., 3., 1.])}
|
||||
|
||||
def __init__(self, control_points):
|
||||
"""
|
||||
*control_points* : location of contol points. It needs have a
|
||||
shpae of n * 2, where n is the order of the bezier line. 1<=
|
||||
n <= 3 is supported.
|
||||
"""
|
||||
_o = len(control_points)
|
||||
self._orders = np.arange(_o)
|
||||
|
||||
_coeff = BezierSegment._binom_coeff[_o - 1]
|
||||
xx, yy = np.asarray(control_points).T
|
||||
self._px = xx * _coeff
|
||||
self._py = yy * _coeff
|
||||
|
||||
def point_at_t(self, t):
|
||||
"evaluate a point at t"
|
||||
tt = ((1 - t) ** self._orders)[::-1] * t ** self._orders
|
||||
_x = np.dot(tt, self._px)
|
||||
_y = np.dot(tt, self._py)
|
||||
return _x, _y
|
||||
|
||||
|
||||
def split_bezier_intersecting_with_closedpath(bezier,
|
||||
inside_closedpath,
|
||||
tolerence=0.01):
|
||||
|
||||
"""
|
||||
bezier : control points of the bezier segment
|
||||
inside_closedpath : a function which returns true if the point is inside
|
||||
the path
|
||||
"""
|
||||
|
||||
bz = BezierSegment(bezier)
|
||||
bezier_point_at_t = bz.point_at_t
|
||||
|
||||
t0, t1 = find_bezier_t_intersecting_with_closedpath(bezier_point_at_t,
|
||||
inside_closedpath,
|
||||
tolerence=tolerence)
|
||||
|
||||
_left, _right = split_de_casteljau(bezier, (t0 + t1) / 2.)
|
||||
return _left, _right
|
||||
|
||||
|
||||
def find_r_to_boundary_of_closedpath(inside_closedpath, xy,
|
||||
cos_t, sin_t,
|
||||
rmin=0., rmax=1., tolerence=0.01):
|
||||
"""
|
||||
Find a radius r (centered at *xy*) between *rmin* and *rmax* at
|
||||
which it intersect with the path.
|
||||
|
||||
inside_closedpath : function
|
||||
cx, cy : center
|
||||
cos_t, sin_t : cosine and sine for the angle
|
||||
rmin, rmax :
|
||||
"""
|
||||
|
||||
cx, cy = xy
|
||||
|
||||
def _f(r):
|
||||
return cos_t * r + cx, sin_t * r + cy
|
||||
|
||||
find_bezier_t_intersecting_with_closedpath(_f, inside_closedpath,
|
||||
t0=rmin, t1=rmax,
|
||||
tolerence=tolerence)
|
||||
|
||||
# matplotlib specific
|
||||
|
||||
|
||||
def split_path_inout(path, inside, tolerence=0.01, reorder_inout=False):
|
||||
""" divide a path into two segment at the point where inside(x, y)
|
||||
becomes False.
|
||||
"""
|
||||
|
||||
path_iter = path.iter_segments()
|
||||
|
||||
ctl_points, command = next(path_iter)
|
||||
begin_inside = inside(ctl_points[-2:]) # true if begin point is inside
|
||||
|
||||
ctl_points_old = ctl_points
|
||||
|
||||
concat = np.concatenate
|
||||
|
||||
iold = 0
|
||||
i = 1
|
||||
|
||||
for ctl_points, command in path_iter:
|
||||
iold = i
|
||||
i += len(ctl_points) // 2
|
||||
if inside(ctl_points[-2:]) != begin_inside:
|
||||
bezier_path = concat([ctl_points_old[-2:], ctl_points])
|
||||
break
|
||||
ctl_points_old = ctl_points
|
||||
else:
|
||||
raise ValueError("The path does not intersect with the patch")
|
||||
|
||||
bp = bezier_path.reshape((-1, 2))
|
||||
left, right = split_bezier_intersecting_with_closedpath(
|
||||
bp, inside, tolerence)
|
||||
if len(left) == 2:
|
||||
codes_left = [Path.LINETO]
|
||||
codes_right = [Path.MOVETO, Path.LINETO]
|
||||
elif len(left) == 3:
|
||||
codes_left = [Path.CURVE3, Path.CURVE3]
|
||||
codes_right = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
|
||||
elif len(left) == 4:
|
||||
codes_left = [Path.CURVE4, Path.CURVE4, Path.CURVE4]
|
||||
codes_right = [Path.MOVETO, Path.CURVE4, Path.CURVE4, Path.CURVE4]
|
||||
else:
|
||||
raise AssertionError("This should never be reached")
|
||||
|
||||
verts_left = left[1:]
|
||||
verts_right = right[:]
|
||||
|
||||
if path.codes is None:
|
||||
path_in = Path(concat([path.vertices[:i], verts_left]))
|
||||
path_out = Path(concat([verts_right, path.vertices[i:]]))
|
||||
|
||||
else:
|
||||
path_in = Path(concat([path.vertices[:iold], verts_left]),
|
||||
concat([path.codes[:iold], codes_left]))
|
||||
|
||||
path_out = Path(concat([verts_right, path.vertices[i:]]),
|
||||
concat([codes_right, path.codes[i:]]))
|
||||
|
||||
if reorder_inout and begin_inside is False:
|
||||
path_in, path_out = path_out, path_in
|
||||
|
||||
return path_in, path_out
|
||||
|
||||
|
||||
def inside_circle(cx, cy, r):
|
||||
r2 = r ** 2
|
||||
|
||||
def _f(xy):
|
||||
x, y = xy
|
||||
return (x - cx) ** 2 + (y - cy) ** 2 < r2
|
||||
return _f
|
||||
|
||||
|
||||
# quadratic bezier lines
|
||||
|
||||
def get_cos_sin(x0, y0, x1, y1):
|
||||
dx, dy = x1 - x0, y1 - y0
|
||||
d = (dx * dx + dy * dy) ** .5
|
||||
# Account for divide by zero
|
||||
if d == 0:
|
||||
return 0.0, 0.0
|
||||
return dx / d, dy / d
|
||||
|
||||
|
||||
def check_if_parallel(dx1, dy1, dx2, dy2, tolerence=1.e-5):
|
||||
""" returns
|
||||
* 1 if two lines are parralel in same direction
|
||||
* -1 if two lines are parralel in opposite direction
|
||||
* 0 otherwise
|
||||
"""
|
||||
theta1 = np.arctan2(dx1, dy1)
|
||||
theta2 = np.arctan2(dx2, dy2)
|
||||
dtheta = np.abs(theta1 - theta2)
|
||||
if dtheta < tolerence:
|
||||
return 1
|
||||
elif np.abs(dtheta - np.pi) < tolerence:
|
||||
return -1
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_parallels(bezier2, width):
|
||||
"""
|
||||
Given the quadratic bezier control points *bezier2*, returns
|
||||
control points of quadratic bezier lines roughly parallel to given
|
||||
one separated by *width*.
|
||||
"""
|
||||
|
||||
# The parallel bezier lines are constructed by following ways.
|
||||
# c1 and c2 are control points representing the begin and end of the
|
||||
# bezier line.
|
||||
# cm is the middle point
|
||||
|
||||
c1x, c1y = bezier2[0]
|
||||
cmx, cmy = bezier2[1]
|
||||
c2x, c2y = bezier2[2]
|
||||
|
||||
parallel_test = check_if_parallel(c1x - cmx, c1y - cmy,
|
||||
cmx - c2x, cmy - c2y)
|
||||
|
||||
if parallel_test == -1:
|
||||
warnings.warn(
|
||||
"Lines do not intersect. A straight line is used instead.")
|
||||
cos_t1, sin_t1 = get_cos_sin(c1x, c1y, c2x, c2y)
|
||||
cos_t2, sin_t2 = cos_t1, sin_t1
|
||||
else:
|
||||
# t1 and t2 is the angle between c1 and cm, cm, c2. They are
|
||||
# also a angle of the tangential line of the path at c1 and c2
|
||||
cos_t1, sin_t1 = get_cos_sin(c1x, c1y, cmx, cmy)
|
||||
cos_t2, sin_t2 = get_cos_sin(cmx, cmy, c2x, c2y)
|
||||
|
||||
# find c1_left, c1_right which are located along the lines
|
||||
# through c1 and perpendicular to the tangential lines of the
|
||||
# bezier path at a distance of width. Same thing for c2_left and
|
||||
# c2_right with respect to c2.
|
||||
c1x_left, c1y_left, c1x_right, c1y_right = (
|
||||
get_normal_points(c1x, c1y, cos_t1, sin_t1, width)
|
||||
)
|
||||
c2x_left, c2y_left, c2x_right, c2y_right = (
|
||||
get_normal_points(c2x, c2y, cos_t2, sin_t2, width)
|
||||
)
|
||||
|
||||
# find cm_left which is the intersectng point of a line through
|
||||
# c1_left with angle t1 and a line through c2_left with angle
|
||||
# t2. Same with cm_right.
|
||||
if parallel_test != 0:
|
||||
# a special case for a straight line, i.e., angle between two
|
||||
# lines are smaller than some (arbitrtay) value.
|
||||
cmx_left, cmy_left = (
|
||||
0.5 * (c1x_left + c2x_left), 0.5 * (c1y_left + c2y_left)
|
||||
)
|
||||
cmx_right, cmy_right = (
|
||||
0.5 * (c1x_right + c2x_right), 0.5 * (c1y_right + c2y_right)
|
||||
)
|
||||
else:
|
||||
cmx_left, cmy_left = get_intersection(c1x_left, c1y_left, cos_t1,
|
||||
sin_t1, c2x_left, c2y_left,
|
||||
cos_t2, sin_t2)
|
||||
|
||||
cmx_right, cmy_right = get_intersection(c1x_right, c1y_right, cos_t1,
|
||||
sin_t1, c2x_right, c2y_right,
|
||||
cos_t2, sin_t2)
|
||||
|
||||
# the parallel bezier lines are created with control points of
|
||||
# [c1_left, cm_left, c2_left] and [c1_right, cm_right, c2_right]
|
||||
path_left = [(c1x_left, c1y_left),
|
||||
(cmx_left, cmy_left),
|
||||
(c2x_left, c2y_left)]
|
||||
path_right = [(c1x_right, c1y_right),
|
||||
(cmx_right, cmy_right),
|
||||
(c2x_right, c2y_right)]
|
||||
|
||||
return path_left, path_right
|
||||
|
||||
|
||||
def find_control_points(c1x, c1y, mmx, mmy, c2x, c2y):
|
||||
""" Find control points of the bezier line through c1, mm, c2. We
|
||||
simply assume that c1, mm, c2 which have parametric value 0, 0.5, and 1.
|
||||
"""
|
||||
|
||||
cmx = .5 * (4 * mmx - (c1x + c2x))
|
||||
cmy = .5 * (4 * mmy - (c1y + c2y))
|
||||
|
||||
return [(c1x, c1y), (cmx, cmy), (c2x, c2y)]
|
||||
|
||||
|
||||
def make_wedged_bezier2(bezier2, width, w1=1., wm=0.5, w2=0.):
|
||||
"""
|
||||
Being similar to get_parallels, returns control points of two quadrativ
|
||||
bezier lines having a width roughly parallel to given one separated by
|
||||
*width*.
|
||||
"""
|
||||
|
||||
# c1, cm, c2
|
||||
c1x, c1y = bezier2[0]
|
||||
cmx, cmy = bezier2[1]
|
||||
c3x, c3y = bezier2[2]
|
||||
|
||||
# t1 and t2 is the angle between c1 and cm, cm, c3.
|
||||
# They are also a angle of the tangential line of the path at c1 and c3
|
||||
cos_t1, sin_t1 = get_cos_sin(c1x, c1y, cmx, cmy)
|
||||
cos_t2, sin_t2 = get_cos_sin(cmx, cmy, c3x, c3y)
|
||||
|
||||
# find c1_left, c1_right which are located along the lines
|
||||
# through c1 and perpendicular to the tangential lines of the
|
||||
# bezier path at a distance of width. Same thing for c3_left and
|
||||
# c3_right with respect to c3.
|
||||
c1x_left, c1y_left, c1x_right, c1y_right = (
|
||||
get_normal_points(c1x, c1y, cos_t1, sin_t1, width * w1)
|
||||
)
|
||||
c3x_left, c3y_left, c3x_right, c3y_right = (
|
||||
get_normal_points(c3x, c3y, cos_t2, sin_t2, width * w2)
|
||||
)
|
||||
|
||||
# find c12, c23 and c123 which are middle points of c1-cm, cm-c3 and
|
||||
# c12-c23
|
||||
c12x, c12y = (c1x + cmx) * .5, (c1y + cmy) * .5
|
||||
c23x, c23y = (cmx + c3x) * .5, (cmy + c3y) * .5
|
||||
c123x, c123y = (c12x + c23x) * .5, (c12y + c23y) * .5
|
||||
|
||||
# tangential angle of c123 (angle between c12 and c23)
|
||||
cos_t123, sin_t123 = get_cos_sin(c12x, c12y, c23x, c23y)
|
||||
|
||||
c123x_left, c123y_left, c123x_right, c123y_right = (
|
||||
get_normal_points(c123x, c123y, cos_t123, sin_t123, width * wm)
|
||||
)
|
||||
|
||||
path_left = find_control_points(c1x_left, c1y_left,
|
||||
c123x_left, c123y_left,
|
||||
c3x_left, c3y_left)
|
||||
path_right = find_control_points(c1x_right, c1y_right,
|
||||
c123x_right, c123y_right,
|
||||
c3x_right, c3y_right)
|
||||
|
||||
return path_left, path_right
|
||||
|
||||
|
||||
def make_path_regular(p):
|
||||
"""
|
||||
fill in the codes if None.
|
||||
"""
|
||||
c = p.codes
|
||||
if c is None:
|
||||
c = np.empty(p.vertices.shape[:1], "i")
|
||||
c.fill(Path.LINETO)
|
||||
c[0] = Path.MOVETO
|
||||
|
||||
return Path(p.vertices, c)
|
||||
else:
|
||||
return p
|
||||
|
||||
|
||||
def concatenate_paths(paths):
|
||||
"""
|
||||
concatenate list of paths into a single path.
|
||||
"""
|
||||
|
||||
vertices = []
|
||||
codes = []
|
||||
for p in paths:
|
||||
p = make_path_regular(p)
|
||||
vertices.append(p.vertices)
|
||||
codes.append(p.codes)
|
||||
|
||||
_path = Path(np.concatenate(vertices),
|
||||
np.concatenate(codes))
|
||||
return _path
|
||||
@@ -1,352 +0,0 @@
|
||||
"""
|
||||
This provides several classes used for blocking interaction with figure
|
||||
windows:
|
||||
|
||||
`BlockingInput`
|
||||
Creates a callable object to retrieve events in a blocking way for
|
||||
interactive sessions. Base class of the other classes listed here.
|
||||
|
||||
`BlockingKeyMouseInput`
|
||||
Creates a callable object to retrieve key or mouse clicks in a blocking
|
||||
way for interactive sessions. Used by `waitforbuttonpress`.
|
||||
|
||||
`BlockingMouseInput`
|
||||
Creates a callable object to retrieve mouse clicks in a blocking way for
|
||||
interactive sessions. Used by `ginput`.
|
||||
|
||||
`BlockingContourLabeler`
|
||||
Creates a callable object to retrieve mouse clicks in a blocking way that
|
||||
will then be used to place labels on a `ContourSet`. Used by `clabel`.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from numbers import Integral
|
||||
|
||||
import matplotlib.lines as mlines
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BlockingInput(object):
|
||||
"""Callable for retrieving events in a blocking way."""
|
||||
|
||||
def __init__(self, fig, eventslist=()):
|
||||
self.fig = fig
|
||||
self.eventslist = eventslist
|
||||
|
||||
def on_event(self, event):
|
||||
"""
|
||||
Event handler; will be passed to the current figure to retrieve events.
|
||||
"""
|
||||
# Add a new event to list - using a separate function is overkill for
|
||||
# the base class, but this is consistent with subclasses.
|
||||
self.add_event(event)
|
||||
_log.info("Event %i", len(self.events))
|
||||
|
||||
# This will extract info from events.
|
||||
self.post_event()
|
||||
|
||||
# Check if we have enough events already.
|
||||
if len(self.events) >= self.n > 0:
|
||||
self.fig.canvas.stop_event_loop()
|
||||
|
||||
def post_event(self):
|
||||
"""For baseclass, do nothing but collect events."""
|
||||
|
||||
def cleanup(self):
|
||||
"""Disconnect all callbacks."""
|
||||
for cb in self.callbacks:
|
||||
self.fig.canvas.mpl_disconnect(cb)
|
||||
self.callbacks = []
|
||||
|
||||
def add_event(self, event):
|
||||
"""For base class, this just appends an event to events."""
|
||||
self.events.append(event)
|
||||
|
||||
def pop_event(self, index=-1):
|
||||
"""
|
||||
Remove an event from the event list -- by default, the last.
|
||||
|
||||
Note that this does not check that there are events, much like the
|
||||
normal pop method. If no events exist, this will throw an exception.
|
||||
"""
|
||||
self.events.pop(index)
|
||||
|
||||
pop = pop_event
|
||||
|
||||
def __call__(self, n=1, timeout=30):
|
||||
"""Blocking call to retrieve *n* events."""
|
||||
if not isinstance(n, Integral):
|
||||
raise ValueError("Requires an integer argument")
|
||||
self.n = n
|
||||
self.events = []
|
||||
|
||||
if hasattr(self.fig.canvas, "manager"):
|
||||
# Ensure that the figure is shown, if we are managing it.
|
||||
self.fig.show()
|
||||
# Connect the events to the on_event function call.
|
||||
self.callbacks = [self.fig.canvas.mpl_connect(name, self.on_event)
|
||||
for name in self.eventslist]
|
||||
try:
|
||||
# Start event loop.
|
||||
self.fig.canvas.start_event_loop(timeout=timeout)
|
||||
finally: # Run even on exception like ctrl-c.
|
||||
# Disconnect the callbacks.
|
||||
self.cleanup()
|
||||
# Return the events in this case.
|
||||
return self.events
|
||||
|
||||
|
||||
class BlockingMouseInput(BlockingInput):
|
||||
"""
|
||||
Callable for retrieving mouse clicks in a blocking way.
|
||||
|
||||
This class will also retrieve keypresses and map them to mouse clicks:
|
||||
delete and backspace are like mouse button 3, enter is like mouse button 2
|
||||
and all others are like mouse button 1.
|
||||
"""
|
||||
|
||||
button_add = 1
|
||||
button_pop = 3
|
||||
button_stop = 2
|
||||
|
||||
def __init__(self, fig, mouse_add=1, mouse_pop=3, mouse_stop=2):
|
||||
BlockingInput.__init__(self, fig=fig,
|
||||
eventslist=('button_press_event',
|
||||
'key_press_event'))
|
||||
self.button_add = mouse_add
|
||||
self.button_pop = mouse_pop
|
||||
self.button_stop = mouse_stop
|
||||
|
||||
def post_event(self):
|
||||
"""Process an event."""
|
||||
if len(self.events) == 0:
|
||||
_log.warning("No events yet")
|
||||
elif self.events[-1].name == 'key_press_event':
|
||||
self.key_event()
|
||||
else:
|
||||
self.mouse_event()
|
||||
|
||||
def mouse_event(self):
|
||||
"""Process a mouse click event."""
|
||||
event = self.events[-1]
|
||||
button = event.button
|
||||
if button == self.button_pop:
|
||||
self.mouse_event_pop(event)
|
||||
elif button == self.button_stop:
|
||||
self.mouse_event_stop(event)
|
||||
else:
|
||||
self.mouse_event_add(event)
|
||||
|
||||
def key_event(self):
|
||||
"""
|
||||
Process a key press event, mapping keys to appropriate mouse clicks.
|
||||
"""
|
||||
event = self.events[-1]
|
||||
if event.key is None:
|
||||
# At least in OSX gtk backend some keys return None.
|
||||
return
|
||||
key = event.key.lower()
|
||||
if key in ['backspace', 'delete']:
|
||||
self.mouse_event_pop(event)
|
||||
elif key in ['escape', 'enter']:
|
||||
self.mouse_event_stop(event)
|
||||
else:
|
||||
self.mouse_event_add(event)
|
||||
|
||||
def mouse_event_add(self, event):
|
||||
"""
|
||||
Process an button-1 event (add a click if inside axes).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : `~.backend_bases.MouseEvent`
|
||||
"""
|
||||
if event.inaxes:
|
||||
self.add_click(event)
|
||||
else: # If not a valid click, remove from event list.
|
||||
BlockingInput.pop(self)
|
||||
|
||||
def mouse_event_stop(self, event):
|
||||
"""
|
||||
Process an button-2 event (end blocking input).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : `~.backend_bases.MouseEvent`
|
||||
"""
|
||||
# Remove last event just for cleanliness.
|
||||
BlockingInput.pop(self)
|
||||
# This will exit even if not in infinite mode. This is consistent with
|
||||
# MATLAB and sometimes quite useful, but will require the user to test
|
||||
# how many points were actually returned before using data.
|
||||
self.fig.canvas.stop_event_loop()
|
||||
|
||||
def mouse_event_pop(self, event):
|
||||
"""
|
||||
Process an button-3 event (remove the last click).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : `~.backend_bases.MouseEvent`
|
||||
"""
|
||||
# Remove this last event.
|
||||
BlockingInput.pop(self)
|
||||
# Now remove any existing clicks if possible.
|
||||
if self.events:
|
||||
self.pop(event)
|
||||
|
||||
def add_click(self, event):
|
||||
"""
|
||||
Add the coordinates of an event to the list of clicks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : `~.backend_bases.MouseEvent`
|
||||
"""
|
||||
self.clicks.append((event.xdata, event.ydata))
|
||||
_log.info("input %i: %f, %f",
|
||||
len(self.clicks), event.xdata, event.ydata)
|
||||
# If desired, plot up click.
|
||||
if self.show_clicks:
|
||||
line = mlines.Line2D([event.xdata], [event.ydata],
|
||||
marker='+', color='r')
|
||||
event.inaxes.add_line(line)
|
||||
self.marks.append(line)
|
||||
self.fig.canvas.draw()
|
||||
|
||||
def pop_click(self, event, index=-1):
|
||||
"""
|
||||
Remove a click (by default, the last) from the list of clicks.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : `~.backend_bases.MouseEvent`
|
||||
"""
|
||||
self.clicks.pop(index)
|
||||
if self.show_clicks:
|
||||
self.marks.pop(index).remove()
|
||||
self.fig.canvas.draw()
|
||||
|
||||
def pop(self, event, index=-1):
|
||||
"""
|
||||
Removes a click and the associated event from the list of clicks.
|
||||
|
||||
Defaults to the last click.
|
||||
"""
|
||||
self.pop_click(event, index)
|
||||
BlockingInput.pop(self, index)
|
||||
|
||||
def cleanup(self, event=None):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
event : `~.backend_bases.MouseEvent`, optional
|
||||
Not used
|
||||
"""
|
||||
# Clean the figure.
|
||||
if self.show_clicks:
|
||||
for mark in self.marks:
|
||||
mark.remove()
|
||||
self.marks = []
|
||||
self.fig.canvas.draw()
|
||||
# Call base class to remove callbacks.
|
||||
BlockingInput.cleanup(self)
|
||||
|
||||
def __call__(self, n=1, timeout=30, show_clicks=True):
|
||||
"""
|
||||
Blocking call to retrieve *n* coordinate pairs through mouse clicks.
|
||||
"""
|
||||
self.show_clicks = show_clicks
|
||||
self.clicks = []
|
||||
self.marks = []
|
||||
BlockingInput.__call__(self, n=n, timeout=timeout)
|
||||
return self.clicks
|
||||
|
||||
|
||||
class BlockingContourLabeler(BlockingMouseInput):
|
||||
"""
|
||||
Callable for retrieving mouse clicks and key presses in a blocking way.
|
||||
|
||||
Used to place contour labels.
|
||||
"""
|
||||
|
||||
def __init__(self, cs):
|
||||
self.cs = cs
|
||||
BlockingMouseInput.__init__(self, fig=cs.ax.figure)
|
||||
|
||||
def add_click(self, event):
|
||||
self.button1(event)
|
||||
|
||||
def pop_click(self, event, index=-1):
|
||||
self.button3(event)
|
||||
|
||||
def button1(self, event):
|
||||
"""
|
||||
Process an button-1 event (add a label to a contour).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : `~.backend_bases.MouseEvent`
|
||||
"""
|
||||
# Shorthand
|
||||
if event.inaxes == self.cs.ax:
|
||||
self.cs.add_label_near(event.x, event.y, self.inline,
|
||||
inline_spacing=self.inline_spacing,
|
||||
transform=False)
|
||||
self.fig.canvas.draw()
|
||||
else: # Remove event if not valid
|
||||
BlockingInput.pop(self)
|
||||
|
||||
def button3(self, event):
|
||||
"""
|
||||
Process an button-3 event (remove a label if not in inline mode).
|
||||
|
||||
Unfortunately, if one is doing inline labels, then there is currently
|
||||
no way to fix the broken contour - once humpty-dumpty is broken, he
|
||||
can't be put back together. In inline mode, this does nothing.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
event : `~.backend_bases.MouseEvent`
|
||||
"""
|
||||
if self.inline:
|
||||
pass
|
||||
else:
|
||||
self.cs.pop_label()
|
||||
self.cs.ax.figure.canvas.draw()
|
||||
|
||||
def __call__(self, inline, inline_spacing=5, n=-1, timeout=-1):
|
||||
self.inline = inline
|
||||
self.inline_spacing = inline_spacing
|
||||
BlockingMouseInput.__call__(self, n=n, timeout=timeout,
|
||||
show_clicks=False)
|
||||
|
||||
|
||||
class BlockingKeyMouseInput(BlockingInput):
|
||||
"""
|
||||
Callable for retrieving mouse clicks and key presses in a blocking way.
|
||||
"""
|
||||
|
||||
def __init__(self, fig):
|
||||
BlockingInput.__init__(self, fig=fig, eventslist=(
|
||||
'button_press_event', 'key_press_event'))
|
||||
|
||||
def post_event(self):
|
||||
"""Determine if it is a key event."""
|
||||
if self.events:
|
||||
self.keyormouse = self.events[-1].name == 'key_press_event'
|
||||
else:
|
||||
_log.warning("No events yet.")
|
||||
|
||||
def __call__(self, timeout=30):
|
||||
"""
|
||||
Blocking call to retrieve a single mouse click or key press.
|
||||
|
||||
Returns ``True`` if key press, ``False`` if mouse click, or ``None`` if
|
||||
timed out.
|
||||
"""
|
||||
self.keyormouse = None
|
||||
BlockingInput.__call__(self, n=1, timeout=timeout)
|
||||
|
||||
return self.keyormouse
|
||||
@@ -1,197 +0,0 @@
|
||||
"""
|
||||
Module that allows plotting of string "category" data. i.e.
|
||||
``plot(['d', 'f', 'a'],[1, 2, 3])`` will plot three points with x-axis
|
||||
values of 'd', 'f', 'a'.
|
||||
|
||||
See :doc:`/gallery/lines_bars_and_markers/categorical_variables` for an
|
||||
example.
|
||||
|
||||
The module uses Matplotlib's `matplotlib.units` mechanism to convert from
|
||||
strings to integers, provides a tick locator and formatter, and the
|
||||
class:`.UnitData` that creates and stores the string-to-integer mapping.
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
import itertools
|
||||
|
||||
import numpy as np
|
||||
|
||||
import matplotlib.units as units
|
||||
import matplotlib.ticker as ticker
|
||||
|
||||
|
||||
class StrCategoryConverter(units.ConversionInterface):
|
||||
@staticmethod
|
||||
def convert(value, unit, axis):
|
||||
"""Converts strings in value to floats using
|
||||
mapping information store in the unit object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : string or iterable
|
||||
value or list of values to be converted
|
||||
unit : :class:`.UnitData`
|
||||
object string unit information for value
|
||||
axis : :class:`~matplotlib.Axis.axis`
|
||||
axis on which the converted value is plotted
|
||||
|
||||
Returns
|
||||
-------
|
||||
mapped_ value : float or ndarray[float]
|
||||
|
||||
.. note:: axis is not used in this function
|
||||
"""
|
||||
# dtype = object preserves numerical pass throughs
|
||||
values = np.atleast_1d(np.array(value, dtype=object))
|
||||
|
||||
# pass through sequence of non binary numbers
|
||||
if all((units.ConversionInterface.is_numlike(v) and
|
||||
not isinstance(v, (str, bytes))) for v in values):
|
||||
return np.asarray(values, dtype=float)
|
||||
|
||||
# force an update so it also does type checking
|
||||
unit.update(values)
|
||||
|
||||
str2idx = np.vectorize(unit._mapping.__getitem__,
|
||||
otypes=[float])
|
||||
|
||||
mapped_value = str2idx(values)
|
||||
return mapped_value
|
||||
|
||||
@staticmethod
|
||||
def axisinfo(unit, axis):
|
||||
"""Sets the default axis ticks and labels
|
||||
|
||||
Parameters
|
||||
---------
|
||||
unit : :class:`.UnitData`
|
||||
object string unit information for value
|
||||
axis : :class:`~matplotlib.Axis.axis`
|
||||
axis for which information is being set
|
||||
|
||||
Returns
|
||||
-------
|
||||
:class:~matplotlib.units.AxisInfo~
|
||||
Information to support default tick labeling
|
||||
|
||||
.. note: axis is not used
|
||||
"""
|
||||
# locator and formatter take mapping dict because
|
||||
# args need to be pass by reference for updates
|
||||
majloc = StrCategoryLocator(unit._mapping)
|
||||
majfmt = StrCategoryFormatter(unit._mapping)
|
||||
return units.AxisInfo(majloc=majloc, majfmt=majfmt)
|
||||
|
||||
@staticmethod
|
||||
def default_units(data, axis):
|
||||
"""Sets and updates the :class:`~matplotlib.Axis.axis` units.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : string or iterable of strings
|
||||
axis : :class:`~matplotlib.Axis.axis`
|
||||
axis on which the data is plotted
|
||||
|
||||
Returns
|
||||
-------
|
||||
class:~.UnitData~
|
||||
object storing string to integer mapping
|
||||
"""
|
||||
# the conversion call stack is supposed to be
|
||||
# default_units->axis_info->convert
|
||||
if axis.units is None:
|
||||
axis.set_units(UnitData(data))
|
||||
else:
|
||||
axis.units.update(data)
|
||||
return axis.units
|
||||
|
||||
|
||||
class StrCategoryLocator(ticker.Locator):
|
||||
"""tick at every integer mapping of the string data"""
|
||||
def __init__(self, units_mapping):
|
||||
"""
|
||||
Parameters
|
||||
-----------
|
||||
units_mapping : Dict[str, int]
|
||||
string:integer mapping
|
||||
"""
|
||||
self._units = units_mapping
|
||||
|
||||
def __call__(self):
|
||||
return list(self._units.values())
|
||||
|
||||
def tick_values(self, vmin, vmax):
|
||||
return self()
|
||||
|
||||
|
||||
class StrCategoryFormatter(ticker.Formatter):
|
||||
"""String representation of the data at every tick"""
|
||||
def __init__(self, units_mapping):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
units_mapping : Dict[Str, int]
|
||||
string:integer mapping
|
||||
"""
|
||||
self._units = units_mapping
|
||||
|
||||
def __call__(self, x, pos=None):
|
||||
if pos is None:
|
||||
return ""
|
||||
r_mapping = {v: StrCategoryFormatter._text(k)
|
||||
for k, v in self._units.items()}
|
||||
return r_mapping.get(int(np.round(x)), '')
|
||||
|
||||
@staticmethod
|
||||
def _text(value):
|
||||
"""Converts text values into utf-8 or ascii strings.
|
||||
"""
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode(encoding='utf-8')
|
||||
elif not isinstance(value, str):
|
||||
value = str(value)
|
||||
return value
|
||||
|
||||
|
||||
class UnitData(object):
|
||||
def __init__(self, data=None):
|
||||
"""
|
||||
Create mapping between unique categorical values and integer ids.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: iterable
|
||||
sequence of string values
|
||||
"""
|
||||
self._mapping = OrderedDict()
|
||||
self._counter = itertools.count()
|
||||
if data is not None:
|
||||
self.update(data)
|
||||
|
||||
def update(self, data):
|
||||
"""Maps new values to integer identifiers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: iterable
|
||||
sequence of string values
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
If the value in data is not a string, unicode, bytes type
|
||||
"""
|
||||
data = np.atleast_1d(np.array(data, dtype=object))
|
||||
|
||||
for val in OrderedDict.fromkeys(data):
|
||||
if not isinstance(val, (str, bytes)):
|
||||
raise TypeError("{val!r} is not a string".format(val=val))
|
||||
if val not in self._mapping:
|
||||
self._mapping[val] = next(self._counter)
|
||||
|
||||
|
||||
# Connects the convertor to matplotlib
|
||||
units.registry[str] = StrCategoryConverter()
|
||||
units.registry[np.str_] = StrCategoryConverter()
|
||||
units.registry[bytes] = StrCategoryConverter()
|
||||
units.registry[np.bytes_] = StrCategoryConverter()
|
||||
@@ -1,264 +0,0 @@
|
||||
import functools
|
||||
import textwrap
|
||||
import warnings
|
||||
|
||||
|
||||
class MatplotlibDeprecationWarning(UserWarning):
|
||||
"""
|
||||
A class for issuing deprecation warnings for Matplotlib users.
|
||||
|
||||
In light of the fact that Python builtin DeprecationWarnings are ignored
|
||||
by default as of Python 2.7 (see link below), this class was put in to
|
||||
allow for the signaling of deprecation, but via UserWarnings which are not
|
||||
ignored by default.
|
||||
|
||||
https://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x
|
||||
"""
|
||||
|
||||
|
||||
mplDeprecation = MatplotlibDeprecationWarning
|
||||
"""mplDeprecation is deprecated. Use MatplotlibDeprecationWarning instead."""
|
||||
|
||||
|
||||
def _generate_deprecation_message(
|
||||
since, message='', name='', alternative='', pending=False,
|
||||
obj_type='attribute', addendum='', *, removal=''):
|
||||
|
||||
if removal == "":
|
||||
removal = {"2.2": "in 3.1", "3.0": "in 3.2"}.get(
|
||||
since, "two minor releases later")
|
||||
elif removal:
|
||||
if pending:
|
||||
raise ValueError(
|
||||
"A pending deprecation cannot have a scheduled removal")
|
||||
removal = "in {}".format(removal)
|
||||
|
||||
if not message:
|
||||
message = (
|
||||
"The %(name)s %(obj_type)s"
|
||||
+ (" will be deprecated in a future version"
|
||||
if pending else
|
||||
(" was deprecated in Matplotlib %(since)s"
|
||||
+ (" and will be removed %(removal)s"
|
||||
if removal else
|
||||
"")))
|
||||
+ "."
|
||||
+ (" Use %(alternative)s instead." if alternative else "")
|
||||
+ (" %(addendum)s" if addendum else ""))
|
||||
|
||||
return message % dict(
|
||||
func=name, name=name, obj_type=obj_type, since=since, removal=removal,
|
||||
alternative=alternative, addendum=addendum)
|
||||
|
||||
|
||||
def warn_deprecated(
|
||||
since, message='', name='', alternative='', pending=False,
|
||||
obj_type='attribute', addendum='', *, removal=''):
|
||||
"""
|
||||
Used to display deprecation in a standard way.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
since : str
|
||||
The release at which this API became deprecated.
|
||||
|
||||
message : str, optional
|
||||
Override the default deprecation message. The format
|
||||
specifier `%(name)s` may be used for the name of the function,
|
||||
and `%(alternative)s` may be used in the deprecation message
|
||||
to insert the name of an alternative to the deprecated
|
||||
function. `%(obj_type)s` may be used to insert a friendly name
|
||||
for the type of object being deprecated.
|
||||
|
||||
name : str, optional
|
||||
The name of the deprecated object.
|
||||
|
||||
alternative : str, optional
|
||||
An alternative API that the user may use in place of the deprecated
|
||||
API. The deprecation warning will tell the user about this alternative
|
||||
if provided.
|
||||
|
||||
pending : bool, optional
|
||||
If True, uses a PendingDeprecationWarning instead of a
|
||||
DeprecationWarning. Cannot be used together with *removal*.
|
||||
|
||||
removal : str, optional
|
||||
The expected removal version. With the default (an empty string), a
|
||||
removal version is automatically computed from *since*. Set to other
|
||||
Falsy values to not schedule a removal date. Cannot be used together
|
||||
with *pending*.
|
||||
|
||||
obj_type : str, optional
|
||||
The object type being deprecated.
|
||||
|
||||
addendum : str, optional
|
||||
Additional text appended directly to the final message.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Basic example::
|
||||
|
||||
# To warn of the deprecation of "matplotlib.name_of_module"
|
||||
warn_deprecated('1.4.0', name='matplotlib.name_of_module',
|
||||
obj_type='module')
|
||||
|
||||
"""
|
||||
message = '\n' + _generate_deprecation_message(
|
||||
since, message, name, alternative, pending, obj_type, addendum,
|
||||
removal=removal)
|
||||
category = (PendingDeprecationWarning if pending
|
||||
else MatplotlibDeprecationWarning)
|
||||
warnings.warn(message, category, stacklevel=2)
|
||||
|
||||
|
||||
def deprecated(since, message='', name='', alternative='', pending=False,
|
||||
obj_type=None, addendum='', *, removal=''):
|
||||
"""
|
||||
Decorator to mark a function or a class as deprecated.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
since : str
|
||||
The release at which this API became deprecated. This is
|
||||
required.
|
||||
|
||||
message : str, optional
|
||||
Override the default deprecation message. The format
|
||||
specifier `%(name)s` may be used for the name of the object,
|
||||
and `%(alternative)s` may be used in the deprecation message
|
||||
to insert the name of an alternative to the deprecated
|
||||
object.
|
||||
|
||||
name : str, optional
|
||||
The name of the deprecated object; if not provided the name
|
||||
is automatically determined from the passed in object,
|
||||
though this is useful in the case of renamed functions, where
|
||||
the new function is just assigned to the name of the
|
||||
deprecated function. For example::
|
||||
|
||||
def new_function():
|
||||
...
|
||||
oldFunction = new_function
|
||||
|
||||
alternative : str, optional
|
||||
An alternative API that the user may use in place of the deprecated
|
||||
API. The deprecation warning will tell the user about this alternative
|
||||
if provided.
|
||||
|
||||
pending : bool, optional
|
||||
If True, uses a PendingDeprecationWarning instead of a
|
||||
DeprecationWarning. Cannot be used together with *removal*.
|
||||
|
||||
removal : str, optional
|
||||
The expected removal version. With the default (an empty string), a
|
||||
removal version is automatically computed from *since*. Set to other
|
||||
Falsy values to not schedule a removal date. Cannot be used together
|
||||
with *pending*.
|
||||
|
||||
addendum : str, optional
|
||||
Additional text appended directly to the final message.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Basic example::
|
||||
|
||||
@deprecated('1.4.0')
|
||||
def the_function_to_deprecate():
|
||||
pass
|
||||
"""
|
||||
|
||||
if obj_type is not None:
|
||||
warn_deprecated(
|
||||
"3.0", "Passing 'obj_type' to the 'deprecated' decorator has no "
|
||||
"effect, and is deprecated since Matplotlib %(since)s; support "
|
||||
"for it will be removed %(removal)s.")
|
||||
|
||||
def deprecate(obj, message=message, name=name, alternative=alternative,
|
||||
pending=pending, addendum=addendum):
|
||||
|
||||
if not name:
|
||||
name = obj.__name__
|
||||
|
||||
if isinstance(obj, type):
|
||||
obj_type = "class"
|
||||
old_doc = obj.__doc__
|
||||
func = obj.__init__
|
||||
|
||||
def finalize(wrapper, new_doc):
|
||||
obj.__doc__ = new_doc
|
||||
obj.__init__ = wrapper
|
||||
return obj
|
||||
|
||||
elif isinstance(obj, property):
|
||||
obj_type = "attribute"
|
||||
func = None
|
||||
name = name or obj.fget.__name__
|
||||
old_doc = obj.__doc__
|
||||
|
||||
class _deprecated_property(property):
|
||||
def __get__(self, instance, owner):
|
||||
if instance is not None:
|
||||
from . import _warn_external
|
||||
_warn_external(message, category)
|
||||
return super().__get__(instance, owner)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
if instance is not None:
|
||||
from . import _warn_external
|
||||
_warn_external(message, category)
|
||||
return super().__set__(instance, value)
|
||||
|
||||
def __delete__(self, instance):
|
||||
if instance is not None:
|
||||
from . import _warn_external
|
||||
_warn_external(message, category)
|
||||
return super().__delete__(instance)
|
||||
|
||||
def finalize(_, new_doc):
|
||||
return _deprecated_property(
|
||||
fget=obj.fget, fset=obj.fset, fdel=obj.fdel, doc=new_doc)
|
||||
|
||||
else:
|
||||
obj_type = "function"
|
||||
if isinstance(obj, classmethod):
|
||||
func = obj.__func__
|
||||
old_doc = func.__doc__
|
||||
|
||||
def finalize(wrapper, new_doc):
|
||||
wrapper = functools.wraps(func)(wrapper)
|
||||
wrapper.__doc__ = new_doc
|
||||
return classmethod(wrapper)
|
||||
else:
|
||||
func = obj
|
||||
old_doc = func.__doc__
|
||||
|
||||
def finalize(wrapper, new_doc):
|
||||
wrapper = functools.wraps(func)(wrapper)
|
||||
wrapper.__doc__ = new_doc
|
||||
return wrapper
|
||||
|
||||
message = _generate_deprecation_message(
|
||||
since, message, name, alternative, pending, obj_type, addendum,
|
||||
removal=removal)
|
||||
category = (PendingDeprecationWarning if pending
|
||||
else MatplotlibDeprecationWarning)
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
warnings.warn(message, category, stacklevel=2)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
old_doc = textwrap.dedent(old_doc or '').strip('\n')
|
||||
message = message.strip()
|
||||
new_doc = (('\n.. deprecated:: %(since)s'
|
||||
'\n %(message)s\n\n' %
|
||||
{'since': since, 'message': message}) + old_doc)
|
||||
if not old_doc:
|
||||
# This is to prevent a spurious 'unexected unindent' warning from
|
||||
# docutils when the original docstring was blank.
|
||||
new_doc += r'\ '
|
||||
|
||||
return finalize(wrapper, new_doc)
|
||||
|
||||
return deprecate
|
||||
@@ -1,404 +0,0 @@
|
||||
"""
|
||||
Builtin colormaps, colormap handling utilities, and the `ScalarMappable` mixin.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:doc:`/gallery/color/colormap_reference` for a list of builtin
|
||||
colormaps.
|
||||
|
||||
:doc:`/tutorials/colors/colormap-manipulation` for examples of how to
|
||||
make colormaps and
|
||||
|
||||
:doc:`/tutorials/colors/colormaps` an in-depth discussion of
|
||||
choosing colormaps.
|
||||
|
||||
:doc:`/tutorials/colors/colormapnorms` for more details about data
|
||||
normalization
|
||||
|
||||
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
import numpy as np
|
||||
from numpy import ma
|
||||
|
||||
import matplotlib as mpl
|
||||
import matplotlib.colors as colors
|
||||
import matplotlib.cbook as cbook
|
||||
from matplotlib._cm import datad
|
||||
from matplotlib._cm_listed import cmaps as cmaps_listed
|
||||
|
||||
|
||||
cmap_d = {}
|
||||
|
||||
|
||||
# reverse all the colormaps.
|
||||
# reversed colormaps have '_r' appended to the name.
|
||||
|
||||
|
||||
def _reverser(f, x=None):
|
||||
"""Helper such that ``_reverser(f)(x) == f(1 - x)``."""
|
||||
if x is None:
|
||||
# Returning a partial object keeps it picklable.
|
||||
return functools.partial(_reverser, f)
|
||||
return f(1 - x)
|
||||
|
||||
|
||||
def revcmap(data):
|
||||
"""Can only handle specification *data* in dictionary format."""
|
||||
data_r = {}
|
||||
for key, val in data.items():
|
||||
if callable(val):
|
||||
valnew = _reverser(val)
|
||||
# This doesn't work: lambda x: val(1-x)
|
||||
# The same "val" (the first one) is used
|
||||
# each time, so the colors are identical
|
||||
# and the result is shades of gray.
|
||||
else:
|
||||
# Flip x and exchange the y values facing x = 0 and x = 1.
|
||||
valnew = [(1.0 - x, y1, y0) for x, y0, y1 in reversed(val)]
|
||||
data_r[key] = valnew
|
||||
return data_r
|
||||
|
||||
|
||||
def _reverse_cmap_spec(spec):
|
||||
"""Reverses cmap specification *spec*, can handle both dict and tuple
|
||||
type specs."""
|
||||
|
||||
if 'listed' in spec:
|
||||
return {'listed': spec['listed'][::-1]}
|
||||
|
||||
if 'red' in spec:
|
||||
return revcmap(spec)
|
||||
else:
|
||||
revspec = list(reversed(spec))
|
||||
if len(revspec[0]) == 2: # e.g., (1, (1.0, 0.0, 1.0))
|
||||
revspec = [(1.0 - a, b) for a, b in revspec]
|
||||
return revspec
|
||||
|
||||
|
||||
def _generate_cmap(name, lutsize):
|
||||
"""Generates the requested cmap from its *name*. The lut size is
|
||||
*lutsize*."""
|
||||
|
||||
spec = datad[name]
|
||||
|
||||
# Generate the colormap object.
|
||||
if 'red' in spec:
|
||||
return colors.LinearSegmentedColormap(name, spec, lutsize)
|
||||
elif 'listed' in spec:
|
||||
return colors.ListedColormap(spec['listed'], name)
|
||||
else:
|
||||
return colors.LinearSegmentedColormap.from_list(name, spec, lutsize)
|
||||
|
||||
LUTSIZE = mpl.rcParams['image.lut']
|
||||
|
||||
# Generate the reversed specifications (all at once, to avoid
|
||||
# modify-when-iterating).
|
||||
datad.update({cmapname + '_r': _reverse_cmap_spec(spec)
|
||||
for cmapname, spec in datad.items()})
|
||||
|
||||
# Precache the cmaps with ``lutsize = LUTSIZE``.
|
||||
# Also add the reversed ones added in the section above:
|
||||
for cmapname in datad:
|
||||
cmap_d[cmapname] = _generate_cmap(cmapname, LUTSIZE)
|
||||
|
||||
cmap_d.update(cmaps_listed)
|
||||
|
||||
locals().update(cmap_d)
|
||||
|
||||
|
||||
# Continue with definitions ...
|
||||
|
||||
|
||||
def register_cmap(name=None, cmap=None, data=None, lut=None):
|
||||
"""
|
||||
Add a colormap to the set recognized by :func:`get_cmap`.
|
||||
|
||||
It can be used in two ways::
|
||||
|
||||
register_cmap(name='swirly', cmap=swirly_cmap)
|
||||
|
||||
register_cmap(name='choppy', data=choppydata, lut=128)
|
||||
|
||||
In the first case, *cmap* must be a :class:`matplotlib.colors.Colormap`
|
||||
instance. The *name* is optional; if absent, the name will
|
||||
be the :attr:`~matplotlib.colors.Colormap.name` attribute of the *cmap*.
|
||||
|
||||
In the second case, the three arguments are passed to
|
||||
the :class:`~matplotlib.colors.LinearSegmentedColormap` initializer,
|
||||
and the resulting colormap is registered.
|
||||
|
||||
"""
|
||||
if name is None:
|
||||
try:
|
||||
name = cmap.name
|
||||
except AttributeError:
|
||||
raise ValueError("Arguments must include a name or a Colormap")
|
||||
|
||||
if not isinstance(name, str):
|
||||
raise ValueError("Colormap name must be a string")
|
||||
|
||||
if isinstance(cmap, colors.Colormap):
|
||||
cmap_d[name] = cmap
|
||||
return
|
||||
|
||||
# For the remainder, let exceptions propagate.
|
||||
if lut is None:
|
||||
lut = mpl.rcParams['image.lut']
|
||||
cmap = colors.LinearSegmentedColormap(name, data, lut)
|
||||
cmap_d[name] = cmap
|
||||
|
||||
|
||||
def get_cmap(name=None, lut=None):
|
||||
"""
|
||||
Get a colormap instance, defaulting to rc values if *name* is None.
|
||||
|
||||
Colormaps added with :func:`register_cmap` take precedence over
|
||||
built-in colormaps.
|
||||
|
||||
If *name* is a :class:`matplotlib.colors.Colormap` instance, it will be
|
||||
returned.
|
||||
|
||||
If *lut* is not None it must be an integer giving the number of
|
||||
entries desired in the lookup table, and *name* must be a standard
|
||||
mpl colormap name.
|
||||
"""
|
||||
if name is None:
|
||||
name = mpl.rcParams['image.cmap']
|
||||
|
||||
if isinstance(name, colors.Colormap):
|
||||
return name
|
||||
|
||||
if name in cmap_d:
|
||||
if lut is None:
|
||||
return cmap_d[name]
|
||||
else:
|
||||
return cmap_d[name]._resample(lut)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Colormap %s is not recognized. Possible values are: %s"
|
||||
% (name, ', '.join(sorted(cmap_d))))
|
||||
|
||||
|
||||
class ScalarMappable(object):
|
||||
"""
|
||||
This is a mixin class to support scalar data to RGBA mapping.
|
||||
The ScalarMappable makes use of data normalization before returning
|
||||
RGBA colors from the given colormap.
|
||||
|
||||
"""
|
||||
def __init__(self, norm=None, cmap=None):
|
||||
r"""
|
||||
|
||||
Parameters
|
||||
----------
|
||||
norm : :class:`matplotlib.colors.Normalize` instance
|
||||
The normalizing object which scales data, typically into the
|
||||
interval ``[0, 1]``.
|
||||
If *None*, *norm* defaults to a *colors.Normalize* object which
|
||||
initializes its scaling based on the first data processed.
|
||||
cmap : str or :class:`~matplotlib.colors.Colormap` instance
|
||||
The colormap used to map normalized data values to RGBA colors.
|
||||
"""
|
||||
|
||||
self.callbacksSM = cbook.CallbackRegistry()
|
||||
|
||||
if cmap is None:
|
||||
cmap = get_cmap()
|
||||
if norm is None:
|
||||
norm = colors.Normalize()
|
||||
|
||||
self._A = None
|
||||
#: The Normalization instance of this ScalarMappable.
|
||||
self.norm = norm
|
||||
#: The Colormap instance of this ScalarMappable.
|
||||
self.cmap = get_cmap(cmap)
|
||||
#: The last colorbar associated with this ScalarMappable. May be None.
|
||||
self.colorbar = None
|
||||
self.update_dict = {'array': False}
|
||||
|
||||
def to_rgba(self, x, alpha=None, bytes=False, norm=True):
|
||||
"""
|
||||
Return a normalized rgba array corresponding to *x*.
|
||||
|
||||
In the normal case, *x* is a 1-D or 2-D sequence of scalars, and
|
||||
the corresponding ndarray of rgba values will be returned,
|
||||
based on the norm and colormap set for this ScalarMappable.
|
||||
|
||||
There is one special case, for handling images that are already
|
||||
rgb or rgba, such as might have been read from an image file.
|
||||
If *x* is an ndarray with 3 dimensions,
|
||||
and the last dimension is either 3 or 4, then it will be
|
||||
treated as an rgb or rgba array, and no mapping will be done.
|
||||
The array can be uint8, or it can be floating point with
|
||||
values in the 0-1 range; otherwise a ValueError will be raised.
|
||||
If it is a masked array, the mask will be ignored.
|
||||
If the last dimension is 3, the *alpha* kwarg (defaulting to 1)
|
||||
will be used to fill in the transparency. If the last dimension
|
||||
is 4, the *alpha* kwarg is ignored; it does not
|
||||
replace the pre-existing alpha. A ValueError will be raised
|
||||
if the third dimension is other than 3 or 4.
|
||||
|
||||
In either case, if *bytes* is *False* (default), the rgba
|
||||
array will be floats in the 0-1 range; if it is *True*,
|
||||
the returned rgba array will be uint8 in the 0 to 255 range.
|
||||
|
||||
If norm is False, no normalization of the input data is
|
||||
performed, and it is assumed to be in the range (0-1).
|
||||
|
||||
"""
|
||||
# First check for special case, image input:
|
||||
try:
|
||||
if x.ndim == 3:
|
||||
if x.shape[2] == 3:
|
||||
if alpha is None:
|
||||
alpha = 1
|
||||
if x.dtype == np.uint8:
|
||||
alpha = np.uint8(alpha * 255)
|
||||
m, n = x.shape[:2]
|
||||
xx = np.empty(shape=(m, n, 4), dtype=x.dtype)
|
||||
xx[:, :, :3] = x
|
||||
xx[:, :, 3] = alpha
|
||||
elif x.shape[2] == 4:
|
||||
xx = x
|
||||
else:
|
||||
raise ValueError("third dimension must be 3 or 4")
|
||||
if xx.dtype.kind == 'f':
|
||||
if norm and (xx.max() > 1 or xx.min() < 0):
|
||||
raise ValueError("Floating point image RGB values "
|
||||
"must be in the 0..1 range.")
|
||||
if bytes:
|
||||
xx = (xx * 255).astype(np.uint8)
|
||||
elif xx.dtype == np.uint8:
|
||||
if not bytes:
|
||||
xx = xx.astype(np.float32) / 255
|
||||
else:
|
||||
raise ValueError("Image RGB array must be uint8 or "
|
||||
"floating point; found %s" % xx.dtype)
|
||||
return xx
|
||||
except AttributeError:
|
||||
# e.g., x is not an ndarray; so try mapping it
|
||||
pass
|
||||
|
||||
# This is the normal case, mapping a scalar array:
|
||||
x = ma.asarray(x)
|
||||
if norm:
|
||||
x = self.norm(x)
|
||||
rgba = self.cmap(x, alpha=alpha, bytes=bytes)
|
||||
return rgba
|
||||
|
||||
def set_array(self, A):
|
||||
"""Set the image array from numpy array *A*.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A : ndarray
|
||||
"""
|
||||
self._A = A
|
||||
self.update_dict['array'] = True
|
||||
|
||||
def get_array(self):
|
||||
'Return the array'
|
||||
return self._A
|
||||
|
||||
def get_cmap(self):
|
||||
'return the colormap'
|
||||
return self.cmap
|
||||
|
||||
def get_clim(self):
|
||||
'return the min, max of the color limits for image scaling'
|
||||
return self.norm.vmin, self.norm.vmax
|
||||
|
||||
def set_clim(self, vmin=None, vmax=None):
|
||||
"""
|
||||
set the norm limits for image scaling; if *vmin* is a length2
|
||||
sequence, interpret it as ``(vmin, vmax)`` which is used to
|
||||
support setp
|
||||
|
||||
ACCEPTS: a length 2 sequence of floats; may be overridden in methods
|
||||
that have ``vmin`` and ``vmax`` kwargs.
|
||||
"""
|
||||
if vmax is None:
|
||||
try:
|
||||
vmin, vmax = vmin
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
if vmin is not None:
|
||||
self.norm.vmin = colors._sanitize_extrema(vmin)
|
||||
if vmax is not None:
|
||||
self.norm.vmax = colors._sanitize_extrema(vmax)
|
||||
self.changed()
|
||||
|
||||
def set_cmap(self, cmap):
|
||||
"""
|
||||
set the colormap for luminance data
|
||||
|
||||
Parameters
|
||||
----------
|
||||
cmap : colormap or registered colormap name
|
||||
"""
|
||||
cmap = get_cmap(cmap)
|
||||
self.cmap = cmap
|
||||
self.changed()
|
||||
|
||||
def set_norm(self, norm):
|
||||
"""Set the normalization instance.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
norm : `.Normalize`
|
||||
"""
|
||||
if norm is None:
|
||||
norm = colors.Normalize()
|
||||
self.norm = norm
|
||||
self.changed()
|
||||
|
||||
def autoscale(self):
|
||||
"""
|
||||
Autoscale the scalar limits on the norm instance using the
|
||||
current array
|
||||
"""
|
||||
if self._A is None:
|
||||
raise TypeError('You must first set_array for mappable')
|
||||
self.norm.autoscale(self._A)
|
||||
self.changed()
|
||||
|
||||
def autoscale_None(self):
|
||||
"""
|
||||
Autoscale the scalar limits on the norm instance using the
|
||||
current array, changing only limits that are None
|
||||
"""
|
||||
if self._A is None:
|
||||
raise TypeError('You must first set_array for mappable')
|
||||
self.norm.autoscale_None(self._A)
|
||||
self.changed()
|
||||
|
||||
def add_checker(self, checker):
|
||||
"""
|
||||
Add an entry to a dictionary of boolean flags
|
||||
that are set to True when the mappable is changed.
|
||||
"""
|
||||
self.update_dict[checker] = False
|
||||
|
||||
def check_update(self, checker):
|
||||
"""
|
||||
If mappable has changed since the last check,
|
||||
return True; else return False
|
||||
"""
|
||||
if self.update_dict[checker]:
|
||||
self.update_dict[checker] = False
|
||||
return True
|
||||
return False
|
||||
|
||||
def changed(self):
|
||||
"""
|
||||
Call this whenever the mappable is changed to notify all the
|
||||
callbackSM listeners to the 'changed' signal
|
||||
"""
|
||||
self.callbacksSM.process('changed', self)
|
||||
|
||||
for key in self.update_dict:
|
||||
self.update_dict[key] = True
|
||||
self.stale = True
|
||||