adashof: Functions used in the notebooks of the blog

Blog Repo

  • 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)