././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643822949.0458584 mpl_animators-1.0.1/0000755000175100001710000000000000000000000013404 5ustar00vstsdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/LICENSE.rst0000644000175100001710000000272400000000000015225 0ustar00vstsdockerCopyright (c) 2021, The SunPy Developers All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the Astropy Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/MANIFEST.in0000644000175100001710000000071100000000000015141 0ustar00vstsdocker# Exclude specific files # All files which are tracked by git and not explicitly excluded here are included by setuptools_scm # Prune folders prune build prune docs/_build prune docs/api prune .circleci prune .github prune .jupyter prune binder # exclude a bunch of common hidden files, you probably want to add your own here exclude .mailmap exclude .gitignore exclude .gitattributes exclude .editorconfig exclude .zenodo.json exclude *.yml exclude *.yaml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643822949.0458584 mpl_animators-1.0.1/PKG-INFO0000644000175100001710000000501500000000000014502 0ustar00vstsdockerMetadata-Version: 2.1 Name: mpl_animators Version: 1.0.1 Summary: An interative animation framework for matplotlib Home-page: https://sunpy.org Author: The SunPy Developers Author-email: sunpy@googlegroups.com License: BSD 3-Clause Platform: UNKNOWN Requires-Python: >=3.7 Provides-Extra: all Provides-Extra: wcs Provides-Extra: test Provides-Extra: docs License-File: LICENSE.rst An interative animation framework for matplotlib ------------------------------------------------ This package has been spun out of ``sunpy`` to be more generally useful. License ------- This project is Copyright (c) The SunPy Developers and licensed under the terms of the BSD 3-Clause license. This package is based upon the `Openastronomy packaging guide `_ which is licensed under the BSD 3-clause licence. See the licenses folder for more information. Contributing ------------ We love contributions! mpl-animators is open source, built on open source, and we'd love to have you hang out in our community. **Imposter syndrome disclaimer**: We want your help. No, really. There may be a little voice inside your head that is telling you that you're not ready to be an open source contributor; that your skills aren't nearly good enough to contribute. What could you possibly offer a project like this one? We assure you - the little voice in your head is wrong. If you can write code at all, you can contribute code to open source. Contributing to open source projects is a fantastic way to advance one's coding skills. Writing perfect code isn't the measure of a good developer (that would disqualify all of us!); it's trying to create something, making mistakes, and learning from those mistakes. That's how we all improve, and we are happy to help others learn. Being an open source contributor doesn't just mean writing code, either. You can help out by writing documentation, tests, or even giving feedback about the project (and yes - that includes giving feedback about the contribution process). Some of these contributions may be the most valuable to the project as a whole, because you're coming to the project with fresh eyes, so you can see the errors and assumptions that seasoned contributors have glossed over. Note: This disclaimer was originally written by `Adrienne Lowe `_ for a `PyCon talk `_, and was adapted by mpl-animators based on its use in the README file for the `MetPy project `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/README.rst0000644000175100001710000000421500000000000015075 0ustar00vstsdockerAn interative animation framework for matplotlib ------------------------------------------------ This package has been spun out of ``sunpy`` to be more generally useful. License ------- This project is Copyright (c) The SunPy Developers and licensed under the terms of the BSD 3-Clause license. This package is based upon the `Openastronomy packaging guide `_ which is licensed under the BSD 3-clause licence. See the licenses folder for more information. Contributing ------------ We love contributions! mpl-animators is open source, built on open source, and we'd love to have you hang out in our community. **Imposter syndrome disclaimer**: We want your help. No, really. There may be a little voice inside your head that is telling you that you're not ready to be an open source contributor; that your skills aren't nearly good enough to contribute. What could you possibly offer a project like this one? We assure you - the little voice in your head is wrong. If you can write code at all, you can contribute code to open source. Contributing to open source projects is a fantastic way to advance one's coding skills. Writing perfect code isn't the measure of a good developer (that would disqualify all of us!); it's trying to create something, making mistakes, and learning from those mistakes. That's how we all improve, and we are happy to help others learn. Being an open source contributor doesn't just mean writing code, either. You can help out by writing documentation, tests, or even giving feedback about the project (and yes - that includes giving feedback about the contribution process). Some of these contributions may be the most valuable to the project as a whole, because you're coming to the project with fresh eyes, so you can see the errors and assumptions that seasoned contributors have glossed over. Note: This disclaimer was originally written by `Adrienne Lowe `_ for a `PyCon talk `_, and was adapted by mpl-animators based on its use in the README file for the `MetPy project `_. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643822949.0458584 mpl_animators-1.0.1/docs/0000755000175100001710000000000000000000000014334 5ustar00vstsdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/docs/Makefile0000644000175100001710000000117200000000000015775 0ustar00vstsdocker# Minimal makefile for Sphinx documentation # # You can set these variables from the command line, and also # from the environment for the first two. SPHINXOPTS ?= SPHINXBUILD ?= sphinx-build SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/docs/conf.py0000644000175100001710000000541100000000000015634 0ustar00vstsdocker# -*- coding: utf-8 -*- # # Configuration file for the Sphinx documentation builder. # # This file does only contain a selection of the most common options. For a # full list see the documentation: # http://www.sphinx-doc.org/en/master/config # -- Project information ----------------------------------------------------- project = 'mpl-animators' copyright = '2021, The SunPy Developers' author = 'The SunPy Developers' # The full version, including alpha/beta/rc tags from mpl_animators import __version__ release = __version__ # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', 'sphinx.ext.doctest', 'sphinx.ext.mathjax', 'sphinx_automodapi.automodapi', 'sphinx_automodapi.smart_resolver', ] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: source_suffix = '.rst' # The master toctree document. master_doc = 'index' # The reST default role (used for this markup: `text`) to use for all # documents. Set to the "smart" one. default_role = 'obj' # -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ( "https://docs.python.org/3/", (None, "http://www.astropy.org/astropy-data/intersphinx/python3.inv"), ), "numpy": ( "https://numpy.org/doc/stable/", (None, "http://www.astropy.org/astropy-data/intersphinx/numpy.inv"), ), "matplotlib": ( "https://matplotlib.org/", (None, "http://www.astropy.org/astropy-data/intersphinx/matplotlib.inv"), ), "astropy": ("https://docs.astropy.org/en/stable/", None), } # -- Options for HTML output ------------------------------------------------- from sunpy_sphinx_theme.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/docs/index.rst0000644000175100001710000000171500000000000016201 0ustar00vstsdockerMatplotlib Animators Documentation ---------------------------------- The ``mpl_animators`` package provides a set of classes which allow the easy construction of interactive `matplotlib` widget based animations. "Out of the box" classes are provided for making line or image plots from numpy arrays, with sliders to control the animation automatically added for all dimensions not on the axes of the plot. As well as this there is a specialised `.ArrayAnimatorWCS` class which can make line or image plots for a numpy array and associated World Coordinate System (WCS) object from `astropy`. Finally, there are two base classes: `.BaseFuncAnimator` which can be extended to generate an interactive visualization from any data structure and set of functions to update the plot, and `.ArrayAnimator` which can be extended to generate any visualisation based on the axes of a numpy array. .. automodapi:: mpl_animators .. toctree:: :maxdepth: 2 :caption: Contents: ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/docs/make.bat0000644000175100001710000000137000000000000015742 0ustar00vstsdocker@ECHO OFF pushd %~dp0 REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set SOURCEDIR=. set BUILDDIR=_build if "%1" == "" goto help %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end popd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643822949.0458584 mpl_animators-1.0.1/mpl_animators/0000755000175100001710000000000000000000000016251 5ustar00vstsdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/__init__.py0000644000175100001710000000034600000000000020365 0ustar00vstsdocker# Licensed under a 3-clause BSD style license - see LICENSE.rst from mpl_animators.base import * from mpl_animators.image import * from mpl_animators.line import * from mpl_animators.wcs import * from .version import __version__ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/base.py0000644000175100001710000006574300000000000017554 0ustar00vstsdockerimport abc from functools import partial import matplotlib.animation as mplanim import matplotlib.pyplot as plt import matplotlib.widgets as widgets import mpl_toolkits.axes_grid1.axes_size as Size import numpy as np from mpl_toolkits.axes_grid1 import make_axes_locatable try: from astropy import units except ImportError: units = None __all__ = ['BaseFuncAnimator', 'ArrayAnimator'] class BaseFuncAnimator(metaclass=abc.ABCMeta): """ Create a Matplotlib backend independent data explorer which allows definition of figure update functions for each slider. The following keyboard shortcuts are defined in the viewer: * 'left': previous step on active slider. * 'right': next step on active slider. * 'top': change the active slider up one. * 'bottom': change the active slider down one. * 'p': play/pause active slider. This viewer can have user defined buttons added by specifying the labels and functions called when those buttons are clicked as keyword arguments. To make this class useful the subclass must implement ``_plot_start_image`` which must define a ``self.im`` attribute which is an instance of `matplotlib.image.AxesImage`. Parameters ---------- data: `iterable` Some arbitrary data. slider_functions: `list` A list of functions to call when that slider is changed. These functions will have ``val``, the axes image object and the slider widget instance passed to them, e.g., ``update_slider(val, im, slider)`` slider_ranges: `list` A list of ``[min,max]`` pairs to set the ranges for each slider or an array of values for all points of the slider. (The slider update function decides which to support.) fig: `matplotlib.figure.Figure`, optional `~matplotlib.figure.Figure` to use. Defaults to `None`, in which case a new figure is created. interval: `int`, optional Animation interval in milliseconds. Defaults to 200. colorbar: `bool`, optional Plot a colorbar. Defaults to `False`. button_labels: `list`, optional A list of strings to label buttons. Defaults to `None`. If `None` and ``button_func`` is specified, it will default to the names of the functions. button_func: `list`, optional A list of functions to map to the buttons. These functions are called with two arguments, ``(animator, event)`` where the first argument is the animator object, and the second is a `matplotlib.backend_bases.MouseEvent` object. Defaults to `None`. slider_labels: `list`, optional A list of labels to draw in the slider, must be the same length as ``slider_functions``. Attributes ---------- fig : `matplotlib.figure.Figure` axes : `matplotlib.axes.Axes` Notes ----- Extra keywords are passed to `matplotlib.pyplot.imshow`. """ def __init__(self, data, slider_functions, slider_ranges, fig=None, interval=200, colorbar=False, button_func=None, button_labels=None, start_image_func=None, slider_labels=None, **kwargs): # Allow the user to specify the button func: self.button_func = button_func or [] if button_func and not button_labels: button_labels = [a.__name__ for a in button_func] self.button_labels = button_labels or [] self.num_buttons = len(self.button_func) if not fig: fig = plt.figure() self.fig = fig self.data = data self.interval = interval self.if_colorbar = colorbar self.imshow_kwargs = kwargs if len(slider_functions) != len(slider_ranges): raise ValueError("slider_functions and slider_ranges must be the same length.") if slider_labels is not None: if len(slider_labels) != len(slider_functions): raise ValueError("slider_functions and slider_labels must be the same length.") self.num_sliders = len(slider_functions) self.slider_functions = slider_functions self.slider_ranges = slider_ranges self.slider_labels = slider_labels or [''] * len(slider_functions) # Set active slider self.active_slider = 0 # Set a blank timer self.timer = None # Set up axes self.axes = None self._make_axes_grid() self._add_widgets() self._set_active_slider(0) # Set the current axes to the main axes so commands like plt.ylabel() work. # # Only do this if figure has a manager, so directly constructed figures # (ie. via matplotlib.figure.Figure()) work. if hasattr(self.fig.canvas, "manager") and self.fig.canvas.manager is not None: plt.sca(self.axes) # Do Plot self.im = self.plot_start_image(self.axes) # Connect fig events self._connect_fig_events() def label_slider(self, i, label): """ Change the slider label. Parameters ---------- i: `int` The index of the slider to change (0 is bottom). label: `str` The label to set. """ self.sliders[i]._slider.label.set_text(label) def get_animation(self, axes=None, slider=0, startframe=0, endframe=None, stepframe=1, **kwargs): """ Return a `~matplotlib.animation.FuncAnimation` instance for the selected slider. This will allow easy saving of the animation to a file. Parameters ---------- axes: `matplotlib.axes.Axes`, optional The `matplotlib.axes.Axes` to animate. Defaults to `None`, in which case the Axes associated with this animator are used. Passing a custom Axes can be useful if you want to create the animation on a custom figure that is not the figure set up by this Animator. slider: `int`, optional The slider to animate along. Defaults to 0. startframe: `int`, optional The frame to start the animation. Defaults to 0. endframe: `int`, optional The frame to end the animation. Defaults to `None`. stepframe: `int`, optional The step between frames. Defaults to 1. Notes ----- Extra keywords are passed to `matplotlib.animation.FuncAnimation`. """ if not axes: axes = self.axes anim_fig = axes.get_figure() if endframe is None: endframe = self.slider_ranges[slider][1] im = self.plot_start_image(axes) anim_kwargs = {'frames': list(range(startframe, endframe, stepframe)), 'fargs': [im, self.sliders[slider]._slider]} anim_kwargs.update(kwargs) ani = mplanim.FuncAnimation(anim_fig, self.slider_functions[slider], **anim_kwargs) return ani @abc.abstractmethod def plot_start_image(self, ax): """ This method creates the initial image on the `matplotlib.axes.Axes`. .. warning:: This method needs to be implemented in subclasses. Parameters ---------- ax: `matplotlib.axes.Axes` This is the axes on which to plot the image. Returns ------- `matplotlib.artist.Artist` The matplotlib object to be animated, this is usually either a `~matplotlib.image.AxesImage` object, or a `~matplotlib.lines.Line2D`. """ raise NotImplementedError("Please define this function.") def _connect_fig_events(self): self.fig.canvas.mpl_connect('button_press_event', self._mouse_click) self.fig.canvas.mpl_connect('key_press_event', self._key_press) def _add_colorbar(self, im): self.colorbar = self.fig.colorbar(im, self.cax) # ============================================================================= # Figure event callback functions # ============================================================================= def _mouse_click(self, event): if event.inaxes in self.sliders: slider = self.sliders.index(event.inaxes) self._set_active_slider(slider) def _key_press(self, event): if event.key == 'left': self._previous(self.sliders[self.active_slider]._slider) elif event.key == 'right': self._step(self.sliders[self.active_slider]._slider) elif event.key == 'up': self._set_active_slider((self.active_slider+1) % self.num_sliders) elif event.key == 'down': self._set_active_slider((self.active_slider-1) % self.num_sliders) elif event.key == 'p': self._click_slider_button(event, self.slider_buttons[self.active_slider]._button, self.sliders[self.active_slider]._slider) # ============================================================================= # Active Slider methods # ============================================================================= def _set_active_slider(self, ind): self._dehighlight_slider(self.active_slider) self._highlight_slider(ind) self.active_slider = ind def _highlight_slider(self, ind): ax = self.sliders[ind] [a.set_linewidth(2.0) for n, a in ax.spines.items()] self.fig.canvas.draw() def _dehighlight_slider(self, ind): ax = self.sliders[ind] [a.set_linewidth(1.0) for n, a in ax.spines.items()] self.fig.canvas.draw() # ============================================================================= # Build the figure and place the widgets # ============================================================================= def _setup_main_axes(self): """ Allow replacement of main axes by subclassing. This method must set the ``axes`` attribute. """ if self.axes is None: self.axes = self.fig.add_subplot(111) def _make_axes_grid(self): self._setup_main_axes() # Split up the current axes so there is space for start & stop buttons self.divider = make_axes_locatable(self.axes) pad = 0.01 # Padding between axes pad_size = Size.Fraction(pad, Size.AxesX(self.axes)) large_pad_size = Size.Fraction(0.1, Size.AxesY(self.axes)) button_grid = max((7, self.num_buttons)) # Define size of useful axes cells, 50% each in x 20% for buttons in y. ysize = Size.Fraction((1.-2.*pad)/15., Size.AxesY(self.axes)) xsize = Size.Fraction((1.-2.*pad)/button_grid, Size.AxesX(self.axes)) # Set up grid, 3x3 with cells for padding. if self.num_buttons > 0: horiz = [xsize] + [pad_size, xsize]*(button_grid-1) vert = [ysize, pad_size] * self.num_sliders + \ [large_pad_size, large_pad_size, Size.AxesY(self.axes)] else: vert = [ysize, large_pad_size] * self.num_sliders + \ [large_pad_size, Size.AxesY(self.axes)] horiz = [Size.Fraction(0.1, Size.AxesX(self.axes))] + \ [Size.Fraction(0.05, Size.AxesX(self.axes))] + \ [Size.Fraction(0.65, Size.AxesX(self.axes))] + \ [Size.Fraction(0.1, Size.AxesX(self.axes))] + \ [Size.Fraction(0.1, Size.AxesX(self.axes))] self.divider.set_horizontal(horiz) self.divider.set_vertical(vert) self.button_ny = len(vert) - 3 # If we are going to add a colorbar it'll need an axis next to the plot if self.if_colorbar: nx1 = -3 self.cax = self.fig.add_axes((0., 0., 0.141, 1.)) locator = self.divider.new_locator(nx=-2, ny=len(vert)-1, nx1=-1) self.cax.set_axes_locator(locator) else: # Main figure spans all horiz and is in the top (2) in vert. nx1 = -1 self.axes.set_axes_locator( self.divider.new_locator(nx=0, ny=len(vert)-1, nx1=nx1)) def _add_widgets(self): self.buttons = [] for i in range(0, self.num_buttons): x = i * 2 # The i+1/10. is a bug that if you make two axes directly on top of # one another then the divider doesn't work. self.buttons.append(self.fig.add_axes((0., 0., 0.+i/10., 1.))) locator = self.divider.new_locator(nx=x, ny=self.button_ny) self.buttons[-1].set_axes_locator(locator) self.buttons[-1]._button = widgets.Button(self.buttons[-1], self.button_labels[i]) self.buttons[-1]._button.on_clicked(partial(self.button_func[i], self)) self.sliders = [] self.slider_buttons = [] for i in range(self.num_sliders): y = i * 2 self.sliders.append(self.fig.add_axes((0., 0., 0.01+i/10., 1.))) if self.num_buttons == 0: nx1 = 3 else: nx1 = -2 locator = self.divider.new_locator(nx=2, ny=y, nx1=nx1) self.sliders[-1].set_axes_locator(locator) self.sliders[-1].text(0.5, 0.5, self.slider_labels[i], transform=self.sliders[-1].transAxes, horizontalalignment="center", verticalalignment="center") sframe = widgets.Slider(self.sliders[-1], "", self.slider_ranges[i][0], self.slider_ranges[i][-1]-1, valinit=self.slider_ranges[i][0], valfmt='%4.1f') sframe.on_changed(partial(self._slider_changed, slider=sframe)) sframe.slider_ind = i sframe.cval = sframe.val self.sliders[-1]._slider = sframe self.slider_buttons.append( self.fig.add_axes((0., 0., 0.05+y/10., 1.))) locator = self.divider.new_locator(nx=0, ny=y) self.slider_buttons[-1].set_axes_locator(locator) butt = widgets.Button(self.slider_buttons[-1], ">") butt.on_clicked(partial(self._click_slider_button, button=butt, slider=sframe)) butt.clicked = False self.slider_buttons[-1]._button = butt # ============================================================================= # Widget callbacks # ============================================================================= def _slider_changed(self, val, slider): self.slider_functions[slider.slider_ind](val, self.im, slider) def _click_slider_button(self, event, button, slider): self._set_active_slider(slider.slider_ind) if button.clicked: self._stop_play(event) button.clicked = False button.label.set_text(">") else: button.clicked = True self._start_play(event, button, slider) button.label.set_text("||") self.fig.canvas.draw() def _start_play(self, event, button, slider): if not self.timer: self.timer = self.fig.canvas.new_timer() self.timer.interval = self.interval self.timer.add_callback(self._step, slider) self.timer.start() def _stop_play(self, event): if self.timer: self.timer.remove_callback(self._step) self.timer = None def _step(self, slider): s = slider if s.val >= s.valmax: s.set_val(s.valmin) else: s.set_val(s.val+1) self.fig.canvas.draw() def _previous(self, slider): s = slider if s.val <= s.valmin: s.set_val(s.valmax) else: s.set_val(s.val-1) self.fig.canvas.draw() class ArrayAnimator(BaseFuncAnimator, metaclass=abc.ABCMeta): """ Create a Matplotlib backend independent data explorer. The following keyboard shortcuts are defined in the viewer: * 'left': previous step on active slider. * 'right': next step on active slider. * 'top': change the active slider up one. * 'bottom': change the active slider down one. * 'p': play/pause active slider. This viewer can have user defined buttons added by specifying the labels and functions called when those buttons are clicked as keyword arguments. Parameters ---------- data: `numpy.ndarray` The data to be visualized. image_axes: `list`, optional A list of the axes order that make up the image. axis_ranges: `list` of physical coordinates for the `numpy.ndarray`, optional Defaults to `None` and array indices will be used for all axes. The `list` should contain one element for each axis of the `numpy.ndarray`. For the image axes a ``[min, max]`` pair should be specified which will be passed to `matplotlib.pyplot.imshow` as an extent. For the slider axes a ``[min, max]`` pair can be specified or an array the same length as the axis which will provide all values for that slider. Notes ----- Extra keywords are passed to `~sunpy.visualization.animator.BaseFuncAnimator`. """ def __init__(self, data, image_axes=[-2, -1], axis_ranges=None, **kwargs): all_axes = list(range(self.naxis)) # Handle negative indexes self.image_axes = [all_axes[i] for i in image_axes] slider_axes = list(range(self.naxis)) for x in self.image_axes: slider_axes.remove(x) if len(slider_axes) != self.num_sliders: raise ValueError("Number of sliders doesn't match the number of slider axes.") self.slider_axes = slider_axes # Verify that combined slider_axes and image_axes make all axes ax = self.slider_axes + self.image_axes ax.sort() if ax != list(range(self.naxis)): raise ValueError("Number of image and slider axes do not match total number of axes.") self.axis_ranges, self.extent = self._sanitize_axis_ranges(axis_ranges, data.shape) # create data slice self.frame_slice = [slice(None)] * self.naxis for i in self.slider_axes: self.frame_slice[i] = 0 slider_functions = kwargs.pop("slider_functions", []) slider_ranges = kwargs.pop("slider_ranges", []) base_kwargs = { 'slider_functions': ([self.update_plot] * self.num_sliders) + slider_functions, 'slider_ranges': [[0, dim] for dim in np.array(data.shape)[self.slider_axes]] + slider_ranges } self.num_sliders = len(base_kwargs["slider_functions"]) base_kwargs.update(kwargs) super().__init__(data, **base_kwargs) @property def frame_index(self): """ A tuple version of ``frame_slice`` to be used when indexing arrays. """ return tuple(self.frame_slice) def label_slider(self, i, label): """ Change the Slider label. Parameters ---------- i: `int` The index of the slider to change (0 is bottom). label: `str` The label to set. """ self.sliders[i]._slider.label.set_text(label) def _sanitize_axis_ranges(self, axis_ranges, data_shape): """ This method takes the various allowed values of ``axis_ranges`` and returns them in a standardized way for the rest of the class to use. The outputted axis range describes the physical coordinates of the array axes. The allowed values of axis range is either `None` or a `list`. If ``axis_ranges`` is `None` then all axis are assumed to be not scaled and will use array indices. Where ``axis_ranges`` is a `list` it must have the same length as the number of axis as the array and each element must be one of the following: * `None`: Build a "min,max" pair or `numpy.linspace` array of array indices. * ``[min, max]``: Either leave for the image axes or convert to a array for slider axes (from min to max in axis length steps) * ``[min, max]`` pair where ``min == max``: convert to array indies "min,max" pair or array. * array of axis length, check that it was passed for a slider axes and do nothing if it was, error if it is not. * For slider axes: a function which maps from pixel to world value. """ ndim = len(data_shape) # If no axis range at all make it all [min,max] pairs if axis_ranges is None: axis_ranges = [None] * ndim # need the same number of axis ranges as axes if len(axis_ranges) != ndim: raise ValueError("Length of axis_ranges must equal number of axes") # Define error message for incompatible axis_range input. def incompatible_axis_ranges_error_message(j): return \ (f"Unrecognized format for {j}th entry in axis_ranges: {axis_ranges[j]}" "axis_ranges must be None, a ``[min, max]`` pair, or " "an array-like giving the edge values of each pixel, " "i.e. length must be length of axis + 1.") # If axis range not given, define a function such that the range goes # from -0.5 to number of pixels-0.5. Thus, the center of the pixels # along the axis will correspond to integer values. def none_image_axis_range(j): return [-0.5, data_shape[j]-0.5] # For each axis validate and translate the axis_ranges. For image axes, # also determine the plot extent. To do this, iterate through image and slider # axes separately. Iterate through image axes in reverse order # because numpy is in y-x and extent is x-y. extent = [] for i in self.image_axes[::-1]: if axis_ranges[i] is None: extent = extent + none_image_axis_range(i) axis_ranges[i] = np.array(none_image_axis_range(i)) else: # Depending on length of axis_ranges[i], leave unchanged, # convert to pixel centers or raise an error due to incompatible format. axis_ranges[i] = np.asarray(axis_ranges[i]) if len(axis_ranges[i]) == 2: # Set extent. extent += [axis_ranges[i][0], axis_ranges[i][-1]] elif axis_ranges[i].ndim == 1 and len(axis_ranges[i]) == data_shape[i]+1: # If array of individual pixel edges supplied, first set extent # from first and last pixel edge, then convert axis_ranges to pixel centers. # The reason that pixel edges are required as input rather than centers # is so that the plot extent can be derived from axis_ranges (above) # and APIs using both [min, max] pair and manual definition of each pixel # values can be unambiguously and simultanously supported. extent += [axis_ranges[i][0], axis_ranges[i][-1]] axis_ranges[i] = edges_to_centers_nd(axis_ranges[i], 0) elif axis_ranges[i].ndim == ndim and axis_ranges[i].shape[i] == data_shape[i]+1: extent += [axis_ranges[i].min(), axis_ranges[i].max()] axis_ranges[i] = edges_to_centers_nd(axis_ranges[i], i) else: raise ValueError(incompatible_axis_ranges_error_message(i)) # For each slider axis validate and translate the axis_ranges. def get_pixel_to_world_callable(array): def pixel_to_world(pixel): return array[pixel] return pixel_to_world for sidx in self.slider_axes: if axis_ranges[sidx] is None: # If axis range not supplied, set pixel center values as integers starting at 0. axis_ranges[sidx] = get_pixel_to_world_callable(np.arange(data_shape[sidx])) elif not callable(axis_ranges[sidx]): axis_ranges[sidx] = np.array(axis_ranges[sidx]) if len(axis_ranges[sidx]) == 2: # If axis range given as a min, max pair, derive the center of each pixel # assuming they are equally spaced. axis_ranges[sidx] = np.linspace(axis_ranges[sidx][0], axis_ranges[sidx][-1], data_shape[sidx]+1) axis_ranges[sidx] = get_pixel_to_world_callable( edges_to_centers_nd(axis_ranges[sidx], sidx)) elif axis_ranges[sidx].ndim == 1 and len(axis_ranges[sidx]) == data_shape[sidx]+1: # If axis range given as 1D array of pixel edges (i.e. axis is independent), # derive pixel centers. axis_ranges[sidx] = get_pixel_to_world_callable( edges_to_centers_nd(np.asarray(axis_ranges[sidx]), 0)) elif axis_ranges[sidx].ndim == ndim and axis_ranges[sidx].shape[sidx] == data_shape[sidx]+1: # If axis range given as array of pixel edges the same shape as # the data array (i.e. axis is not independent), derive pixel centers. axis_ranges[sidx] = get_pixel_to_world_callable( edges_to_centers_nd(np.asarray(axis_ranges[sidx]), i)) else: raise ValueError(incompatible_axis_ranges_error_message(i)) return axis_ranges, extent @abc.abstractmethod def plot_start_image(self, ax): """ Abstract method for plotting first slice of array. Must exist here but be defined in subclass. """ @abc.abstractmethod def update_plot(self, val, artist, slider): """ Abstract method for updating the plot. Must exist here but be defined in subclass. """ ind = int(val) ax_ind = self.slider_axes[slider.slider_ind] # Update slider label to reflect real world values in axis_ranges. label = self.axis_ranges[ax_ind](ind) if units is not None and isinstance(label, units.Quantity): slider.valtext.set_text(label.to_string(precision=5, format='latex', subfmt='inline')) elif isinstance(label, str): slider.valtext.set_text(label) else: slider.valtext.set_text(f"{label:10.2f}") def edges_to_centers_nd(axis_range, edges_axis): """ Converts ND array of pixel edges to pixel centers along one axis. Parameters ---------- axis_range: `numpy.ndarray` Array of pixel edges. edges_axis: `int` Index of axis along which centers are to be calculated. """ upper_edge_indices = [slice(None)] * axis_range.ndim upper_edge_indices[edges_axis] = slice(1, axis_range.shape[edges_axis]) upper_edges = axis_range[tuple(upper_edge_indices)] lower_edge_indices = [slice(None)] * axis_range.ndim lower_edge_indices[edges_axis] = slice(0, -1) lower_edges = axis_range[tuple(lower_edge_indices)] return (upper_edges - lower_edges) / 2 + lower_edges ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643822949.0458584 mpl_animators-1.0.1/mpl_animators/extern/0000755000175100001710000000000000000000000017556 5ustar00vstsdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/extern/__init__.py0000644000175100001710000000000000000000000021655 0ustar00vstsdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/extern/modest_image.py0000644000175100001710000002704700000000000022577 0ustar00vstsdocker""" Modification of Chris Beaumont's mpl-modest-image package to allow the use of set_extent. """ # This file is copied from glue under the terms of the 3 Clause BSD licence. See licenses/GLUE.rst from __future__ import print_function, division import matplotlib rcParams = matplotlib.rcParams import matplotlib.image as mi import matplotlib.colors as mcolors import matplotlib.cbook as cbook from matplotlib.transforms import IdentityTransform, Affine2D import numpy as np IDENTITY_TRANSFORM = IdentityTransform() class ModestImage(mi.AxesImage): """ Computationally modest image class. ModestImage is an extension of the Matplotlib AxesImage class better suited for the interactive display of larger images. Before drawing, ModestImage resamples the data array based on the screen resolution and view window. This has very little affect on the appearance of the image, but can substantially cut down on computation since calculations of unresolved or clipped pixels are skipped. The interface of ModestImage is the same as AxesImage. However, it does not currently support setting the 'extent' property. There may also be weird coordinate warping operations for images that I'm not aware of. Don't expect those to work either. """ def __init__(self, *args, **kwargs): self._pressed = False self._full_res = None self._full_extent = kwargs.get('extent', None) super(ModestImage, self).__init__(*args, **kwargs) self.invalidate_cache() self.axes.figure.canvas.mpl_connect('button_press_event', self._press) self.axes.figure.canvas.mpl_connect('button_release_event', self._release) self.axes.figure.canvas.mpl_connect('resize_event', self._resize) self._timer = self.axes.figure.canvas.new_timer(interval=500) self._timer.single_shot = True self._timer.add_callback(self._resize_paused) def remove(self): super(ModestImage, self).remove() self._timer.stop() self._timer = None def _resize(self, *args): self._pressed = True self._timer.start() def _resize_paused(self, *args): # If the artist has been removed, self.axes is no longer defined, so # we can return early here. if self.axes is None: return self._pressed = False self.axes.figure.canvas.draw_idle() def _press(self, *args): self._pressed = True def _release(self, *args): self._pressed = False self.stale = True self.axes.figure.canvas.draw_idle() def set_data(self, A): """ Set the image array ACCEPTS: numpy/PIL Image A """ self._full_res = A self._A = A if self._A.dtype != np.uint8 and not np.can_cast(self._A.dtype, float): raise TypeError("Image data can not convert to float") if (self._A.ndim not in (2, 3) or (self._A.ndim == 3 and self._A.shape[-1] not in (3, 4))): raise TypeError("Invalid dimensions for image data") self.invalidate_cache() def invalidate_cache(self): self._bounds = None self._imcache = None self._rgbacache = None self._oldxslice = None self._oldyslice = None self._sx, self._sy = None, None self._pixel2world_cache = None self._world2pixel_cache = None def get_cursor_data(self, event): return None def contains(self, mouseevent): if self._A is None or self._A.shape is None: return False else: return super(ModestImage, self).contains(mouseevent) def set_extent(self, extent): self._full_extent = extent self.invalidate_cache() mi.AxesImage.set_extent(self, extent) def get_array(self): """Override to return the full-resolution array""" return self._full_res @property def _pixel2world(self): if self._pixel2world_cache is None: # Pre-compute affine transforms to convert between the 'world' # coordinates of the axes (what is shown by the axis labels) to # 'pixel' coordinates in the underlying array. extent = self._full_extent if extent is None: self._pixel2world_cache = IDENTITY_TRANSFORM else: self._pixel2world_cache = Affine2D() self._pixel2world.translate(+0.5, +0.5) self._pixel2world.scale((extent[1] - extent[0]) / self._full_res.shape[1], (extent[3] - extent[2]) / self._full_res.shape[0]) self._pixel2world.translate(extent[0], extent[2]) self._world2pixel_cache = None return self._pixel2world_cache @property def _world2pixel(self): if self._world2pixel_cache is None: self._world2pixel_cache = self._pixel2world.inverted() return self._world2pixel_cache def _scale_to_res(self): """ Change self._A and _extent to render an image whose resolution is matched to the eventual rendering. """ # Find out how we need to slice the array to make sure we match the # resolution of the display. We pass self._world2pixel which matters # for cases where the extent has been set. x0, x1, sx, y0, y1, sy = extract_matched_slices(axes=self.axes, shape=self._full_res.shape, transform=self._world2pixel) # Check whether we've already calculated what we need, and if so just # return without doing anything further. if (self._bounds is not None and sx >= self._sx and sy >= self._sy and x0 >= self._bounds[0] and x1 <= self._bounds[1] and y0 >= self._bounds[2] and y1 <= self._bounds[3]): return # Slice the array using the slices determined previously to optimally # match the display self._A = self._full_res[y0:y1:sy, x0:x1:sx] self._A = cbook.safe_masked_invalid(self._A) # We now determine the extent of the subset of the image, by determining # it first in pixel space, and converting it to the 'world' coordinates. # See https://github.com/matplotlib/matplotlib/issues/8693 for a # demonstration of why origin='upper' and extent=None needs to be # special-cased. if self.origin == 'upper' and self._full_extent is None: xmin, xmax, ymin, ymax = x0 - .5, x1 - .5, y1 - .5, y0 - .5 else: xmin, xmax, ymin, ymax = x0 - .5, x1 - .5, y0 - .5, y1 - .5 xmin, ymin, xmax, ymax = self._pixel2world.transform([(xmin, ymin), (xmax, ymax)]).ravel() mi.AxesImage.set_extent(self, [xmin, xmax, ymin, ymax]) # self.set_extent([xmin, xmax, ymin, ymax]) # Finally, we cache the current settings to avoid re-computing similar # arrays in future. self._sx = sx self._sy = sy self._bounds = (x0, x1, y0, y1) self.changed() def draw(self, renderer, *args, **kwargs): if self._full_res.shape is None: return if not self._pressed or self._bounds is None: self._scale_to_res() # Due to a bug in Matplotlib, we need to return here if all values # in the array are masked. if hasattr(self._A, 'mask') and np.all(self._A.mask): return super(ModestImage, self).draw(renderer, *args, **kwargs) def main(): from time import time import matplotlib.pyplot as plt x, y = np.mgrid[0:2000, 0:2000] data = np.sin(x / 10.) * np.cos(y / 30.) f = plt.figure() ax = f.add_subplot(111) # try switching between artist = ModestImage(ax, data=data) ax.set_aspect('equal') artist.norm.vmin = -1 artist.norm.vmax = 1 ax.add_artist(artist) t0 = time() plt.gcf().canvas.draw_idle() t1 = time() print("Draw time for %s: %0.1f ms" % (artist.__class__.__name__, (t1 - t0) * 1000)) plt.show() def imshow(axes, X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, origin=None, extent=None, shape=None, filternorm=1, filterrad=4.0, imlim=None, resample=None, url=None, **kwargs): """Similar to matplotlib's imshow command, but produces a ModestImage Unlike matplotlib version, must explicitly specify axes """ if norm is not None: assert(isinstance(norm, mcolors.Normalize)) if aspect is None: aspect = rcParams['image.aspect'] axes.set_aspect(aspect) im = ModestImage(axes, cmap=cmap, norm=norm, interpolation=interpolation, origin=origin, extent=extent, filternorm=filternorm, filterrad=filterrad, resample=resample, **kwargs) im.set_data(X) im.set_alpha(alpha) axes._set_artist_props(im) if im.get_clip_path() is None: # image does not already have clipping set, clip to axes patch im.set_clip_path(axes.patch) # if norm is None and shape is None: # im.set_clim(vmin, vmax) if vmin is not None or vmax is not None: im.set_clim(vmin, vmax) # elif norm is None: # im.autoscale_None() im.set_url(url) # update ax.dataLim, and, if autoscaling, set viewLim # to tightly fit the image, regardless of dataLim. if extent is not None: im.set_extent(extent) axes.add_image(im) def remove(h): axes.images.remove(h) im._remove_method = remove return im def extract_matched_slices(axes=None, shape=None, extent=None, transform=IDENTITY_TRANSFORM): """Determine the slice parameters to use, matched to the screen. :param ax: Axes object to query. It's extent and pixel size determine the slice parameters :param shape: Tuple of the full image shape to slice into. Upper boundaries for slices will be cropped to fit within this shape. :rtype: tulpe of x0, x1, sx, y0, y1, sy Indexing the full resolution array as array[y0:y1:sy, x0:x1:sx] returns a view well-matched to the axes' resolution and extent """ # Find extent in display pixels (this gives the resolution we need # to sample the array to) ext = (axes.transAxes.transform([(1, 1)]) - axes.transAxes.transform([(0, 0)]))[0] # Find the extent of the axes in 'world' coordinates xlim, ylim = axes.get_xlim(), axes.get_ylim() # Transform the limits to pixel coordinates ind0 = transform.transform([min(xlim), min(ylim)]) ind1 = transform.transform([max(xlim), max(ylim)]) def _clip(val, lo, hi): return int(max(min(val, hi), lo)) # Determine the range of pixels to extract from the array, including a 5 # pixel margin all around. We ensure that the shape of the resulting array # will always be at least (1, 1) even if there is really no overlap, to # avoid issues. y0 = _clip(ind0[1] - 5, 0, shape[0] - 1) y1 = _clip(ind1[1] + 5, 1, shape[0]) x0 = _clip(ind0[0] - 5, 0, shape[1] - 1) x1 = _clip(ind1[0] + 5, 1, shape[1]) # Determine the strides that can be used when extracting the array sy = int(max(1, min((y1 - y0) / 5., np.ceil(abs((ind1[1] - ind0[1]) / ext[1]))))) sx = int(max(1, min((x1 - x0) / 5., np.ceil(abs((ind1[0] - ind0[0]) / ext[0]))))) return x0, x1, sx, y0, y1, sy if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/image.py0000644000175100001710000001170000000000000017704 0ustar00vstsdockerimport matplotlib as mpl from .base import ArrayAnimator __all__ = ['ImageAnimator'] class ImageAnimator(ArrayAnimator): """ Create a matplotlib backend independent data explorer for 2D images. The following keyboard shortcuts are defined in the viewer: * 'left': previous step on active slider. * 'right': next step on active slider. * 'top': change the active slider up one. * 'bottom': change the active slider down one. * 'p': play/pause active slider. This viewer can have user defined buttons added by specifying the labels and functions called when those buttons are clicked as keyword arguments. Parameters ---------- data: `numpy.ndarray` The data to be visualized. image_axes: `list`, optional A list of the axes order that make up the image. axis_ranges: `list` of physical coordinates for the `numpy.ndarray`, optional Defaults to `None` and array indices will be used for all axes. The `list` should contain one element for each axis of the `numpy.ndarray`. For the image axes a ``[min, max]`` pair should be specified which will be passed to `matplotlib.pyplot.imshow` as an extent. For the slider axes a ``[min, max]`` pair can be specified or an array the same length as the axis which will provide all values for that slider. Notes ----- Extra keywords are passed to `~sunpy.visualization.animator.ArrayAnimator`. """ def __init__(self, data, image_axes=[-2, -1], axis_ranges=None, **kwargs): # Check that number of axes is 2. if len(image_axes) != 2: raise ValueError("There can only be two spatial axes") # Define number of slider axes. self.naxis = data.ndim self.num_sliders = self.naxis-2 # Define marker to determine if plot axes values are supplied via array of # pixel values or min max pair. This will determine the type of image produced # and hence how to plot and update it. self._non_regular_plot_axis = False # Run init for parent class super().__init__(data, image_axes=image_axes, axis_ranges=axis_ranges, **kwargs) def plot_start_image(self, ax): """ Sets up plot of initial image. """ # Create extent arg extent = [] # reverse because numpy is in y-x and extent is x-y if max([len(self.axis_ranges[i]) for i in self.image_axes[::-1]]) > 2: self._non_regular_plot_axis = True for i in self.image_axes[::-1]: if self._non_regular_plot_axis is False and len(self.axis_ranges[i]) > 2: self._non_regular_plot_axis = True extent.append(self.axis_ranges[i][0]) extent.append(self.axis_ranges[i][-1]) imshow_args = {'interpolation': 'nearest', 'origin': 'lower'} imshow_args.update(self.imshow_kwargs) # If value along an axis is set with an array, generate a NonUniformImage if self._non_regular_plot_axis: # If user has inverted the axes, transpose the data so the dimensions match. if self.image_axes[0] < self.image_axes[1]: data = self.data[self.frame_index].transpose() else: data = self.data[self.frame_index] # Initialize a NonUniformImage with the relevant data and axis values and # add the image to the axes. im = mpl.image.NonUniformImage(ax, **imshow_args) im.set_data(self.axis_ranges[self.image_axes[0]], self.axis_ranges[self.image_axes[1]], data) ax.add_image(im) # Define the xlim and ylim from the pixel edges. ax.set_xlim(self.extent[0], self.extent[1]) ax.set_ylim(self.extent[2], self.extent[3]) else: # Else produce a more basic plot with regular axes. imshow_args.update({'extent': extent}) im = ax.imshow(self.data[self.frame_index], **imshow_args) if self.if_colorbar: self._add_colorbar(im) return im def update_plot(self, val, im, slider): """ Updates plot based on slider/array dimension being iterated. """ ind = int(val) ax_ind = self.slider_axes[slider.slider_ind] self.frame_slice[ax_ind] = ind if val != slider.cval: if self._non_regular_plot_axis: if self.image_axes[0] < self.image_axes[1]: data = self.data[self.frame_index].transpose() else: data = self.data[self.frame_index] im.set_data(self.axis_ranges[self.image_axes[0]], self.axis_ranges[self.image_axes[1]], data) else: im.set_array(self.data[self.frame_index]) slider.cval = val # Update slider label to reflect real world values in axis_ranges. super().update_plot(val, im, slider) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/line.py0000644000175100001710000001666700000000000017572 0ustar00vstsdockerimport numpy as np from .base import ArrayAnimator, edges_to_centers_nd __all__ = ['LineAnimator'] class LineAnimator(ArrayAnimator): """ Create a matplotlib backend independent data explorer for 1D plots. The following keyboard shortcuts are defined in the viewer: * 'left': previous step on active slider. * 'right': next step on active slider. * 'top': change the active slider up one. * 'bottom': change the active slider down one. * 'p': play/pause active slider. This viewer can have user defined buttons added by specifying the labels and functions called when those buttons are clicked as keyword arguments. Parameters ---------- data: `numpy.ndarray` The y-axis to be visualized. plot_axis_index: `int`, optional The axis used to plot against ``data``. Defaults to ``-1``, i.e., the last dimension of the array. axis_ranges: `list` of physical coordinates for the `numpy.ndarray`, optional Defaults to `None` and array indices will be used for all axes. The `list` should contain one element for each axis of the `numpy.ndarray`. For the image axes a ``[min, max]`` pair should be specified which will be passed to `matplotlib.pyplot.imshow` as an extent. For the slider axes a ``[min, max]`` pair can be specified or an array the same length as the axis which will provide all values for that slider. For more information, see the Notes section of this docstring. xlabel: `str`, optional Label of x-axis. Defaults to `None`. ylabel: `str`, optional Label of y-axis. Defaults to `None`. xlim: `tuple`, optional Limits of x-axis of plot. Defaults to `None`. ylim: `tuple`, optional Limits of y-axis of plot. Defaults to `None`. Notes ----- Additional information on API of ``axes_ranges`` keyword argument. #. x-axis values must be supplied (if desired) as an array in the element of the ``axis_ranges`` `list` corresponding to the ``plot_axis_index ``in the data array, i.e., ``x_axis_values == axis_ranges[plot_axis_index]`` #. The x-axis values represent the edges of the pixels/bins along the plotted axis, not the centers. Therefore there must be 1 more x-axis value than there are data points along the x-axis. #. The shape of the x-axis values array can take two forms. a) First, it can have a length 1 greater than the length of the data array along the dimension corresponding to the x-axis, i.e., ``len(axis_ranges[plot_axis_index]) == len(data[plot_axis_index])+1``. In this scenario the same x-axis values are used in every frame of the animation. b) Second, the x-axis array can have the same shape as the data array, with the exception of the plotted axis which, as above, must be 1 greater than the length of the data array along that dimension. In this scenario the x-axis is refreshed for each frame. For example, if ``data.shape == axis_ranges[plot_axis_index].shape == (4, 3)``, where ``plot_axis_index == 0``, the 0th frame of the animation will show data from ``data[:, 0]`` with the x-axis described by ``axis_ranges[plot_axis_index][:, 0]``, while the 1st frame will show data from ``data[:, 1]`` with the x-axis described by ``axis_ranges[plot_axis_index][:, 1]``. #. This API holds for slider axes. Extra keywords are passed to `~sunpy.visualization.animator.ArrayAnimator`. """ def __init__(self, data, plot_axis_index=-1, axis_ranges=None, ylabel=None, xlabel=None, xlim=None, ylim=None, aspect='auto', **kwargs): # Check inputs. self.plot_axis_index = int(plot_axis_index) if self.plot_axis_index not in range(-data.ndim, data.ndim): raise ValueError("plot_axis_index must be within range of number of data dimensions" " (or equivalent negative indices).") if data.ndim < 2: raise ValueError("data must have at least two dimensions. One for data " "for each single plot and at least one for time/iteration.") # Define number of slider axes. self.naxis = data.ndim self.num_sliders = self.naxis - 1 # Attach data to class. if axis_ranges is not None and all(axis_range is None for axis_range in axis_ranges): axis_ranges = None if axis_ranges is None or axis_ranges[self.plot_axis_index] is None: self.xdata = np.arange(data.shape[self.plot_axis_index]) # Else derive the xdata as pixel centers from the pixel edges supplied by # the user in axis_ranges[plot_axis_index] else: # If the shape of the array is a 1D array, get the centers about axis=0 if np.asarray(axis_ranges[self.plot_axis_index]).ndim == 1: self.xdata = edges_to_centers_nd(np.asarray(axis_ranges[self.plot_axis_index]), 0) # Else derive the xdata as pixel centers from the pixel edges supplied by # the user in axis_ranges[plot_axis_index] along axis=plot_axis_index else: self.xdata = edges_to_centers_nd(np.asarray(axis_ranges[self.plot_axis_index]), plot_axis_index) if ylim is None: ylim = (np.nanmin(data), np.nanmax(data)) if xlim is None: xlim = (np.nanmin(self.xdata), np.nanmax(self.xdata)) self.ylim = ylim self.xlim = xlim self.xlabel = xlabel self.ylabel = ylabel self.aspect = aspect # Run init for base class super().__init__(data, image_axes=[self.plot_axis_index], axis_ranges=axis_ranges, **kwargs) def plot_start_image(self, ax): """ Sets up a plot of initial image. """ ax.set_xlim(self.xlim) ax.set_ylim(self.ylim) ax.set_aspect(self.aspect, adjustable='datalim') if self.xlabel is not None: ax.set_xlabel(self.xlabel) if self.ylabel is not None: ax.set_ylabel(self.ylabel) plot_args = {} plot_args.update(self.imshow_kwargs) if self.xdata.shape == self.data.shape: item = [0] * self.data.ndim item[self.plot_axis_index] = slice(None) xdata = np.squeeze(self.xdata[tuple(item)]) else: xdata = self.xdata line, = ax.plot(xdata, self.data[self.frame_index], **plot_args) return line def update_plot(self, val, line, slider): """ Updates plot based on slider/array dimension being iterated. """ val = int(val) ax_ind = self.slider_axes[slider.slider_ind] self.frame_slice[ax_ind] = val if val != slider.cval: line.set_ydata(self.data[self.frame_index]) if self.xdata.shape == self.data.shape: item = [int(slid._slider.val) for slid in self.sliders] item[ax_ind] = val if self.plot_axis_index < 0: i = self.data.ndim + self.plot_axis_index else: i = self.plot_axis_index item.insert(i, slice(None)) line.set_xdata(self.xdata[tuple(item)]) slider.cval = val # Update slider label to reflect real world values in axis_ranges. super().update_plot(val, line, slider) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643822949.0458584 mpl_animators-1.0.1/mpl_animators/tests/0000755000175100001710000000000000000000000017413 5ustar00vstsdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/tests/__init__.py0000644000175100001710000000000000000000000021512 0ustar00vstsdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/tests/figure_hashes_mpl_343_ft_261_astropy_431.json0000644000175100001710000000376400000000000027626 0ustar00vstsdocker{ "mpl_animators.tests.test_basefuncanimator.test_lineanimator_figure": "dbf47ab983a844621f912b2f5a2d9ee657d3f661678c16a07afc7217f50e3fd4", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_simple_plot": "03ab823148c6071a81c9edad2bc58e0c0b401a3dcf5108d749a80ac0f24a07b7", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_clip_interval": "18e3b9433d7115d3f05d5fbc771774e6908d00e20b6d390bcc3d525b5af48632", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_celestial_sliders": "460980d5e2d2b06e1b7ce07613f2169078edc759515ca1c1cff242f72cca1158", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_update_plot": "377a48ea45516d91bd483e0044868d2a2da0bb45569ed15ead305987ad18dc00", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_transpose_update_plot": "52af2bd727302c27526e5c4c459ae1a5e7e452c193164a77095febb0d0412dfd", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_colorbar_buttons": "76d23dd55302f95fd7185db807f05fc10ab0e7bc84b54ef2881cdbdd978e52cd", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_colorbar_buttons_default_labels": "5c503d9f26caf038936b1edb23b7d3963b3af0d1aef63fc22f1c621446407370", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_extra_sliders": "4a28bcbd429c0ea891dd40ef4f587939b7ff1fc75f1b564cd7e97dcbbfbc50c3", "mpl_animators.tests.test_wcs.test_array_animator_wcs_1d_update_plot": "58e799859b877fd8a5f73377eeab2686f0567241093c244779143797d4cef778", "mpl_animators.tests.test_wcs.test_array_animator_wcs_1d_update_plot_masked": "157c52fdcf235e09ebb5389f1d9c0957795e73f695fa1a839b077259f9fb8240", "mpl_animators.tests.test_wcs.test_array_animator_wcs_coord_params": "e1678a7b513917e177547b8ee0a93ee2f29a6d3dacff5fa213355ff414e706b1", "mpl_animators.tests.test_wcs.test_array_animator_wcs_coord_params_no_ticks": "ce35b25eb2c4eb6d2ce56d6a78c8a33b1bfdf8ef00b098c7c2c08feae2a82356", "mpl_animators.tests.test_wcs.test_array_animator_wcs_coord_params_grid": "4bb5930ec573f8399470df023d4686601728d35a70527c397f7eed39d07ba0e3" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/tests/figure_hashes_mpl_dev_ft_261_astropy_dev.json0000644000175100001710000000376400000000000030342 0ustar00vstsdocker{ "mpl_animators.tests.test_basefuncanimator.test_lineanimator_figure": "963222572ddcfb2c2dbb35a13e19ba30b3f36b6f921a1ff315cb075bc8cd6c0e", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_simple_plot": "8beb85da6733fee9e74ddc0ea133c2b9cf6ce488c9457690fba41746bf3d0382", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_clip_interval": "a07976f353a5ad80e20c9e8a8c4a9369ec89ca56d92401126e32e920fff4a1b4", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_celestial_sliders": "000954368bfe2e66004e94df8562d12d28755f19f1a80f2cf93be45ad8abc3af", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_update_plot": "9d165bb1cefd64816e0603c031a110bd0b8a482b325dd0dc3702387d3f14505a", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_transpose_update_plot": "0bcb4b020f9dbfaf7f8beb1b203d252c68cb2b208f1fad99cc72d1b18b15d9ec", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_colorbar_buttons": "02081436da6052e8daf2099e2ef323ff06f710203eceb438d215b4252b2e776e", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_colorbar_buttons_default_labels": "36d958cbb0fbf990035988f0f01162748d821553bca63917657636f7d90f21c0", "mpl_animators.tests.test_wcs.test_array_animator_wcs_2d_extra_sliders": "fd94e25c0b6a813818c8ab1b42c6d8828e546acf30ea9bc73164e50b22f456ba", "mpl_animators.tests.test_wcs.test_array_animator_wcs_1d_update_plot": "0469fd9632b2e19a642f9d5cad14f584e80fcba85e1d6a7a3c0303ca40dd3c80", "mpl_animators.tests.test_wcs.test_array_animator_wcs_1d_update_plot_masked": "0a45469f2100fb423ee087e98c9dddbce86ceea27fa2fce215e5e8c9f32a70f0", "mpl_animators.tests.test_wcs.test_array_animator_wcs_coord_params": "844721f7a86ad7923e2c299c4db5dcfed8580df15d956805b48f68fd659099aa", "mpl_animators.tests.test_wcs.test_array_animator_wcs_coord_params_no_ticks": "54cc3b0265457c4dd9a0691b11b7801cdd4ee6dd673abf6f5d43805dbf6f899c", "mpl_animators.tests.test_wcs.test_array_animator_wcs_coord_params_grid": "4f6799e2a0df342467318ea83a6d250db554b9d4cd6fb565f69be9321e17af2a" } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/tests/helpers.py0000644000175100001710000000314100000000000021426 0ustar00vstsdockerfrom functools import wraps from pathlib import Path import astropy import matplotlib as mpl import matplotlib.pyplot as plt import pytest def get_hash_library_name(): """ Generate the hash library name for this env. """ ft2_version = f"{mpl.ft2font.__freetype_version__.replace('.', '')}" mpl_version = "dev" if "+" in mpl.__version__ else mpl.__version__.replace('.', '') astropy_version = "dev" if "dev" in astropy.__version__ else astropy.__version__.replace('.', '') return f"figure_hashes_mpl_{mpl_version}_ft_{ft2_version}_astropy_{astropy_version}.json" def figure_test(test_function): """ A decorator for a test that verifies the hash of the current figure or the returned figure, with the name of the test function as the hash identifier in the library. A PNG is also created in the 'result_image' directory, which is created on the current path. All such decorated tests are marked with `pytest.mark.mpl_image` for convenient filtering. Examples -------- @figure_test def test_simple_plot(): plt.plot([0,1]) """ hash_library_name = get_hash_library_name() hash_library_file = Path(__file__).parent / hash_library_name @pytest.mark.mpl_image_compare(hash_library=hash_library_file, savefig_kwargs={'metadata': {'Software': None}}, style='default') @wraps(test_function) def test_wrapper(*args, **kwargs): ret = test_function(*args, **kwargs) if ret is None: ret = plt.gcf() return ret return test_wrapper ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/tests/test_basefuncanimator.py0000644000175100001710000001651500000000000024355 0ustar00vstsdockerfrom functools import partial import matplotlib.animation as mplanim import matplotlib.axes as maxes import matplotlib.backend_bases as mback import matplotlib.figure as mfigure import matplotlib.pyplot as plt import numpy as np import pytest from mpl_animators import ArrayAnimator, BaseFuncAnimator, LineAnimator, base from mpl_animators.tests.helpers import figure_test class FuncAnimatorTest(BaseFuncAnimator): def plot_start_image(self, ax): im = ax.imshow(self.data[0]) if self.if_colorbar: self._add_colorbar(im) return im def update_plotval(val, im, slider, data): i = int(val) im.set_array(data[i]) def button_func1(*args, **kwargs): print(*args, **kwargs) @pytest.mark.parametrize('fig, colorbar, buttons', ((None, False, [[], []]), (mfigure.Figure(), True, [[button_func1], ["hi"]]))) def test_base_func_init(fig, colorbar, buttons): data = np.random.random((3, 10, 10)) func0 = partial(update_plotval, data=data) func1 = partial(update_plotval, data=data*10) funcs = [func0, func1] ranges = [(0, 3), (0, 3)] tfa = FuncAnimatorTest(data, funcs, ranges, fig=fig, colorbar=colorbar, button_func=buttons[0], button_labels=buttons[1]) tfa.label_slider(0, "hello") assert tfa.sliders[0]._slider.label.get_text() == "hello" tfa._set_active_slider(1) assert tfa.active_slider == 1 fig = tfa.fig event = mback.KeyEvent(name='key_press_event', canvas=fig.canvas, key='down') tfa._key_press(event) assert tfa.active_slider == 0 event.key = 'up' tfa._key_press(event) assert tfa.active_slider == 1 tfa.slider_buttons[tfa.active_slider]._button.clicked = False event.key = 'p' tfa._click_slider_button(event=event, button=tfa.slider_buttons[tfa.active_slider]._button, slider=tfa.sliders[tfa.active_slider]._slider) assert tfa.slider_buttons[tfa.active_slider]._button.label._text == "||" tfa._key_press(event) assert tfa.slider_buttons[tfa.active_slider]._button.label._text == ">" event.key = 'left' tfa._key_press(event) assert tfa.sliders[tfa.active_slider]._slider.val == tfa.sliders[tfa.active_slider]._slider.valmax event.key = 'right' tfa._key_press(event) assert tfa.sliders[tfa.active_slider]._slider.val == tfa.sliders[tfa.active_slider]._slider.valmin event.key = 'right' tfa._key_press(event) assert tfa.sliders[tfa.active_slider]._slider.val == tfa.sliders[tfa.active_slider]._slider.valmin + 1 event.key = 'left' tfa._key_press(event) assert tfa.sliders[tfa.active_slider]._slider.val == tfa.sliders[tfa.active_slider]._slider.valmin tfa._start_play(event, tfa.slider_buttons[tfa.active_slider]._button, tfa.sliders[tfa.active_slider]._slider) assert tfa.timer tfa._stop_play(event) assert tfa.timer is None tfa._slider_changed(val=2, slider=tfa.sliders[tfa.active_slider]._slider) assert np.array(tfa.im.get_array()).all() == data[2].all() event.inaxes = tfa.sliders[0] tfa._mouse_click(event) assert tfa.active_slider == 0 # Make sure figures created directly and through pyplot work @pytest.fixture(params=[plt.figure, mfigure.Figure]) def funcanimator(request): data = np.random.random((3, 10, 10)) func = partial(update_plotval, data=data) funcs = [func] ranges = [(0, 3)] fig = request.param() return FuncAnimatorTest(data, funcs, ranges, fig=fig) def test_to_anim(funcanimator): ani = funcanimator.get_animation() assert isinstance(ani, mplanim.FuncAnimation) def test_to_axes(funcanimator): assert isinstance(funcanimator.axes, maxes.SubplotBase) def test_axes_set(): data = np.random.random((3, 10, 10)) funcs = [partial(update_plotval, data=data)] ranges = [(0, 3)] # Create Figure for animator fig1 = plt.figure() # Create new Figure, Axes, and set current axes fig2, ax = plt.subplots() plt.sca(ax) ani = FuncAnimatorTest(data, funcs, ranges, fig=fig1) # Make sure the animator axes is now the current axes assert plt.gca() is ani.axes [plt.close(f) for f in [fig1, fig2]] def test_edges_to_centers_nd(): edges_axis = 0 axis_range = np.zeros((10, 2)) axis_range[:, 0] = np.arange(10, 20) expected = np.zeros((9, 2)) expected[:, edges_axis] = np.arange(10.5, 19) output = base.edges_to_centers_nd(axis_range, edges_axis) assert np.array_equal(output, expected) class ArrayAnimatorTest(ArrayAnimator): def __init__(self, data): self.naxis = data.ndim self.image_axes = [1] self.slider_axes = [0] def plot_start_image(self, ax): pass def update_plot(self, val, artist, slider): super().update_plot(val, artist, slider) axis_ranges1 = np.tile(np.linspace(0, 100, 21), (10, 1)) @pytest.mark.parametrize('axis_ranges, exp_extent, exp_axis_ranges', [([None, None], [-0.5, 19.5], [np.arange(10), np.array([-0.5, 19.5])]), ([[0, 10], [0, 20]], [0, 20], [np.arange(0.5, 10.5), np.asarray([0, 20])]), ([np.arange(0, 11), np.arange(0, 21)], [0, 20], [np.arange(0.5, 10.5), np.arange(0.5, 20.5)]), ([None, axis_ranges1], [0.0, 100.0], [np.arange(10), base.edges_to_centers_nd(axis_ranges1, 1)])]) def test_sanitize_axis_ranges(axis_ranges, exp_extent, exp_axis_ranges): data_shape = (10, 20) data = np.random.rand(*data_shape) aanim = ArrayAnimatorTest(data=data) out_axis_ranges, out_extent = aanim._sanitize_axis_ranges(axis_ranges=axis_ranges, data_shape=data_shape) assert exp_extent == out_extent assert np.array_equal(exp_axis_ranges[1], out_axis_ranges[1]) assert callable(out_axis_ranges[0]) assert np.array_equal(exp_axis_ranges[0], out_axis_ranges[0](np.arange(10))) XDATA = np.tile(np.linspace(0, 100, 11), (5, 5, 1)) @pytest.mark.parametrize('plot_axis_index, axis_ranges, xlabel, xlim', [(-1, None, None, None), (-1, [None, None, XDATA], 'x-axis', None)]) def test_lineanimator_init(plot_axis_index, axis_ranges, xlabel, xlim): data = np.random.random((5, 5, 10)) LineAnimator(data=data, plot_axis_index=plot_axis_index, axis_ranges=axis_ranges, xlabel=xlabel, xlim=xlim) def test_lineanimator_init_nans(): data = np.random.random((5, 5, 10)) data[0][0][:] = np.nan line_anim = LineAnimator(data=data, plot_axis_index=-1, axis_ranges=[None, None, XDATA], xlabel='x-axis', xlim=None, ylim=None) assert line_anim.ylim[0] is not None assert line_anim.ylim[1] is not None assert line_anim.xlim[0] is not None assert line_anim.xlim[1] is not None @figure_test def test_lineanimator_figure(): np.random.seed(1) data_shape0 = (10, 20) data0 = np.random.rand(*data_shape0) plot_axis0 = 1 slider_axis0 = 0 xdata = np.tile(np.linspace( 0, 100, (data_shape0[plot_axis0] + 1)), (data_shape0[slider_axis0], 1)) ani = LineAnimator(data0, plot_axis_index=plot_axis0, axis_ranges=[None, xdata]) return ani.fig ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/tests/test_wcs.py0000644000175100001710000002554200000000000021630 0ustar00vstsdockerfrom textwrap import dedent import astropy.units as u import numpy as np import pytest from astropy.io import fits from astropy.visualization.wcsaxes import WCSAxes from astropy.wcs import WCS from mpl_animators.tests.helpers import figure_test from mpl_animators.wcs import ArrayAnimatorWCS # See https://github.com/astropy/astropy/pull/10400 pytestmark = pytest.mark.filterwarnings('ignore:target cannot be converted to ICRS, so will not be ' 'set on SpectralCoord') @pytest.fixture def wcs_4d(): header = dedent("""\ WCSAXES = 4 / Number of coordinate axes CRPIX1 = 0.0 / Pixel coordinate of reference point CRPIX2 = 0.0 / Pixel coordinate of reference point CRPIX3 = 0.0 / Pixel coordinate of reference point CRPIX4 = 5.0 / Pixel coordinate of reference point CDELT1 = 0.4 / [min] Coordinate increment at reference point CDELT2 = 2E-11 / [m] Coordinate increment at reference point CDELT3 = 0.0027777777777778 / [deg] Coordinate increment at reference point CDELT4 = 0.0013888888888889 / [deg] Coordinate increment at reference point CUNIT1 = 'min' / Units of coordinate increment and value CUNIT2 = 'm' / Units of coordinate increment and value CUNIT3 = 'deg' / Units of coordinate increment and value CUNIT4 = 'deg' / Units of coordinate increment and value CTYPE1 = 'TIME' / Coordinate type code CTYPE2 = 'WAVE' / Vacuum wavelength (linear) CTYPE3 = 'HPLT-TAN' / Coordinate type codegnomonic projection CTYPE4 = 'HPLN-TAN' / Coordinate type codegnomonic projection CRVAL1 = 0.0 / [min] Coordinate value at reference point CRVAL2 = 0.0 / [m] Coordinate value at reference point CRVAL3 = 0.0 / [deg] Coordinate value at reference point CRVAL4 = 0.0 / [deg] Coordinate value at reference point LONPOLE = 180.0 / [deg] Native longitude of celestial pole LATPOLE = 0.0 / [deg] Native latitude of celestial pole """) return WCS(header=fits.Header.fromstring(header, sep='\n')) @pytest.fixture def wcs_3d(): header = dedent("""\ NAXIS = 3 / Number of data axes NAXIS1 = 205 / NAXIS2 = 77 / NAXIS3 = 64 / CDELT1 = 0.0129800001159 / CDELT2 = 0.166350 / CDELT3 = 1.99544040740 / CRPIX1 = 1.00000 / CRPIX2 = 386.500 / CRPIX3 = 32.0000 / CRVAL1 = 1331.68328015 / CRVAL2 = -107.579 / CRVAL3 = 817.863 / CTYPE1 = 'WAVE ' / CTYPE2 = 'HPLT-TAN' / CTYPE3 = 'HPLN-TAN' / CUNIT1 = 'Angstrom' / CUNIT2 = 'arcsec ' / CUNIT3 = 'arcsec ' / PC1_1 = 1.00000000000 / PC1_2 = 0.00000000000 / PC2_1 = 0.00000000000 / PC2_2 = 0.999988496304 / PC3_1 = 0.00000000000 / PC3_2 = 0.000939457726278 / PC3_3 = 0.999988496304 / PC2_3 = -0.135178965950 / """) return WCS(header=fits.Header.fromstring(header, sep='\n')) @pytest.mark.parametrize("data, slices, dim", ( (np.arange(120).reshape((5, 4, 3, 2)), [0, 0, 'x', 'y'], 2), (np.arange(120).reshape((5, 4, 3, 2)), [0, 'x', 0, 'y'], 2), (np.arange(120).reshape((5, 4, 3, 2)), ['x', 0, 0, 'y'], 2), (np.arange(120).reshape((5, 4, 3, 2)), ['y', 0, 'x', 0], 2), (np.arange(120).reshape((5, 4, 3, 2)), ['x', 'y', 0, 0], 2), (np.arange(120).reshape((5, 4, 3, 2)), [0, 0, 0, 'x'], 1), )) def test_construct_array_animator(wcs_4d, data, slices, dim): array_animator = ArrayAnimatorWCS(data, wcs_4d, slices) assert isinstance(array_animator, ArrayAnimatorWCS) assert array_animator.plot_dimensionality == dim assert array_animator.num_sliders == data.ndim - dim for i, (wslice, arange) in enumerate(zip(slices, array_animator.axis_ranges[::-1])): if wslice not in ['x', 'y']: assert callable(arange) a = arange(0) if "pos" in wcs_4d.world_axis_physical_types[i]: assert isinstance(a, u.Quantity) assert u.allclose(a, 0 * u.pix) else: assert isinstance(a, u.Quantity) assert a.value == wcs_4d.pixel_to_world_values(*[0] * wcs_4d.world_n_dim)[i] assert a.unit == wcs_4d.world_axis_units[i] else: assert arange is None def test_constructor_errors(wcs_4d): # WCS is not BaseLowLevelWCS with pytest.raises(ValueError, match="provided that implements the astropy WCS API."): ArrayAnimatorWCS(np.arange(25).reshape((5, 5)), {}, ['x', 'y']) # Data has wrong number of dimensions with pytest.raises(ValueError, match="Dimensionality of the data and WCS object do not match."): ArrayAnimatorWCS(np.arange(25).reshape((5, 5)), wcs_4d, ['x', 'y']) # Slices is wrong length with pytest.raises(ValueError, match="slices should be the same length"): ArrayAnimatorWCS(np.arange(16).reshape((2, 2, 2, 2)), wcs_4d, ['x', 'y']) # x not in slices with pytest.raises(ValueError, match="slices should contain at least"): ArrayAnimatorWCS(np.arange(16).reshape((2, 2, 2, 2)), wcs_4d, [0, 0, 0, 'y']) @figure_test def test_array_animator_wcs_2d_simple_plot(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'x', 'y']) return a.fig @figure_test def test_array_animator_wcs_2d_clip_interval(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'x', 'y'], clip_interval=(1, 99)*u.percent) return a.fig def test_array_animator_wcs_2d_clip_interval_change(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) pclims = [5, 95] a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'x', 'y'], clip_interval=pclims * u.percent) lims0 = a._get_2d_plot_limits() a.update_plot(1, a.im, a.sliders[0]._slider) lims1 = a._get_2d_plot_limits() assert np.all(lims0 != lims1) assert np.all(lims0 == np.percentile(data[..., 0, 0], pclims)) assert np.all(lims1 == np.percentile(data[..., 1, 0], pclims)) @figure_test def test_array_animator_wcs_2d_celestial_sliders(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, ['x', 'y', 0, 0]) return a.fig def test_to_axes(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, ['x', 'y', 0, 0]) assert isinstance(a.axes, WCSAxes) @figure_test def test_array_animator_wcs_2d_update_plot(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'x', 'y']) a.update_plot(1, a.im, a.sliders[0]._slider) return a.fig @figure_test def test_array_animator_wcs_2d_transpose_update_plot(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'y', 'x'], colorbar=True) a.update_plot(1, a.im, a.sliders[0]._slider) return a.fig @figure_test def test_array_animator_wcs_2d_colorbar_buttons(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) bf = [lambda x: x] * 10 bl = ['h'] * 10 a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'y', 'x'], colorbar=True, button_func=bf, button_labels=bl) a.update_plot(1, a.im, a.sliders[0]._slider) return a.fig @figure_test def test_array_animator_wcs_2d_colorbar_buttons_default_labels(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) bf = [lambda x: x] * 10 a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'y', 'x'], colorbar=True, button_func=bf) a.update_plot(1, a.im, a.sliders[0]._slider) return a.fig @figure_test def test_array_animator_wcs_2d_extra_sliders(wcs_4d): def vmin_slider(val, im, slider): im.set_clim(vmin=val) def vmax_slider(val, im, slider): im.set_clim(vmax=val) data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'y', 'x'], colorbar=True, slider_functions=[vmin_slider, vmax_slider], slider_ranges=[[0, 100], [0, 100]]) a.update_plot(1, a.im, a.sliders[0]._slider) return a.fig @figure_test def test_array_animator_wcs_1d_update_plot(wcs_4d): data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'x', 0], ylabel="Y axis!") a.sliders[0]._slider.set_val(1) return a.fig @figure_test def test_array_animator_wcs_1d_update_plot_masked(wcs_3d): """ This test ensures the x axis of the line plot is correct even if the whole of the initial line plotted at construction of the animator is masked out. """ nelem = np.prod(wcs_3d.array_shape) data = np.arange(nelem, dtype=np.float64).reshape(wcs_3d.array_shape) data = np.ma.MaskedArray(data, data < nelem / 2) # Check that the generated data satisfies the test condition assert data.mask[0, 0].all() a = ArrayAnimatorWCS(data, wcs_3d, ['x', 0, 0], ylabel="Y axis!") a.sliders[0]._slider.set_val(wcs_3d.array_shape[0] / 2) return a.fig @figure_test def test_array_animator_wcs_coord_params(wcs_4d): coord_params = { 'hpln': { 'format_unit': u.deg, 'major_formatter': 'hh:mm:ss', 'axislabel': 'Longitude', 'ticks': {'spacing': 10*u.arcsec} } } data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'x', 'y'], coord_params=coord_params) return a.fig @figure_test def test_array_animator_wcs_coord_params_no_ticks(wcs_4d): coord_params = { 'hpln': { 'format_unit': u.deg, 'major_formatter': 'hh:mm:ss', 'axislabel': 'Longitude', 'ticks': False } } data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'x', 'y'], coord_params=coord_params) return a.fig @figure_test def test_array_animator_wcs_coord_params_grid(wcs_4d): coord_params = { 'hpln': { 'format_unit': u.deg, 'major_formatter': 'hh:mm:ss', 'axislabel': 'Longitude', 'grid': True } } data = np.arange(120).reshape((5, 4, 3, 2)) a = ArrayAnimatorWCS(data, wcs_4d, [0, 0, 'x', 'y'], coord_params=coord_params) return a.fig ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822948.0 mpl_animators-1.0.1/mpl_animators/version.py0000644000175100001710000000053100000000000020307 0ustar00vstsdocker# Note that we need to fall back to the hard-coded version if either # setuptools_scm can't be imported or setuptools_scm can't determine the # version, so we catch the generic 'Exception'. try: from setuptools_scm import get_version __version__ = get_version(root='..', relative_to=__file__) except Exception: __version__ = '1.0.1' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/mpl_animators/wcs.py0000644000175100001710000003107400000000000017424 0ustar00vstsdockerfrom functools import partial import astropy.units as u import numpy as np from astropy.visualization import AsymmetricPercentileInterval from astropy.wcs.wcsapi import BaseLowLevelWCS from mpl_animators.extern import modest_image from .base import ArrayAnimator __all__ = ['ArrayAnimatorWCS'] class ArrayAnimatorWCS(ArrayAnimator): """ Animate an array with associated `~astropy.wcs.wcsapi.BaseLowLevelWCS` object. The following keyboard shortcuts are defined in the viewer: * 'left': previous step on active slider. * 'right': next step on active slider. * 'top': change the active slider up one. * 'bottom': change the active slider down one. * 'p': play/pause active slider. Parameters ---------- data: `numpy.ndarray` The data to be visualized. wcs: `astropy.wcs.wcsapi.BaseLowLevelWCS` The world coordinate object associated with the array. slices: `tuple` or `list` A list specifying which axes of the array should be plotted on which axes. The list should be the same length as the number of pixel dimensions with ``'x'`` and (optionally) ``'y'`` in the elements corresponding to the axes to be plotted. If only ``'x'`` is present a line plot will be drawn. All other elements should be ``0``. coord_params: `dict`, optional This dict allows you to override `~astropy.visualization.wcsaxes.WCSAxes` parameters for each world coordinate. The keys of this dictionary should be a value which can be looked up in ``WCSAxes.coords`` (i.e. ``em.wl`` or ``hpln``) and the values should be a dict which supports the following keys, and passes their values to the associated `~astropy.visualization.wcsaxes.WCSAxes` methods. * ``format_unit``: `~astropy.visualization.wcsaxes.CoordinateHelper.set_format_unit` * ``major_formatter``: `~astropy.visualization.wcsaxes.CoordinateHelper.set_major_formatter` * ``axislabel``: `~astropy.visualization.wcsaxes.CoordinateHelper.set_axislabel` * ``grid``: `~astropy.visualization.wcsaxes.CoordinateHelper.grid` (The value should be a dict of keyword arguments to ``grid()`` or `True`). * ``ticks``: `dict` or `bool` the keyword arguments to the `~astropy.visualization.wcsaxes.CoordinateHelper.set_ticks` method, or `False` to display no ticks for this coord. ylim: `tuple` or `str`, optional The yaxis limits to use when drawing a line plot, if 'fixed' then use the global data limits, if 'dynamic' then set the y limit for each frame individually (meaning the y limits change as you animate). ylabel: `string`, optional The yaxis label to use when drawing a line plot. Setting the label on the y-axis on an image plot should be done via ``coord_params``. clip_interval : two-element `~astropy.units.Quantity`, optional If provided, the data for each step will be clipped to the percentile interval bounded by the two numbers. """ def __init__(self, data, wcs, slices, coord_params=None, ylim='dynamic', ylabel=None, clip_interval: u.percent = None, **kwargs): if not isinstance(wcs, BaseLowLevelWCS): raise ValueError("A WCS object should be provided that implements the astropy WCS API.") if wcs.pixel_n_dim != data.ndim: raise ValueError("Dimensionality of the data and WCS object do not match.") if len(slices) != wcs.pixel_n_dim: raise ValueError("slices should be the same length as the number of pixel dimensions.") if "x" not in slices: raise ValueError( "slices should contain at least 'x' to indicate the axis to plot on the x axis.") self.plot_dimensionality = 1 image_axes = [slices[::-1].index("x")] if "y" in slices: image_axes.append(slices[::-1].index("y")) self.plot_dimensionality = 2 self.naxis = data.ndim self.num_sliders = self.naxis - self.plot_dimensionality self.slices_wcsaxes = list(slices) self.wcs = wcs self.coord_params = coord_params self.ylim = ylim self.ylabel = ylabel if clip_interval is not None and len(clip_interval) != 2: raise ValueError('A range of 2 values must be specified for clip_interval.') self.clip_interval = clip_interval extra_slider_labels = [] if "slider_functions" in kwargs and "slider_labels" not in kwargs: extra_slider_labels = [a.__name__ for a in kwargs['slider_functions']] slider_labels = self._compute_slider_labels_from_wcs(slices) + extra_slider_labels super().__init__(data, image_axes=image_axes, axis_ranges=None, slider_labels=slider_labels, **kwargs) def _get_wcs_labels(self): """ Read first the axes names property of the wcs and fall back to physical types. """ # Return the name if it is set, or the physical type if it is not. return [l or t for l, t in zip(self.wcs.world_axis_names, self.wcs.world_axis_physical_types)] def _compute_slider_labels_from_wcs(self, slices): """ For each pixel dimension, not used in the plot, calculate the world names which are correlated with that pixel dimension. This can return more than one world name per pixel dimension (i.e. lat & lon) so join them if there are. """ labels = [] wal = np.array(self._get_wcs_labels()) pixel_indicies = np.array([a not in ['x', 'y'] for a in slices]) for sliced_axis in self.wcs.axis_correlation_matrix[:, pixel_indicies].T: labels.append(" / ".join(list(map(str, wal[sliced_axis])))) return labels[::-1] def _partial_pixel_to_world(self, pixel_dimension, pixel_coord): """ Return the world coordinate along one axis, if it is only correlated to that axis. """ wcs_dimension = self.wcs.pixel_n_dim - pixel_dimension - 1 corr = self.wcs.axis_correlation_matrix[:, wcs_dimension] # If more than one world axis is linked to this dimension we can't # display the world coordinate because we have no way of picking, # so we just display pixel index. if len(np.nonzero(corr)[0]) != 1: return pixel_coord * u.pix # We know that the coordinate we care about is independent of the # other axes, so we can set the pixel coordinates to 0. coords = [0] * self.wcs.pixel_n_dim coords[wcs_dimension] = pixel_coord wc = self.wcs.pixel_to_world_values(*coords)[wcs_dimension] return u.Quantity(wc, unit=self.wcs.world_axis_units[wcs_dimension]) def _sanitize_axis_ranges(self, *args): """ This overrides the behaviour of ArrayAnimator to generate axis_ranges based on the WCS. """ axis_ranges = [None] * self.wcs.pixel_n_dim for i in self.slider_axes: axis_ranges[i] = partial(self._partial_pixel_to_world, i) return axis_ranges, None def _apply_coord_params(self, axes): if self.coord_params is None: return for coord_name in self.coord_params: coord = axes.coords[coord_name] params = self.coord_params[coord_name] format_unit = params.get("format_unit", None) if format_unit: coord.set_format_unit(format_unit) major_formatter = params.get("major_formatter", None) if major_formatter: coord.set_major_formatter(major_formatter) axislabel = params.get("axislabel", None) if axislabel: coord.set_axislabel(axislabel) grid = params.get("grid", None) if grid is not None: if not isinstance(grid, dict): grid = {} coord.grid(**grid) ticks = params.get("ticks", None) if ticks is not None: if isinstance(ticks, bool): coord.set_ticks_visible(ticks) coord.set_ticklabel_visible(ticks) elif isinstance(ticks, dict): coord.set_ticks(**ticks) else: raise TypeError( "The 'ticks' value in the coord_params dictionary must be a dict or a boolean." ) def _setup_main_axes(self): self.axes = self.fig.add_axes([0.1, 0.1, 0.8, 0.8], projection=self.wcs, slices=self.slices_wcsaxes) self._apply_coord_params(self.axes) def plot_start_image(self, ax): if self.plot_dimensionality == 1: artist = self.plot_start_image_1d(ax) elif self.plot_dimensionality == 2: artist = self.plot_start_image_2d(ax) return artist def update_plot(self, val, artist, slider): """ Update the plot when a slider changes. This method both updates the state of the Animator and also re-draws the matplotlib artist. """ ind = int(val) if ind == int(slider.cval): return ax_ind = self.slider_axes[slider.slider_ind] self.frame_slice[ax_ind] = ind self.slices_wcsaxes[self.wcs.pixel_n_dim - ax_ind - 1] = ind if self.plot_dimensionality == 1: self.update_plot_1d(val, artist, slider) elif self.plot_dimensionality == 2: self.update_plot_2d(val, artist, slider) self._apply_coord_params(self.axes) return super().update_plot(val, artist, slider) def plot_start_image_1d(self, ax): """ Set up a line plot. When plotting with WCSAxes, we always plot against pixel coordinate. """ if self.ylim != 'dynamic': ylim = self.ylim if ylim == 'fixed': ylim = (self.data.min(), self.data.max()) ax.set_ylim(ylim) if self.ylabel: ax.set_ylabel(self.ylabel) ydata = self.data[self.frame_index] line, = ax.plot(ydata, **self.imshow_kwargs) if isinstance(self.data, np.ma.MaskedArray): ax.set_xlim((0, ydata.shape[0])) return line @property def data_transposed(self): """ Return data for 2D plotting, transposed if needed. """ if self.slices_wcsaxes.index('y') < self.slices_wcsaxes.index("x"): return self.data[self.frame_index].transpose() else: return self.data[self.frame_index] def update_plot_1d(self, val, line, slider): """ Update the line plot. """ self.axes.reset_wcs(wcs=self.wcs, slices=self.slices_wcsaxes) line.set_ydata(self.data[self.frame_index]) # If we are not setting ylim globally then we set it per frame. if self.ylim == 'dynamic': self.axes.set_ylim(self.data[self.frame_index].min(), self.data[self.frame_index].max()) slider.cval = val def plot_start_image_2d(self, ax): """ Setup an image plot. """ imshow_args = {'interpolation': 'nearest', 'origin': 'lower'} imshow_args.update(self.imshow_kwargs) if self.clip_interval is not None: imshow_args['vmin'], imshow_args['vmax'] = self._get_2d_plot_limits() im = modest_image.imshow(ax, self.data_transposed, **imshow_args) if 'extent' in imshow_args: ax.set_xlim(imshow_args['extent'][:2]) ax.set_ylim(imshow_args['extent'][2:]) else: ny, nx = self.data_transposed.shape ax.set_xlim(-0.5, nx - 0.5) ax.set_ylim(-0.5, ny - 0.5) ax.dataLim.intervalx = ax.get_xlim() ax.dataLim.intervaly = ax.get_ylim() if self.if_colorbar: self._add_colorbar(im) return im def _get_2d_plot_limits(self): """ Get vmin, vmax of a data slice when clip_interval is specified. """ percent_limits = self.clip_interval.to('%').value vmin, vmax = AsymmetricPercentileInterval(*percent_limits).get_limits(self.data_transposed) return vmin, vmax def update_plot_2d(self, val, im, slider): """ Update the image plot. """ self.axes.reset_wcs(wcs=self.wcs, slices=self.slices_wcsaxes) im.set_array(self.data_transposed) if self.clip_interval is not None: vmin, vmax = self._get_2d_plot_limits() im.set_clim(vmin, vmax) slider.cval = val ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643822949.0458584 mpl_animators-1.0.1/mpl_animators.egg-info/0000755000175100001710000000000000000000000017743 5ustar00vstsdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822949.0 mpl_animators-1.0.1/mpl_animators.egg-info/PKG-INFO0000644000175100001710000000501500000000000021041 0ustar00vstsdockerMetadata-Version: 2.1 Name: mpl-animators Version: 1.0.1 Summary: An interative animation framework for matplotlib Home-page: https://sunpy.org Author: The SunPy Developers Author-email: sunpy@googlegroups.com License: BSD 3-Clause Platform: UNKNOWN Requires-Python: >=3.7 Provides-Extra: all Provides-Extra: wcs Provides-Extra: test Provides-Extra: docs License-File: LICENSE.rst An interative animation framework for matplotlib ------------------------------------------------ This package has been spun out of ``sunpy`` to be more generally useful. License ------- This project is Copyright (c) The SunPy Developers and licensed under the terms of the BSD 3-Clause license. This package is based upon the `Openastronomy packaging guide `_ which is licensed under the BSD 3-clause licence. See the licenses folder for more information. Contributing ------------ We love contributions! mpl-animators is open source, built on open source, and we'd love to have you hang out in our community. **Imposter syndrome disclaimer**: We want your help. No, really. There may be a little voice inside your head that is telling you that you're not ready to be an open source contributor; that your skills aren't nearly good enough to contribute. What could you possibly offer a project like this one? We assure you - the little voice in your head is wrong. If you can write code at all, you can contribute code to open source. Contributing to open source projects is a fantastic way to advance one's coding skills. Writing perfect code isn't the measure of a good developer (that would disqualify all of us!); it's trying to create something, making mistakes, and learning from those mistakes. That's how we all improve, and we are happy to help others learn. Being an open source contributor doesn't just mean writing code, either. You can help out by writing documentation, tests, or even giving feedback about the project (and yes - that includes giving feedback about the contribution process). Some of these contributions may be the most valuable to the project as a whole, because you're coming to the project with fresh eyes, so you can see the errors and assumptions that seasoned contributors have glossed over. Note: This disclaimer was originally written by `Adrienne Lowe `_ for a `PyCon talk `_, and was adapted by mpl-animators based on its use in the README file for the `MetPy project `_. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822949.0 mpl_animators-1.0.1/mpl_animators.egg-info/SOURCES.txt0000644000175100001710000000150100000000000021624 0ustar00vstsdockerLICENSE.rst MANIFEST.in README.rst pyproject.toml setup.cfg setup.py tox.ini docs/Makefile docs/conf.py docs/index.rst docs/make.bat mpl_animators/__init__.py mpl_animators/base.py mpl_animators/image.py mpl_animators/line.py mpl_animators/version.py mpl_animators/wcs.py mpl_animators.egg-info/PKG-INFO mpl_animators.egg-info/SOURCES.txt mpl_animators.egg-info/dependency_links.txt mpl_animators.egg-info/not-zip-safe mpl_animators.egg-info/requires.txt mpl_animators.egg-info/top_level.txt mpl_animators/extern/__init__.py mpl_animators/extern/modest_image.py mpl_animators/tests/__init__.py mpl_animators/tests/figure_hashes_mpl_343_ft_261_astropy_431.json mpl_animators/tests/figure_hashes_mpl_dev_ft_261_astropy_dev.json mpl_animators/tests/helpers.py mpl_animators/tests/test_basefuncanimator.py mpl_animators/tests/test_wcs.py././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822949.0 mpl_animators-1.0.1/mpl_animators.egg-info/dependency_links.txt0000644000175100001710000000000100000000000024011 0ustar00vstsdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822949.0 mpl_animators-1.0.1/mpl_animators.egg-info/not-zip-safe0000644000175100001710000000000100000000000022171 0ustar00vstsdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822949.0 mpl_animators-1.0.1/mpl_animators.egg-info/requires.txt0000644000175100001710000000024500000000000022344 0ustar00vstsdockermatplotlib>=3.2.0 numpy>=1.17.0 [all] astropy>=4.2.0 [docs] sphinx sphinx-automodapi sunpy-sphinx-theme [test] pytest pytest-cov pytest-mpl [wcs] astropy>=4.2.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822949.0 mpl_animators-1.0.1/mpl_animators.egg-info/top_level.txt0000644000175100001710000000001600000000000022472 0ustar00vstsdockermpl_animators ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/pyproject.toml0000644000175100001710000000020600000000000016316 0ustar00vstsdocker[build-system] requires = ["setuptools", "setuptools_scm", "wheel"] build-backend = 'setuptools.build_meta' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643822949.0458584 mpl_animators-1.0.1/setup.cfg0000644000175100001710000000254200000000000015230 0ustar00vstsdocker[metadata] name = mpl_animators author = The SunPy Developers author_email = sunpy@googlegroups.com license = BSD 3-Clause license_file = LICENSE.rst url = https://sunpy.org description = An interative animation framework for matplotlib long_description = file: README.rst [options] zip_safe = False packages = find: include_package_data = True python_requires = >=3.7 setup_requires = setuptools_scm install_requires = matplotlib>=3.2.0 numpy>=1.17.0 [options.extras_require] all = astropy>=4.2.0 wcs = astropy>=4.2.0 test = pytest pytest-cov pytest-mpl docs = sphinx sphinx-automodapi sunpy-sphinx-theme [tool:pytest] testpaths = "mpl_animators" "docs" mpl-results-path = figure_test_images mpl-use-full-test-name = True [coverage:run] omit = mpl_animators/__init* mpl_animators/conftest.py mpl_animators/*setup_package* mpl_animators/tests/* mpl_animators/*/tests/* mpl_animators/extern/* mpl_animators/version* */mpl_animators/__init* */mpl_animators/conftest.py */mpl_animators/*setup_package* */mpl_animators/tests/* */mpl_animators/*/tests/* */mpl_animators/extern/* */mpl_animators/version* [coverage:report] exclude_lines = pragma: no cover except ImportError raise AssertionError raise NotImplementedError def main\(.*\): pragma: py{ignore_python_version} def _ipython_key_completions_ [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/setup.py0000755000175100001710000000122700000000000015123 0ustar00vstsdocker#!/usr/bin/env python # Licensed under a 3-clause BSD style license - see LICENSE.rst import os from setuptools import setup VERSION_TEMPLATE = """ # Note that we need to fall back to the hard-coded version if either # setuptools_scm can't be imported or setuptools_scm can't determine the # version, so we catch the generic 'Exception'. try: from setuptools_scm import get_version __version__ = get_version(root='..', relative_to=__file__) except Exception: __version__ = '{version}' """.lstrip() setup( use_scm_version={'write_to': os.path.join('mpl_animators', 'version.py'), 'write_to_template': VERSION_TEMPLATE}, ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643822925.0 mpl_animators-1.0.1/tox.ini0000644000175100001710000000507600000000000014727 0ustar00vstsdocker[tox] envlist = py{37,38,39,310}{,-devdeps,-figure} build_docs codestyle isolated_build = true [testenv] # Pass through the following environemnt variables which may be needed for the CI passenv = HOME WINDIR LC_ALL LC_CTYPE CC CI TRAVIS # Run the tests in a temporary directory to make sure that we don't import # the package from the source tree changedir = .tmp/{envname} # tox environments are constructued with so-called 'factors' (or terms) # separated by hyphens, e.g. test-devdeps-cov. Lines below starting with factor: # will only take effect if that factor is included in the environment name. To # see a list of example environments that can be run, along with a description, # run: # # tox -l -v # description = run tests deps = # The devdeps factor is intended to be used to install the latest developer version. # of key dependencies. devdeps: git+https://github.com/astropy/astropy devdeps: git+https://github.com/matplotlib/matplotlib # Figure tests need a tightly controlled environment figure-!devdeps: astropy==4.3.1 figure-!devdeps: matplotlib==3.4.3 # The following indicates which extras_require from setup.cfg will be installed extras = test all setenv = PYTEST_COMMAND = pytest -vvv -s -raR --pyargs mpl_animators --cov-report=xml --cov=mpl_animators --cov-config={toxinidir}/setup.cfg {toxinidir}/docs commands = pip freeze !figure: {env:PYTEST_COMMAND} {posargs} figure: /bin/bash -c "mkdir -p ./figure_test_images; python -c 'import matplotlib as mpl; print(mpl.ft2font.__file__, mpl.ft2font.__freetype_version__, mpl.ft2font.__freetype_build_type__)' > ./figure_test_images/figure_version_info.txt" figure: /bin/bash -c "pip freeze >> ./figure_test_images/figure_version_info.txt" figure: /bin/bash -c "cat ./figure_test_images/figure_version_info.txt" figure: python -c "import mpl_animators.tests.helpers as h; print(h.get_hash_library_name())" figure: {env:PYTEST_COMMAND} -m "mpl_image_compare" --mpl --mpl-generate-summary=html --mpl-baseline-path=https://raw.githubusercontent.com/sunpy/sunpy-figure-tests/mpl-animators-main/figures/{envname}/ {posargs} [testenv:codestyle] pypi_filter = skip_install = true description = Run all style and file checks with pre-commit deps = pre-commit commands = pre-commit install-hooks pre-commit run --color always --all-files --show-diff-on-failure [testenv:build_docs] changedir = docs description = invoke sphinx-build to build the HTML docs extras = docs all commands = pip freeze sphinx-build -W -b html . _build/html {posargs}