Fixed database typo and removed unnecessary class identifier.

This commit is contained in:
Batuhan Berk Başoğlu 2020-10-14 10:10:37 -04:00
parent 00ad49a143
commit 45fb349a7d
5098 changed files with 952558 additions and 85 deletions

View file

@ -0,0 +1,2 @@
# NOTE: plt.switch_backend() (called at import time) will add a "backend"
# attribute here for backcompat.

View file

@ -0,0 +1,121 @@
"""
Common functionality between the PDF and PS backends.
"""
import functools
import matplotlib as mpl
from .. import font_manager, ft2font
from ..afm import AFM
from ..backend_bases import RendererBase
@functools.lru_cache(50)
def _cached_get_afm_from_fname(fname):
with open(fname, "rb") as fh:
return AFM(fh)
class CharacterTracker:
"""
Helper for font subsetting by the pdf and ps backends.
Maintains a mapping of font paths to the set of character codepoints that
are being used from that font.
"""
def __init__(self):
self.used = {}
@mpl.cbook.deprecated("3.3")
@property
def used_characters(self):
d = {}
for fname, chars in self.used.items():
realpath, stat_key = mpl.cbook.get_realpath_and_stat(fname)
d[stat_key] = (realpath, chars)
return d
def track(self, font, s):
"""Record that string *s* is being typeset using font *font*."""
if isinstance(font, str):
# Unused, can be removed after removal of track_characters.
fname = font
else:
fname = font.fname
self.used.setdefault(fname, set()).update(map(ord, s))
def merge(self, other):
"""Update self with a font path to character codepoints."""
for fname, charset in other.items():
self.used.setdefault(fname, set()).update(charset)
class RendererPDFPSBase(RendererBase):
# The following attributes must be defined by the subclasses:
# - _afm_font_dir
# - _use_afm_rc_name
def __init__(self, width, height):
super().__init__()
self.width = width
self.height = height
def flipy(self):
# docstring inherited
return False # y increases from bottom to top.
def option_scale_image(self):
# docstring inherited
return True # PDF and PS support arbitrary image scaling.
def option_image_nocomposite(self):
# docstring inherited
# Decide whether to composite image based on rcParam value.
return not mpl.rcParams["image.composite_image"]
def get_canvas_width_height(self):
# docstring inherited
return self.width * 72.0, self.height * 72.0
def get_text_width_height_descent(self, s, prop, ismath):
# docstring inherited
if ismath == "TeX":
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
elif ismath:
parse = self.mathtext_parser.parse(s, 72, prop)
return parse.width, parse.height, parse.depth
elif mpl.rcParams[self._use_afm_rc_name]:
font = self._get_font_afm(prop)
l, b, w, h, d = font.get_str_bbox_and_descent(s)
scale = prop.get_size_in_points() / 1000
w *= scale
h *= scale
d *= scale
return w, h, d
else:
font = self._get_font_ttf(prop)
font.set_text(s, 0.0, flags=ft2font.LOAD_NO_HINTING)
w, h = font.get_width_height()
d = font.get_descent()
scale = 1 / 64
w *= scale
h *= scale
d *= scale
return w, h, d
def _get_font_afm(self, prop):
fname = font_manager.findfont(
prop, fontext="afm", directory=self._afm_font_dir)
return _cached_get_afm_from_fname(fname)
def _get_font_ttf(self, prop):
fname = font_manager.findfont(prop)
font = font_manager.get_font(fname)
font.clear()
font.set_size(prop.get_size_in_points(), 72)
return font

View file

@ -0,0 +1,916 @@
from contextlib import contextmanager
import logging
import math
import os.path
import sys
import tkinter as tk
from tkinter.simpledialog import SimpleDialog
import tkinter.filedialog
import tkinter.messagebox
import numpy as np
import matplotlib as mpl
from matplotlib import backend_tools, cbook
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
StatusbarBase, TimerBase, ToolContainerBase, cursors, _Mode)
from matplotlib._pylab_helpers import Gcf
from matplotlib.figure import Figure
from matplotlib.widgets import SubplotTool
from . import _tkagg
try:
from ._tkagg import Win32_GetForegroundWindow, Win32_SetForegroundWindow
except ImportError:
@contextmanager
def _restore_foreground_window_at_end():
yield
else:
@contextmanager
def _restore_foreground_window_at_end():
foreground = Win32_GetForegroundWindow()
try:
yield
finally:
if mpl.rcParams['tk.window_focus']:
Win32_SetForegroundWindow(foreground)
_log = logging.getLogger(__name__)
backend_version = tk.TkVersion
cursord = {
cursors.MOVE: "fleur",
cursors.HAND: "hand2",
cursors.POINTER: "arrow",
cursors.SELECT_REGION: "tcross",
cursors.WAIT: "watch",
}
def blit(photoimage, aggimage, offsets, bbox=None):
"""
Blit *aggimage* to *photoimage*.
*offsets* is a tuple describing how to fill the ``offset`` field of the
``Tk_PhotoImageBlock`` struct: it should be (0, 1, 2, 3) for RGBA8888 data,
(2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0)
for big-endian ARGB32 (i.e. ARGB8888) data.
If *bbox* is passed, it defines the region that gets blitted.
"""
data = np.asarray(aggimage)
height, width = data.shape[:2]
dataptr = (height, width, data.ctypes.data)
if bbox is not None:
(x1, y1), (x2, y2) = bbox.__array__()
x1 = max(math.floor(x1), 0)
x2 = min(math.ceil(x2), width)
y1 = max(math.floor(y1), 0)
y2 = min(math.ceil(y2), height)
bboxptr = (x1, x2, y1, y2)
else:
photoimage.blank()
bboxptr = (0, width, 0, height)
_tkagg.blit(
photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr)
class TimerTk(TimerBase):
"""Subclass of `backend_bases.TimerBase` using Tk timer events."""
def __init__(self, parent, *args, **kwargs):
self._timer = None
TimerBase.__init__(self, *args, **kwargs)
self.parent = parent
def _timer_start(self):
self._timer_stop()
self._timer = self.parent.after(self._interval, self._on_timer)
def _timer_stop(self):
if self._timer is not None:
self.parent.after_cancel(self._timer)
self._timer = None
def _on_timer(self):
TimerBase._on_timer(self)
# Tk after() is only a single shot, so we need to add code here to
# reset the timer if we're not operating in single shot mode. However,
# if _timer is None, this means that _timer_stop has been called; so
# don't recreate the timer in that case.
if not self._single and self._timer:
self._timer = self.parent.after(self._interval, self._on_timer)
else:
self._timer = None
class FigureCanvasTk(FigureCanvasBase):
required_interactive_framework = "tk"
keyvald = {65507: 'control',
65505: 'shift',
65513: 'alt',
65515: 'super',
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',
}
_keycode_lookup = {
262145: 'control',
524320: 'alt',
524352: 'alt',
1048584: 'super',
1048592: 'super',
131074: 'shift',
131076: 'shift',
}
"""_keycode_lookup is used for badly mapped (i.e. no event.key_sym set)
keys on apple keyboards."""
def __init__(self, figure, master=None, resize_callback=None):
super(FigureCanvasTk, self).__init__(figure)
self._idle = True
self._idle_callback = None
w, h = self.figure.bbox.size.astype(int)
self._tkcanvas = tk.Canvas(
master=master, background="white",
width=w, height=h, borderwidth=0, highlightthickness=0)
self._tkphoto = tk.PhotoImage(
master=self._tkcanvas, width=w, height=h)
self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto)
self._resize_callback = resize_callback
self._tkcanvas.bind("<Configure>", self.resize)
self._tkcanvas.bind("<Key>", self.key_press)
self._tkcanvas.bind("<Motion>", self.motion_notify_event)
self._tkcanvas.bind("<Enter>", self.enter_notify_event)
self._tkcanvas.bind("<Leave>", self.leave_notify_event)
self._tkcanvas.bind("<KeyRelease>", self.key_release)
for name in ["<Button-1>", "<Button-2>", "<Button-3>"]:
self._tkcanvas.bind(name, self.button_press_event)
for name in [
"<Double-Button-1>", "<Double-Button-2>", "<Double-Button-3>"]:
self._tkcanvas.bind(name, self.button_dblclick_event)
for name in [
"<ButtonRelease-1>", "<ButtonRelease-2>", "<ButtonRelease-3>"]:
self._tkcanvas.bind(name, self.button_release_event)
# Mouse wheel on Linux generates button 4/5 events
for name in "<Button-4>", "<Button-5>":
self._tkcanvas.bind(name, self.scroll_event)
# Mouse wheel for windows goes to the window with the focus.
# Since the canvas won't usually have the focus, bind the
# event to the window containing the canvas instead.
# See http://wiki.tcl.tk/3893 (mousewheel) for details
root = self._tkcanvas.winfo_toplevel()
root.bind("<MouseWheel>", self.scroll_event_windows, "+")
# Can't get destroy events by binding to _tkcanvas. Therefore, bind
# to the window and filter.
def filter_destroy(event):
if event.widget is self._tkcanvas:
self._master.update_idletasks()
self.close_event()
root.bind("<Destroy>", filter_destroy, "+")
self._master = master
self._tkcanvas.focus_set()
def resize(self, event):
width, height = event.width, event.height
if self._resize_callback is not None:
self._resize_callback(event)
# compute desired figure size in inches
dpival = self.figure.dpi
winch = width / dpival
hinch = height / dpival
self.figure.set_size_inches(winch, hinch, forward=False)
self._tkcanvas.delete(self._tkphoto)
self._tkphoto = tk.PhotoImage(
master=self._tkcanvas, width=int(width), height=int(height))
self._tkcanvas.create_image(
int(width / 2), int(height / 2), image=self._tkphoto)
self.resize_event()
self.draw()
def draw_idle(self):
# docstring inherited
if not self._idle:
return
self._idle = False
def idle_draw(*args):
try:
self.draw()
finally:
self._idle = True
self._idle_callback = self._tkcanvas.after_idle(idle_draw)
def get_tk_widget(self):
"""
Return the Tk widget used to implement FigureCanvasTkAgg.
Although the initial implementation uses a Tk canvas, this routine
is intended to hide that fact.
"""
return self._tkcanvas
def motion_notify_event(self, event):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.figure.bbox.height - event.y
FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event)
def enter_notify_event(self, event):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.figure.bbox.height - event.y
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
def button_press_event(self, event, dblclick=False):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.figure.bbox.height - event.y
num = getattr(event, 'num', None)
if sys.platform == 'darwin':
# 2 and 3 were reversed on the OSX platform I tested under tkagg.
if num == 2:
num = 3
elif num == 3:
num = 2
FigureCanvasBase.button_press_event(
self, x, y, num, dblclick=dblclick, guiEvent=event)
def button_dblclick_event(self, event):
self.button_press_event(event, dblclick=True)
def button_release_event(self, event):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.figure.bbox.height - event.y
num = getattr(event, 'num', None)
if sys.platform == 'darwin':
# 2 and 3 were reversed on the OSX platform I tested under tkagg.
if num == 2:
num = 3
elif num == 3:
num = 2
FigureCanvasBase.button_release_event(self, x, y, num, guiEvent=event)
def scroll_event(self, event):
x = event.x
y = self.figure.bbox.height - event.y
num = getattr(event, 'num', None)
step = 1 if num == 4 else -1 if num == 5 else 0
FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
def scroll_event_windows(self, event):
"""MouseWheel event processor"""
# need to find the window that contains the mouse
w = event.widget.winfo_containing(event.x_root, event.y_root)
if w == self._tkcanvas:
x = event.x_root - w.winfo_rootx()
y = event.y_root - w.winfo_rooty()
y = self.figure.bbox.height - y
step = event.delta/120.
FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event)
def _get_key(self, event):
val = event.keysym_num
if val in self.keyvald:
key = self.keyvald[val]
elif (val == 0 and sys.platform == 'darwin'
and event.keycode in self._keycode_lookup):
key = self._keycode_lookup[event.keycode]
elif val < 256:
key = chr(val)
else:
key = None
# add modifier keys to the key string. Bit details originate from
# http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
# BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004;
# BIT_LEFT_ALT = 0x008; BIT_NUMLOCK = 0x010; BIT_RIGHT_ALT = 0x080;
# BIT_MB_1 = 0x100; BIT_MB_2 = 0x200; BIT_MB_3 = 0x400;
# In general, the modifier key is excluded from the modifier flag,
# however this is not the case on "darwin", so double check that
# we aren't adding repeat modifier flags to a modifier key.
if sys.platform == 'win32':
modifiers = [(17, 'alt', 'alt'),
(2, 'ctrl', 'control'),
]
elif sys.platform == 'darwin':
modifiers = [(3, 'super', 'super'),
(4, 'alt', 'alt'),
(2, 'ctrl', 'control'),
]
else:
modifiers = [(6, 'super', 'super'),
(3, 'alt', 'alt'),
(2, 'ctrl', 'control'),
]
if key is not None:
# shift is not added to the keys as this is already accounted for
for bitmask, prefix, key_name in modifiers:
if event.state & (1 << bitmask) and key_name not in key:
key = '{0}+{1}'.format(prefix, key)
return key
def key_press(self, event):
key = self._get_key(event)
FigureCanvasBase.key_press_event(self, key, guiEvent=event)
def key_release(self, event):
key = self._get_key(event)
FigureCanvasBase.key_release_event(self, key, guiEvent=event)
def new_timer(self, *args, **kwargs):
# docstring inherited
return TimerTk(self._tkcanvas, *args, **kwargs)
def flush_events(self):
# docstring inherited
self._master.update()
class FigureManagerTk(FigureManagerBase):
"""
Attributes
----------
canvas : `FigureCanvas`
The FigureCanvas instance
num : int or str
The Figure number
toolbar : tk.Toolbar
The tk.Toolbar
window : tk.Window
The tk.Window
"""
def __init__(self, canvas, num, window):
FigureManagerBase.__init__(self, canvas, num)
self.window = window
self.window.withdraw()
self.set_window_title("Figure %d" % num)
# packing toolbar first, because if space is getting low, last packed
# widget is getting shrunk first (-> the canvas)
self.toolbar = self._get_toolbar()
self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
if self.toolmanager:
backend_tools.add_tools_to_manager(self.toolmanager)
if self.toolbar:
backend_tools.add_tools_to_container(self.toolbar)
self._shown = False
def _get_toolbar(self):
if mpl.rcParams['toolbar'] == 'toolbar2':
toolbar = NavigationToolbar2Tk(self.canvas, self.window)
elif mpl.rcParams['toolbar'] == 'toolmanager':
toolbar = ToolbarTk(self.toolmanager, self.window)
else:
toolbar = None
return toolbar
def resize(self, width, height):
max_size = 1_400_000 # the measured max on xorg 1.20.8 was 1_409_023
if (width > max_size or height > max_size) and sys.platform == 'linux':
raise ValueError(
'You have requested to resize the '
f'Tk window to ({width}, {height}), one of which '
f'is bigger than {max_size}. At larger sizes xorg will '
'either exit with an error on newer versions (~1.20) or '
'cause corruption on older version (~1.19). We '
'do not expect a window over a million pixel wide or tall '
'to be intended behavior.')
self.canvas._tkcanvas.configure(width=width, height=height)
def show(self):
with _restore_foreground_window_at_end():
if not self._shown:
def destroy(*args):
self.window = None
Gcf.destroy(self)
self.canvas._tkcanvas.bind("<Destroy>", destroy)
self.window.deiconify()
else:
self.canvas.draw_idle()
if mpl.rcParams['figure.raise_window']:
self.canvas.manager.window.attributes('-topmost', 1)
self.canvas.manager.window.attributes('-topmost', 0)
self._shown = True
def destroy(self, *args):
if self.window is not None:
#self.toolbar.destroy()
if self.canvas._idle_callback:
self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback)
self.window.destroy()
if Gcf.get_num_fig_managers() == 0:
if self.window is not None:
self.window.quit()
self.window = None
def get_window_title(self):
return self.window.wm_title()
def set_window_title(self, title):
self.window.wm_title(title)
def full_screen_toggle(self):
is_fullscreen = bool(self.window.attributes('-fullscreen'))
self.window.attributes('-fullscreen', not is_fullscreen)
class NavigationToolbar2Tk(NavigationToolbar2, tk.Frame):
"""
Attributes
----------
canvas : `FigureCanvas`
The figure canvas on which to operate.
win : tk.Window
The tk.Window which owns this toolbar.
pack_toolbar : bool, default: True
If True, add the toolbar to the parent's pack manager's packing list
during initialization with ``side='bottom'`` and ``fill='x'``.
If you want to use the toolbar with a different layout manager, use
``pack_toolbar=False``.
"""
def __init__(self, canvas, window, *, pack_toolbar=True):
# Avoid using self.window (prefer self.canvas.get_tk_widget().master),
# so that Tool implementations can reuse the methods.
self.window = window
tk.Frame.__init__(self, master=window, borderwidth=2,
width=int(canvas.figure.bbox.width), height=50)
self._buttons = {}
for text, tooltip_text, image_file, callback in self.toolitems:
if text is None:
# Add a spacer; return value is unused.
self._Spacer()
else:
self._buttons[text] = button = self._Button(
text,
str(cbook._get_data_path(f"images/{image_file}.gif")),
toggle=callback in ["zoom", "pan"],
command=getattr(self, callback),
)
if tooltip_text is not None:
ToolTip.createToolTip(button, tooltip_text)
# This filler item ensures the toolbar is always at least two text
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
# over images because those use two-line messages which resize the
# toolbar.
label = tk.Label(master=self,
text='\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}')
label.pack(side=tk.RIGHT)
self.message = tk.StringVar(master=self)
self._message_label = tk.Label(master=self, textvariable=self.message)
self._message_label.pack(side=tk.RIGHT)
NavigationToolbar2.__init__(self, canvas)
if pack_toolbar:
self.pack(side=tk.BOTTOM, fill=tk.X)
def destroy(self, *args):
del self.message
tk.Frame.destroy(self, *args)
def _update_buttons_checked(self):
# sync button checkstates to match active mode
for text, mode in [('Zoom', _Mode.ZOOM), ('Pan', _Mode.PAN)]:
if text in self._buttons:
if self.mode == mode:
self._buttons[text].select() # NOT .invoke()
else:
self._buttons[text].deselect()
def pan(self, *args):
super().pan(*args)
self._update_buttons_checked()
def zoom(self, *args):
super().zoom(*args)
self._update_buttons_checked()
def set_message(self, s):
self.message.set(s)
def draw_rubberband(self, event, x0, y0, x1, y1):
height = self.canvas.figure.bbox.height
y0 = height - y0
y1 = height - y1
if hasattr(self, "lastrect"):
self.canvas._tkcanvas.delete(self.lastrect)
self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1)
def release_zoom(self, event):
super().release_zoom(event)
if hasattr(self, "lastrect"):
self.canvas._tkcanvas.delete(self.lastrect)
del self.lastrect
def set_cursor(self, cursor):
window = self.canvas.get_tk_widget().master
try:
window.configure(cursor=cursord[cursor])
except tkinter.TclError:
pass
else:
window.update_idletasks()
def _Button(self, text, image_file, toggle, command):
image = (tk.PhotoImage(master=self, file=image_file)
if image_file is not None else None)
if not toggle:
b = tk.Button(master=self, text=text, image=image, command=command)
else:
# There is a bug in tkinter included in some python 3.6 versions
# that without this variable, produces a "visual" toggling of
# other near checkbuttons
# https://bugs.python.org/issue29402
# https://bugs.python.org/issue25684
var = tk.IntVar(master=self)
b = tk.Checkbutton(
master=self, text=text, image=image, command=command,
indicatoron=False, variable=var)
b.var = var
b._ntimage = image
b.pack(side=tk.LEFT)
return b
def _Spacer(self):
# Buttons are 30px high. Make this 26px tall +2px padding to center it.
s = tk.Frame(
master=self, height=26, relief=tk.RIDGE, pady=2, bg="DarkGray")
s.pack(side=tk.LEFT, padx=5)
return s
def configure_subplots(self):
toolfig = Figure(figsize=(6, 3))
window = tk.Toplevel()
canvas = type(self.canvas)(toolfig, master=window)
toolfig.subplots_adjust(top=0.9)
canvas.tool = SubplotTool(self.canvas.figure, toolfig)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
window.grab_set()
def save_figure(self, *args):
filetypes = self.canvas.get_supported_filetypes().copy()
default_filetype = self.canvas.get_default_filetype()
# Tk doesn't provide a way to choose a default filetype,
# so we just have to put it first
default_filetype_name = filetypes.pop(default_filetype)
sorted_filetypes = ([(default_filetype, default_filetype_name)]
+ sorted(filetypes.items()))
tk_filetypes = [(name, '*.%s' % ext) for ext, name in sorted_filetypes]
# adding a default extension seems to break the
# asksaveasfilename dialog when you choose various save types
# from the dropdown. Passing in the empty string seems to
# work - JDH!
#defaultextension = self.canvas.get_default_filetype()
defaultextension = ''
initialdir = os.path.expanduser(mpl.rcParams['savefig.directory'])
initialfile = self.canvas.get_default_filename()
fname = tkinter.filedialog.asksaveasfilename(
master=self.canvas.get_tk_widget().master,
title='Save the figure',
filetypes=tk_filetypes,
defaultextension=defaultextension,
initialdir=initialdir,
initialfile=initialfile,
)
if fname in ["", ()]:
return
# Save dir for next time, unless empty str (i.e., use cwd).
if initialdir != "":
mpl.rcParams['savefig.directory'] = (
os.path.dirname(str(fname)))
try:
# This method will handle the delegation to the correct type
self.canvas.figure.savefig(fname)
except Exception as e:
tkinter.messagebox.showerror("Error saving file", str(e))
def set_history_buttons(self):
state_map = {True: tk.NORMAL, False: tk.DISABLED}
can_back = self._nav_stack._pos > 0
can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1
if "Back" in self._buttons:
self._buttons['Back']['state'] = state_map[can_back]
if "Forward" in self._buttons:
self._buttons['Forward']['state'] = state_map[can_forward]
class ToolTip:
"""
Tooltip recipe from
http://www.voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml#e387
"""
@staticmethod
def createToolTip(widget, text):
toolTip = ToolTip(widget)
def enter(event):
toolTip.showtip(text)
def leave(event):
toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0
def showtip(self, text):
"""Display text in tooltip window."""
self.text = text
if self.tipwindow or not self.text:
return
x, y, _, _ = self.widget.bbox("insert")
x = x + self.widget.winfo_rootx() + 27
y = y + self.widget.winfo_rooty()
self.tipwindow = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(1)
tw.wm_geometry("+%d+%d" % (x, y))
try:
# For Mac OS
tw.tk.call("::tk::unsupported::MacWindowStyle",
"style", tw._w,
"help", "noActivates")
except tk.TclError:
pass
label = tk.Label(tw, text=self.text, justify=tk.LEFT,
relief=tk.SOLID, borderwidth=1)
label.pack(ipadx=1)
def hidetip(self):
tw = self.tipwindow
self.tipwindow = None
if tw:
tw.destroy()
class RubberbandTk(backend_tools.RubberbandBase):
def draw_rubberband(self, x0, y0, x1, y1):
height = self.figure.canvas.figure.bbox.height
y0 = height - y0
y1 = height - y1
if hasattr(self, "lastrect"):
self.figure.canvas._tkcanvas.delete(self.lastrect)
self.lastrect = self.figure.canvas._tkcanvas.create_rectangle(
x0, y0, x1, y1)
def remove_rubberband(self):
if hasattr(self, "lastrect"):
self.figure.canvas._tkcanvas.delete(self.lastrect)
del self.lastrect
class SetCursorTk(backend_tools.SetCursorBase):
def set_cursor(self, cursor):
NavigationToolbar2Tk.set_cursor(
self._make_classic_style_pseudo_toolbar(), cursor)
class ToolbarTk(ToolContainerBase, tk.Frame):
_icon_extension = '.gif'
def __init__(self, toolmanager, window):
ToolContainerBase.__init__(self, toolmanager)
xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx
height, width = 50, xmax - xmin
tk.Frame.__init__(self, master=window,
width=int(width), height=int(height),
borderwidth=2)
self._message = tk.StringVar(master=self)
self._message_label = tk.Label(master=self, textvariable=self._message)
self._message_label.pack(side=tk.RIGHT)
self._toolitems = {}
self.pack(side=tk.TOP, fill=tk.X)
self._groups = {}
def add_toolitem(
self, name, group, position, image_file, description, toggle):
frame = self._get_groupframe(group)
button = NavigationToolbar2Tk._Button(self, name, image_file, toggle,
lambda: self._button_click(name))
if description is not None:
ToolTip.createToolTip(button, description)
self._toolitems.setdefault(name, [])
self._toolitems[name].append(button)
def _get_groupframe(self, group):
if group not in self._groups:
if self._groups:
self._add_separator()
frame = tk.Frame(master=self, borderwidth=0)
frame.pack(side=tk.LEFT, fill=tk.Y)
self._groups[group] = frame
return self._groups[group]
def _add_separator(self):
separator = tk.Frame(master=self, bd=5, width=1, bg='black')
separator.pack(side=tk.LEFT, fill=tk.Y, padx=2)
def _button_click(self, name):
self.trigger_tool(name)
def toggle_toolitem(self, name, toggled):
if name not in self._toolitems:
return
for toolitem in self._toolitems[name]:
if toggled:
toolitem.select()
else:
toolitem.deselect()
def remove_toolitem(self, name):
for toolitem in self._toolitems[name]:
toolitem.pack_forget()
del self._toolitems[name]
def set_message(self, s):
self._message.set(s)
@cbook.deprecated("3.3")
class StatusbarTk(StatusbarBase, tk.Frame):
def __init__(self, window, *args, **kwargs):
StatusbarBase.__init__(self, *args, **kwargs)
xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx
height, width = 50, xmax - xmin
tk.Frame.__init__(self, master=window,
width=int(width), height=int(height),
borderwidth=2)
self._message = tk.StringVar(master=self)
self._message_label = tk.Label(master=self, textvariable=self._message)
self._message_label.pack(side=tk.RIGHT)
self.pack(side=tk.TOP, fill=tk.X)
def set_message(self, s):
self._message.set(s)
class SaveFigureTk(backend_tools.SaveFigureBase):
def trigger(self, *args):
NavigationToolbar2Tk.save_figure(
self._make_classic_style_pseudo_toolbar())
class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase):
def __init__(self, *args, **kwargs):
backend_tools.ConfigureSubplotsBase.__init__(self, *args, **kwargs)
self.window = None
def trigger(self, *args):
self.init_window()
self.window.lift()
def init_window(self):
if self.window:
return
toolfig = Figure(figsize=(6, 3))
self.window = tk.Tk()
canvas = type(self.canvas)(toolfig, master=self.window)
toolfig.subplots_adjust(top=0.9)
SubplotTool(self.figure, toolfig)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.window.protocol("WM_DELETE_WINDOW", self.destroy)
def destroy(self, *args, **kwargs):
if self.window is not None:
self.window.destroy()
self.window = None
class HelpTk(backend_tools.ToolHelpBase):
def trigger(self, *args):
dialog = SimpleDialog(
self.figure.canvas._tkcanvas, self._get_help_text(), ["OK"])
dialog.done = lambda num: dialog.frame.master.withdraw()
backend_tools.ToolSaveFigure = SaveFigureTk
backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk
backend_tools.ToolSetCursor = SetCursorTk
backend_tools.ToolRubberband = RubberbandTk
backend_tools.ToolHelp = HelpTk
backend_tools.ToolCopyToClipboard = backend_tools.ToolCopyToClipboardBase
Toolbar = ToolbarTk
@_Backend.export
class _BackendTk(_Backend):
FigureManager = FigureManagerTk
@classmethod
def new_figure_manager_given_figure(cls, num, figure):
"""
Create a new figure manager instance for the given figure.
"""
with _restore_foreground_window_at_end():
window = tk.Tk(className="matplotlib")
window.withdraw()
# Put a Matplotlib icon on the window rather than the default tk
# icon. Tkinter doesn't allow colour icons on linux systems, but
# tk>=8.5 has a iconphoto command which we call directly. See
# http://mail.python.org/pipermail/tkinter-discuss/2006-November/000954.html
icon_fname = str(cbook._get_data_path(
'images/matplotlib_128.ppm'))
icon_img = tk.PhotoImage(file=icon_fname, master=window)
try:
window.iconphoto(False, icon_img)
except Exception as exc:
# log the failure (due e.g. to Tk version), but carry on
_log.info('Could not load matplotlib icon: %s', exc)
canvas = cls.FigureCanvas(figure, master=window)
manager = cls.FigureManager(canvas, num, window)
if mpl.is_interactive():
manager.show()
canvas.draw_idle()
return manager
@staticmethod
def trigger_manager_draw(manager):
manager.show()
@staticmethod
def mainloop():
managers = Gcf.get_all_fig_managers()
if managers:
managers[0].window.mainloop()

View file

@ -0,0 +1,615 @@
"""
An agg_ backend.
.. _agg: http://antigrain.com/
Features that are implemented:
* capstyles and join styles
* dashes
* linewidth
* lines, rectangles, ellipses
* clipping to a rectangle
* output to RGBA and Pillow-supported image formats
* 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
try:
from contextlib import nullcontext
except ImportError:
from contextlib import ExitStack as nullcontext # Py 3.6.
from math import radians, cos, sin
import numpy as np
from PIL import Image
import matplotlib as mpl
from matplotlib import cbook
from matplotlib import colors as mcolors
from matplotlib.backend_bases import (
_Backend, _check_savefig_extra_args, 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.backends._backend_agg import RendererAgg as _RendererAgg
backend_version = 'v2.2'
def get_hinting_flag():
mapping = {
'default': LOAD_DEFAULT,
'no_autohint': LOAD_NO_AUTOHINT,
'force_autohint': LOAD_FORCE_AUTOHINT,
'no_hinting': LOAD_NO_HINTING,
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[mpl.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
# This is its own method for the duration of the deprecation of
# offset_position = "data".
# 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):
# docstring inherited
nmax = mpl.rcParams['agg.path.chunksize'] # here at least for testing
npts = path.vertices.shape[0]
if (npts > nmax > 100 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 as err:
raise OverflowError(
"Exceeded cell block limit (set 'agg.path.chunksize' "
"rcparam)") from err
else:
try:
self._renderer.draw_path(gc, path, transform, rgbFace)
except OverflowError as err:
raise OverflowError("Exceeded cell block limit (set "
"'agg.path.chunksize' rcparam)") from err
def draw_path_collection(self, gc, master_transform, paths, all_transforms,
offsets, offsetTrans, facecolors, edgecolors,
linewidths, linestyles, antialiaseds, urls,
offset_position):
if offset_position == "data":
cbook.warn_deprecated(
"3.3", message="Support for offset_position='data' is "
"deprecated since %(since)s and will be removed %(removal)s.")
return self._renderer.draw_path_collection(
gc, master_transform, paths, all_transforms, offsets, offsetTrans,
facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls,
offset_position)
def draw_mathtext(self, gc, x, y, s, prop, angle):
"""Draw mathtext using :mod:`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 = round(x + ox + xd)
y = 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):
# docstring inherited
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
# 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=mpl.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))
x = round(x + xo + xd)
y = round(y + yo + yd)
self._renderer.draw_text_image(font, x, y + 1, angle, gc)
def get_text_width_height_descent(self, s, prop, ismath):
# docstring inherited
if ismath in ["TeX", "TeX!"]:
if ismath == "TeX!":
cbook._warn_deprecated(
"3.3", message="Support for ismath='TeX!' is deprecated "
"since %(since)s and will be removed %(removal)s; use "
"ismath='TeX' instead.")
# todo: handle props
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
@cbook._delete_parameter("3.2", "ismath")
def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None):
# docstring inherited
# 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="TeX")
xd = d * sin(radians(angle))
yd = d * cos(radians(angle))
x = round(x + xd)
y = round(y + yd)
self._renderer.draw_text_image(Z, x, y, angle, gc)
def get_canvas_width_height(self):
# docstring inherited
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):
# docstring inherited
return points * self.dpi / 72
def buffer_rgba(self):
return memoryview(self._renderer)
def tostring_argb(self):
return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes()
def tostring_rgb(self):
return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes()
def clear(self):
self._renderer.clear()
def option_image_nocomposite(self):
# docstring inherited
# 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):
# docstring inherited
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 pair of floats) 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.frombuffer(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):
# docstring inherited
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):
# docstring inherited
self.renderer = self.get_renderer(cleared=True)
# Acquire a lock on the shared font cache.
with RendererAgg.lock, \
(self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
else nullcontext()):
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()
def get_renderer(self, cleared=False):
w, h = self.figure.bbox.size
key = w, h, self.figure.dpi
reuse_renderer = (hasattr(self, "renderer")
and getattr(self, "_lastKey", None) == key)
if not reuse_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 RGB `bytes`.
`draw` must be called at least once before this function will work and
to update the renderer for any subsequent changes to the Figure.
"""
return self.renderer.tostring_rgb()
def tostring_argb(self):
"""
Get the image as ARGB `bytes`.
`draw` must be called at least once before this function will work and
to update the renderer for any subsequent changes to the Figure.
"""
return self.renderer.tostring_argb()
def buffer_rgba(self):
"""
Get the image as a `memoryview` to the renderer's buffer.
`draw` must be called at least once before this function will work and
to update the renderer for any subsequent changes to the Figure.
"""
return self.renderer.buffer_rgba()
@_check_savefig_extra_args
def print_raw(self, filename_or_obj, *args):
FigureCanvasAgg.draw(self)
renderer = self.get_renderer()
with cbook.open_file_cm(filename_or_obj, "wb") as fh:
fh.write(renderer.buffer_rgba())
print_rgba = print_raw
@_check_savefig_extra_args
def print_png(self, filename_or_obj, *args,
metadata=None, pil_kwargs=None):
"""
Write the figure to a PNG file.
Parameters
----------
filename_or_obj : str or path-like or file-like
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. This can be removed by setting it to *None*.
For more details see the `PNG specification`_.
.. _PNG specification: \
https://www.w3.org/TR/2003/REC-PNG-20031110/#11keywords
pil_kwargs : dict, optional
Keyword arguments passed to `PIL.Image.Image.save`.
If the 'pnginfo' key is present, it completely overrides
*metadata*, including the default 'Software' key.
"""
FigureCanvasAgg.draw(self)
mpl.image.imsave(
filename_or_obj, self.buffer_rgba(), format="png", origin="upper",
dpi=self.figure.dpi, metadata=metadata, pil_kwargs=pil_kwargs)
def print_to_buffer(self):
FigureCanvasAgg.draw(self)
renderer = self.get_renderer()
return (bytes(renderer.buffer_rgba()),
(int(renderer.width), int(renderer.height)))
# Note that these methods should typically be called via savefig() and
# print_figure(), and the latter ensures that `self.figure.dpi` already
# matches the dpi kwarg (if any).
@_check_savefig_extra_args(
extra_kwargs=["quality", "optimize", "progressive"])
@cbook._delete_parameter("3.2", "dryrun")
@cbook._delete_parameter("3.3", "quality",
alternative="pil_kwargs={'quality': ...}")
@cbook._delete_parameter("3.3", "optimize",
alternative="pil_kwargs={'optimize': ...}")
@cbook._delete_parameter("3.3", "progressive",
alternative="pil_kwargs={'progressive': ...}")
def print_jpg(self, filename_or_obj, *args, dryrun=False, pil_kwargs=None,
**kwargs):
"""
Write the figure to a JPEG file.
Parameters
----------
filename_or_obj : str or path-like or file-like
The file to write to.
Other Parameters
----------------
quality : int, default: :rc:`savefig.jpeg_quality`
The image quality, on a scale from 1 (worst) to 95 (best).
Values above 95 should be avoided; 100 disables portions of
the JPEG compression algorithm, and results in large files
with hardly any gain in image quality. This parameter is
deprecated.
optimize : bool, default: False
Whether the encoder should make an extra pass over the image
in order to select optimal encoder settings. This parameter is
deprecated.
progressive : bool, default: False
Whether the image should be stored as a progressive JPEG file.
This parameter is deprecated.
pil_kwargs : dict, optional
Additional keyword arguments that are passed to
`PIL.Image.Image.save` when saving the figure. These take
precedence over *quality*, *optimize* and *progressive*.
"""
# Remove transparency by alpha-blending on an assumed white background.
r, g, b, a = mcolors.to_rgba(self.figure.get_facecolor())
try:
self.figure.set_facecolor(a * np.array([r, g, b]) + 1 - a)
FigureCanvasAgg.draw(self)
finally:
self.figure.set_facecolor((r, g, b, a))
if dryrun:
return
if pil_kwargs is None:
pil_kwargs = {}
for k in ["quality", "optimize", "progressive"]:
if k in kwargs:
pil_kwargs.setdefault(k, kwargs.pop(k))
if "quality" not in pil_kwargs:
quality = pil_kwargs["quality"] = \
dict.__getitem__(mpl.rcParams, "savefig.jpeg_quality")
if quality not in [0, 75, 95]: # default qualities.
cbook.warn_deprecated(
"3.3", name="savefig.jpeg_quality", obj_type="rcParam",
addendum="Set the quality using "
"`pil_kwargs={'quality': ...}`; the future default "
"quality will be 75, matching the default of Pillow and "
"libjpeg.")
pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
# Drop alpha channel now.
return (Image.fromarray(np.asarray(self.buffer_rgba())[..., :3])
.save(filename_or_obj, format='jpeg', **pil_kwargs))
print_jpeg = print_jpg
@_check_savefig_extra_args
@cbook._delete_parameter("3.2", "dryrun")
def print_tif(self, filename_or_obj, *, dryrun=False, pil_kwargs=None):
FigureCanvasAgg.draw(self)
if dryrun:
return
if pil_kwargs is None:
pil_kwargs = {}
pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi))
return (Image.fromarray(np.asarray(self.buffer_rgba()))
.save(filename_or_obj, format='tiff', **pil_kwargs))
print_tiff = print_tif
@_Backend.export
class _BackendAgg(_Backend):
FigureCanvas = FigureCanvasAgg
FigureManager = FigureManagerBase

View file

@ -0,0 +1,539 @@
"""
A Cairo backend for matplotlib
==============================
:Author: Steve Chaplin and others
This backend depends on cairocffi or pycairo.
"""
import gzip
import math
import numpy as np
try:
import cairo
if cairo.version_info < (1, 11, 0):
# Introduced create_for_data for Py3.
raise ImportError
except ImportError:
try:
import cairocffi as cairo
except ImportError as err:
raise ImportError(
"cairo backend requires that pycairo>=1.11.0 or cairocffi"
"is installed") from err
from .. import cbook, font_manager
from matplotlib.backend_bases import (
_Backend, _check_savefig_extra_args, 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
backend_version = cairo.version
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
def _append_path(ctx, path, transform, clip=None):
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 = np.asarray(ctx.get_current_point())
a = points[:2]
b = points[-2:]
ctx.curve_to(*(cur / 3 + a * 2 / 3), *(a * 2 / 3 + b / 3), *b)
elif code == Path.CURVE4:
ctx.curve_to(*points)
def _cairo_font_args_from_font_prop(prop):
"""
Convert a `.FontProperties` or a `.FontEntry` to arguments that can be
passed to `.Context.select_font_face`.
"""
def attr(field):
try:
return getattr(prop, f"get_{field}")()
except AttributeError:
return getattr(prop, field)
name = attr("name")
slant = getattr(cairo, f"FONT_SLANT_{attr('style').upper()}")
weight = attr("weight")
weight = (cairo.FONT_WEIGHT_NORMAL
if font_manager.weight_dict.get(weight, weight) < 550
else cairo.FONT_WEIGHT_BOLD)
return name, slant, weight
class RendererCairo(RendererBase):
@cbook.deprecated("3.3")
@property
def fontweights(self):
return {
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,
}
@cbook.deprecated("3.3")
@property
def fontangles(self):
return {
'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()
def draw_path(self, gc, path, transform, rgbFace=None):
# docstring inherited
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):
# docstring inherited
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_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):
# docstring inherited
# 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(*_cairo_font_args_from_font_prop(prop))
ctx.save()
ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72)
if angle:
ctx.rotate(np.deg2rad(-angle))
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)
ctx.select_font_face(
*_cairo_font_args_from_font_prop(ttfFontProperty(font)))
ctx.set_font_size(fontsize * self.dpi / 72)
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):
# docstring inherited
return self.width, self.height
def get_text_width_height_descent(self, s, prop, ismath):
# docstring inherited
if ismath:
width, height, descent, fonts, used_characters = \
self.mathtext_parser.parse(s, self.dpi, prop)
return width, height, descent
ctx = self.text_ctx
# problem - scale remembers last setting and font can become
# enormous causing program to crash
# save/restore prevents the problem
ctx.save()
ctx.select_font_face(*_cairo_font_args_from_font_prop(prop))
# 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
ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72)
y_bearing, w, h = ctx.text_extents(s)[1:4]
ctx.restore()
return w, h, h + y_bearing
def new_gc(self):
# docstring inherited
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):
# docstring inherited
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):
self.ctx.set_line_cap(cbook._check_getitem(self._capd, capstyle=cs))
self._capstyle = 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):
self.ctx.set_line_join(cbook._check_getitem(self._joind, joinstyle=js))
self._joinstyle = js
def set_linewidth(self, w):
self._linewidth = float(w)
self.ctx.set_line_width(self.renderer.points_to_pixels(w))
class _CairoRegion:
def __init__(self, slices, data):
self._slices = slices
self._data = data
class FigureCanvasCairo(FigureCanvasBase):
def copy_from_bbox(self, bbox):
surface = self._renderer.gc.ctx.get_target()
if not isinstance(surface, cairo.ImageSurface):
raise RuntimeError(
"copy_from_bbox only works when rendering to an ImageSurface")
sw = surface.get_width()
sh = surface.get_height()
x0 = math.ceil(bbox.x0)
x1 = math.floor(bbox.x1)
y0 = math.ceil(sh - bbox.y1)
y1 = math.floor(sh - bbox.y0)
if not (0 <= x0 and x1 <= sw and bbox.x0 <= bbox.x1
and 0 <= y0 and y1 <= sh and bbox.y0 <= bbox.y1):
raise ValueError("Invalid bbox")
sls = slice(y0, y0 + max(y1 - y0, 0)), slice(x0, x0 + max(x1 - x0, 0))
data = (np.frombuffer(surface.get_data(), np.uint32)
.reshape((sh, sw))[sls].copy())
return _CairoRegion(sls, data)
def restore_region(self, region):
surface = self._renderer.gc.ctx.get_target()
if not isinstance(surface, cairo.ImageSurface):
raise RuntimeError(
"restore_region only works when rendering to an ImageSurface")
surface.flush()
sw = surface.get_width()
sh = surface.get_height()
sly, slx = region._slices
(np.frombuffer(surface.get_data(), np.uint32)
.reshape((sh, sw))[sly, slx]) = region._data
surface.mark_dirty_rectangle(
slx.start, sly.start, slx.stop - slx.start, sly.stop - sly.start)
@_check_savefig_extra_args
def print_png(self, fobj):
self._get_printed_image_surface().write_to_png(fobj)
@_check_savefig_extra_args
def print_rgba(self, fobj):
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)
@_check_savefig_extra_args
def _save(self, fo, fmt, *, orientation='portrait'):
# save PDF/PS/SVG
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:
raise ValueError("Unknown format: {!r}".format(fmt))
# 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

View file

@ -0,0 +1,980 @@
import functools
import logging
import os
from pathlib import Path
import sys
import matplotlib as mpl
from matplotlib import backend_tools, cbook
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
StatusbarBase, TimerBase, ToolContainerBase, cursors)
from matplotlib.figure import Figure
from matplotlib.widgets import SubplotTool
try:
import gi
except ImportError as err:
raise ImportError("The GTK3 backends require PyGObject") from err
try:
# :raises ValueError: If module/version is already loaded, already
# required, or unavailable.
gi.require_version("Gtk", "3.0")
except ValueError as e:
# in this case we want to re-raise as ImportError so the
# auto-backend selection logic correctly skips.
raise ImportError from e
from gi.repository import Gio, 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())
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) from exc
class TimerGTK3(TimerBase):
"""Subclass of `.TimerBase` using GTK3 timer events."""
def __init__(self, *args, **kwargs):
self._timer = None
TimerBase.__init__(self, *args, **kwargs)
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):
required_interactive_framework = "gtk3"
_timer_cls = TimerGTK3
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._rubberband_rect = 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('draw', self._post_draw)
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)
renderer_init = cbook._deprecate_method_override(
__class__._renderer_init, self, allow_empty=True, since="3.3",
addendum="Please initialize the renderer, if needed, in the "
"subclass' __init__; a fully empty _renderer_init implementation "
"may be kept for compatibility with earlier versions of "
"Matplotlib.")
if renderer_init:
renderer_init()
@cbook.deprecated("3.3", alternative="__init__")
def _renderer_init(self):
pass
def destroy(self):
#Gtk.DrawingArea.destroy(self)
self.close_event()
def scroll_event(self, widget, event):
x = event.x
# flipy so y=0 is bottom of canvas
y = self.get_allocation().height - event.y
step = 1 if event.direction == Gdk.ScrollDirection.UP else -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 = event.x, event.y
# 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 _draw_rubberband(self, rect):
self._rubberband_rect = rect
# TODO: Only update the rubberband area.
self.queue_draw()
def _post_draw(self, widget, ctx):
if self._rubberband_rect is None:
return
x0, y0, w, h = self._rubberband_rect
x1 = x0 + w
y1 = y0 + h
# Draw the lines from x0, y0 towards x1, y1 so that the
# dashes don't "jump" when moving the zoom box.
ctx.move_to(x0, y0)
ctx.line_to(x0, y1)
ctx.move_to(x0, y0)
ctx.line_to(x1, y0)
ctx.move_to(x0, y1)
ctx.line_to(x1, y1)
ctx.move_to(x1, y0)
ctx.line_to(x1, y1)
ctx.set_antialias(1)
ctx.set_line_width(1)
ctx.set_dash((3, 3), 0)
ctx.set_source_rgb(0, 0, 0)
ctx.stroke_preserve()
ctx.set_dash((3, 3), 3)
ctx.set_source_rgb(1, 1, 1)
ctx.stroke()
def on_draw_event(self, widget, ctx):
# to be overwritten by GTK3Agg or GTK3Cairo
pass
def draw(self):
# docstring inherited
if self.is_drawable():
self.queue_draw()
def draw_idle(self):
# docstring inherited
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 flush_events(self):
# docstring inherited
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.toolbar = self._get_toolbar()
def add_widget(child):
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)
if self.toolbar is not None:
self.toolbar.show()
h += add_widget(self.toolbar)
self.window.set_default_size(w, h)
self._destroying = False
self.window.connect("destroy", lambda *args: Gcf.destroy(self))
self.window.connect("delete_event", lambda *args: Gcf.destroy(self))
if mpl.is_interactive():
self.window.show()
self.canvas.draw_idle()
self.canvas.grab_focus()
def destroy(self, *args):
if self._destroying:
# Otherwise, this can be called twice when the user presses 'q',
# which calls Gcf.destroy(self), then this destroy(), then triggers
# Gcf.destroy(self) once again via
# `connect("destroy", lambda *args: Gcf.destroy(self))`.
return
self._destroying = True
self.vbox.destroy()
self.window.destroy()
self.canvas.destroy()
if self.toolbar:
self.toolbar.destroy()
if (Gcf.get_num_fig_managers() == 0 and not mpl.is_interactive() and
Gtk.main_level() >= 1):
Gtk.main_quit()
def show(self):
# show the figure window
self.window.show()
self.canvas.draw()
if mpl.rcParams['figure.raise_window']:
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 mpl.rcParams['toolbar'] == 'toolbar2':
toolbar = NavigationToolbar2GTK3(self.canvas, self.window)
elif mpl.rcParams['toolbar'] == 'toolmanager':
toolbar = ToolbarGTK3(self.toolmanager)
else:
toolbar = None
return toolbar
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."""
if self.toolbar:
toolbar_size = self.toolbar.size_request()
height += toolbar_size.height
canvas_size = self.canvas.get_allocation()
if canvas_size.width == canvas_size.height == 1:
# A canvas size of (1, 1) cannot exist in most cases, because
# window decorations would prevent such a small window. This call
# must be before the window has been mapped and widgets have been
# sized, so just change the window's starting size.
self.window.set_default_size(width, height)
else:
self.window.resize(width, height)
class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar):
def __init__(self, canvas, window):
self.win = window
GObject.GObject.__init__(self)
self.set_style(Gtk.ToolbarStyle.ICONS)
self._gtk_ids = {}
for text, tooltip_text, image_file, callback in self.toolitems:
if text is None:
self.insert(Gtk.SeparatorToolItem(), -1)
continue
image = Gtk.Image.new_from_gicon(
Gio.Icon.new_for_string(
str(cbook._get_data_path('images',
f'{image_file}-symbolic.svg'))),
Gtk.IconSize.LARGE_TOOLBAR)
self._gtk_ids[text] = tbutton = (
Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else
Gtk.ToolButton())
tbutton.set_label(text)
tbutton.set_icon_widget(image)
self.insert(tbutton, -1)
# Save the handler id, so that we can block it as needed.
tbutton._signal_handler = 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)
# This filler item ensures the toolbar is always at least two text
# lines high. Otherwise the canvas gets redrawn as the mouse hovers
# over images because those use two-line messages which resize the
# toolbar.
toolitem = Gtk.ToolItem()
self.insert(toolitem, -1)
label = Gtk.Label()
label.set_markup(
'<small>\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}</small>')
toolitem.add(label)
toolitem = Gtk.ToolItem()
self.insert(toolitem, -1)
self.message = Gtk.Label()
toolitem.add(self.message)
self.show_all()
NavigationToolbar2.__init__(self, canvas)
@cbook.deprecated("3.3")
@property
def ctx(self):
return self.canvas.get_property("window").cairo_create()
def set_message(self, s):
escaped = GLib.markup_escape_text(s)
self.message.set_markup(f'<small>{escaped}</small>')
def set_cursor(self, cursor):
window = self.canvas.get_property("window")
if window is not None:
window.set_cursor(cursord[cursor])
Gtk.main_iteration()
def draw_rubberband(self, event, x0, y0, x1, y1):
height = self.canvas.figure.bbox.height
y1 = height - y1
y0 = height - y0
rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)]
self.canvas._draw_rubberband(rect)
def remove_rubberband(self):
self.canvas._draw_rubberband(None)
def _update_buttons_checked(self):
for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]:
button = self._gtk_ids.get(name)
if button:
with button.handler_block(button._signal_handler):
button.set_active(self.mode.name == active)
def pan(self, *args):
super().pan(*args)
self._update_buttons_checked()
def zoom(self, *args):
super().zoom(*args)
self._update_buttons_checked()
def save_figure(self, *args):
dialog = Gtk.FileChooserDialog(
title="Save the figure",
parent=self.canvas.get_toplevel(),
action=Gtk.FileChooserAction.SAVE,
buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_SAVE, Gtk.ResponseType.OK),
)
for name, fmts \
in self.canvas.get_supported_filetypes_grouped().items():
ff = Gtk.FileFilter()
ff.set_name(name)
for fmt in fmts:
ff.add_pattern("*." + fmt)
dialog.add_filter(ff)
if self.canvas.get_default_filetype() in fmts:
dialog.set_filter(ff)
@functools.partial(dialog.connect, "notify::filter")
def on_notify_filter(*args):
name = dialog.get_filter().get_name()
fmt = self.canvas.get_supported_filetypes_grouped()[name][0]
dialog.set_current_name(
str(Path(dialog.get_current_name()).with_suffix("." + fmt)))
dialog.set_current_folder(mpl.rcParams["savefig.directory"])
dialog.set_current_name(self.canvas.get_default_filename())
dialog.set_do_overwrite_confirmation(True)
response = dialog.run()
fname = dialog.get_filename()
ff = dialog.get_filter() # Doesn't autoadjust to filename :/
fmt = self.canvas.get_supported_filetypes_grouped()[ff.get_name()][0]
dialog.destroy()
if response != Gtk.ResponseType.OK:
return
# Save dir for next time, unless empty str (which means use cwd).
if mpl.rcParams['savefig.directory']:
mpl.rcParams['savefig.directory'] = os.path.dirname(fname)
try:
self.canvas.figure.savefig(fname, format=fmt)
except Exception as e:
error_msg_gtk(str(e), parent=self)
def configure_subplots(self, button):
toolfig = Figure(figsize=(6, 3))
canvas = type(self.canvas)(toolfig)
toolfig.subplots_adjust(top=0.9)
# Need to keep a reference to the tool.
_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 set_history_buttons(self):
can_backward = self._nav_stack._pos > 0
can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1
if 'Back' in self._gtk_ids:
self._gtk_ids['Back'].set_sensitive(can_backward)
if 'Forward' in self._gtk_ids:
self._gtk_ids['Forward'].set_sensitive(can_forward)
class ToolbarGTK3(ToolContainerBase, Gtk.Box):
_icon_extension = '-symbolic.svg'
def __init__(self, toolmanager):
ToolContainerBase.__init__(self, toolmanager)
Gtk.Box.__init__(self)
self.set_property('orientation', Gtk.Orientation.HORIZONTAL)
self._message = Gtk.Label()
self.pack_end(self._message, False, False, 0)
self.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.new_from_gicon(
Gio.Icon.new_for_string(image_file),
Gtk.IconSize.LARGE_TOOLBAR)
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.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.pack_start(sep, False, True, 0)
sep.show_all()
def set_message(self, s):
self._message.set_label(s)
@cbook.deprecated("3.3")
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 RubberbandGTK3(backend_tools.RubberbandBase):
def draw_rubberband(self, x0, y0, x1, y1):
NavigationToolbar2GTK3.draw_rubberband(
self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1)
def remove_rubberband(self):
NavigationToolbar2GTK3.remove_rubberband(
self._make_classic_style_pseudo_toolbar())
class SaveFigureGTK3(backend_tools.SaveFigureBase):
def trigger(self, *args, **kwargs):
class PseudoToolbar:
canvas = self.figure.canvas
return NavigationToolbar2GTK3.save_figure(PseudoToolbar())
class SetCursorGTK3(backend_tools.SetCursorBase):
def set_cursor(self, cursor):
NavigationToolbar2GTK3.set_cursor(
self._make_classic_style_pseudo_toolbar(), cursor)
class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window):
@cbook.deprecated("3.2")
@property
def window(self):
if not hasattr(self, "_window"):
self._window = None
return self._window
@window.setter
@cbook.deprecated("3.2")
def window(self, window):
self._window = window
@cbook.deprecated("3.2")
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()
@cbook.deprecated("3.2")
def destroy(self, *args):
self.window.destroy()
self.window = None
def _get_canvas(self, fig):
return self.canvas.__class__(fig)
def trigger(self, *args):
NavigationToolbar2GTK3.configure_subplots(
self._make_classic_style_pseudo_toolbar(), None)
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 _is_valid_shortcut(self, key):
"""
Check for a valid shortcut to be displayed.
- GTK will never send 'cmd+' (see `FigureCanvasGTK3._get_key`).
- The shortcut window only shows keyboard shortcuts, not mouse buttons.
"""
return 'cmd+' not in key and not key.startswith('MouseButton.')
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)
if self._is_valid_shortcut(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 = str(cbook._get_data_path('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):
FigureCanvas = FigureCanvasGTK3
FigureManager = FigureManagerGTK3
@staticmethod
def trigger_manager_draw(manager):
manager.canvas.draw_idle()
@staticmethod
def mainloop():
if Gtk.main_level() == 0:
Gtk.main()

View file

@ -0,0 +1,86 @@
import numpy as np
from .. import cbook
try:
from . import backend_cairo
except ImportError as e:
raise ImportError('backend Gtk3Agg requires cairo') from e
from . import backend_agg, backend_gtk3
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 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):
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()
x = int(bbox.x0)
y = allocation.height - 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 draw(self):
backend_agg.FigureCanvasAgg.draw(self)
super().draw()
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

View file

@ -0,0 +1,40 @@
try:
from contextlib import nullcontext
except ImportError:
from contextlib import ExitStack as nullcontext # Py 3.6.
from . import backend_cairo, backend_gtk3
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 __init__(self, figure):
super().__init__(figure)
self._renderer = RendererGTK3Cairo(self.figure.dpi)
def on_draw_event(self, widget, ctx):
"""GtkDrawable draw event."""
with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
else nullcontext()):
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._renderer.set_width_height(
allocation.width, allocation.height)
self.figure.draw(self._renderer)
@_BackendGTK3.export
class _BackendGTK3Cairo(_BackendGTK3):
FigureCanvas = FigureCanvasGTK3Cairo

View file

@ -0,0 +1,171 @@
import matplotlib as mpl
from matplotlib import cbook
from matplotlib._pylab_helpers import Gcf
from matplotlib.backends import _macosx
from matplotlib.backends.backend_agg import FigureCanvasAgg
from matplotlib.backend_bases import (
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
TimerBase)
from matplotlib.figure import Figure
from matplotlib.widgets import SubplotTool
########################################################################
#
# The following functions and classes are for pylab and implement
# window/figure managers, etc...
#
########################################################################
class TimerMac(_macosx.Timer, TimerBase):
"""Subclass of `.TimerBase` using CFRunLoop timer events."""
# 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
"""
required_interactive_framework = "macosx"
_timer_cls = TimerMac
def __init__(self, figure):
FigureCanvasBase.__init__(self, figure)
width, height = self.get_width_height()
_macosx.FigureCanvas.__init__(self, width, height)
self._dpi_ratio = 1.0
def _set_device_scale(self, value):
if self._dpi_ratio != value:
# Need the new value in place before setting figure.dpi, which
# will trigger a resize
self._dpi_ratio, old_value = value, self._dpi_ratio
self.figure.dpi = self.figure.dpi / old_value * self._dpi_ratio
def _draw(self):
renderer = self.get_renderer(cleared=self.figure.stale)
if self.figure.stale:
self.figure.draw(renderer)
return renderer
def draw(self):
# docstring inherited
self.draw_idle()
self.flush_events()
# draw_idle is provided by _macosx.FigureCanvas
@cbook.deprecated("3.2", alternative="draw_idle()")
def invalidate(self):
return self.draw_idle()
def blit(self, bbox=None):
self.draw_idle()
def resize(self, width, height):
dpi = self.figure.dpi
width /= dpi
height /= dpi
self.figure.set_size_inches(width * self._dpi_ratio,
height * self._dpi_ratio,
forward=False)
FigureCanvasBase.resize_event(self)
self.draw_idle()
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 mpl.rcParams['toolbar'] == 'toolbar2':
self.toolbar = NavigationToolbar2Mac(canvas)
else:
self.toolbar = None
if self.toolbar is not None:
self.toolbar.update()
if mpl.is_interactive():
self.show()
self.canvas.draw_idle()
def close(self):
Gcf.destroy(self)
class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):
def __init__(self, canvas):
self.canvas = canvas # Needed by the _macosx __init__.
data_path = cbook._get_data_path('images')
_, tooltips, image_names, _ = zip(*NavigationToolbar2.toolitems)
_macosx.NavigationToolbar2.__init__(
self,
tuple(str(data_path / image_name) + ".pdf"
for image_name in image_names if image_name is not None),
tuple(tooltip for tooltip in tooltips if tooltip is not None))
NavigationToolbar2.__init__(self, canvas)
def draw_rubberband(self, event, x0, y0, x1, y1):
self.canvas.set_rubberband(int(x0), int(y0), int(x1), int(y1))
def release_zoom(self, event):
super().release_zoom(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)
# Need to keep a reference to the tool.
_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):
FigureCanvas = FigureCanvasMac
FigureManager = FigureManagerMac
@staticmethod
def trigger_manager_draw(manager):
manager.canvas.draw_idle()
@staticmethod
def mainloop():
_macosx.show()

View file

@ -0,0 +1,133 @@
import numpy as np
from matplotlib.backends.backend_agg import RendererAgg
from matplotlib.tight_bbox import process_figure_for_rasterizing
class MixedModeRenderer:
"""
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 : float
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._renderer = vector_renderer
def __getattr__(self, attr):
# Proxy everything that hasn't been overridden to the base
# renderer. Things that *are* overridden can call methods
# on self._renderer directly, but must not cache/store
# methods (because things like RendererAgg change their
# methods on the fly in order to optimise proxying down
# to the underlying C implementation).
return getattr(self._renderer, attr)
def start_rasterizing(self):
"""
Enter "raster" mode. All subsequent drawing commands (until
`stop_rasterizing` is called) will be drawn with the raster backend.
"""
# 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._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` call will be copied to the
vector backend by calling draw_image.
If `start_rasterizing` has been called multiple times,
`stop_rasterizing` must be called the same number of times before
"raster" mode is exited.
"""
self._rasterizing -= 1
if self._rasterizing == 0:
self._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

View file

@ -0,0 +1,263 @@
"""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 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 cbook, is_interactive
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import _Backend, 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 = [
'{fig} - {socket}'.format(
fig=(manager.canvas.figure.get_label()
or "Figure {}".format(manager.num)),
socket=manager.web_sockets)
for manager in Gcf.get_all_fig_managers()
]
if not is_interactive():
result.append(f'Figures pending show: {len(Gcf.figs)}')
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 socket.comm.comm_id != comm_id}
class FigureCanvasNbAgg(FigureCanvasWebAggCore):
_timer_cls = TimerTornado
class CommSocket:
"""
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 as err:
raise RuntimeError('Unable to create an IPython notebook Comm '
'instance. Are you in the IPython '
'notebook?') from err
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(manager))
return manager
@staticmethod
def trigger_manager_draw(manager):
manager.show()
@staticmethod
def show(block=None):
## 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 makes the figure in focus the
# active one. 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:
Gcf.figs.pop(manager.num, None)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,15 @@
from .. import cbook
from .backend_qt5 import (
backend_version, SPECIAL_KEYS,
SUPER, ALT, CTRL, SHIFT, MODIFIER_KEYS, # These are deprecated.
cursord, _create_qApp, _BackendQT5, TimerQT, MainWindow, FigureCanvasQT,
FigureManagerQT, NavigationToolbar2QT, SubplotToolQt, exception_handler)
cbook.warn_deprecated("3.3", name=__name__, obj_type="backend")
@_BackendQT5.export
class _BackendQT4(_BackendQT5):
class FigureCanvas(FigureCanvasQT):
required_interactive_framework = "qt4"

View file

@ -0,0 +1,16 @@
"""
Render to qt from agg
"""
from .. import cbook
from .backend_qt5agg import (
_BackendQT5Agg, FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT)
cbook.warn_deprecated("3.3", name=__name__, obj_type="backend")
@_BackendQT5Agg.export
class _BackendQT4Agg(_BackendQT5Agg):
class FigureCanvas(FigureCanvasQTAgg):
required_interactive_framework = "qt4"

View file

@ -0,0 +1,11 @@
from .. import cbook
from .backend_qt5cairo import _BackendQT5Cairo, FigureCanvasQTCairo
cbook.warn_deprecated("3.3", name=__name__, obj_type="backend")
@_BackendQT5Cairo.export
class _BackendQT4Cairo(_BackendQT5Cairo):
class FigureCanvas(FigureCanvasQTCairo):
required_interactive_framework = "qt4"

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,87 @@
"""
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, _setDevicePixelRatioF
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)
try:
# See documentation of QRect: bottom() and right() are off
# by 1, so use left() + width() and top() + height().
rect = event.rect()
# scale rect dimensions using the screen dpi ratio to get
# correct values for the Figure coordinates (rather than
# QT5's coords)
width = rect.width() * self._dpi_ratio
height = rect.height() * self._dpi_ratio
left, top = self.mouseEventCoords(rect.topLeft())
# shift the "top" by the height of the image to get the
# correct corner for our coordinate system
bottom = top - height
# same with the right side of the image
right = left + width
# create a buffer using the image bounding box
bbox = Bbox([[left, bottom], [right, top]])
reg = self.copy_from_bbox(bbox)
buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32(
memoryview(reg))
# clear the widget canvas
painter.eraseRect(rect)
qimage = QtGui.QImage(buf, buf.shape[1], buf.shape[0],
QtGui.QImage.Format_ARGB32_Premultiplied)
_setDevicePixelRatioF(qimage, self._dpi_ratio)
# set origin using original QT coordinates
origin = QtCore.QPoint(rect.left(), rect.top())
painter.drawImage(origin, 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)
finally:
painter.end()
def print_figure(self, *args, **kwargs):
super().print_figure(*args, **kwargs)
self.draw()
@_BackendQT5.export
class _BackendQT5Agg(_BackendQT5):
FigureCanvas = FigureCanvasQTAgg

View file

@ -0,0 +1,46 @@
import ctypes
from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo
from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT
from .qt_compat import QT_API, _setDevicePixelRatioF
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 = int(dpi_ratio * self.width())
height = int(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
_setDevicePixelRatioF(qimage, dpi_ratio)
painter = QtGui.QPainter(self)
painter.eraseRect(event.rect())
painter.drawImage(0, 0, qimage)
self._draw_rect_callback(painter)
painter.end()
@_BackendQT5.export
class _BackendQT5Cairo(_BackendQT5):
FigureCanvas = FigureCanvasQTCairo

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,232 @@
"""
A fully functional, do-nothing backend intended as a template for backend
writers. It is fully functional in that you can select it as a backend e.g.
with ::
import matplotlib
matplotlib.use("template")
and your program will (should!) run without error, though no output is
produced. This provides a starting point for backend writers; you can
selectively implement drawing methods (`~.RendererTemplate.draw_path`,
`~.RendererTemplate.draw_image`, etc.) and slowly see your figure come to life
instead having to have a full blown implementation before getting any results.
Copy this file to a directory outside of the Matplotlib source tree, somewhere
where Python can import it (by adding the directory to your ``sys.path`` or by
packaging it as a normal Python package); if the backend is importable as
``import my.backend`` you can then select it using ::
import matplotlib
matplotlib.use("module://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")
"""
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 methods.
"""
def __init__(self, dpi):
super().__init__()
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):
# docstring inherited
return True
def get_canvas_width_height(self):
# docstring inherited
return 100, 100
def get_text_width_height_descent(self, s, prop, ismath):
return 1, 1, 1
def new_gc(self):
# docstring inherited
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 pyplot 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, '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.
"""
self.draw()
def get_default_filetype(self):
return 'foo'
class FigureManagerTemplate(FigureManagerBase):
"""
Helper class for pyplot mode, wraps everything up into a neat bundle.
For non-interactive backends, the base class is sufficient.
"""
########################################################################
#
# Now just provide the standard names that backend.__init__ is expecting
#
########################################################################
FigureCanvas = FigureCanvasTemplate
FigureManager = FigureManagerTemplate

View file

@ -0,0 +1,21 @@
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

View file

@ -0,0 +1,31 @@
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

View file

@ -0,0 +1,329 @@
"""
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 mimetypes
from pathlib import Path
import random
import sys
import signal
import socket
import threading
try:
import tornado
except ImportError as err:
raise RuntimeError("The WebAgg backend requires Tornado.") from err
import tornado.web
import tornado.ioloop
import tornado.websocket
import matplotlib as mpl
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):
_timer_cls = TimerTornado
def show(self):
# show the figure window
global show # placates pyflakes: created by @_Backend.export below
show()
class WebAggApplication(tornado.web.Application):
initialized = False
started = False
class FavIcon(tornado.web.RequestHandler):
def get(self):
self.set_header('Content-Type', 'image/png')
self.write(Path(mpl.get_data_path(),
'images/matplotlib.png').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)
self.set_header(
'Content-Type', mimetypes.types_map.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()}),
# Static images for the toolbar
(url_prefix + r'/_images/(.*)',
tornado.web.StaticFileHandler,
{'path': Path(mpl.get_data_path(), 'images')}),
# A Matplotlib 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 = mpl.rcParams['webagg.address']
else:
cls.address = address
cls.port = mpl.rcParams['webagg.port']
for port in random_ports(cls.port,
mpl.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://{address}:{port}{prefix}".format(
address=WebAggApplication.address,
port=WebAggApplication.port,
prefix=WebAggApplication.url_prefix)
if mpl.rcParams['webagg.open_in_browser']:
import webbrowser
if not webbrowser.open(url):
print("To view figure, visit {0}".format(url))
else:
print("To view figure, visit {0}".format(url))
WebAggApplication.start()

View file

@ -0,0 +1,543 @@
"""
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 BytesIO, StringIO
import json
import logging
import os
from pathlib import Path
import numpy as np
from PIL import Image
import tornado
from matplotlib import backend_bases, cbook
from matplotlib.backends import backend_agg
from matplotlib.backend_bases import _Backend
_log = logging.getLogger(__name__)
# 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.
"""
cbook._check_in_list(['full', 'diff'], mode=mode)
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)
buf = BytesIO()
data = output.view(dtype=np.uint8).reshape((*output.shape, 4))
Image.fromarray(data).save(buf, format="png")
# Swap the renderer frames
self._renderer, self._last_renderer = (
self._last_renderer, renderer)
self._force_full = False
self._png_is_old = False
return buf.getvalue()
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.size.astype(int)
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):
_log.warning('Unhandled message type {0}. {1}'.format(
event['type'], event))
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
if self.toolbar:
# Normal toolbar init would refresh this, but it happens before the
# browser canvas is set up.
self.toolbar.set_history_buttons()
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)
# 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(*fig.bbox.size, forward=False)
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):
if self.manager:
self.manager._send_event(event_type, **kwargs)
_ALLOWED_TOOL_ITEMS = {
'home',
'back',
'forward',
'pan',
'zoom',
'download',
None,
}
class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2):
# Use the standard toolbar items + download button
toolitems = [
(text, tooltip_text, image_file, name_of_method)
for text, tooltip_text, image_file, name_of_method
in (*backend_bases.NavigationToolbar2.toolitems,
('Download', 'Download plot', 'filesave', 'download'))
if name_of_method in _ALLOWED_TOOL_ITEMS
]
def __init__(self, canvas):
self.message = ''
self.cursor = 0
super().__init__(canvas)
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')
def pan(self):
super().pan()
self.canvas.send_event('navigate_mode', mode=self.mode.name)
def zoom(self):
super().zoom()
self.canvas.send_event('navigate_mode', mode=self.mode.name)
def set_history_buttons(self):
can_backward = self._nav_stack._pos > 0
can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1
self.canvas.send_event('history_buttons',
Back=can_backward, Forward=can_forward)
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, forward=True):
self._send_event(
'resize',
size=(w / self.canvas._dpi_ratio, h / self.canvas._dpi_ratio),
forward=forward)
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)
self.resize(*self.canvas.figure.bbox.size)
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 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 __init__(self, *args, **kwargs):
self._timer = None
super().__init__(*args, **kwargs)
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

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,92 @@
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):
# docstring inherited
if bbox is None:
self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None)
self.gui_repaint()
return
srcBmp = _convert_agg_to_wx_bitmap(self.get_renderer(), None)
srcDC = wx.MemoryDC()
srcDC.SelectObject(srcBmp)
destDC = wx.MemoryDC()
destDC.SelectObject(self.bitmap)
x = int(bbox.x0)
y = int(self.bitmap.GetHeight() - bbox.y1)
destDC.Blit(x, y, int(bbox.width), int(bbox.height), srcDC, x, y)
destDC.SelectObject(wx.NullBitmap)
srcDC.SelectObject(wx.NullBitmap)
self.gui_repaint()
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
srcBmp = wx.Bitmap.FromBufferRGBA(int(agg.width), int(agg.height),
agg.buffer_rgba())
srcDC = wx.MemoryDC()
srcDC.SelectObject(srcBmp)
destBmp = wx.Bitmap(int(bbox.width), int(bbox.height))
destDC = wx.MemoryDC()
destDC.SelectObject(destBmp)
x = int(bbox.x0)
y = int(int(agg.height) - bbox.y1)
destDC.Blit(0, 0, int(bbox.width), int(bbox.height), srcDC, x, y)
srcDC.SelectObject(wx.NullBitmap)
destDC.SelectObject(wx.NullBitmap)
return destBmp
@_BackendWx.export
class _BackendWxAgg(_BackendWx):
FigureCanvas = FigureCanvasWxAgg
_frame_class = FigureFrameWxAgg

View file

@ -0,0 +1,47 @@
import wx.lib.wxcairo as wxcairo
from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo
from .backend_wx import (
_BackendWx, _FigureCanvasWxBase, FigureFrameWx,
NavigationToolbar2Wx as NavigationToolbar2WxCairo)
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

View file

@ -0,0 +1,220 @@
"""
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 Qt5Agg backend is requested but QT_API is set to "pyqt4",
then actually use Qt5 with PyQt5 or PySide2 (whichever can be imported);
- otherwise, use whatever the rcParams indicate.
Support for PyQt4 is deprecated.
"""
from distutils.version import LooseVersion
import os
import sys
import matplotlib as mpl
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.QtCore" in sys.modules:
QT_API = QT_API_PYQT5
elif "PySide2.QtCore" in sys.modules:
QT_API = QT_API_PYSIDE2
elif "PyQt4.QtCore" in sys.modules:
QT_API = QT_API_PYQTv2
elif "PySide.QtCore" in sys.modules:
QT_API = QT_API_PYSIDE
# 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). Use dict.__getitem__ to avoid
# triggering backend resolution (which can result in a partially but
# incompletely imported backend_qt5).
elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt5Agg", "Qt5Cairo"]:
if QT_API_ENV in ["pyqt5", "pyside2"]:
QT_API = _ETS[QT_API_ENV]
else:
QT_API = None
elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt4Agg", "Qt4Cairo"]:
if QT_API_ENV in ["pyqt4", "pyside"]:
QT_API = _ETS[QT_API_ENV]
else:
QT_API = None
# 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 as err:
raise RuntimeError(
"The environment variable QT_API has the unrecognized value {!r};"
"valid values are 'pyqt5', 'pyside2', 'pyqt', and "
"'pyside'") from err
def _setup_pyqt5():
global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \
_isdeleted, _getSaveFileName
if QT_API == QT_API_PYQT5:
from PyQt5 import QtCore, QtGui, QtWidgets
import sip
__version__ = QtCore.PYQT_VERSION_STR
QtCore.Signal = QtCore.pyqtSignal
QtCore.Slot = QtCore.pyqtSlot
QtCore.Property = QtCore.pyqtProperty
_isdeleted = sip.isdeleted
elif QT_API == QT_API_PYSIDE2:
from PySide2 import QtCore, QtGui, QtWidgets, __version__
import shiboken2
def _isdeleted(obj): return not shiboken2.isValid(obj)
else:
raise ValueError("Unexpected value for the 'backend.qt5' rcparam")
_getSaveFileName = QtWidgets.QFileDialog.getSaveFileName
@mpl.cbook.deprecated("3.3", alternative="QtCore.qVersion()")
def is_pyqt5():
return True
def _setup_pyqt4():
global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \
_isdeleted, _getSaveFileName
def _setup_pyqt4_internal(api):
global QtCore, QtGui, QtWidgets, \
__version__, is_pyqt5, _isdeleted, _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
import sip # Always succeeds *after* importing PyQt4.
__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
_isdeleted = sip.isdeleted
_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__
import shiboken
# 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")
def _isdeleted(obj): return not shiboken.isValid(obj)
_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
@mpl.cbook.deprecated("3.3", alternative="QtCore.qVersion()")
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: # See above re: dict.__getitem__.
if dict.__getitem__(mpl.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 backcompatibility 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 = int(QtCore.qVersion().split(".")[0])
if QT_RC_MAJOR_VERSION == 4:
mpl.cbook.warn_deprecated("3.3", name="support for Qt4")
def _devicePixelRatioF(obj):
"""
Return obj.devicePixelRatioF() with graceful fallback for older Qt.
This can be replaced by the direct call when we require Qt>=5.6.
"""
try:
# Not available on Qt<5.6
return obj.devicePixelRatioF() or 1
except AttributeError:
pass
try:
# Not available on Qt4 or some older Qt5.
# self.devicePixelRatio() returns 0 in rare cases
return obj.devicePixelRatio() or 1
except AttributeError:
return 1
def _setDevicePixelRatioF(obj, val):
"""
Call obj.setDevicePixelRatioF(val) with graceful fallback for older Qt.
This can be replaced by the direct call when we require Qt>=5.6.
"""
if hasattr(obj, 'setDevicePixelRatioF'):
# Not available on Qt<5.6
obj.setDevicePixelRatioF(val)
elif hasattr(obj, 'setDevicePixelRatio'):
# Not available on Qt4 or some older Qt5.
obj.setDevicePixelRatio(val)

View file

@ -0,0 +1,569 @@
"""
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
import logging
from numbers import Integral, Real
from matplotlib import cbook, colors as mcolors
from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore
_log = logging.getLogger(__name__)
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:
cbook._warn_external('Ignoring invalid color %r' % color)
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) # defaults to black if not qcolor.isValid()
self.colorbtn.color = qcolor
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="", with_margin=False, parent=None):
"""
Parameters
----------
data : list of (label, value) pairs
The data to be edited in the form.
comment : str, optional
with_margin : bool, default: False
If False, the form elements reach to the border of the widget.
This is the desired behavior if the FormWidget is used as a widget
alongside with other widgets such as a QComboBox, which also do
not have a margin around them.
However, a margin can be desired if the FormWidget is the only
widget within a container, e.g. a tab in a QTabWidget.
parent : QWidget or None
The parent widget.
"""
QtWidgets.QWidget.__init__(self, parent)
self.data = copy.deepcopy(data)
self.widgets = []
self.formlayout = QtWidgets.QFormLayout(self)
if not with_margin:
self.formlayout.setContentsMargins(0, 0, 0, 0)
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):
_log.warning(
"index '%s' is invalid (label: %s, value: %s)",
selindex, label, value)
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(-10**9, 10**9)
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)
layout.setContentsMargins(0, 0, 0, 0)
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, with_margin=True, 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: str
comment: str
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"))

View file

@ -0,0 +1,40 @@
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 = {}
main_layout = QtWidgets.QHBoxLayout()
self.setLayout(main_layout)
for group, spinboxes, buttons in [
("Borders",
["top", "bottom", "left", "right"], ["Export values"]),
("Spacings",
["hspace", "wspace"], ["Tight layout", "Reset", "Close"]),
]:
layout = QtWidgets.QVBoxLayout()
main_layout.addLayout(layout)
box = QtWidgets.QGroupBox(group)
layout.addWidget(box)
inner = QtWidgets.QFormLayout(box)
for name in spinboxes:
self._widgets[name] = widget = QtWidgets.QDoubleSpinBox()
widget.setMinimum(0)
widget.setMaximum(1)
widget.setDecimals(3)
widget.setSingleStep(0.005)
widget.setKeyboardTracking(False)
inner.addRow(name, widget)
layout.addStretch(1)
for name in buttons:
self._widgets[name] = widget = QtWidgets.QPushButton(name)
# Don't trigger on <enter>, which is used to input values.
widget.setAutoDefault(False)
layout.addWidget(widget)
self._widgets["Close"].setFocus()

View file

@ -0,0 +1,260 @@
# Copyright © 2009 Pierre Raybaut
# Licensed under the terms of the MIT License
# see the Matplotlib licenses directory for a copy of the license
"""Module that provides a GUI-based editor for Matplotlib's figure options."""
import re
from matplotlib import cbook, cm, colors as mcolors, markers, image as mimage
from matplotlib.backends.qt_compat import QtGui
from matplotlib.backends.qt_editor import _formlayout
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 ScalarMappables.
mappabledict = {}
for mappable in [*axes.images, *axes.collections]:
label = mappable.get_label()
if label == '_nolegend_' or mappable.get_array() is None:
continue
mappabledict[label] = mappable
mappablelabels = sorted(mappabledict, key=cmp_key)
mappables = []
cmaps = [(cmap, name) for name, cmap in sorted(cm._cmap_registry.items())]
for label in mappablelabels:
mappable = mappabledict[label]
cmap = mappable.get_cmap()
if cmap not in cm._cmap_registry.values():
cmaps = [(cmap, cmap.name), *cmaps]
low, high = mappable.get_clim()
mappabledata = [
('Label', label),
('Colormap', [cmap.name] + cmaps),
('Min. value', low),
('Max. value', high),
]
if hasattr(mappable, "get_interpolation"): # Images.
interpolations = [
(name, name) for name in sorted(mimage.interpolations_names)]
mappabledata.append((
'Interpolation',
[mappable.get_interpolation(), *interpolations]))
mappables.append([mappabledata, label, ""])
# Is there a scalarmappable displayed?
has_sm = bool(mappables)
datalist = [(general, "Axes", "")]
if curves:
datalist.append((curves, "Curves", ""))
if mappables:
datalist.append((mappables, "Images, etc.", ""))
def apply_callback(data):
"""A callback 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 []
mappables = data.pop(0) if has_sm 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 != 'none':
line.set_marker(marker)
line.set_markersize(markersize)
line.set_markerfacecolor(markerfacecolor)
line.set_markeredgecolor(markeredgecolor)
# Set ScalarMappables.
for index, mappable_settings in enumerate(mappables):
mappable = mappabledict[mappablelabels[index]]
if len(mappable_settings) == 5:
label, cmap, low, high, interpolation = mappable_settings
mappable.set_interpolation(interpolation)
elif len(mappable_settings) == 4:
label, cmap, low, high = mappable_settings
mappable.set_label(label)
mappable.set_cmap(cm.get_cmap(cmap))
mappable.set_clim(*sorted([low, high]))
# 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=QtGui.QIcon(
str(cbook._get_data_path('images', 'qt4_editor_options.svg'))),
apply=apply_callback)
if data is not None:
apply_callback(data)

View file

@ -0,0 +1,8 @@
from matplotlib import cbook
from ._formsubplottool import UiSubplotTool
cbook.warn_deprecated(
"3.3", obj_type="module", name=__name__,
alternative="matplotlib.backends.backend_qt5.SubplotToolQt")
__all__ = ["UiSubplotTool"]

View file

@ -0,0 +1,32 @@
module.exports = {
root: true,
ignorePatterns: ["jquery-ui-*/", "node_modules/"],
env: {
browser: true,
jquery: true,
},
extends: ["eslint:recommended", "prettier"],
globals: {
IPython: "readonly",
MozWebSocket: "readonly",
},
rules: {
indent: ["error", 2, { SwitchCase: 1 }],
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
},
],
quotes: ["error", "double", { avoidEscape: true }],
},
overrides: [
{
files: "js/**/*.js",
rules: {
indent: ["error", 4, { SwitchCase: 1 }],
quotes: ["error", "single", { avoidEscape: true }],
},
},
],
};

View file

@ -0,0 +1,7 @@
node_modules/
# Vendored dependencies
css/boilerplate.css
css/fbm.css
css/page.css
jquery-ui-*/

View file

@ -0,0 +1,11 @@
{
"overrides": [
{
"files": "js/**/*.js",
"options": {
"singleQuote": true,
"tabWidth": 4,
}
}
]
}

View file

@ -0,0 +1,49 @@
<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/css/mpl.css" type="text/css">
<script src="{{ prefix }}/_static/js/mpl_tornado.js"></script>
<script src="{{ prefix }}/js/mpl.js"></script>
<script>
function ready(fn) {
if (document.readyState != "loading") {
fn();
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
function figure_ready(fig_id) {
return function () {
var main_div = document.querySelector("div#figures");
var figure_div = document.createElement("div");
figure_div.id = "figure-div";
main_div.appendChild(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.setAttribute("tabindex", fig_id);
}
};
{% for (fig_id, fig_manager) in figures %}
ready(figure_ready({{ str(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>

View file

@ -0,0 +1,77 @@
/**
* 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; }

View file

@ -0,0 +1,97 @@
/* 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;
}

View file

@ -0,0 +1,84 @@
/* General styling */
.ui-helper-clearfix:before,
.ui-helper-clearfix:after {
content: "";
display: table;
border-collapse: collapse;
}
.ui-helper-clearfix:after {
clear: both;
}
/* Header */
.ui-widget-header {
border: 1px solid #dddddd;
border-top-left-radius: 6px;
border-top-right-radius: 6px;
background: #e9e9e9;
color: #333333;
font-weight: bold;
}
/* Toolbar and items */
.mpl-toolbar {
width: 100%;
}
.mpl-toolbar div.mpl-button-group {
display: inline-block;
}
.mpl-button-group + .mpl-button-group {
margin-left: 0.5em;
}
.mpl-widget {
background-color: #fff;
border: 1px solid #ccc;
display: inline-block;
cursor: pointer;
color: #333;
padding: 6px;
vertical-align: middle;
}
.mpl-widget:disabled,
.mpl-widget[disabled] {
background-color: #ddd;
border-color: #ddd !important;
cursor: not-allowed;
}
.mpl-widget:disabled img,
.mpl-widget[disabled] img {
/* Convert black to grey */
filter: contrast(0%);
}
.mpl-widget.active img {
/* Convert black to tab:blue, approximately */
filter: invert(34%) sepia(97%) saturate(468%) hue-rotate(162deg) brightness(96%) contrast(91%);
}
button.mpl-widget:focus,
button.mpl-widget:hover {
background-color: #ddd;
border-color: #aaa;
}
.mpl-button-group button.mpl-widget {
margin-left: -1px;
}
.mpl-button-group button.mpl-widget:first-child {
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
margin-left: 0px;
}
.mpl-button-group button.mpl-widget:last-child {
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
select.mpl-widget {
cursor: default;
}

View file

@ -0,0 +1,83 @@
/**
* 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;
vertical-align: top;
}

View file

@ -0,0 +1,34 @@
<!-- 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.querySelector('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>

View file

@ -0,0 +1,671 @@
/* Put everything inside the global mpl namespace */
/* global mpl */
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 = document.createElement('div');
this.root.setAttribute('style', 'display: inline-block');
this._root_extra_style(this.root);
parent_element.appendChild(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 (fig.ratio !== 1) {
fig.send_message('set_dpi_ratio', { dpi_ratio: fig.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 = document.createElement('div');
titlebar.classList =
'ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix';
var titletext = document.createElement('div');
titletext.classList = 'ui-dialog-title';
titletext.setAttribute(
'style',
'width: 100%; text-align: center; padding: 3px;'
);
titlebar.appendChild(titletext);
this.root.appendChild(titlebar);
this.header = titletext;
};
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 = (this.canvas_div = document.createElement('div'));
canvas_div.setAttribute(
'style',
'border: 1px solid #ddd;' +
'box-sizing: content-box;' +
'clear: both;' +
'min-height: 1px;' +
'min-width: 1px;' +
'outline: 0;' +
'overflow: hidden;' +
'position: relative;' +
'resize: both;'
);
function on_keyboard_event_closure(name) {
return function (event) {
return fig.key_event(event, name);
};
}
canvas_div.addEventListener(
'keydown',
on_keyboard_event_closure('key_press')
);
canvas_div.addEventListener(
'keyup',
on_keyboard_event_closure('key_release')
);
this._canvas_extra_style(canvas_div);
this.root.appendChild(canvas_div);
var canvas = (this.canvas = document.createElement('canvas'));
canvas.classList.add('mpl-canvas');
canvas.setAttribute('style', 'box-sizing: content-box;');
this.context = canvas.getContext('2d');
var backingStore =
this.context.backingStorePixelRatio ||
this.context.webkitBackingStorePixelRatio ||
this.context.mozBackingStorePixelRatio ||
this.context.msBackingStorePixelRatio ||
this.context.oBackingStorePixelRatio ||
this.context.backingStorePixelRatio ||
1;
this.ratio = (window.devicePixelRatio || 1) / backingStore;
if (this.ratio !== 1) {
fig.send_message('set_dpi_ratio', { dpi_ratio: this.ratio });
}
var rubberband_canvas = (this.rubberband_canvas = document.createElement(
'canvas'
));
rubberband_canvas.setAttribute(
'style',
'box-sizing: content-box; position: absolute; left: 0; top: 0; z-index: 1;'
);
var resizeObserver = new ResizeObserver(function (entries) {
var nentries = entries.length;
for (var i = 0; i < nentries; i++) {
var entry = entries[i];
var width, height;
if (entry.contentBoxSize) {
if (entry.contentBoxSize instanceof Array) {
// Chrome 84 implements new version of spec.
width = entry.contentBoxSize[0].inlineSize;
height = entry.contentBoxSize[0].blockSize;
} else {
// Firefox implements old version of spec.
width = entry.contentBoxSize.inlineSize;
height = entry.contentBoxSize.blockSize;
}
} else {
// Chrome <84 implements even older version of spec.
width = entry.contentRect.width;
height = entry.contentRect.height;
}
// Keep the size of the canvas and rubber band canvas in sync with
// the canvas container.
if (entry.devicePixelContentBoxSize) {
// Chrome 84 implements new version of spec.
canvas.setAttribute(
'width',
entry.devicePixelContentBoxSize[0].inlineSize
);
canvas.setAttribute(
'height',
entry.devicePixelContentBoxSize[0].blockSize
);
} else {
canvas.setAttribute('width', width * fig.ratio);
canvas.setAttribute('height', height * fig.ratio);
}
canvas.setAttribute(
'style',
'width: ' + width + 'px; height: ' + height + 'px;'
);
rubberband_canvas.setAttribute('width', width);
rubberband_canvas.setAttribute('height', height);
// And update the size in Python. We ignore the initial 0/0 size
// that occurs as the element is placed into the DOM, which should
// otherwise not happen due to the minimum size styling.
if (width != 0 && height != 0) {
fig.request_resize(width, height);
}
}
});
resizeObserver.observe(canvas_div);
function on_mouse_event_closure(name) {
return function (event) {
return fig.mouse_event(event, name);
};
}
rubberband_canvas.addEventListener(
'mousedown',
on_mouse_event_closure('button_press')
);
rubberband_canvas.addEventListener(
'mouseup',
on_mouse_event_closure('button_release')
);
// Throttle sequential mouse events to 1 every 20ms.
rubberband_canvas.addEventListener(
'mousemove',
on_mouse_event_closure('motion_notify')
);
rubberband_canvas.addEventListener(
'mouseenter',
on_mouse_event_closure('figure_enter')
);
rubberband_canvas.addEventListener(
'mouseleave',
on_mouse_event_closure('figure_leave')
);
canvas_div.addEventListener('wheel', function (event) {
if (event.deltaY < 0) {
event.step = 1;
} else {
event.step = -1;
}
on_mouse_event_closure('scroll')(event);
});
canvas_div.appendChild(canvas);
canvas_div.appendChild(rubberband_canvas);
this.rubberband_context = rubberband_canvas.getContext('2d');
this.rubberband_context.strokeStyle = '#000000';
this._resize_canvas = function (width, height, forward) {
if (forward) {
canvas_div.style.width = width + 'px';
canvas_div.style.height = height + 'px';
}
};
// Disable right mouse context menu.
this.rubberband_canvas.addEventListener('contextmenu', function (_e) {
event.preventDefault();
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 toolbar = document.createElement('div');
toolbar.classList = 'mpl-toolbar';
this.root.appendChild(toolbar);
function on_click_closure(name) {
return function (_event) {
return fig.toolbar_button_onclick(name);
};
}
function on_mouseover_closure(tooltip) {
return function (event) {
if (!event.currentTarget.disabled) {
return fig.toolbar_button_onmouseover(tooltip);
}
};
}
fig.buttons = {};
var buttonGroup = document.createElement('div');
buttonGroup.classList = 'mpl-button-group';
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) {
/* Instead of a spacer, we start a new button group. */
if (buttonGroup.hasChildNodes()) {
toolbar.appendChild(buttonGroup);
}
buttonGroup = document.createElement('div');
buttonGroup.classList = 'mpl-button-group';
continue;
}
var button = (fig.buttons[name] = document.createElement('button'));
button.classList = 'mpl-widget';
button.setAttribute('role', 'button');
button.setAttribute('aria-disabled', 'false');
button.addEventListener('click', on_click_closure(method_name));
button.addEventListener('mouseover', on_mouseover_closure(tooltip));
var icon_img = document.createElement('img');
icon_img.src = '_images/' + image + '.png';
icon_img.srcset = '_images/' + image + '_large.png 2x';
icon_img.alt = tooltip;
button.appendChild(icon_img);
buttonGroup.appendChild(button);
}
if (buttonGroup.hasChildNodes()) {
toolbar.appendChild(buttonGroup);
}
var fmt_picker = document.createElement('select');
fmt_picker.classList = 'mpl-widget';
toolbar.appendChild(fmt_picker);
this.format_dropdown = fmt_picker;
for (var ind in mpl.extensions) {
var fmt = mpl.extensions[ind];
var option = document.createElement('option');
option.selected = fmt === mpl.default_extension;
option.innerHTML = fmt;
fmt_picker.appendChild(option);
}
var status_bar = document.createElement('span');
status_bar.classList = 'mpl-message';
toolbar.appendChild(status_bar);
this.message = status_bar;
};
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], msg['forward']);
fig.send_message('refresh', {});
}
};
mpl.figure.prototype.handle_rubberband = function (fig, msg) {
var x0 = msg['x0'] / fig.ratio;
var y0 = (fig.canvas.height - msg['y0']) / fig.ratio;
var x1 = msg['x1'] / fig.ratio;
var y1 = (fig.canvas.height - msg['y1']) / fig.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.ratio,
fig.canvas.height / fig.ratio
);
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.handle_history_buttons = function (fig, msg) {
for (var key in msg) {
if (!(key in fig.buttons)) {
continue;
}
fig.buttons[key].disabled = !msg[key];
fig.buttons[key].setAttribute('aria-disabled', !msg[key]);
}
};
mpl.figure.prototype.handle_navigate_mode = function (fig, msg) {
if (msg['mode'] === 'PAN') {
fig.buttons['Pan'].classList.add('active');
fig.buttons['Zoom'].classList.remove('active');
} else if (msg['mode'] === 'ZOOM') {
fig.buttons['Pan'].classList.remove('active');
fig.buttons['Zoom'].classList.add('active');
} else {
fig.buttons['Pan'].classList.remove('active');
fig.buttons['Zoom'].classList.remove('active');
}
};
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;
}
// pageX,Y are the mouse positions relative to the document
var boundingRect = targ.getBoundingClientRect();
var x = e.pageX - (boundingRect.left + document.body.scrollLeft);
var y = e.pageY - (boundingRect.top + document.body.scrollTop);
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 * this.ratio;
var y = canvas_pos.y * this.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;
};

View file

@ -0,0 +1,8 @@
/* This .js file contains functions for matplotlib's built-in
tornado-based server, that are not relevant when embedding WebAgg
in another web application. */
/* exported mpl_ondownload */
function mpl_ondownload(figure, format) {
window.open(figure.id + '/download.' + format, '_blank');
}

View file

@ -0,0 +1,256 @@
/* global mpl */
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 = document.getElementById(id);
var ws_proxy = comm_websocket_adapter(comm);
function ondownload(figure, _format) {
window.open(figure.canvas.toDataURL());
}
var fig = new mpl.figure(id, ws_proxy, ondownload, element);
// 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;
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;
}
fig.cell_info[0].output_area.element.one(
'cleared',
{ fig: fig },
fig._remove_fig_handler
);
};
mpl.figure.prototype.handle_close = function (fig, msg) {
var width = fig.canvas.width / fig.ratio;
fig.cell_info[0].output_area.element.off(
'cleared',
fig._remove_fig_handler
);
// 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.innerHTML =
'<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 / this.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 toolbar = document.createElement('div');
toolbar.classList = 'btn-toolbar';
this.root.appendChild(toolbar);
function on_click_closure(name) {
return function (_event) {
return fig.toolbar_button_onclick(name);
};
}
function on_mouseover_closure(tooltip) {
return function (event) {
if (!event.currentTarget.disabled) {
return fig.toolbar_button_onmouseover(tooltip);
}
};
}
fig.buttons = {};
var buttonGroup = document.createElement('div');
buttonGroup.classList = 'btn-group';
var button;
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) {
/* Instead of a spacer, we start a new button group. */
if (buttonGroup.hasChildNodes()) {
toolbar.appendChild(buttonGroup);
}
buttonGroup = document.createElement('div');
buttonGroup.classList = 'btn-group';
continue;
}
button = fig.buttons[name] = document.createElement('button');
button.classList = 'btn btn-default';
button.href = '#';
button.title = name;
button.innerHTML = '<i class="fa ' + image + ' fa-lg"></i>';
button.addEventListener('click', on_click_closure(method_name));
button.addEventListener('mouseover', on_mouseover_closure(tooltip));
buttonGroup.appendChild(button);
}
if (buttonGroup.hasChildNodes()) {
toolbar.appendChild(buttonGroup);
}
// Add the status bar.
var status_bar = document.createElement('span');
status_bar.classList = 'mpl-message pull-right';
toolbar.appendChild(status_bar);
this.message = status_bar;
// Add the close button to the window.
var buttongrp = document.createElement('div');
buttongrp.classList = 'btn-group inline pull-right';
button = document.createElement('button');
button.classList = 'btn btn-mini btn-primary';
button.href = '#';
button.title = 'Stop Interaction';
button.innerHTML = '<i class="fa fa-power-off icon-remove icon-large"></i>';
button.addEventListener('click', function (_evt) {
fig.handle_close(fig, {});
});
button.addEventListener(
'mouseover',
on_mouseover_closure('Stop Interaction')
);
buttongrp.appendChild(button);
var titlebar = this.root.querySelector('.ui-dialog-titlebar');
titlebar.insertBefore(buttongrp, titlebar.firstChild);
};
mpl.figure.prototype._remove_fig_handler = function (event) {
var fig = event.data.fig;
fig.close_ws(fig, {});
};
mpl.figure.prototype._root_extra_style = function (el) {
el.style.boxSizing = 'content-box'; // override notebook setting of border-box.
};
mpl.figure.prototype._canvas_extra_style = function (el) {
// this is important to make the div 'focusable
el.setAttribute('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();
// select the cell after this one
var index = IPython.notebook.find_cell_index(this.cell_info[0]);
IPython.notebook.select(index + 1);
}
};
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
);
}

View file

@ -0,0 +1,639 @@
{
"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
}

View file

@ -0,0 +1,15 @@
{
"devDependencies": {
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.10.1",
"prettier": "^2.0.2"
},
"scripts": {
"eslint": "eslint . --fix",
"eslint:check": "eslint .",
"lint": "npm run prettier && npm run eslint",
"lint:check": "npm run prettier:check && npm run eslint:check",
"prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json}\"",
"prettier:check": "prettier --check \"**/*{.ts,.tsx,.js,.jsx,.css,.json}\""
}
}

View file

@ -0,0 +1,37 @@
<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/css/mpl.css" type="text/css">
<script src="{{ prefix }}/_static/js/mpl_tornado.js"></script>
<script src="{{ prefix }}/js/mpl.js"></script>
<script>
function ready(fn) {
if (document.readyState != "loading") {
fn();
} else {
document.addEventListener("DOMContentLoaded", fn);
}
}
ready(
function () {
var websocket_type = mpl.get_websocket_type();
var websocket = new websocket_type(
"{{ ws_uri }}" + {{ str(fig_id) }} + "/ws");
var fig = new mpl.figure(
{{ str(fig_id) }}, websocket, mpl_ondownload,
document.getElementById("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>