mirror of
https://github.com/xonsh/xonsh.git
synced 2025-03-04 08:24:40 +01:00
Merge pull request #1790 from derekbrokeit/xontrib-mpl_copy-fig
xontrib-mpl makes a copy of the current figure before modifying it
This commit is contained in:
commit
b8e4adbaf3
5 changed files with 253 additions and 29 deletions
|
@ -1,10 +1,10 @@
|
||||||
if [[ $CIRCLE_NODE_INDEX == 0 ]]
|
if [[ $CIRCLE_NODE_INDEX == 0 ]]
|
||||||
then
|
then
|
||||||
conda create -q -n test_env python=3.4 pygments prompt_toolkit ply pytest pytest-timeout psutil
|
conda create -q -n test_env python=3.4 pygments prompt_toolkit ply pytest pytest-timeout psutil numpy matplotlib
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
if [[ $CIRCLE_NODE_INDEX == 1 ]]
|
if [[ $CIRCLE_NODE_INDEX == 1 ]]
|
||||||
then
|
then
|
||||||
conda create -q -n test_env python=3.5 pygments prompt_toolkit ply pytest pytest-timeout psutil
|
conda create -q -n test_env python=3.5 pygments prompt_toolkit ply pytest pytest-timeout psutil numpy matplotlib
|
||||||
fi
|
fi
|
||||||
|
|
16
news/xontrib-mpl_copy-fig.rst
Normal file
16
news/xontrib-mpl_copy-fig.rst
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
**Added:**
|
||||||
|
|
||||||
|
* $XONTRIB_MPL_MINIMAL environment variable can be set to change if plots are minimalist or as-seen
|
||||||
|
* xontrib-mpl now supports iTerm2 inline image display if iterm2_tools python package is installed
|
||||||
|
|
||||||
|
**Changed:** None
|
||||||
|
|
||||||
|
**Deprecated:** None
|
||||||
|
|
||||||
|
**Removed:** None
|
||||||
|
|
||||||
|
**Fixed:**
|
||||||
|
|
||||||
|
* xontrib-mpl now preserves the figure and does not permanently alter it for viewing
|
||||||
|
|
||||||
|
**Security:** None
|
|
@ -10,3 +10,5 @@ pygments==2.1.3
|
||||||
codecov==2.0.5
|
codecov==2.0.5
|
||||||
flake8==2.6.2
|
flake8==2.6.2
|
||||||
coverage==4.2
|
coverage==4.2
|
||||||
|
numpy==1.10.4
|
||||||
|
matplotlib==1.5.1
|
||||||
|
|
104
tests/test_mpl.py
Normal file
104
tests/test_mpl.py
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# make sure to skip these tests entirely if numpy/matplotlib are not present
|
||||||
|
np = pytest.importorskip("numpy")
|
||||||
|
matplotlib = pytest.importorskip("matplotlib")
|
||||||
|
plt = pytest.importorskip("matplotlib.pyplot")
|
||||||
|
|
||||||
|
from xontrib import mplhooks
|
||||||
|
|
||||||
|
|
||||||
|
# some default settings that are temporarily changed by mpl
|
||||||
|
FONT_SIZE = 22
|
||||||
|
FACE_COLOR = (0.0, 1.0, 0.0, 1.0)
|
||||||
|
DPI = 80
|
||||||
|
|
||||||
|
|
||||||
|
def create_figure():
|
||||||
|
"""Simply create a figure with the default settings"""
|
||||||
|
f, ax = plt.subplots()
|
||||||
|
ax.plot(np.arange(20), np.arange(20))
|
||||||
|
# set the figure parameters such that mpl will require changes
|
||||||
|
f.set_facecolor(FACE_COLOR)
|
||||||
|
f.dpi = DPI
|
||||||
|
matplotlib.rcParams.update({'font.size': FONT_SIZE})
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpl_preserve_font_size():
|
||||||
|
"""Make sure that matplotlib preserves font size settings"""
|
||||||
|
f = create_figure()
|
||||||
|
width, height = f.canvas.get_width_height()
|
||||||
|
s = mplhooks.figure_to_tight_array(f, 0.5*width, 0.5*height, True)
|
||||||
|
exp = FONT_SIZE
|
||||||
|
obs = matplotlib.rcParams['font.size']
|
||||||
|
plt.close(f)
|
||||||
|
assert exp == obs
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpl_preserve_face_color():
|
||||||
|
"""Make sure that the figure preserves face color settings"""
|
||||||
|
f = create_figure()
|
||||||
|
width, height = f.canvas.get_width_height()
|
||||||
|
s = mplhooks.figure_to_tight_array(f, 0.5*width, 0.5*height, True)
|
||||||
|
exp = FACE_COLOR
|
||||||
|
obs = f.get_facecolor()
|
||||||
|
plt.close(f)
|
||||||
|
assert exp == obs
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpl_preserve_width():
|
||||||
|
"""Make sure that the figure preserves width settings"""
|
||||||
|
f = create_figure()
|
||||||
|
width, height = f.canvas.get_width_height()
|
||||||
|
s = mplhooks.figure_to_tight_array(f, 0.5*width, 0.5*height, True)
|
||||||
|
exp = width
|
||||||
|
newwidth, newheight = f.canvas.get_width_height()
|
||||||
|
obs = newwidth
|
||||||
|
plt.close(f)
|
||||||
|
assert exp == obs
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpl_preserve_height():
|
||||||
|
"""Make sure that the figure preserves height settings"""
|
||||||
|
f = create_figure()
|
||||||
|
width, height = f.canvas.get_width_height()
|
||||||
|
s = mplhooks.figure_to_tight_array(f, 0.5*width, 0.5*height, True)
|
||||||
|
exp = height
|
||||||
|
newwidth, newheight = f.canvas.get_width_height()
|
||||||
|
obs = newheight
|
||||||
|
plt.close(f)
|
||||||
|
assert exp == obs
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpl_preserve_dpi():
|
||||||
|
"""Make sure that the figure preserves height settings"""
|
||||||
|
f = create_figure()
|
||||||
|
width, height = f.canvas.get_width_height()
|
||||||
|
s = mplhooks.figure_to_tight_array(f, 0.5*width, 0.5*height, False)
|
||||||
|
exp = DPI
|
||||||
|
obs = f.dpi
|
||||||
|
plt.close(f)
|
||||||
|
assert exp == obs
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpl_preserve_image_tight():
|
||||||
|
"""Make sure that the figure preserves height settings"""
|
||||||
|
f = create_figure()
|
||||||
|
exp = mplhooks.figure_to_rgb_array(f)
|
||||||
|
width, height = f.canvas.get_width_height()
|
||||||
|
s = mplhooks.figure_to_tight_array(f, 0.5*width, 0.5*height, True)
|
||||||
|
obs = mplhooks.figure_to_rgb_array(f)
|
||||||
|
plt.close(f)
|
||||||
|
assert np.all(exp == obs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mpl_preserve_standard():
|
||||||
|
"""Make sure that the figure preserves height settings"""
|
||||||
|
f = create_figure()
|
||||||
|
exp = mplhooks.figure_to_rgb_array(f)
|
||||||
|
width, height = f.canvas.get_width_height()
|
||||||
|
s = mplhooks.figure_to_tight_array(f, 0.5*width, 0.5*height, False)
|
||||||
|
obs = mplhooks.figure_to_rgb_array(f)
|
||||||
|
plt.close(f)
|
||||||
|
assert np.all(exp == obs)
|
|
@ -1,4 +1,5 @@
|
||||||
"""Matplotlib hooks, for what its worth."""
|
"""Matplotlib hooks, for what its worth."""
|
||||||
|
from io import BytesIO
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -7,33 +8,114 @@ import matplotlib.pyplot as plt
|
||||||
|
|
||||||
from xonsh.tools import print_color, ON_WINDOWS
|
from xonsh.tools import print_color, ON_WINDOWS
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Use iterm2_tools as an indicator for the iterm2 terminal emulator
|
||||||
|
from iterm2_tools.images import display_image_bytes
|
||||||
|
except ImportError:
|
||||||
|
_use_iterm = False
|
||||||
|
else:
|
||||||
|
_use_iterm = True
|
||||||
|
|
||||||
def figure_to_rgb_array(fig, width, height):
|
XONTRIB_MPL_MINIMAL_DEFAULT = True
|
||||||
"""Converts figure to a numpy array of rgb values
|
|
||||||
|
|
||||||
Forked from http://www.icare.univ-lille1.fr/wiki/index.php/How_to_convert_a_matplotlib_figure_to_a_numpy_array_or_a_PIL_image
|
|
||||||
|
def _get_buffer(fig, **kwargs):
|
||||||
|
b = BytesIO()
|
||||||
|
fig.savefig(b, **kwargs)
|
||||||
|
b.seek(0)
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def figure_to_rgb_array(fig, shape=None):
|
||||||
|
"""Converts figure to a numpy array
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fig : matplotlib.figure.Figure
|
||||||
|
the figure to be plotted
|
||||||
|
shape : iterable
|
||||||
|
with the shape of the output array. by default this attempts to use the
|
||||||
|
pixel height and width of the figure
|
||||||
|
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
array : np.ndarray
|
||||||
|
An RGBA array of the image represented by the figure.
|
||||||
|
|
||||||
|
Note: the method will throw an exception if the given shape is wrong.
|
||||||
"""
|
"""
|
||||||
|
array = np.frombuffer(_get_buffer(fig, dpi=fig.dpi, format='raw').read(), dtype='uint8')
|
||||||
|
if shape is None:
|
||||||
|
w, h = fig.canvas.get_width_height()
|
||||||
|
shape = (h, w, 4)
|
||||||
|
return array.reshape(*shape)
|
||||||
|
|
||||||
|
|
||||||
|
def figure_to_tight_array(fig, width, height, minimal=True):
|
||||||
|
"""Converts figure to a numpy array of rgb values of tight value
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fig : matplotlib.figure.Figure
|
||||||
|
the figure to be plotted
|
||||||
|
width : int
|
||||||
|
pixel width of the final array
|
||||||
|
height : int
|
||||||
|
pixel height of the final array
|
||||||
|
minimal : bool
|
||||||
|
whether or not to reduce the output array to minimized margins/whitespace
|
||||||
|
text is also eliminated
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
array : np.ndarray
|
||||||
|
An RGBA array of the image represented by the figure.
|
||||||
|
"""
|
||||||
|
# store the properties of the figure in order to restore it
|
||||||
w, h = fig.canvas.get_width_height()
|
w, h = fig.canvas.get_width_height()
|
||||||
dpi = fig.get_dpi()
|
dpi_fig = fig.dpi
|
||||||
fig.set_size_inches(width/dpi, height/dpi, forward=True)
|
if minimal:
|
||||||
width, height = fig.canvas.get_width_height()
|
# perform reversible operations to produce an optimally tight layout
|
||||||
ax = fig.gca()
|
dpi = dpi_fig
|
||||||
ax.set_xticklabels([])
|
subplotpars = {
|
||||||
ax.set_yticklabels([])
|
k: getattr(fig.subplotpars, k)
|
||||||
fig.set_tight_layout(True)
|
for k in ['wspace', 'hspace', 'bottom', 'top', 'left', 'right']
|
||||||
fig.set_frameon(False)
|
}
|
||||||
fig.set_facecolor('w')
|
|
||||||
font_size = matplotlib.rcParams['font.size']
|
# set the figure dimensions to the terminal size
|
||||||
matplotlib.rcParams.update({'font.size': 1})
|
fig.set_size_inches(width/dpi, height/dpi, forward=True)
|
||||||
|
width, height = fig.canvas.get_width_height()
|
||||||
|
|
||||||
|
# remove all space between subplots
|
||||||
|
fig.subplots_adjust(wspace=0, hspace=0)
|
||||||
|
# move all subplots to take the entirety of space in the figure
|
||||||
|
# leave only one line for top and bottom
|
||||||
|
fig.subplots_adjust(bottom=1/height, top=1-1/height, left=0, right=1)
|
||||||
|
|
||||||
|
# redeuce font size in order to reduce text impact on the image
|
||||||
|
font_size = matplotlib.rcParams['font.size']
|
||||||
|
matplotlib.rcParams.update({'font.size': 0})
|
||||||
|
else:
|
||||||
|
dpi = min([width * fig.dpi // w, height * fig.dpi // h])
|
||||||
|
fig.dpi = dpi
|
||||||
|
width, height = fig.canvas.get_width_height()
|
||||||
|
|
||||||
# Draw the renderer and get the RGB buffer from the figure
|
# Draw the renderer and get the RGB buffer from the figure
|
||||||
fig.canvas.draw()
|
array = figure_to_rgb_array(fig, shape=(height, width, 4))
|
||||||
buf = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8)
|
|
||||||
buf.shape = (height, width, 3)
|
|
||||||
|
|
||||||
# clean up and return
|
if minimal:
|
||||||
matplotlib.rcParams.update({'font.size': font_size})
|
# cleanup after tight layout
|
||||||
return buf
|
# clean up rcParams
|
||||||
|
matplotlib.rcParams.update({'font.size': font_size})
|
||||||
|
|
||||||
|
# reset the axis positions and figure dimensions
|
||||||
|
fig.set_size_inches(w/dpi, h/dpi, forward=True)
|
||||||
|
fig.subplots_adjust(**subplotpars)
|
||||||
|
else:
|
||||||
|
fig.dpi = dpi_fig
|
||||||
|
|
||||||
|
return array
|
||||||
|
|
||||||
|
|
||||||
def buf_to_color_str(buf):
|
def buf_to_color_str(buf):
|
||||||
|
@ -55,12 +137,32 @@ def buf_to_color_str(buf):
|
||||||
return ''.join(pixels)
|
return ''.join(pixels)
|
||||||
|
|
||||||
|
|
||||||
|
def display_figure_with_iterm2(fig):
|
||||||
|
"""Displays a matplotlib figure using iterm2 inline-image escape sequence.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
fig : matplotlib.figure.Figure
|
||||||
|
the figure to be plotted
|
||||||
|
"""
|
||||||
|
print(display_image_bytes(_get_buffer(fig, format='png', dpi=fig.dpi).read()))
|
||||||
|
|
||||||
|
|
||||||
def show():
|
def show():
|
||||||
|
'''Run the mpl display sequence by printing the most recent figure to console'''
|
||||||
|
try:
|
||||||
|
minimal = __xonsh_env__['XONTRIB_MPL_MINIMAL']
|
||||||
|
except KeyError:
|
||||||
|
minimal = XONTRIB_MPL_MINIMAL_DEFAULT
|
||||||
fig = plt.gcf()
|
fig = plt.gcf()
|
||||||
w, h = shutil.get_terminal_size()
|
if _use_iterm:
|
||||||
if ON_WINDOWS:
|
display_figure_with_iterm2(fig)
|
||||||
w -= 1 # @melund reports that win terminals are too thin
|
else:
|
||||||
h -= 1 # leave space for next prompt
|
# Display the image using terminal characters to fit into the console
|
||||||
buf = figure_to_rgb_array(fig, w, h)
|
w, h = shutil.get_terminal_size()
|
||||||
s = buf_to_color_str(buf)
|
if ON_WINDOWS:
|
||||||
print_color(s)
|
w -= 1 # @melund reports that win terminals are too thin
|
||||||
|
h -= 1 # leave space for next prompt
|
||||||
|
buf = figure_to_tight_array(fig, w, h, minimal)
|
||||||
|
s = buf_to_color_str(buf)
|
||||||
|
print_color(s)
|
||||||
|
|
Loading…
Add table
Reference in a new issue