Source code for texfigure.texfigure

# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import sys
import glob
from collections import OrderedDict, Sequence
import six

import numpy as np

import matplotlib
import matplotlib.pyplot as plt

try:
    import mayavi
    from mayavi import mlab
    HAVE_MAYAVI = True
except ImportError:
    HAVE_MAYAVI = False

try:
    import yt
    HAVE_YT = True
except ImportError:
    HAVE_YT = False


__all__ = ['Manager', 'Figure', 'MultiFigure']


[docs]class Figure(object): r""" A class for holding a figure file, that knows how to represent itself as a latex environment. Parameters ---------- file_name : `str` The full path of the figure file. reference : `str` A reference label for this figure, used as default values for caption and label. Attributes ---------- fname : `str` The base name of the full file path base_dir : `str` The directory containing the figure file caption : `str` The caption to use when representing the figure. (Default ``Figure reference``) label : `str` The latex label assigned to the figure envrionment, will bre prefixed with ``'fig``. (Default ``fig:reference``) placement : `str` The figure envrionment placement value. (Default ``h``) figure_env_name : `str` The string used for the figure environment i.e. ``\begin{figure}``. Useful for changing to ``figure*`` etc. (Default ``figure``) figure_width : `str` The latex figure width, not used for pgf files. (Default ``0.95\columnwidth``) subfig_width : `str` LaTeX figure width for when the figure is included in a subfigure. (Default ``0.45\columnwidth``) subfig_placement : `str` The subfigure environment placement. (Default ``b``) extension_mapping : `dict` A mapping of file extensions to methods to return LaTeX includes for the file type. fig_str : `str` The LaTeX template for representing this `~texfigure.Figure` as a figure environment. subfig_str : `str` The LaTeX template for representing this `~texfigure.Figure` as a subfigure. Examples -------- .. code-block:: latex \begin{pycode} import matplotlib.pyplot as plt fig = plt.figure() plt.plot([1,2], [3,4], 'o') myfig = texfigure.Figure(fig, 'myfig') \end{pycode} \py|myfig| """ fig_str = r""" \begin{{{figure_env_name}}}[{placement}] \centering {myfig} \caption{{{caption}}} \label{{{label}}} \end{{{figure_env_name}}} """ # Note the different indentation here, this is deliberate. subfig_str = r""" \begin{{subfigure}}[{placement}]{{{width}}} {myfig} \caption{{{caption}}} \label{{{label}}} \end{{subfigure}}""" def __init__(self, file_name, reference=None): file_name = os.path.abspath(file_name) if not reference: self.reference = os.path.splitext(os.path.basename(file_name))[1] else: self.reference = reference self.reference = self.reference.replace('_', '-') self.file_name = file_name self.fname = os.path.basename(file_name) self.base_dir = os.path.dirname(file_name) + '/' self.caption = "Figure {}".format(self.reference) self.label = "fig:{}".format(self.reference) self.placement = 'h' self.figure_env_name = "figure" self.figure_width = r'0.95\columnwidth' self.subfig_width = r'0.45\columnwidth' self.subfig_placement = 'b' self.extension_mapping = {'.pgf': self.get_pgf_include, '.png': self.get_standard_include, '.pdf': self.get_standard_include} @property def extension(self): """ File extension of fname. """ return os.path.splitext(self.fname)[1]
[docs] def get_pgf_include(self): """ Return the import statement for this `~texfigure.Figure` as a pgf file.close. """ return r"\IfFileExists{{{file_name}}}{{\import{{{base_dir}}}{{{fname}}}}}{{}}".format( fname=self.fname, base_dir=self.base_dir, file_name=self.file_name)
[docs] def get_standard_include(self): """ Return the includegraphics command for most other file types. """ return "\includegraphics[width={width}]{{{file_name}}}".format( width=self.figure_width, file_name=self.file_name)
[docs] def repr_figure(self): """ Return a string LaTeX figure environment for this `~texfigure.Figure` """ default_kwargs = {'placement': self.placement, 'caption': self.caption, 'label': self.label, 'figure_env_name': self.figure_env_name} myfig = self.extension_mapping[self.extension]() return self.fig_str.format(myfig=myfig, **default_kwargs)
[docs] def repr_subfigure(self): """ Return a string subfigure environment for this `~texfigure.Figure` """ default_kwargs = {'placement': self.subfig_placement, 'width': self.subfig_width, 'caption': self.caption, 'label': self.label} myfig = self.extension_mapping[self.extension]() return self.subfig_str.format(myfig=myfig, **default_kwargs)
def _repr_latex_(self): return self.repr_figure()
[docs]class MultiFigure(Sequence): r""" A Multifigure is a container object for building subfigures from `texfigure.Figure` classes. Parameters ---------- nrows : `int` Number of rows for the MultiFigure. ncols : `int` Number of columns for the MultiFigure. Attributes ---------- figures : `numpy.ndarray` Array holding `texfigure.Figure` objects, has a shape of (nrows, ncols). caption : `str` The caption to use when representing the figure. (Default ``Figure reference``) label : `str` The latex label assigned to the figure envrionment. (Default ``fig:reference``) placement : `str` The figure envrionment placement value. (Default ``h``) frontmatter : `str` LaTeX code included in the first line of the figure environment. (Default ``\centering``) Examples -------- .. code-block:: latex \begin{pycode} muti = texfigure.MultiFigure(1,2) X = [[5,6], [7,8]] Y = [[1,2], [3,4]] for x,y in zip(X,Y): fig = plt.figure() plt.plot(x, y, 'o') Fig1 = texfigure.Figure(fig) multi.append(Fig) \end{pycode} \py|multi| """ fig_str = r""" \begin{{figure*}} {frontmatter} {myfig} \caption{{{caption}}} \label{{{label}}} \end{{figure*}} """ def __init__(self, nrows, ncols, reference='', continuation=False): self.nrows = nrows self.ncols = ncols self.reference = reference self.caption = "MultiFigure {}".format(self.reference) self.label = "fig:{}".format(self.reference) self.placement = 'H' self.frontmatter = '\centering' if continuation: self.frontmatter += '\n' + r'\ContinuedFloat' self.figures = np.zeros([nrows, ncols], dtype=object) self.figures[:] = None def __len__(self): return self.figures.size() # TODO: This is not really felxible enough. It dosen't work when ncols > 1. # It should probably be a method which can split it into x vertical chunks. def __getitem__(self, key): """ Return a continuation when indexed, unless indexed for a single figure, when we return the `texfigure.Figure` instance. """ if isinstance(key, int): return self.figures.flat[key] elif isinstance(key, slice): # If this is the start of the list then it's not a cont. if key.start is None or key.start == 0: continuation = False new_ref = self.reference else: continuation = True new_ref = self.reference + "-c" new_figures = self.figures[key] new_mf = MultiFigure(*new_figures.shape, reference=new_ref, continuation=continuation) new_mf.figures = new_figures # If we are at the end then add the caption if key.stop is None or key.stop == self.figures.size: new_mf.caption = self.caption else: new_mf.caption = '' return new_mf else: raise KeyError("MultiFigure only supports 1D indexing.")
[docs] def append(self, figure): """ Add a `texfigure.Figure` object to the next empty slot in the `~texfigure.MultiFigure`. """ if not isinstance(figure, Figure): raise TypeError("Only texfigure.Figures can be" " appended to a MultiFigure") empties = np.logical_not(self.figures.flat).nonzero()[0] if len(empties): self.figures.flat[empties[0]] = figure else: raise ValueError("This MultiFigure is full")
def _repr_latex_(self): default_kwargs = {'placement': self.placement, 'caption': self.caption, 'label': self.label, 'frontmatter': self.frontmatter} subfigures = "" for i, fig in enumerate(self.figures.flat): if fig: if i % self.ncols == 0: subfigures += '\n' subfigures += fig.repr_subfigure() return self.fig_str.format(myfig=subfigures, **default_kwargs)
[docs]class Manager(object): """ A class holding information about different figures and data. Parameters ---------- pytex : ``PythonTeXUtils`` instance. The pytex object from the PythonTeX session. number : `int` Numerical index for this chapter. base_path : `str` Path in which ``Data``, ``Figs`` and ``Python`` directories will be created to hold files related to this manager. If ``python_dir``, ``data_dir`` or ``figure_dir`` are specified then they will override this setting. python_dir : `bool` or `str` Path to a directory containing Python code to be added to the Python path. Overrides ``base_path/Python``. data_dir : `bool` or `str` Path to a directory containing data files which can be accessed through the manager. Overrides ``base_path/Data`` figure_dir : `bool` or `str` Path to a directory containing generated figures. Overrides ``base_path/Figs`` Attributes ---------- savefig_functions : `dict` A mapping between figure types and functions to save them to a given filename. Functions in the mapping must accept two arguments, the figure object and a filename, the function must return the filename as saved to disk. """ def __init__(self, pytex, base_path, number=1, python_dir=True, data_dir=True, fig_dir=True): self.pytex = pytex self._number = number self._base_path = base_path self._python_dir = None self.python_dir = python_dir self._data_dir = None self.data_dir = data_dir self._fig_dir = None self.fig_dir = fig_dir self.fig_count = 1 self._figure_registry = OrderedDict() self.savefigure_functions = {matplotlib.figure.Figure: self._save_mpl_figure} if HAVE_MAYAVI: self.savefigure_functions[mayavi.core.scene.Scene] = self._save_mayavi_figure if HAVE_YT: self.savefigure_functions[yt.visualization.plot_container.ImagePlotContainer] = self._save_yt_ipc def _add_dir(self, adir, attr, default): if adir: if not isinstance(adir, six.string_types): setattr(self, attr, os.path.join(self._base_path, default)) else: setattr(self, attr, adir) if not os.path.exists(getattr(self, attr)): os.makedirs(getattr(self, attr)) @property def data_dir(self): """ Data directory for files tracked with this manager. If data_dir is set to False no directory will be used, if set to True the default directory of ``manager.base_path/Data`` will be used, if data_dir is set to a `str` then that dir will be used. """ return self._data_dir @data_dir.setter def data_dir(self, value): self._add_dir(value, '_data_dir', 'Data') @property def fig_dir(self): """ Figure directory for figures tracked with this manager. If fig_dir is set to False no directory will be used, if set to True the default directory of ``manager.base_path/Figs`` will be used, if fig_dir is set to a `str` then that dir will be used. """ return self._fig_dir @fig_dir.setter def fig_dir(self, value): self._add_dir(value, '_fig_dir', 'Figs') @property def python_dir(self): """ Python directory for custom python code, this directory will be added to your Python Path. If python_dir is set to False no directory will be used, if set to True the default directory of ``manager.base_path/Python`` will be used, if python_dir is set to a `str` then that dir will be used. Notes ----- If this attribute is changed, the old directory will not be removed from the python path. """ return self._python_dir @python_dir.setter def python_dir(self, value): self._add_dir(value, '_python_dir', 'Python') if self._python_dir: sys.path.append(self._python_dir) @property def number(self): """ A Number indicating the position of this manager in a series of managers, i.e. chapters in a thesis. """ return self._number
[docs] def data_file(self, file_name): """ Get the full path of a data file in this chapters data directory, add it to the pytex tracked files. Parameters ---------- file_name : `str` The filename in the data directory """ fpattern = os.path.join(self.data_dir, file_name) fpaths = glob.glob(fpattern) for fpath in fpaths: self.pytex.add_dependencies(fpath) if not fpaths: raise ValueError("No files found matching this name or pattern.") elif len(fpaths) == 1: return fpaths[0] else: return fpaths
[docs] def make_figure_filename(self, ref, fname=None, fext='', fullpath=False): """ Return the standard template figure name with number. Parameters ---------- ref : `str` The latex reference for this figure (excluding 'fig:') fname : `str` Overwrite the default file name template with this name. Returns ------- fname : `str` The file name """ if not fname: fname = 'Chapter{}-Figure{}-{}{}'.format(self.number, self.fig_count, ref, fext) if fullpath: fname = os.path.join(self.fig_dir, fname) return fname
def _save_mpl_figure(self, fig, filename, **kwargs): """ A wrapper to save a matplotlib figure object to a file. """ fig.savefig(filename, **kwargs) return filename def _save_mayavi_figure(self, fig, filename, azimuth=153, elevation=62, distance=400, focalpoint=[25., 63., 60.], aa=16, size=(1024, 1024)): """ A wrapper to save a mayavi figure object """ scene = fig.scene scene.anti_aliasing_frames = aa mlab.view(azimuth=azimuth, elevation=elevation, distance=distance, focalpoint=focalpoint) scene.save(filename, size=size) return filename def _save_yt_ipc(self, slc, filename, **kwargs): if len(slc.plots) != 1: raise NotImplementedError("Can't currently handle a container with" " more than one plot!") fname, suffix = os.path.splitext(filename) filename, = slc.save(fname, suffix=suffix[1:], **kwargs) return filename
[docs] def add_figure(self, ref, Fig): """ Add the figure to the tracked files and increment the figure count. Parameters ---------- ref : `str` The latex reference for this figure (excluding 'fig:') Fig : `texfigure.Figure` The `~texfigure.Figure` object to add to the manager. """ self.pytex.add_created(Fig.file_name) self._figure_registry[ref] = {'number': self.fig_count, 'Figure': Fig} self.fig_count += 1
[docs] def save_figure(self, ref, fig=None, fname=None, fext='.pdf', **kwargs): """ Save a figure to a file, and track it using this manager object. Parameters ---------- ref : `str` A `str` to use as a key inside this manager, and to add to the filename and to use a the latex reference. fig : object A figure object of a type that has an entry in the `~texfigure.Manager.savefig_functions` dictionary. If None it will be assumed that the current ``pyplot`` figure is to be used and `~matplotlib.pyplot.gcf` will be called. fname : `str` The file name to be used, not including the extension or the path. fext : `str` The file extension to be used to save the file. kwargs : `dict` Other keyword arguments are passed onto the save figure function. Returns ------- Fig : `texfigure.Figure` The `~texfigure.Figure` object added to this `~texfigure.Manager`. """ if fig is None: fig = plt.gcf() fname = self.make_figure_filename(ref, fname=fname, fext=fext, fullpath=True) for atype in self.savefigure_functions.keys(): if issubclass(type(fig), atype): fname = self.savefigure_functions[atype](fig, fname, **kwargs) Fig = Figure(fname, reference=ref) self.add_figure(ref, Fig) return Fig
[docs] def get_figure(self, ref): """ Get the `~texfigure.Figure` object corresponding to the given reference. Parameters ---------- ref : `str` The Figure reference Returns ------- Figure : `texfigure.Figure` The Figure object. """ return self._figure_registry[ref]['Figure']
[docs] def get_multifigure(self, nrows, ncols, refs, reference=''): """ Return a `texfigure.MultiFigure` object made up of a set of figure references stored in this `~texfigure.Manager` instance. Parameters ---------- nrows : `int` Number of rows for the MultiFigure. ncols : `int` Number of columns for the MultiFigure. refs : `list` A list of figure references no more than nrows * ncols long. reference : `str` The reference for the `texfigure.MultiFigure` object. Returns ------- multfigure : `texfigure.MultiFigure` The initilised and populated multifigure object. """ if len(refs) > nrows * ncols: raise ValueError("You can not specify more references than number " "of spaces in your multifigure grid.") mf = MultiFigure(nrows, ncols, reference=reference) for ref in refs: lfig = self.get_figure(ref) mf.append(lfig) return mf