"""Core package for interactive visualization in the psyplot package

This package defines the :class:`Plotter` and :class:`Formatoption` classes,
the core of the visualization in the :mod:`psyplot` package. Each
:class:`Plotter` combines a set of formatoption keys where each formatoption
key is represented by a :class:`Formatoption` subclass."""

import six
import weakref
from abc import ABCMeta, abstractmethod
from textwrap import TextWrapper
import logging
from itertools import chain, groupby, tee, repeat, starmap
from collections import defaultdict
from threading import RLock
from datetime import datetime, timedelta
from numpy import datetime64, timedelta64, ndarray, inf
from xarray.core.formatting import format_timestamp, format_timedelta
from psyplot import rcParams
from psyplot.warning import warn, critical, PsyPlotRuntimeWarning
from psyplot.compat.pycompat import map, filter, zip, range
from psyplot.config.rcsetup import SubDict
from psyplot.docstring import docstrings, dedent
from psyplot.data import (
    InteractiveList, _no_auto_update_getter, CFDecoder)
from psyplot.utils import (DefaultOrderedDict, _TempBool, _temp_bool_prop,
                           unique_everseen, check_key)

#: the default function to use when printing formatoption infos (the default is
#: use print or in the gui, use the help explorer)
default_print_func = six.print_


#: :class:`dict`. Mapping from group to group names
groups = {
    'data': 'Data manipulation formatoptions',
    'axes': 'Axes formatoptions',
    'labels': 'Label formatoptions',
    'plotting': 'Plot formatoptions',
    'post_processing': 'Post processing formatoptions',
    'colors': 'Color coding formatoptions',
    'misc': 'Miscallaneous formatoptions',
    'ticks': 'Axis tick formatoptions',
    'vector': 'Vector plot formatoptions',
    'masking': 'Masking formatoptions',
    'regression': 'Fitting formatoptions',
    }


def _identity(*args):
    """identity function to make no validation

    Returns
    -------
    object
        just return the last argument in ``*args``"""
    return args[-1]


def format_time(x):
    """Formats date values

    This function formats :class:`datetime.datetime` and
    :class:`datetime.timedelta` objects (and the corresponding numpy objects)
    using the :func:`xarray.core.formatting.format_timestamp` and the
    :func:`xarray.core.formatting.format_timedelta` functions.

    Parameters
    ----------
    x: object
        The value to format. If not a time object, the value is returned

    Returns
    -------
    str or `x`
        Either the formatted time object or the initial `x`"""
    if isinstance(x, (datetime64, datetime)):
        return format_timestamp(x)
    elif isinstance(x, (timedelta64, timedelta)):
        return format_timedelta(x)
    elif isinstance(x, ndarray):
        return list(x) if x.ndim else x[()]
    return x


def is_data_dependent(fmto, data):
    """Check whether a formatoption is data dependent

    Parameters
    ----------
    fmto: Formatoption
        The :class:`Formatoption` instance to check
    data: xarray.DataArray
        The data array to use if the :attr:`~Formatoption.data_dependent`
        attribute is a callable

    Returns
    -------
    bool
        True, if the formatoption depends on the data"""
    if callable(fmto.data_dependent):
        return fmto.data_dependent(data)
    return fmto.data_dependent


def _child_property(childname):
    def get_x(self):
        return getattr(self.plotter, self._child_mapping[childname])

    return property(
        get_x, doc=childname + " Formatoption instance in the plotter")


class FormatoptionMeta(ABCMeta):
    """Meta class for formatoptions

    This class serves as a meta class for formatoptions and allows a more
    efficient docstring generation by using the
    :attr:`psyplot.docstring.docstrings` when creating a new formatoption
    class"""
    def __new__(cls, clsname, bases, dct):
        """Assign an automatic documentation to the formatoption"""
        doc = dct.get('__doc__')
        if doc is not None:
            dct['__doc__'] = docstrings.dedent(doc)
        new_cls = super(FormatoptionMeta, cls).__new__(cls, clsname, bases,
                                                       dct)
        for childname in chain(new_cls.children, new_cls.dependencies,
                               new_cls.connections, new_cls.parents):
            setattr(new_cls, childname, _child_property(childname))
        if new_cls.plot_fmt:
            new_cls.data_dependent = True
        return new_cls


# priority values

#: Priority value of formatoptions that are updated before the data is loaded.
START = 30
#: Priority value of formatoptions that are updated before the plot it made.
BEFOREPLOTTING = 20
#: Priority value of formatoptions that are updated at the end.
END = 10


@six.add_metaclass(FormatoptionMeta)
class Formatoption(object):
    """Abstract formatoption

    This class serves as an abstract version of an formatoption descriptor
    that can be used by :class:`~psyplot.plotter.Plotter` instances."""

    priority = END
    """:class:`int`. Priority value of the the formatoption determining when
    the formatoption is updated.

    - 10: at the end (for labels, etc.)
    - 20: before the plotting (e.g. for colormaps, etc.)
    - 30: before loading the data (e.g. for lonlatbox)"""

    #: :class:`str`. Formatoption key of this class in the
    #: :class:`~psyplot.plotter.Plotter` class
    key = None

    _plotter = None

    @property
    def plotter(self):
        """:class:`~psyplot.plotter.Plotter`. Plotter instance this
        formatoption belongs to"""
        if self._plotter is None:
            return
        return self._plotter()

    @plotter.setter
    def plotter(self, value):
        if value is not None:
            self._plotter = weakref.ref(value)
        else:
            self._plotter = value

    #: `list of str`. List of formatoptions that have to be updated before this
    #: one is updated. Those formatoptions are only updated if they exist in
    #: the update parameters.
    children = []

    #: `list of str`. List of formatoptions that force an update of this
    #: formatoption if they are updated.
    dependencies = []

    #: `list of str`. Connections to other formatoptions that are (different
    #: from :attr:`dependencies` and :attr:`children`) not important for the
    #: update process
    connections = []

    #: `list of str`. List of formatoptions that, if included in the update,
    #: prevent the update of this formatoption.
    parents = []

    #: :class:`bool`. Has to be True if the formatoption has a ``make_plot``
    #: method to make the plot.
    plot_fmt = False

    #: :class:`bool`. True if an update of this formatoption requires a
    #: clearing of the axes and reinitialization of the plot
    requires_clearing = False

    #: :class:`str`. Key of the group name in :data:`groups` of this
    #: formatoption keyword
    group = 'misc'

    #: :class:`bool` or a callable. This attribute indicates whether this
    #: :class:`Formatoption` depends on the data and should be updated if the
    #: data changes. If it is a callable, it must accept one argument: the
    #: new data. (Note: This is automatically set to True for plot
    #: formatoptions)
    data_dependent = False

    #: :class:`bool`. True if this formatoption needs an update after the plot
    #: has changed
    update_after_plot = False

    #: :class:`set` of the :class:`Formatoption` instance that are shared
    #: with this instance.
    shared = set()

    #: int or None. Index that is used in case the plotting data is a
    #: :class:`psyplot.InteractiveList`
    index_in_list = 0

    #: :class:`str`. A bit more verbose name than the formatoption key to be
    #: included in the gui. If None, the key is used in the gui
    name = None

    #: Boolean that is True if an update of the formatoption requires a replot
    requires_replot = False

    @property
    def init_kwargs(self):
        """:class:`dict` key word arguments that are passed to the
        initialization of a new instance when accessed from the descriptor"""
        return self._child_mapping

    @property
    def project(self):
        """Project of the plotter of this instance"""
        return self.plotter.project

    @property
    def ax(self):
        """The axes this Formatoption plots on"""
        return self.plotter.ax

    @property
    def lock(self):
        """A :class:`threading.Rlock` instance to lock while updating

        This lock is used when multiple :class:`plotter` instances are
        updated at the same time while sharing formatoptions."""
        try:
            return self._lock
        except AttributeError:
            self._lock = RLock()
            return self._lock

    @property
    def logger(self):
        """Logger of the plotter"""
        return self.plotter.logger.getChild(self.key)

    @property
    def groupname(self):
        """Long name of the group this formatoption belongs too."""
        try:
            return groups[self.group]
        except KeyError:
            warn("Unknown formatoption group " + str(self.group),
                 PsyPlotRuntimeWarning)
            return self.group

    @property
    def raw_data(self):
        """The original data of the plotter of this formatoption"""
        if self.index_in_list is not None and isinstance(
                self.plotter.data, InteractiveList):
            return self.plotter.data[self.index_in_list]
        else:
            return self.plotter.data

    @property
    def decoder(self):
        """The :class:`~psyplot.data.CFDecoder` instance that decodes the
        :attr:`raw_data`"""
        # If the decoder is modified by one of the formatoptions, use this one
        if self.plotter.plot_data_decoder is not None:
            if self.index_in_list is not None and isinstance(
                    self.plotter.plot_data, InteractiveList):
                ret = self.plotter.plot_data_decoder[self.index_in_list]
                if ret is not None:
                    return ret
            else:
                return self.plotter.plot_data_decoder
        data = self.raw_data
        check = isinstance(data, InteractiveList)
        while check:
            data = data[0]
            check = isinstance(data, InteractiveList)
        return data.psy.decoder

    @decoder.setter
    def decoder(self, value):
        self.set_decoder(value, self.index_in_list)

    @property
    def any_decoder(self):
        """Return the first possible decoder"""
        ret = self.decoder
        while not isinstance(ret, CFDecoder):
            ret = ret[0]
        return ret

    @property
    def data(self):
        """The data that is plotted"""
        if self.index_in_list is not None and isinstance(
                self.plotter.plot_data, InteractiveList):
            return self.plotter.plot_data[self.index_in_list]
        else:
            return self.plotter.plot_data

    @data.setter
    def data(self, value):
        self.set_data(value, self.index_in_list)

    @property
    def iter_data(self):
        """Returns an iterator over the plot data arrays"""
        data = self.data
        if isinstance(data, InteractiveList):
            return iter(data)
        return iter([data])

    @property
    def iter_raw_data(self):
        """Returns an iterator over the original data arrays"""
        data = self.raw_data
        if isinstance(data, InteractiveList):
            return iter(data)
        return iter([data])

    @property
    def validate(self):
        """Validation method of the formatoption"""
        try:
            return self._validate
        except AttributeError:
            try:
                self._validate = self.plotter.get_vfunc(self.key)
            except KeyError:
                warn("Could not find a validation function for %s "
                     "formatoption keyword! No validation will be made!" % (
                         self.key), PsyPlotRuntimeWarning, logger=self.logger)
                self._validate = _identity
        return self._validate

    @validate.setter
    def validate(self, value):
        self._validate = value

    @property
    def default(self):
        """Default value of this formatoption"""
        return self.plotter.rc[self.key]

    @property
    def default_key(self):
        """The key of this formatoption in the :attr:`psyplot.rcParams`"""
        return self.plotter.rc._get_val_and_base(self.key)[0]

    @property
    def shared_by(self):
        """None if the formatoption is not controlled by another formatoption
        of another plotter, otherwise the corresponding :class:`Formatoption`
        instance"""
        return self.plotter._shared.get(self.key)

    @property
    def value(self):
        """Value of the formatoption in the corresponding :attr:`plotter` or
        the shared value"""
        shared_by = self.shared_by
        if shared_by:
            return shared_by.value2share
        return self.plotter[self.key]

    @property
    @dedent
    def changed(self):
        """
        :class:`bool` indicating whether the value changed compared to the
        default or not."""
        return self.diff(self.default)

    @property
    @dedent
    def value2share(self):
        """
        The value that is passed to shared formatoptions (by default, the
        :attr:`value` attribute)"""
        return self.value

    @property
    @dedent
    def value2pickle(self):
        """
        The value that can be used when pickling the information of the project
        """
        return self.value

    @docstrings.get_sections(base='Formatoption')
    @dedent
    def __init__(self, key, plotter=None, index_in_list=None,
                 additional_children=[], additional_dependencies=[],
                 **kwargs):
        """
        Parameters
        ----------
        key: str
            formatoption key in the `plotter`
        plotter: psyplot.plotter.Plotter
            Plotter instance that holds this formatoption. If None, it is
            assumed that this instance serves as a descriptor.
        index_in_list: int or None
            The index that shall be used if the data is a
            :class:`psyplot.InteractiveList`
        additional_children: list or str
            Additional children to use (see the :attr:`children` attribute)
        additional_dependencies: list or str
            Additional dependencies to use (see the :attr:`dependencies`
            attribute)
        ``**kwargs``
            Further keywords may be used to specify different names for
            children, dependencies and connection formatoptions that match the
            setup of the plotter. Hence, keywords may be anything of the
            :attr:`children`, :attr:`dependencies` and :attr:`connections`
            attributes, with values being the name of the new formatoption in
            this plotter."""
        self.key = key
        self.plotter = plotter
        self.index_in_list = index_in_list
        self.shared = set()
        self.additional_children = additional_children
        self.additional_dependencies = additional_dependencies
        self.children = self.children + additional_children
        self.dependencies = self.dependencies + additional_dependencies
        self._child_mapping = dict(zip(*tee(chain(
            self.children, self.dependencies, self.connections,
            self.parents), 2)))
        # check kwargs
        for key in (key for key in kwargs if key not in self._child_mapping):
            raise TypeError(
                '%s.__init__() got an unexpected keyword argument %r' % (
                    self.__class__.__name__, key))
        # set up child mapping
        self._child_mapping.update(kwargs)
        # reset the dependency lists to match the current plotter setup
        for attr in ['children', 'dependencies', 'connections', 'parents']:
            setattr(self, attr,
                    [self._child_mapping[key] for key in getattr(self, attr)])

    def __set__(self, instance, value):
        if isinstance(value, Formatoption):
            setattr(instance, '_' + self.key, value)
        else:
            fmto = getattr(instance, self.key)
            fmto.set_value(value)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        try:
            return getattr(instance, '_' + self.key)
        except AttributeError:
            fmto = self.__class__(
                self.key, instance, self.index_in_list,
                additional_children=self.additional_children,
                additional_dependencies=self.additional_dependencies,
                **self.init_kwargs)
            setattr(instance, '_' + self.key, fmto)
            return fmto

    def __delete__(self, instance, owner):
        fmto = getattr(instance, '_' + self.key)
        with instance.no_validation:
            instance[self.key] = fmto.default

    @docstrings.get_sections(base='Formatoption.set_value')
    @dedent
    def set_value(self, value, validate=True, todefault=False):
        """
        Set (and validate) the value in the plotter. This method is called by
        the plotter when it attempts to change the value of the formatoption.

        Parameters
        ----------
        value
            Value to set
        validate: bool
            if True, validate the `value` before it is set
        todefault: bool
            True if the value is updated to the default value"""
        # do nothing if the key is shared
        if self.key in self.plotter._shared:
            return
        with self.plotter.no_validation:
            self.plotter[self.key] = value if not validate else \
                self.validate(value)

    def set_data(self, data, i=None):
        """
        Replace the data to plot

        This method may be used to replace the data that is visualized by the
        plotter. It changes it's behaviour depending on whether an
        :class:`psyplot.data.InteractiveList` is visualized or a single
        :class:`pysplot.data.InteractiveArray`

        Parameters
        ----------
        data: psyplot.data.InteractiveBase
            The data to insert
        i: int
            The position in the InteractiveList where to insert the data (if
            the plotter visualizes a list anyway)

        Notes
        -----
        This method uses the :attr:`Formatoption.data` attribute
        """
        if self.index_in_list is not None:
            i = self.index_in_list
        if i is not None and isinstance(self.plotter.plot_data,
                                        InteractiveList):
            self.plotter.plot_data[i] = data
        else:
            self.plotter.plot_data = data

    def set_decoder(self, decoder, i=None):
        """
        Replace the data to plot

        This method may be used to replace the data that is visualized by the
        plotter. It changes it's behaviour depending on whether an
        :class:`psyplot.data.InteractiveList` is visualized or a single
        :class:`pysplot.data.InteractiveArray`

        Parameters
        ----------
        decoder: psyplot.data.CFDecoder
            The decoder to insert
        i: int
            The position in the InteractiveList where to insert the data (if
            the plotter visualizes a list anyway)
        """
        # we do not modify the raw data but instead set it on the plotter
        # TODO: This is not safe for encapsulated InteractiveList instances!
        if i is not None and isinstance(
                self.plotter.plot_data, InteractiveList):
            n = len(self.plotter.plot_data)
            decoders = self.plotter.plot_data_decoder or [None] * n
            decoders[i] = decoder
            self.plotter.plot_data_decoder = decoders
        else:
            if (isinstance(self.plotter.plot_data, InteractiveList) and
                    isinstance(decoder, CFDecoder)):
                decoder = [decoder] * len(self.plotter.plot_data)
            self.plotter.plot_data_decoder = decoder

    def get_decoder(self, i=None):
        # we do not modify the raw data but instead set it on the plotter
        # TODO: This is not safe for encapsulated InteractiveList instances!
        if i is not None and isinstance(
                self.plotter.plot_data, InteractiveList):
            n = len(self.plotter.plot_data)
            decoders = self.plotter.plot_data_decoder or [None] * n
            return decoders[i] or self.plotter.plot_data[i].psy.decoder
        else:
            return self.decoder

    def check_and_set(self, value, todefault=False, validate=True):
        """Checks the value and sets the value if it changed

        This method checks the value and sets it only if the :meth:`diff`
        method result of the given `value` is True

        Parameters
        ----------
        value
            A possible value to set
        todefault: bool
            True if the value is updated to the default value

        Returns
        -------
        bool
            A boolean to indicate whether it has been set or not"""
        if validate:
            value = self.validate(value)
        if self.diff(value):
            self.set_value(value, validate=False, todefault=todefault)
            return True
        return False

    def diff(self, value):
        """Checks whether the given value differs from what is currently set

        Parameters
        ----------
        value
            A possible value to set (make sure that it has been validate via
            the :attr:`validate` attribute before)

        Returns
        -------
        bool
            True if the value differs from what is currently set"""
        return value != self.value

    def initialize_plot(self, value, *args, **kwargs):
        """Method that is called when the plot is made the first time

        Parameters
        ----------
        value
            The value to use for the initialization"""
        self.update(value, *args, **kwargs)

    @abstractmethod
    def update(self, value):
        """Method that is call to update the formatoption on the axes

        Parameters
        ----------
        value
            Value to update"""
        pass

    def get_fmt_widget(self, parent, project):
        """Get a widget to update the formatoption in the GUI

        This method should return a QWidget that is loaded by the psyplot-gui
        when the formatoption is selected in the
        :attr:`psyplot_gui.main.Mainwindow.fmt_widget`. It should call the
        :meth:`~psyplot_gui.fmt_widget.FormatoptionWidget.insert_text` method
        when the update text for the formatoption should be changed.

        Parameters
        ----------
        parent: psyplot_gui.fmt_widget.FormatoptionWidget
            The parent widget that contains the returned QWidget
        project: psyplot.project.Project
            The current subproject (see :func:`psyplot.project.gcp`)

        Returns
        -------
        PyQt5.QtWidgets.QWidget
            The widget to control the formatoption"""
        return None

    def share(self, fmto, initializing=False, **kwargs):
        """Share the settings of this formatoption with other data objects

        Parameters
        ----------
        fmto: Formatoption
            The :class:`Formatoption` instance to share the attributes with
        ``**kwargs``
            Any other keyword argument that shall be passed to the update
            method of `fmto`"""
        # lock all  the childrens and the formatoption itself
        self.lock.acquire()
        fmto._lock_children()
        fmto.lock.acquire()
        # update the other plotter
        if initializing:
            fmto.initialize_plot(self.value2share, **kwargs)
        else:
            fmto.update(self.value2share, **kwargs)
        self.shared.add(fmto)
        # release the locks
        fmto.lock.release()
        fmto._release_children()
        self.lock.release()

    def _lock_children(self):
        """acquire the locks of the children"""
        plotter = self.plotter
        for key in self.children + self.dependencies:
            try:
                getattr(plotter, key).lock.acquire()
            except AttributeError:
                pass

    def _release_children(self):
        """release the locks of the children"""
        plotter = self.plotter
        for key in self.children + self.dependencies:
            try:
                getattr(plotter, key).lock.release()
            except AttributeError:
                pass

    def finish_update(self):
        """Finish the update, initialization and sharing process

        This function is called at the end of the :meth:`Plotter.start_update`,
        :meth:`Plotter.initialize_plot` or the :meth:`Plotter.share` methods.
        """
        pass

    @dedent
    def remove(self):
        """
        Method to remove the effects of this formatoption

        This method is called when the axes is cleared due to a
        formatoption with :attr:`requires_clearing` set to True. You don't
        necessarily have to implement this formatoption if your plot results
        are removed by the usual :meth:`matplotlib.axes.Axes.clear` method."""
        pass


class DictFormatoption(Formatoption):
    """
    Base formatoption class defining an alternative set_value that works for
    dictionaries."""

    @docstrings.dedent
    def set_value(self, value, validate=True, todefault=False):
        """
        Set (and validate) the value in the plotter

        Parameters
        ----------
        %(Formatoption.set_value.parameters)s

        Notes
        -----
        - If the current value in the plotter is None, then it will be set with
          the given `value`, otherwise the current value in the plotter is
          updated
        - If the value is an empty dictionary, the value in the plotter is
          cleared"""
        value = value if not validate else self.validate(value)
        # if the key in the plotter is not already set (i.e. it is initialized
        # with None, we set it)
        if self.plotter[self.key] is None:
            with self.plotter.no_validation:
                self.plotter[self.key] = value.copy()
        # in case of an empty dict, clear the value
        elif not value:
            self.plotter[self.key].clear()
        # otherwhise we update the dictionary
        else:
            if todefault:
                self.plotter[self.key].clear()
            self.plotter[self.key].update(value)


class PostTiming(Formatoption):
    """
    Determine when to run the :attr:`post` formatoption

    This formatoption determines, whether the :attr:`post` formatoption
    should be run never, after replot or after every update.

    Possible types
    --------------
    'never'
        Never run post processing scripts
    'always'
        Always run post processing scripts
    'replot'
        Only run post processing scripts when the data changes or a replot
        is necessary

    See Also
    --------
    post: The post processing formatoption"""

    default = 'never'

    priority = -inf

    group = 'post_processing'

    name = 'Timing of the post processing'

    @staticmethod
    def validate(value):
        value = six.text_type(value)
        possible_values = ['never', 'always', 'replot']
        if value not in possible_values:
            raise ValueError('String must be one of %s, not %r' % (
                possible_values, value))
        return value

    def update(self, value):
        pass

    def get_fmt_widget(self, parent, project):
        from psyplot_gui.compat.qtcompat import QComboBox
        combo = QComboBox(parent)
        combo.addItems(['never', 'always', 'replot'])
        combo.setCurrentText(
            next((plotter[self.key] for plotter in project.plotters), 'never'))
        combo.currentTextChanged.connect(parent.set_obj)
        return combo


class PostProcDependencies(object):
    """The dependencies of this formatoption"""

    def __get__(self, instance, owner):
        if (instance is None or instance.plotter is None or
                not instance.plotter._initialized):
            return []
        elif instance.post_timing.value == 'always':
            return list(set(instance.plotter) - {instance.key})
        else:
            return []

    def __set__(self, instance, value):
        pass


class PostProcessing(Formatoption):
    """
    Apply your own postprocessing script

    This formatoption let's you apply your own post processing script. Just
    enter the script as a string and it will be executed. The formatoption
    will be made available via the ``self`` variable

    Possible types
    --------------
    None
        Don't do anything
    str
        The post processing script as string

    Note
    ----
    This formatoption uses the built-in :func:`exec` function to compile the
    script. Since this poses a security risk when loading psyplot projects,
    it is by default disabled through the :attr:`Plotter.enable_post`
    attribute. If you are sure that you can trust the script in this
    formatoption, set this attribute of the corresponding :class:`Plotter` to
    ``True``

    Examples
    --------
    Assume, you want to manually add the mean of the data to the title of the
    matplotlib axes. You can simply do this via

    .. code-block:: python

        from psyplot.plotter import Plotter
        from xarray import DataArray
        plotter = Plotter(DataArray([1, 2, 3]))
        # enable the post formatoption
        plotter.enable_post = True
        plotter.update(post="self.ax.set_title(str(self.data.mean()))")
        plotter.ax.get_title()
        '2.0'

    By default, the ``post`` formatoption is only ran, when it is explicitly
    updated. However, you can use the :attr:`post_timing` formatoption, to
    run it automatically. E.g. for running it after every update of the
    plotter, you can set

    .. code-block:: python

        plotter.update(post_timing='always')

    See Also
    --------
    post_timing: Determine the timing of this formatoption"""

    children = ['post_timing']

    default = None

    priority = -inf

    group = 'post_processing'

    name = 'Custom post processing script'

    @staticmethod
    def validate(value):
        if value is None:
            return value
        elif not isinstance(value, six.string_types):
            raise ValueError("Expected a string, not %s" % (type(value), ))
        else:
            return six.text_type(value)

    @property
    def data_dependent(self):
        """True if the corresponding :class:`post_timing <PostTiming>`
        formatoption is set to ``'replot'`` to run the post processing script
        after every change of the data"""
        return self.post_timing.value == 'replot'

    dependencies = PostProcDependencies()

    def update(self, value):
        if value is None:
            return
        if not self.plotter.enable_post:
            warn(
                "Post processing is disabled. Set the ``enable_post`` "
                "attribute to True to run the script")
        else:
            exec(value, {'self': self})


class Plotter(dict):
    """Interactive plotting object for one or more data arrays

    This class is the base for the interactive plotting with the psyplot
    module. It capabilities are determined by it's descriptor classes that are
    derived from the :class:`Formatoption` class"""

    #: List of base strings in the :attr:`psyplot.rcParams` dictionary
    _rcparams_string = []

    post_timing = PostTiming('post_timing')
    post = PostProcessing('post')

    no_validation = _temp_bool_prop('no_validation', """
        Temporarily disable the validation

        Examples
        --------
        Although it is not recommended to set a value with disabled validation,
        you can disable it via::

            >>> with plotter.no_validation:
            ...     plotter['ticksize'] = 'x'

        To permanently disable the validation, simply set

            >>> plotter.no_validation = True
            >>> plotter['ticksize'] = 'x'
            >>> plotter.no_validation = False  # reenable validation""")

    #: Temporarily include links in the key descriptions from
    #: :meth:`show_keys`, :meth:`show_docs` and :meth:`show_summaries`.
    #: Note that this is a class attribute, so each change to the value of this
    #: attribute will affect all instances and subclasses
    include_links = _TempBool()

    @property
    def ax(self):
        """Axes instance of the plot"""
        if self._ax is None:
            import matplotlib.pyplot as plt
            plt.figure()
            self._ax = plt.axes(projection=self._get_sample_projection())
        return self._ax

    @ax.setter
    def ax(self, value):
        self._ax = value

    #: The :class:`psyplot.project.Project` instance this plotter belongs to
    _project = None

    @property
    def project(self):
        """:class:`psyplot.project.Project` instance this plotter belongs to"""
        if self._project is None:
            return
        return self._project()

    @project.setter
    def project(self, value):
        if value is not None:
            self._project = weakref.ref(value)
        else:
            self._project = value

    @property
    @dedent
    def rc(self):
        """
        Default values for this plotter

        This :class:`~psyplot.config.rcsetup.SubDict` stores the default values
        for this plotter. A modification of the dictionary does not affect
        other plotter instances unless you set the
        :attr:`~psyplot.config.rcsetup.SubDict.trace` attribute to True"""
        try:
            return self._rc
        except AttributeError:
            self._set_rc()
            return self._rc

    @property
    def base_variables(self):
        """A mapping from the base_variable names to the variables"""
        if isinstance(self.data, InteractiveList):
            return dict(chain(*map(
                lambda arr: six.iteritems(arr.psy.base_variables),
                self.data)))
        else:
            return self.data.psy.base_variables

    @property
    def iter_base_variables(self):
        """A mapping from the base_variable names to the variables"""
        if isinstance(self.data, InteractiveList):
            return chain(*(arr.psy.iter_base_variables for arr in self.data))
        else:
            return self.data.psy.iter_base_variables

    no_auto_update = property(_no_auto_update_getter,
                              doc=_no_auto_update_getter.__doc__)

    @no_auto_update.setter
    def no_auto_update(self, value):
        self.no_auto_update.value = bool(value)

    @property
    def changed(self):
        """:class:`dict` containing the key value pairs that are not the
        default"""
        return {key: value for key, value in six.iteritems(self)
                if getattr(self, key).changed}

    @property
    def figs2draw(self):
        """All figures that have been manipulated through sharing and the own
        figure.

        Notes
        -----
        Using this property set will reset the figures too draw"""
        return self._figs2draw.union([self.ax.get_figure()])

    @property
    @docstrings
    def _njobs(self):
        """%(InteractiveBase._njobs)s"""
        if self.disabled:
            return [0]
        return [1, 1]

    @property
    def _fmtos(self):
        """Iterator over the formatoptions"""
        return (getattr(self, key) for key in self)

    @property
    def _fmto_groups(self):
        """Mapping from group to a set of formatoptions"""
        ret = defaultdict(set)
        for key in self:
            ret[getattr(self, key).group].add(getattr(self, key))
        return dict(ret)

    @property
    def fmt_groups(self):
        """A mapping from the formatoption group to the formatoptions"""
        ret = defaultdict(set)
        for key in self:
            ret[getattr(self, key).group].add(key)
        return dict(ret)

    @property
    def groups(self):
        """A mapping from the group short name to the group description"""
        return {group: groups[group] for group in self.fmt_groups}

    @property
    def data(self):
        """The :class:`psyplot.InteractiveBase` instance of this plotter"""
        return self._data

    @data.setter
    def data(self, value):
        self._data = value

    @property
    def plot_data(self):
        """The data that is used for plotting"""
        return getattr(self, '_plot_data', self.data)

    @plot_data.setter
    def plot_data(self, value):
        self._set_data(value)

    #: The decoder to use for the formatoptions. If None, the decoder of the
    #: raw data is used
    plot_data_decoder = None

    #: :class:`bool` that has to be ``True`` if the post processing script in
    #: the :attr:`post` formatoption should be enabled
    enable_post = False

    def _set_data(self, value):
        if isinstance(value, InteractiveList):
            self._plot_data = value.copy()
        else:
            self._plot_data = value

    @property
    def logger(self):
        """:class:`logging.Logger` of this plotter"""
        try:
            return self.data.psy.logger.getChild(self.__class__.__name__)
        except AttributeError:
            name = '%s.%s' % (self.__module__, self.__class__.__name__)
            return logging.getLogger(name)

    docstrings.keep_params('InteractiveBase.parameters', 'auto_update')

    @docstrings.get_sections(base='Plotter')
    @docstrings.dedent
    def __init__(self, data=None, ax=None, auto_update=None, project=None,
                 draw=False, make_plot=True, clear=False,
                 enable_post=False, **kwargs):
        """
        Parameters
        ----------
        data: InteractiveArray or ArrayList, optional
            Data object that shall be visualized. If given and `plot` is True,
            the :meth:`initialize_plot` method is called at the end. Otherwise
            you can call this method later by yourself
        ax: matplotlib.axes.Axes
            Matplotlib Axes to plot on. If None, a new one will be created as
            soon as the :meth:`initialize_plot` method is called
        %(InteractiveBase.parameters.auto_update)s
        %(InteractiveBase.start_update.parameters.draw)s
        make_plot: bool
            If True, and `data` is not None, the plot is initialized. Otherwise
            only the framework between plotter and data is set up
        clear: bool
            If True, the axes is cleared first
        enable_post: bool
            If True, the :attr:`post` formatoption is enabled and post
            processing scripts are allowed
        ``**kwargs``
            Any formatoption key from the :attr:`formatoptions` attribute that
            shall be used"""
        self.project = project
        self.ax = ax
        self.data = data
        self.enable_post = enable_post
        if auto_update is None:
            auto_update = rcParams['lists.auto_update']
        self.no_auto_update = not bool(auto_update)
        self._registered_updates = {}
        self._todefault = False
        self._old_fmt = []
        self._figs2draw = set()
        #: formatoptions that have to be updated by other plotters that share
        #: the given formatoption with this Plotter. :attr:`_to_update` is a
        #: mapping from the formatoptions in this plotter to the corresponding
        #: other plotter
        self._to_update = {}
        self.disabled = False
        #: Dictionary holding the Formatoption instances of other plotters
        #: if their value shall be used instead of the one in this instance
        self._shared = {}
        #: list of str. Formatoption keys that were changed during the last
        #: update
        self._last_update = []
        #: The set of formatoptions that shall be updated even if they did not
        #: change
        self._force = set()
        self.replot = True
        self.cleared = clear
        self._updating = False
        # will be set to True when the plot is first initialized
        self._initialized = False

        # first we initialize all keys with None. This is necessary in order
        # to make the validation functioning
        with self.no_validation:
            for key in self._get_formatoptions():
                self[key] = None
        for key in self:  # then we set the default values
            fmto = getattr(self, key)
            self._try2set(fmto, fmto.default, validate=False)
        self._set_rc()
        for key, value in six.iteritems(kwargs):  # then the user values
            self[key] = value
        self.initialize_plot(data, ax=ax, draw=draw, clear=clear,
                             make_plot=make_plot)

    def _try2set(self, fmto, *args, **kwargs):
        """Sets the value in `fmto` and gives additional informations when fail

        Parameters
        ----------
        fmto: Formatoption
        ``*args`` and ``**kwargs``
            Anything that is passed to `fmto`s :meth:`~Formatoption.set_value`
            method"""
        fmto.set_value(*args, **kwargs)

    def __getitem__(self, key):
        try:
            return dict.__getitem__(self, key)
        except KeyError:
            self.check_key(key)

    def __setitem__(self, key, value):
        if not self.no_validation:
            self.check_key(key)
            self._try2set(getattr(self, key), value)
            return
        # prevent from setting during an update process
        getattr(self, key).lock.acquire()
        dict.__setitem__(self, key, value)
        getattr(self, key).lock.release()

    def __delitem__(self, key):
        self[key] = getattr(self, key).default

    docstrings.delete_params('check_key.parameters', 'possible_keys', 'name')

    @docstrings.dedent
    def check_key(self, key, raise_error=True, *args, **kwargs):
        """
        Checks whether the key is a valid formatoption

        Parameters
        ----------
        %(check_key.parameters.no_possible_keys|name)s

        Returns
        -------
        %(check_key.returns)s

        Raises
        ------
        %(check_key.raises)s"""
        return check_key(
            key, possible_keys=list(self), raise_error=raise_error,
            name='formatoption keyword', *args, **kwargs)

    @classmethod
    @docstrings.get_sections(base='Plotter.check_data', sections=['Parameters',
                                                              'Returns'])
    @dedent
    def check_data(cls, name, dims, is_unstructured):
        """
        A validation method for the data shape

        The default method does nothing and should be subclassed to validate
        the results. If the plotter accepts a :class:`InteractiveList`, it
        should accept a list for name and dims

        Parameters
        ----------
        name: str or list of str
            The variable name(s) of the data
        dims: list of str or list of lists of str
            The dimension name(s) of the data
        is_unstructured: bool or list of bool
            True if the corresponding array is unstructured

        Returns
        -------
        list of bool or None
            True, if everything is okay, False in case of a serious error,
            None if it is intermediate. Each object in this list corresponds to
            one in the given `name`
        list of str
            The message giving more information on the reason. Each object in
            this list corresponds to one in the given `name`"""
        if isinstance(name, six.string_types):
            name = [name]
            dims = [dims]
            is_unstructured = [is_unstructured]
        N = len(name)
        if len(dims) != N or len(is_unstructured) != N:
            return [False] * N, [
                'Number of provided names (%i) and dimensions '
                '(%i) or unstructured information (%i) are not the same' % (
                    N, len(dims), len(is_unstructured))] * N
        return [True] * N, [''] * N

    docstrings.keep_params('Plotter.parameters', 'ax', 'make_plot', 'clear')

    @docstrings.dedent
    def initialize_plot(self, data=None, ax=None, make_plot=True, clear=False,
                        draw=False, remove=False, priority=None):
        """
        Initialize the plot for a data array

        Parameters
        ----------
        data: InteractiveArray or ArrayList, optional
            Data object that shall be visualized.

            - If not None and `plot` is True, the given data is visualized.
            - If None and the :attr:`data` attribute is not None, the data in
              the :attr:`data` attribute is visualized
            - If both are None, nothing is done.
        %(Plotter.parameters.ax|make_plot|clear)s
        %(InteractiveBase.start_update.parameters.draw)s
        remove: bool
            If True, old effects by the formatoptions in this plotter are
            undone first
        priority: int
            If given, initialize only the formatoption with the given priority.
            This value must be out of :data:`START`, :data:`BEFOREPLOTTING` or
            :data:`END`
        """
        if data is None and self.data is not None:
            data = self.data
        else:
            self.data = data
        self.ax = ax
        if data is None:  # nothing to do if no data is given
            return
        self.no_auto_update = not (
            not self.no_auto_update or not data.psy.no_auto_update)
        data.psy.plotter = self
        if not make_plot:  # stop here if we shall not plot
            return
        self.logger.debug("Initializing plot...")
        if remove:
            self.logger.debug("    Removing old formatoptions...")
            for fmto in self._fmtos:
                try:
                    fmto.remove()
                except Exception:
                    self.logger.debug(
                        "Could not remove %s while initializing", fmto.key,
                        exc_info=True)
        if clear:
            self.logger.debug("    Clearing axes...")
            self.ax.clear()
            self.cleared = True
        # get the formatoptions. We sort them here by key to make sure that the
        # order always stays the same (easier for debugging)
        fmto_groups = self._grouped_fmtos(self._sorted_by_priority(
            sorted(self._fmtos, key=lambda fmto: fmto.key)))
        self.plot_data = self.data
        self._updating = True
        for fmto_priority, grouper in fmto_groups:
            if priority is None or fmto_priority == priority:
                self._plot_by_priority(fmto_priority, grouper,
                                       initializing=True)
        self._release_all(True)  # finish the update
        self.cleared = False
        self.replot = False
        self._initialized = True
        self._updating = False

        if draw is None:
            draw = rcParams['auto_draw']
        if draw:
            self.draw()
            if rcParams['auto_show']:
                self.show()

    docstrings.keep_params('InteractiveBase._register_update.parameters',
                           'force', 'todefault')

    @docstrings.get_sections(base='Plotter._register_update')
    @docstrings.dedent
    def _register_update(self, fmt={}, replot=False, force=False,
                         todefault=False):
        """
        Register formatoptions for the update

        Parameters
        ----------
        fmt: dict
            Keys can be any valid formatoptions with the corresponding values
            (see the :attr:`formatoptions` attribute)
        replot: bool
            Boolean that determines whether the data specific formatoptions
            shall be updated in any case or not.
        %(InteractiveBase._register_update.parameters.force|todefault)s"""
        if self.disabled:
            return
        self.replot = self.replot or replot
        self._todefault = self._todefault or todefault
        if force is True:
            force = list(fmt)
        self._force.update(
            [ret[0] for ret in map(self.check_key, force or [])])
        # check the keys
        list(map(self.check_key, fmt))
        self._registered_updates.update(fmt)

    def make_plot(self):
        """Method for making the plot

        This method is called at the end of the :attr:`BEFOREPLOTTING` stage if
        and only if the :attr:`plot_fmt` attribute is set to ``True``"""
        pass

    @docstrings.dedent
    def start_update(self, draw=None, queues=None, update_shared=True):
        """
        Conduct the registered plot updates

        This method starts the updates from what has been registered by the
        :meth:`update` method. You can call this method if you did not set the
        `auto_update` parameter to True when calling the :meth:`update` method
        and when the :attr:`no_auto_update` attribute is True.

        Parameters
        ----------
        %(InteractiveBase.start_update.parameters)s

        Returns
        -------
        %(InteractiveBase.start_update.returns)s

        See Also
        --------
        :attr:`no_auto_update`, update"""
        def update_the_others():
            for fmto in fmtos:
                for other_fmto in fmto.shared:
                    if not other_fmto.plotter._updating:
                        other_fmto.plotter._register_update(
                            force=[other_fmto.key])
            for fmto in fmtos:
                for other_fmto in fmto.shared:
                    if not other_fmto.plotter._updating:
                        other_draw = other_fmto.plotter.start_update(
                            draw=False, update_shared=False)
                        if other_draw:
                            self._figs2draw.add(
                                other_fmto.plotter.ax.get_figure())
        if self.disabled:
            return False

        if queues is not None:
            queues[0].get()
        self.logger.debug("Starting update of %r",
                          self._registered_updates.keys())
        # update the formatoptions
        self._save_state()
        try:
            # get the formatoptions. We sort them here by key to make sure that
            # the order always stays the same (easier for debugging)
            fmtos = sorted(self._set_and_filter(), key=lambda fmto: fmto.key)
        except Exception:
            # restore last (working) state
            last_state = self._old_fmt.pop(-1)
            with self.no_validation:
                for key in self:
                    self[key] = last_state.get(key, getattr(self, key).default)
            if queues is not None:
                queues[0].task_done()
            self._release_all(queue=None if queues is None else queues[1])
            # raise the error
            raise
        for fmto in fmtos:
            for fmto2 in fmto.shared:
                fmto2.plotter._to_update[fmto2] = self
        if queues is not None:
            self._updating = True
            queues[0].task_done()
            # wait for the other tasks to finish
            queues[0].join()
            queues[1].get()
        fmtos.extend([fmto for fmto in self._insert_additionals(list(
            self._to_update)) if fmto not in fmtos])
        self._to_update.clear()

        fmto_groups = self._grouped_fmtos(self._sorted_by_priority(fmtos[:]))
        # if any formatoption requires a clearing of the axes is updated,
        # we reinitialize the plot
        try:
            if self.cleared:
                self.reinit(draw=draw)
                update_the_others()
                arr_draw = True
            else:
                # otherwise we update it
                arr_draw = False
                for priority, grouper in fmto_groups:
                    arr_draw = True
                    self._plot_by_priority(priority, grouper)
                update_the_others()
        except Exception:
            raise
        finally:
            # make sure that all locks are released
            self._release_all(finish=True,
                              queue=None if queues is None else queues[1])
        if draw is None:
            draw = rcParams['auto_draw']
        if draw and arr_draw:
            self.draw()
            if rcParams['auto_show']:
                self.show()
        self.replot = False
        return arr_draw

    def _release_all(self, finish=False, queue=None):
        # make sure that all locks are released
        try:
            for fmto in self._fmtos:
                if finish:
                    fmto.finish_update()
                try:
                    fmto.lock.release()
                except RuntimeError:
                    pass
        except:
            raise
        finally:
            if queue is not None:
                queue.task_done()
                queue.join()
            self._updating = False

    def _plot_by_priority(self, priority, fmtos, initializing=False):
        def update(fmto):
            other_fmto = self._shared.get(fmto.key)
            if other_fmto:
                self.logger.debug("%s is shared with %s", fmto.key,
                                  other_fmto.plotter.logger.name)
                other_fmto.share(fmto, initializing=initializing)
            # but if not, share them
            else:
                if initializing:
                    self.logger.debug("Initializing %s", fmto.key)
                    fmto.initialize_plot(fmto.value)
                else:
                    self.logger.debug("Updating %s", fmto.key)
                    fmto.update(fmto.value)
            try:
                fmto.lock.release()
            except RuntimeError:
                pass

        self._initializing = initializing

        self.logger.debug(
            "%s formatoptions with priority %i",
            "Initializing" if initializing else "Updating", priority)

        if priority >= START or priority == END:
            for fmto in fmtos:
                update(fmto)
        elif priority == BEFOREPLOTTING:
            for fmto in fmtos:
                update(fmto)
            self._make_plot()

        self._initializing = False

    @docstrings.dedent
    def reinit(self, draw=None, clear=False):
        """
        Reinitializes the plot with the same data and on the same axes.

        Parameters
        ----------
        %(InteractiveBase.start_update.parameters.draw)s
        clear: bool
            Whether to clear the axes or not

        Warnings
        --------
        The axes may be cleared when calling this method (even if `clear` is
        set to False)!"""
        # call the initialize_plot method. Note that clear can be set to
        # False if any fmto has requires_clearing attribute set to True,
        # because this then has been cleared before
        self.initialize_plot(
            self.data, self._ax, draw=draw, clear=clear or any(
                fmto.requires_clearing for fmto in self._fmtos),
            remove=True)

    def draw(self):
        """Draw the figures and those that are shared and have been changed"""
        for fig in self.figs2draw:
            fig.canvas.draw()
        self._figs2draw.clear()

    def _grouped_fmtos(self, fmtos):
        def key_func(fmto):
            if fmto.priority >= START:
                return START
            elif fmto.priority >= BEFOREPLOTTING:
                return BEFOREPLOTTING
            else:
                return END
        return groupby(fmtos, key_func)

    def _set_and_filter(self):
        """Filters the registered updates and sort out what is not needed

        This method filters out the formatoptions that have not changed, sets
        the new value and returns an iterable that is sorted by the priority
        (highest priority comes first) and dependencies

        Returns
        -------
        list
            list of :class:`Formatoption` objects that have to be updated"""
        fmtos = []
        seen = set()
        for key in self._force:
            self._registered_updates.setdefault(key, getattr(self, key).value)
        for key, value in chain(
                six.iteritems(self._registered_updates),
                six.iteritems(
                    {key: getattr(self, key).default for key in self})
                if self._todefault else ()):
            if key in seen:
                continue
            seen.add(key)
            fmto = getattr(self, key)
            # if the key is shared, a warning will be printed as long as
            # this plotter is not also updating (for example due to a whole
            # project update)
            if key in self._shared and key not in self._force:
                if not self._shared[key].plotter._updating:
                    warn(("%s formatoption is shared with another plotter."
                          " Use the unshare method to enable the updating") % (
                              fmto.key),
                         logger=self.logger)
                changed = False
            else:
                try:
                    changed = fmto.check_and_set(
                        value, todefault=self._todefault,
                        validate=not self.no_validation)
                except Exception as e:
                    self._registered_updates.pop(key, None)
                    self.logger.debug('Failed to set %s', key)
                    raise e
            changed = changed or key in self._force
            if changed:
                fmtos.append(fmto)
        fmtos = self._insert_additionals(fmtos, seen)
        for fmto in fmtos:
            fmto.lock.acquire()
        self._todefault = False
        self._registered_updates.clear()
        self._force.clear()
        return fmtos

    def _insert_additionals(self, fmtos, seen=None):
        """
        Insert additional formatoptions into `fmtos`.

        This method inserts those formatoptions into `fmtos` that are required
        because one of the following criteria is fullfilled:

        1. The :attr:`replot` attribute is True
        2. Any formatoption with START priority is in `fmtos`
        3. A dependency of one formatoption is in `fmtos`

        Parameters
        ----------
        fmtos: list
            The list of formatoptions that shall be updated
        seen: set
            The formatoption keys that shall not be included. If None, all
            formatoptions in `fmtos` are used

        Returns
        -------
        fmtos
            The initial `fmtos` plus further formatoptions

        Notes
        -----
        `fmtos` and `seen` are modified in place (except that any formatoption
        in the initial `fmtos` has :attr:`~Formatoption.requires_clearing`
        attribute set to True)"""
        def get_dependencies(fmto):
            if fmto is None:
                return []
            return fmto.dependencies + list(chain(*map(
                lambda key: get_dependencies(getattr(self, key, None)),
                fmto.dependencies)))
        seen = seen or {fmto.key for fmto in fmtos}
        keys = {fmto.key for fmto in fmtos}
        self.replot = self.replot or any(
            fmto.requires_replot for fmto in fmtos)
        if self.replot or any(fmto.priority >= START for fmto in fmtos):
            self.replot = True
            self.plot_data = self.data
            new_fmtos = dict((f.key, f) for f in self._fmtos
                             if ((f not in fmtos and is_data_dependent(
                                 f, self.data))))
            seen.update(new_fmtos)
            keys.update(new_fmtos)
            fmtos += list(new_fmtos.values())

        # insert the formatoptions that have to be updated if the plot is
        # changed
        if any(fmto.priority >= BEFOREPLOTTING for fmto in fmtos):
            new_fmtos = dict((f.key, f) for f in self._fmtos
                             if ((f not in fmtos and f.update_after_plot)))
            fmtos += list(new_fmtos.values())
        for fmto in set(self._fmtos).difference(fmtos):
            all_dependencies = get_dependencies(fmto)
            if keys.intersection(all_dependencies):
                fmtos.append(fmto)
        if any(fmto.requires_clearing for fmto in fmtos):
            self.cleared = True
            return list(self._fmtos)
        return fmtos

    def _sorted_by_priority(self, fmtos, changed=None):
        """Sort the formatoption objects by their priority and dependency

        Parameters
        ----------
        fmtos: list
            list of :class:`Formatoption` instances
        changed: list
            the list of formatoption keys that have changed

        Yields
        ------
        Formatoption
            The next formatoption as it comes by the sorting

        Warnings
        --------
        The list `fmtos` is cleared by this method!"""
        def pop_fmto(key):
            idx = fmtos_keys.index(key)
            del fmtos_keys[idx]
            return fmtos.pop(idx)

        def get_children(fmto, parents_keys):
            all_fmtos = fmtos_keys + parents_keys
            for key in fmto.children + fmto.dependencies:
                if key not in fmtos_keys:
                    continue
                child_fmto = pop_fmto(key)
                for childs_child in get_children(
                        child_fmto, parents_keys + [child_fmto.key]):
                    yield childs_child
                # filter out if parent is in update list
                if (any(key in all_fmtos for key in child_fmto.parents) or
                        fmto.key in child_fmto.parents):
                    continue
                yield child_fmto

        fmtos.sort(key=lambda fmto: fmto.priority, reverse=True)
        fmtos_keys = [fmto.key for fmto in fmtos]
        self._last_update = changed or fmtos_keys[:]
        self.logger.debug("Update the formatoptions %s", fmtos_keys)
        while fmtos:
            del fmtos_keys[0]
            fmto = fmtos.pop(0)
            # first update children
            for child_fmto in get_children(fmto, [fmto.key]):
                yield child_fmto
            # filter out if parent is in update list
            if any(key in fmtos_keys for key in fmto.parents):
                continue
            yield fmto

    @classmethod
    def _get_formatoptions(cls, include_bases=True):
        """
        Iterator over formatoptions

        This class method returns an iterator that contains all the
        formatoption keys that are in this class and that are defined
        in the base classes

        Notes
        -----
        There is absolutely no need to call this method besides the plotter
        initialization, since all formatoptions are in the plotter itself.
        Just type::

            >>> list(plotter)

        to get the formatoptions.

        See Also
        --------
        _format_keys"""
        def base_fmtos(base):
            return filter(
                lambda key: isinstance(getattr(cls, key), Formatoption),
                getattr(base, '_get_formatoptions', empty)(False))

        def empty(*args, **kwargs):
            return list()
        fmtos = (attr for attr, obj in six.iteritems(cls.__dict__)
                 if isinstance(obj, Formatoption))
        if not include_bases:
            return fmtos
        return unique_everseen(chain(fmtos, *map(base_fmtos, cls.__mro__)))

    docstrings.keep_types('check_key.parameters', 'kwargs',
                          r'``\*args,\*\*kwargs``')

    @classmethod
    @docstrings.get_sections(base='Plotter._enhance_keys')
    @docstrings.dedent
    def _enhance_keys(cls, keys=None, *args, **kwargs):
        """
        Enhance the given keys by groups

        Parameters
        ----------
        keys: list of str or None
            If None, the all formatoptions of the given class are used. Group
            names from the :attr:`psyplot.plotter.groups` mapping are replaced
            by the formatoptions

        Other Parameters
        ----------------
        %(check_key.parameters.kwargs)s

        Returns
        -------
        list of str
            The enhanced list of the formatoptions"""
        all_keys = list(cls._get_formatoptions())
        if isinstance(keys, six.string_types):
            keys = [keys]
        else:
            keys = list(keys or sorted(all_keys))
        fmto_groups = defaultdict(list)
        for key in all_keys:
            fmto_groups[getattr(cls, key).group].append(key)
        new_i = 0
        for i, key in enumerate(keys[:]):

            if key in fmto_groups:
                del keys[new_i]
                for key2 in fmto_groups[key]:
                    if key2 not in keys:
                        keys.insert(new_i, key2)
                        new_i += 1
            else:
                valid, similar, message = check_key(
                    key, all_keys, False, 'formatoption keyword', *args,
                    **kwargs)
                if not valid:
                    keys.remove(key)
                    new_i -= 1
                    warn(message)
            new_i += 1
        return keys

    @classmethod
    @docstrings.get_sections(base=
        'Plotter.show_keys', sections=['Parameters', 'Returns',
                                       'Other Parameters'])
    @docstrings.dedent
    def show_keys(cls, keys=None, indent=0, grouped=False, func=None,
                  include_links=False, *args, **kwargs):
        """
        Classmethod to return a nice looking table with the given formatoptions

        Parameters
        ----------
        %(Plotter._enhance_keys.parameters)s
        indent: int
            The indentation of the table
        grouped: bool, optional
            If True, the formatoptions are grouped corresponding to the
            :attr:`Formatoption.groupname` attribute

        Other Parameters
        ----------------
        func: function or None
            The function the is used for returning (by default it is printed
            via the :func:`print` function or (when using the gui) in the
            help explorer). The given function must take a string as argument
        include_links: bool or None, optional
            Default False. If True, links (in restructured formats) are
            included in the description. If None, the behaviour is determined
            by the :attr:`psyplot.plotter.Plotter.include_links` attribute.
        %(Plotter._enhance_keys.other_parameters)s

        Returns
        -------
        results of `func`
            None if `func` is the print function, otherwise anything else

        See Also
        --------
        show_summaries, show_docs"""
        def titled_group(groupname):
            bars = str_indent + '*' * len(groupname) + '\n'
            return bars + str_indent + groupname + '\n' + bars

        keys = cls._enhance_keys(keys, *args, **kwargs)
        str_indent = " " * indent
        func = func or default_print_func
        # call this function recursively when grouped is True
        if grouped:
            grouped_keys = DefaultOrderedDict(list)
            for fmto in map(lambda key: getattr(cls, key), keys):
                grouped_keys[fmto.groupname].append(fmto.key)
            text = ""
            for group, keys in six.iteritems(grouped_keys):
                text += titled_group(group) + cls.show_keys(
                    keys, indent=indent, grouped=False, func=six.text_type,
                    include_links=include_links) + '\n\n'
            return func(text.rstrip())

        if not keys:
            return
        n = len(keys)
        ncols = min([4, n])  # number of columns
        # The number of cells in the table is one of the following cases:
        #     1. The number of columns and equal to the number of keys
        #     2. The number of keys
        #     3. The number of keys plus the empty cells in the last column
        ncells = n + ((ncols - (n % ncols)) if n != ncols else 0)
        if include_links or (include_links is None and cls.include_links):
            long_keys = list(map(lambda key: ':attr:`~%s.%s.%s`' % (
                cls.__module__, cls.__name__, key), keys))
        else:
            long_keys = keys
        maxn = max(map(len, long_keys))  # maximal lenght of the keys
        # extend with empty cells
        long_keys.extend([' ' * maxn] * (ncells - n))
        bars = (str_indent + '+-' + ("-"*(maxn) + "-+-")*ncols)[:-1]
        lines = ('| %s |\n%s' % (' | '.join(
            key.ljust(maxn) for key in long_keys[i:i+ncols]), bars)
            for i in range(0, n, ncols))
        text = bars + "\n" + str_indent + ("\n" + str_indent).join(
            lines)
        if six.PY2:
            text = text.encode('utf-8')

        return func(text)

    @classmethod
    @docstrings.dedent
    def _show_doc(cls, fmt_func, keys=None, indent=0, grouped=False,
                  func=None, include_links=False, *args, **kwargs):
        """
        Classmethod to print the formatoptions and their documentation

        This function is the basis for the :meth:`show_summaries` and
        :meth:`show_docs` methods

        Parameters
        ----------
        fmt_func: function
            A function that takes the key, the key as it is printed, and the
            documentation of a formatoption as argument and returns what shall
            be printed
        %(Plotter.show_keys.parameters)s

        Other Parameters
        ----------------
        %(Plotter.show_keys.other_parameters)s

        Returns
        -------
        %(Plotter.show_keys.returns)s

        See Also
        --------
        show_summaries, show_docs"""
        def titled_group(groupname):
            bars = str_indent + '*' * len(groupname) + '\n'
            return bars + str_indent + groupname + '\n' + bars

        func = func or default_print_func

        keys = cls._enhance_keys(keys, *args, **kwargs)
        str_indent = " " * indent
        if grouped:
            grouped_keys = DefaultOrderedDict(list)
            for fmto in map(lambda key: getattr(cls, key), keys):
                grouped_keys[fmto.groupname].append(fmto.key)
            text = "\n\n".join(
                titled_group(group) + cls._show_doc(
                    fmt_func, keys, indent=indent, grouped=False,
                    func=str, include_links=include_links)
                for group, keys in six.iteritems(grouped_keys))
            return func(text.rstrip())

        if include_links or (include_links is None and cls.include_links):
            long_keys = list(map(lambda key: ':attr:`~%s.%s.%s`' % (
                cls.__module__, cls.__name__, key), keys))
        else:
            long_keys = keys

        text = '\n'.join(str_indent + long_key + '\n' + fmt_func(
            key, long_key, getattr(cls, key).__doc__) for long_key, key in zip(
                long_keys, keys))
        return func(text)

    @classmethod
    @docstrings.dedent
    def show_summaries(cls, keys=None, indent=0, *args, **kwargs):
        """
        Classmethod to print the summaries of the formatoptions

        Parameters
        ----------
        %(Plotter.show_keys.parameters)s

        Other Parameters
        ----------------
        %(Plotter.show_keys.other_parameters)s

        Returns
        -------
        %(Plotter.show_keys.returns)s

        See Also
        --------
        show_keys, show_docs"""
        def find_summary(key, key_txt, doc):
            return '\n'.join(wrapper.wrap(doc[:doc.find('\n\n')]))
        str_indent = " " * indent
        wrapper = TextWrapper(width=80, initial_indent=str_indent + ' ' * 4,
                              subsequent_indent=str_indent + ' ' * 4)
        return cls._show_doc(find_summary, keys=keys, indent=indent,
                             *args, **kwargs)

    @classmethod
    @docstrings.dedent
    def show_docs(cls, keys=None, indent=0, *args, **kwargs):
        """
        Classmethod to print the full documentations of the formatoptions

        Parameters
        ----------
        %(Plotter.show_keys.parameters)s

        Other Parameters
        ----------------
        %(Plotter.show_keys.other_parameters)s

        Returns
        -------
        %(Plotter.show_keys.returns)s

        See Also
        --------
        show_keys, show_docs"""
        def full_doc(key, key_txt, doc):
            return ('=' * len(key_txt)) + '\n' + doc + '\n'
        return cls._show_doc(full_doc, keys=keys, indent=indent,
                             *args, **kwargs)

    @classmethod
    def _get_rc_strings(cls):
        """
        Recursive method to get the base strings in the rcParams dictionary.

        This method takes the :attr:`_rcparams_string` attribute from the given
        `class` and combines it with the :attr:`_rcparams_string` attributes
        from the base classes.
        The returned frozenset can be used as base strings for the
        :meth:`psyplot.config.rcsetup.RcParams.find_and_replace` method.

        Returns
        -------
        list
            The first entry is the :attr:`_rcparams_string` of this class,
            the following the :attr:`_rcparams_string` attributes of the
            base classes according to the method resolution order of this
            class"""
        return list(unique_everseen(chain(
            *map(lambda base: getattr(base, '_rcparams_string', []),
                 cls.__mro__))))

    def _set_rc(self):
        """Method to set the rcparams and defaultParams for this plotter"""
        base_str = self._get_rc_strings()
        # to make sure that the '.' is not interpreted as a regex pattern,
        # we specify the pattern_base by ourselves
        pattern_base = map(lambda s: s.replace('.', r'\.'), base_str)
        # pattern for valid keys being all formatoptions in this plotter
        pattern = '(%s)(?=$)' % '|'.join(self._get_formatoptions())
        self._rc = rcParams.find_and_replace(base_str, pattern=pattern,
                                             pattern_base=pattern_base)
        user_rc = SubDict(rcParams['plotter.user'], base_str, pattern=pattern,
                          pattern_base=pattern_base)
        self._rc.update(user_rc.data)

        self._defaultParams = SubDict(rcParams.defaultParams, base_str,
                                      pattern=pattern,
                                      pattern_base=pattern_base)

    docstrings.keep_params('InteractiveBase.update.parameters', 'auto_update')

    @docstrings.dedent
    def update(self, fmt={}, replot=False, auto_update=False, draw=None,
               force=False, todefault=False, **kwargs):
        """
        Update the formatoptions and the plot

        If the :attr:`data` attribute of this plotter is None, the plotter is
        updated like a usual dictionary (see :meth:`dict.update`). Otherwise
        the update is registered and the plot is updated if `auto_update` is
        True or if the :meth:`start_update` method is called (see below).

        Parameters
        ----------
        %(Plotter._register_update.parameters)s
        %(InteractiveBase.start_update.parameters)s
        %(InteractiveBase.update.parameters.auto_update)s
        ``**kwargs``
            Any other formatoption that shall be updated (additionally to those
            in `fmt`)

        Notes
        -----
        %(InteractiveBase.update.notes)s"""
        if self.disabled:
            return
        fmt = dict(fmt)
        if kwargs:
            fmt.update(kwargs)
        # if the data is None, update like a usual dictionary (but with
        # validation)
        if not self._initialized:
            for key, val in six.iteritems(fmt):
                self[key] = val
            return

        self._register_update(fmt=fmt, replot=replot, force=force,
                              todefault=todefault)
        if not self.no_auto_update or auto_update:
            self.start_update(draw=draw)

    def _set_sharing_keys(self, keys):
        """
        Set the keys to share or unshare

        Parameters
        ----------
        keys: string or iterable of strings
            The iterable may contain formatoptions that shall be shared (or
            unshared), or group names of formatoptions to share all
            formatoptions of that group (see the :attr:`fmt_groups` property).
            If None, all formatoptions of this plotter are inserted.

        Returns
        -------
        set
            The set of formatoptions to share (or unshare)"""
        if isinstance(keys, str):
            keys = {keys}
        keys = set(self) if keys is None else set(keys)
        fmto_groups = self._fmto_groups
        keys.update(chain(*(map(lambda fmto: fmto.key, fmto_groups[key])
                            for key in keys.intersection(fmto_groups))))
        keys.difference_update(fmto_groups)
        return keys

    @docstrings.get_sections(base='Plotter.share')
    @docstrings.dedent
    def share(self, plotters, keys=None, draw=None, auto_update=False):
        """
        Share the formatoptions of this plotter with others

        This method shares the formatoptions of this :class:`Plotter` instance
        with others to make sure that, if the formatoption of this changes,
        those of the others change as well

        Parameters
        ----------
        plotters: list of :class:`Plotter` instances or a :class:`Plotter`
            The plotters to share the formatoptions with
        keys: string or iterable of strings
            The formatoptions to share, or group names of formatoptions to
            share all formatoptions of that group (see the
            :attr:`fmt_groups` property). If None, all formatoptions of this
            plotter are unshared.
        %(InteractiveBase.start_update.parameters.draw)s
        %(InteractiveBase.update.parameters.auto_update)s

        See Also
        --------
        unshare, unshare_me"""
        auto_update = auto_update or not self.no_auto_update
        if isinstance(plotters, Plotter):
            plotters = [plotters]
        keys = self._set_sharing_keys(keys)
        for plotter in plotters:
            for key in keys:
                fmto = self._shared.get(key, getattr(self, key))
                if not getattr(plotter, key) == fmto:
                    plotter._shared[key] = getattr(self, key)
                    fmto.shared.add(getattr(plotter, key))
        # now exit if we are not initialized
        if self._initialized:
            self.update(force=keys, auto_update=auto_update, draw=draw)
        for plotter in plotters:
            if not plotter._initialized:
                continue
            old_registered = plotter._registered_updates.copy()
            plotter._registered_updates.clear()
            try:
                plotter.update(force=keys, auto_update=auto_update, draw=draw)
            except:
                raise
            finally:
                plotter._registered_updates.clear()
                plotter._registered_updates.update(old_registered)
        if draw is None:
            draw = rcParams['auto_draw']
        if draw:
            self.draw()
            if rcParams['auto_show']:
                self.show()

    @docstrings.dedent
    def unshare(self, plotters, keys=None, auto_update=False, draw=None):
        """
        Close the sharing connection of this plotter with others

        This method undoes the sharing connections made by the :meth:`share`
        method and releases the given `plotters` again, such that the
        formatoptions in this plotter may be updated again to values different
        from this one.

        Parameters
        ----------
        plotters: list of :class:`Plotter` instances or a :class:`Plotter`
            The plotters to release
        keys: string or iterable of strings
            The formatoptions to unshare, or group names of formatoptions to
            unshare all formatoptions of that group (see the
            :attr:`fmt_groups` property). If None, all formatoptions of this
            plotter are unshared.
        %(InteractiveBase.start_update.parameters.draw)s
        %(InteractiveBase.update.parameters.auto_update)s

        See Also
        --------
        share, unshare_me"""
        auto_update = auto_update or not self.no_auto_update
        if isinstance(plotters, Plotter):
            plotters = [plotters]
        keys = self._set_sharing_keys(keys)
        for plotter in plotters:
            plotter.unshare_me(keys, auto_update=auto_update, draw=draw,
                               update_other=False)
        self.update(force=keys, auto_update=auto_update, draw=draw)

    @docstrings.get_sections(base='Plotter.unshare_me')
    @docstrings.dedent
    def unshare_me(self, keys=None, auto_update=False, draw=None,
                   update_other=True):
        """
        Close the sharing connection of this plotter with others

        This method undoes the sharing connections made by the :meth:`share`
        method and release this plotter again.

        Parameters
        ----------
        keys: string or iterable of strings
            The formatoptions to unshare, or group names of formatoptions to
            unshare all formatoptions of that group (see the
            :attr:`fmt_groups` property). If None, all formatoptions of this
            plotter are unshared.
        %(InteractiveBase.start_update.parameters.draw)s
        %(InteractiveBase.update.parameters.auto_update)s

        See Also
        --------
        share, unshare"""
        auto_update = auto_update or not self.no_auto_update
        keys = self._set_sharing_keys(keys)
        to_update = []
        for key in keys:
            fmto = getattr(self, key)
            try:
                other_fmto = self._shared.pop(key)
            except KeyError:
                pass
            else:
                other_fmto.shared.remove(fmto)
                if update_other:
                    other_fmto.plotter._register_update(
                        force=[other_fmto.key])
                    to_update.append(other_fmto.plotter)
        self.update(force=keys, draw=draw, auto_update=auto_update)
        if update_other and auto_update:
            for plotter in to_update:
                plotter.start_update(draw=draw)

    def get_vfunc(self, key):
        """Return the validation function for a specified formatoption

        Parameters
        ----------
        key: str
            Formatoption key in the :attr:`rc` dictionary

        Returns
        -------
        function
            Validation function for this formatoption"""
        return self._defaultParams[key][1]

    def _save_state(self):
        """Saves the current formatoptions"""
        self._old_fmt.append(self.changed)

    def show(self):
        """Shows all open figures"""
        import matplotlib.pyplot as plt
        plt.show(block=False)

    @dedent
    def has_changed(self, key, include_last=True):
        """
        Determine whether a formatoption changed in the last update

        Parameters
        ----------
        key: str
            A formatoption key contained in this plotter
        include_last: bool
            if True and the formatoption has been included in the last update,
            the return value will not be None. Otherwise the return value will
            only be not None if it changed during the last update

        Returns
        -------
        None or list
            - None, if the value has not been changed during the last update or
              `key` is not a valid formatoption key
            - a list of length two with the old value in the first place and
              the given `value` at the second"""
        if self._initializing or key not in self:
            return
        fmto = getattr(self, key)
        if self._old_fmt and key in self._old_fmt[-1]:
            old_val = self._old_fmt[-1][key]
        else:
            old_val = fmto.default
        if (fmto.diff(old_val) or (include_last and
                                   fmto.key in self._last_update)):
            return [old_val, fmto.value]

    def get_enhanced_attrs(self, arr, axes=['x', 'y', 't', 'z']):
        if isinstance(arr, InteractiveList):
            all_attrs = list(starmap(self.get_enhanced_attrs, zip(
                arr, repeat(axes))))
            attrs = {key: val for key, val in six.iteritems(all_attrs[0])
                     if all(key in attrs and attrs[key] == val
                            for attrs in all_attrs[1:])}
            attrs.update(arr.attrs)
        else:
            attrs = arr.attrs.copy()
            base_variables = self.base_variables
            if len(base_variables) > 1:  # multiple variables
                for name, base_var in six.iteritems(base_variables):
                    attrs.update(
                        {six.text_type(name)+key: value
                         for key, value in six.iteritems(base_var.attrs)})
            else:
                base_var = next(six.itervalues(base_variables))
            attrs['name'] = arr.name
            for dim, coord in six.iteritems(getattr(arr, 'coords', {})):
                if coord.size == 1:
                    attrs[dim] = format_time(coord.values)
            if isinstance(self.data, InteractiveList):
                decoder = self.data[0].psy.decoder
            else:
                decoder = self.data.psy.decoder
            for dim in axes:
                for obj in [base_var, arr]:
                    coord = getattr(decoder, 'get_' + dim)(
                        obj, coords=getattr(arr, 'coords', None))
                    if coord is None:
                        continue
                    if coord.size == 1:
                        attrs[dim] = format_time(coord.values)
                    attrs[dim + 'name'] = coord.name
                    for key, val in six.iteritems(coord.attrs):
                        attrs[dim + key] = val
        self._enhanced_attrs = attrs
        return attrs

    def _make_plot(self):
        plot_fmtos = [fmto for fmto in self._fmtos if fmto.plot_fmt]
        plot_fmtos.sort(key=lambda fmto: fmto.priority, reverse=True)
        for fmto in plot_fmtos:
            self.logger.debug("Making plot with %s formatoption", fmto.key)
            fmto.make_plot()

    @classmethod
    def _get_sample_projection(cls):
        """Returns None. May be subclassed to return a projection that
        can be used when creating a subplot"""
        pass
