340 lines
11 KiB
Python
340 lines
11 KiB
Python
|
|
# coding: utf-8
|
|
# flake8: noqa
|
|
|
|
# # `adashof`: Functions used in the notebooks of the blog
|
|
#
|
|
# [Blog](http://werthmuller.org/blog)
|
|
# [Repo](http://github.com/prisae/blog-notebooks)
|
|
#
|
|
# - circle : Create circle on figure with axes of different sizes.
|
|
# - move_sn_y : Move scientific notation exponent from top to the side.
|
|
# - fillgrid : Fill rectangular grid with colours or a colour and transparency.
|
|
# - checksize : Check size of pdf figure, and adjust if required.
|
|
# - cm2in : Convert centimetres to inches
|
|
|
|
# In[1]:
|
|
|
|
import numpy as np
|
|
import matplotlib as mpl
|
|
from matplotlib import cm
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
# In[2]:
|
|
|
|
def circle(xy, radius, kwargs=None):
|
|
"""Create circle on figure with axes of different sizes.
|
|
|
|
Plots a circle on the current axes using `plt.Circle`, taking into account
|
|
the figure size and the axes units.
|
|
|
|
It is done by plotting in the figure coordinate system, taking the aspect
|
|
ratio into account. In this way, the data dimensions do not matter.
|
|
However, if you adjust `xlim` or `ylim` after plotting `circle`, it will
|
|
screw them up; set `plt.axis` before calling `circle`.
|
|
|
|
Parameters
|
|
----------
|
|
xy, radius, kwars :
|
|
As required for `plt.Circle`.
|
|
"""
|
|
|
|
# Get current figure and axis
|
|
fig = mpl.pyplot.gcf()
|
|
ax = fig.gca()
|
|
|
|
# Calculate figure dimension ratio width/height
|
|
pr = fig.get_figwidth()/fig.get_figheight()
|
|
|
|
# Get the transScale (important if one of the axis is in log-scale)
|
|
tscale = ax.transScale + (ax.transLimits + ax.transAxes)
|
|
ctscale = tscale.transform_point(xy)
|
|
cfig = fig.transFigure.inverted().transform(ctscale)
|
|
|
|
# Create circle
|
|
if kwargs == None:
|
|
circ = mpl.patches.Ellipse(cfig, radius, radius*pr,
|
|
transform=fig.transFigure)
|
|
else:
|
|
circ = mpl.patches.Ellipse(cfig, radius, radius*pr,
|
|
transform=fig.transFigure, **kwargs)
|
|
|
|
# Draw circle
|
|
ax.add_artist(circ)
|
|
|
|
|
|
# In[3]:
|
|
|
|
def move_sn_y(offs=0, dig=0, side='left', omit_last=False):
|
|
"""Move scientific notation exponent from top to the side.
|
|
|
|
Additionally, one can set the number of digits after the comma
|
|
for the y-ticks, hence if it should state 1, 1.0, 1.00 and so forth.
|
|
|
|
Parameters
|
|
----------
|
|
offs : float, optional; <0>
|
|
Horizontal movement additional to default.
|
|
dig : int, optional; <0>
|
|
Number of decimals after the comma.
|
|
side : string, optional; {<'left'>, 'right'}
|
|
To choose the side of the y-axis notation.
|
|
omit_last : bool, optional; <False>
|
|
If True, the top y-axis-label is omitted.
|
|
|
|
Returns
|
|
-------
|
|
locs : list
|
|
List of y-tick locations.
|
|
|
|
Note
|
|
----
|
|
This is kind of a non-satisfying hack, which should be handled more
|
|
properly. But it works. Functions to look at for a better implementation:
|
|
ax.ticklabel_format
|
|
ax.yaxis.major.formatter.set_offset_string
|
|
"""
|
|
|
|
# Get the ticks
|
|
locs, _ = mpl.pyplot.yticks()
|
|
|
|
# Put the last entry into a string, ensuring it is in scientific notation
|
|
# E.g: 123456789 => '1.235e+08'
|
|
llocs = '%.3e' % locs[-1]
|
|
|
|
# Get the magnitude, hence the number after the 'e'
|
|
# E.g: '1.235e+08' => 8
|
|
yoff = int(str(llocs).split('e')[1])
|
|
|
|
# If omit_last, remove last entry
|
|
if omit_last:
|
|
slocs = locs[:-1]
|
|
else:
|
|
slocs = locs
|
|
|
|
# Set ticks to the requested precision
|
|
form = r'$%.'+str(dig)+'f$'
|
|
mpl.pyplot.yticks(locs, list(map(lambda x: form % x, slocs/(10**yoff))))
|
|
|
|
# Define offset depending on the side
|
|
if side == 'left':
|
|
offs = -.18 - offs # Default left: -0.18
|
|
elif side == 'right':
|
|
offs = 1 + offs # Default right: 1.0
|
|
|
|
# Plot the exponent
|
|
mpl.pyplot.text(offs, .98, r'$\times10^{%i}$' % yoff, transform =
|
|
mpl.pyplot.gca().transAxes, verticalalignment='top')
|
|
|
|
# Return the locs
|
|
return locs
|
|
|
|
|
|
# In[4]:
|
|
|
|
def fillgrid(xval, yval, values, style='colour', cmap=cm.Spectral,
|
|
unicol='#000000', lc='k', lw=0.5):
|
|
"""Fill rectangular grid with colours or a colour and transparency.
|
|
|
|
Parameters
|
|
----------
|
|
xval, yval : array
|
|
Grid-points in x- and in y-direction.
|
|
values : array, dimension: (x-1)-by-(y-1)
|
|
Values between 0 and 1
|
|
style : string, optional; {<'colour'>, 'alpha'}
|
|
Defines if values represent colour or alpha.
|
|
cmap : mpl.cm-element, optional
|
|
`Colormap` colours are chosen from; only used if style='colour'
|
|
unicol : HEX-colour
|
|
Colour used with transparency; only used if style='alpha'
|
|
lc, lw : optional
|
|
Line colour and width, as in standard plots.
|
|
|
|
Returns
|
|
-------
|
|
rct : list
|
|
List of plotted polygon patches.
|
|
"""
|
|
|
|
# Ravel values, and set NaN's to zero
|
|
rval = values.ravel()
|
|
rvalnan = np.isnan(rval)
|
|
rval[rvalnan] = 0
|
|
|
|
# Define colour depending on style
|
|
if style == 'alpha':
|
|
# Create RGB from HEX
|
|
unicol = mpl.colors.colorConverter.to_rgb(unicol)
|
|
# Repeat colour for all values,
|
|
# filling the value into the transparency column
|
|
colour = np.vstack((np.repeat(unicol, len(rval)).reshape(3, -1),
|
|
rval)).transpose()
|
|
else:
|
|
# Split cmap into 101 points from 0 to 1
|
|
cmcol = cmap(np.linspace(0, 1, 101))
|
|
# Map the values onto these
|
|
colour = cmcol[list(map(int, 100*rval))]
|
|
# Set transparency to 0 for NaN's
|
|
colour[rvalnan, -1] = 0
|
|
|
|
# Draw all rectangles at once
|
|
xxval = np.array([xval[:-1], xval[:-1], xval[1:], xval[1:]]).repeat(
|
|
len(yval)-1, axis=1).reshape(4, -1)
|
|
yyval = np.array([yval[:-1], yval[1:], yval[1:], yval[:-1]]).repeat(
|
|
len(xval)-1, axis=0).reshape(4, -1)
|
|
rct = mpl.pyplot.gca().fill(xxval, yyval, lw=lw, ec=lc)
|
|
|
|
# Map the colour onto a list
|
|
cls = list(map(mpl.colors.rgb2hex, colour))
|
|
|
|
# Adjust colour and transparency for all cells
|
|
for ind in range(len(rct)):
|
|
rct[ind].set_facecolor(cls[ind])
|
|
rct[ind].set_alpha(colour[ind, -1])
|
|
|
|
return rct
|
|
|
|
|
|
# In[5]:
|
|
|
|
def checksize(fhndl, name, dsize, precision=0.01, extent=0.05, kwargs={}, _cf=False):
|
|
"""Print figure with 'name.pdf', check size, compare with dsize, and adjust if required
|
|
|
|
Parameters
|
|
----------
|
|
fhndl : figure-handle
|
|
Figure handle of the figure to be saved.
|
|
name : string
|
|
Figure name.
|
|
dsize : list of two floats
|
|
Desired size of pdf in cm.
|
|
precision : float, optional; <0.01>
|
|
Desired precision in cm of the dimension, defaults to 1 mm.
|
|
extent : float or list of floats, optional; <0.01>
|
|
- If float, then bbox_inches is set to tight, and pad_inches=extent.
|
|
- If it is an array of two numbers it sets the percentaged extent-width,
|
|
`Bbox.expanded`.
|
|
- If it is an array of four numbers it sets [x0, y0, x1, y1] of Bbox.
|
|
kwargs : dict
|
|
Other input arguments that will be passed on to `plt.savefig`; e.g. dpi or facecolor.
|
|
_cf : Internal parameter for recursion and adjustment.
|
|
"""
|
|
|
|
# Import PyPDF2
|
|
from PyPDF2 import PdfFileReader
|
|
|
|
# Check `extent` input and set bbox_inches and pad_inches accordingly
|
|
if np.size(extent) == 1:
|
|
bbox_inches = 'tight'
|
|
pad_inches = extent
|
|
else:
|
|
fext = fhndl.gca().get_window_extent().transformed(
|
|
fhndl.dpi_scale_trans.inverted())
|
|
if np.size(extent) == 2:
|
|
bbox_inches = fext.expanded(extent[0], extent[1])
|
|
elif np.size(extent) == 4:
|
|
fext.x0, fext.y0, fext.x1, fext.y1 = extent
|
|
extent = [1, 1] # set extent to [1, 1] for recursion
|
|
bbox_inches = fext
|
|
pad_inches=0
|
|
|
|
# Save the figure
|
|
fhndl.savefig(name+'.pdf', bbox_inches=bbox_inches, pad_inches=pad_inches, **kwargs)
|
|
|
|
# Get pdf-dimensions in cm
|
|
pdffile = PdfFileReader(open(name+'.pdf', mode='rb'))
|
|
pdfsize = np.array([float(pdffile.getPage(0).mediaBox[2]),
|
|
float(pdffile.getPage(0).mediaBox[3])])
|
|
pdfdim = pdfsize*2.54/72. # points to cm
|
|
|
|
# Define `print`-precision on desired precision
|
|
pprec = abs(int(('%.1e' % precision).split('e')[1]))+1
|
|
|
|
# Get difference btw desired and actual size
|
|
diff = dsize-pdfdim
|
|
|
|
# If diff>precision, adjust, else finish
|
|
if np.any(abs(diff) > precision):
|
|
if not _cf:
|
|
_cf = [1, 1]
|
|
|
|
# Be verbose
|
|
print(' resize...')
|
|
|
|
# Adjust width
|
|
if (abs(diff[0]) > precision):
|
|
print(' X-diff:', np.round(diff[0], pprec), 'cm')
|
|
|
|
# Set new factor to old factor times (desired size)/(actual size)
|
|
_cf[0] = _cf[0]*dsize[0]/pdfdim[0]
|
|
|
|
# Set new figure width
|
|
fhndl.set_figwidth(_cf[0]*dsize[0]/2.54) # cm2in
|
|
|
|
# Adjust height
|
|
if (abs(diff[1]) > precision):
|
|
print(' Y-diff:', np.round(diff[1], pprec), 'cm')
|
|
|
|
# Set new factor to old factor times (desired size)/(actual size)
|
|
_cf[1] = _cf[1]*dsize[1]/pdfdim[1]
|
|
|
|
# Set new figure height
|
|
fhndl.set_figheight(_cf[1]*dsize[1]/2.54) #cm2in
|
|
|
|
# Call the function again, with new factor _cf
|
|
figsize = checksize(fhndl, name, dsize, precision, extent, kwargs, _cf)
|
|
|
|
return figsize
|
|
|
|
else: # Print some info if the desired dimensions are reached
|
|
|
|
# Print figure name and pdf dimensions
|
|
print('Figure saved to '+name +'.pdf;',
|
|
np.round(pdfdim[0], pprec), 'x',
|
|
np.round(pdfdim[1], pprec), 'cm.')
|
|
|
|
# Print the new figsize if it had to be adjusted
|
|
if _cf:
|
|
print(' => NEW FIG-SIZE: figsize=('+
|
|
str(np.round(fhndl.get_size_inches()[0], 2*pprec))+', '+
|
|
str(np.round(fhndl.get_size_inches()[1], 2*pprec))+')')
|
|
|
|
# Return figsize
|
|
return fhndl.get_size_inches()
|
|
|
|
|
|
# In[6]:
|
|
|
|
def cm2in(length, decimals=2):
|
|
"""Convert cm to inch.
|
|
|
|
Parameters
|
|
----------
|
|
length : scalar or vector
|
|
Numbers to be converted.
|
|
decimals : int, optional; <2>
|
|
As in np.round, used to round the result.
|
|
|
|
Returns
|
|
-------
|
|
cm2in : scalar or vector
|
|
Converted numbers.
|
|
|
|
Examples
|
|
--------
|
|
>>> from adashof import cm2in
|
|
>>> cm2in(5)
|
|
1.97
|
|
|
|
"""
|
|
|
|
# Test input
|
|
try:
|
|
length = np.array(length, dtype='float')
|
|
decimals = int(decimals)
|
|
except ValueError:
|
|
print("{length} must be a number, {decimals} an integer")
|
|
|
|
return np.round(length/2.54, decimals) |