Skip to content

Commit

Permalink
support for json import/export (#43)
Browse files Browse the repository at this point in the history
* allow json export/import
* API docs updates for export/import
* recursively convert arrays to nested lists when exporting
* fix python3 urllib import error
* do not add errorbar entries to legend
* implement gridspec support in axpos (in addition to existing subplot fmt)
* fix subplot positioning when not providing axpos
* support for animate_callback passed to fig.animate
  • Loading branch information
kecnry authored Dec 11, 2019
1 parent d38c02b commit e3d2312
Show file tree
Hide file tree
Showing 108 changed files with 2,085 additions and 61 deletions.
15 changes: 9 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ env:
# Set defaults to avoid repeating in most cases
- CONDA_DEPENDENCIES='matplotlib'
- PIP_DEPENDENCIES='nose'
- PYTHON_VERSION=2.7
- NUMPY_VERSION=1.10
- ASTROPY_VERSION=1.0

matrix:

Expand All @@ -25,10 +28,10 @@ matrix:

# Python 2.7: lowest version, stable, dev
- os: linux
env: PYTHON_VERSION=2.7 MATPLOTLIB_VERSION=1.4.3 NUMPY_VERSION=1.10 ASTROPY_VERSION=1.0'
env: PYTHON_VERSION=2.7 MATPLOTLIB_VERSION=1.4.3 NUMPY_VERSION=1.10 ASTROPY_VERSION=1.0

- os: linux
env: PYTHON_VERSION=2.7 MATPLOTLIB_VERSION=2.2 NUMPY_VERSION=stable ASTROPY_VERSION=stable'
env: PYTHON_VERSION=2.7 MATPLOTLIB_VERSION=2.2 NUMPY_VERSION=stable ASTROPY_VERSION=stable

# astropy/numpy/matplotlib dev don't support python < 3.5
# - os: linux
Expand All @@ -37,16 +40,16 @@ matrix:
# Python 3.6: lowest version, stable, dev
# note: NUMPY 1.10 is not supported with PYTHON 3.6, so we'll use stable instead
- os: linux
env: PYTHON_VERSION=3.6 MATPLOTLIB_VERSION=1.4.3 NUMPY_VERSION=stable ASTROPY_VERSION=1.0'
env: PYTHON_VERSION=3.6 MATPLOTLIB_VERSION=1.4.3 NUMPY_VERSION=stable ASTROPY_VERSION=1.0

- os: linux
env: PYTHON_VERSION=3.6 MATPLOTLIB_VERSION=2.2 NUMPY_VERSION=stable ASTROPY_VERSION=stable'
env: PYTHON_VERSION=3.6 MATPLOTLIB_VERSION=2.2 NUMPY_VERSION=stable ASTROPY_VERSION=stable

- os: linux
env: PYTHON_VERSION=3.6 MATPLOTLIB_VERSION=dev NUMPY_VERSION=dev ASTROPY_VERSION=dev'
env: PYTHON_VERSION=3.6 MATPLOTLIB_VERSION=dev NUMPY_VERSION=dev ASTROPY_VERSION=dev

allow_failures:
- env: NUMPY_VERSION=dev
- env: PYTHON_VERSION=3.6 MATPLOTLIB_VERSION=dev NUMPY_VERSION=dev ASTROPY_VERSION=dev

before_install:
# Tricks to avoid matplotlib error about X11:
Expand Down
74 changes: 74 additions & 0 deletions autofig/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,80 @@ def animate(*args, **kwargs):
"""
return gcf().animate(*args, **kwargs)

def to_dict(renders=[]):
"""
Access the dictionary representation of the current <autofig.figure.Figure>.
See also:
* <autofig.gcf>
* <autofig.save>
* <autofig.figure.Figure.to_dict>
Returns
----------
* dict
"""
return gcf().to_dict(renders=renders)

def save(filename, renders=[]):
"""
Save the current <autofig.figure.Figure>. Note: this saves the autofig
figure object itself, not the image. To save the image, call <autofig.draw>
and pass `save`.
See also:
* <autofig.open>
* <autofig.to_dict>
* <autofig.gcf>
* <autofig.figure.Figure.save>
Arguments
-----------
* `filename` (string): path to save the figure instance.
* `renders` (list of dictionaries, default=[]): commands to execute
for rendering when opened by the command-line tool or by passing
`do_renders` to <autofig.open>. The format must
be a list of dictionaries, where each dictionary must at least have
'render': 'draw' or 'render': 'animate'. Any additional key-value
pairs will be passed as keyword arguments to the respective
rendering method.
Returns
-----------
* (str) the path of the saved figure instance.
"""
return gcf().save(filename, renders=renders)

def open(filename, do_renders=False, allow_renders_save=False):
"""
Open and replace the current <autofig.figure.Figure>.
See also:
* <autofig.save>
* <autofig.reset>
* <autofig.gcf>
Arguments
-----------
* `filename` (string): path to the saved figure instance
* `do_renders` (bool, default=False): whether to execute any render
(ie. draw/animate) statements included in the file.
* `allow_renders_save` (bool, default=False): whether to allow render
statements to save images/animations to disk. Be careful if setting
this to True from an untrusted source.
Returns
---------
* the loaded <autofig.figure.Figure> instance.
"""
reset()
global _figure
_figure = Figure.open(filename,
do_renders=do_renders,
allow_renders_save=allow_renders_save)
return gcf()


def inline(inline=True):
"""
Enable/disable inline mode.
Expand Down
99 changes: 84 additions & 15 deletions autofig/axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from mpl_toolkits.mplot3d import Axes3D
from matplotlib.collections import LineCollection
from matplotlib import colorbar as mplcolorbar
from matplotlib import gridspec as gridspec

from . import common
from . import callbacks
Expand All @@ -21,6 +22,9 @@ class AxesGroup(common.Group):
def __init__(self, items):
super(AxesGroup, self).__init__(Axes, [], items)

# from_dict defined in common.Group
# to_dict defined in common.Group

@property
def i(self):
"""
Expand Down Expand Up @@ -167,23 +171,23 @@ def __init__(self, *calls, **kwargs):
self.axorder = kwargs.pop('axorder', None)
self.axpos = kwargs.pop('axpos', None)

self.equal_aspect = kwargs.pop('equal_aspect', None)
self.pad_aspect = kwargs.pop('pad_aspect', None)

self._i = AxDimensionI(self, **kwargs)
self._x = AxDimensionX(self, **kwargs)
self._y = AxDimensionY(self, **kwargs)
self._z = AxDimensionZ(self, **kwargs)

self._elev = AxViewElev(self, **kwargs)
self._azim = AxViewAzim(self, **kwargs)
self._elev = AxViewElev(self, value=kwargs.get('elev', None))
self._azim = AxViewAzim(self, value=kwargs.get('azim', None))

# set default padding
self.xyz.pad = 0.1

self._ss = []
self._cs = []

self.equal_aspect = None
self.pad_aspect = None

self.add_call(*calls)

def __repr__(self):
Expand All @@ -198,6 +202,30 @@ def __repr__(self):
ncalls = len(self.calls)
return "<Axes | {} call(s) | dims: {}>".format(ncalls, ", ".join(dirs))

@classmethod
def from_dict(cls, dict):
return cls(**dict)

def to_dict(self):
return {'projection': self.projection,
'legend': self.legend,
'legend_kwargs': self.legend_kwargs,
'title': self.title,
'axorder': self.axorder,
'axpos': self.axpos,
'equal_aspect': self.equal_aspect,
'pad_aspect': self.pad_aspect,
'i': self.i.to_dict(),
'x': self.x.to_dict(),
'y': self.y.to_dict(),
'z': self.z.to_dict(),
'ss': [s.to_dict() for s in self.ss],
'cs': [c.to_dict() for c in self.cs],
'elev': self._elev.to_dict(), # NOTE: need underscore to avoid projection check error
'azim': self._azim.to_dict() # NOTE: need underscore to avoid projection check error
}


@property
def figure(self):
"""
Expand Down Expand Up @@ -393,14 +421,20 @@ def axpos(self, axpos):

return

if isinstance(axpos, tuple) and len(axpos) == 3 and np.all(isinstance(ap, int) for ap in axpos):
if isinstance(axpos, list) or isinstance(axpos, np.ndarray):
axpos = tuple(axpos)

if isinstance(axpos, tuple) and (len(axpos) == 3 or len(axpos) == 6) and np.all(isinstance(ap, int) for ap in axpos):
self._axpos = axpos

elif isinstance(axpos, int) and axpos >= 100 and axpos < 1000:
self._axpos = (int(axpos/100), int(axpos/10 % 10), int(axpos % 10))

elif isinstance(axpos, int) and axpos >= 110011 and axpos < 999999:
self._axpos = tuple([int(ap) for ap in str(axpos)])

else:
raise ValueError("axpos must be of type int or tuple between 100 and 999")
raise ValueError("axpos must be of type int or tuple between 100 and 999 (subplot syntax: ncols, nrows, ind) or 110011 and 999999 (gridspec syntax: ncols, nrows, indx, indy, widthx, widthy)")

@property
def title(self):
Expand Down Expand Up @@ -949,31 +983,43 @@ def determine_grid(N):
if not np.all([isinstance(s, int) for s in subplot_grid]):
raise ValueError("subplot_grid must be tuple of length 2 (nrows [int], ncols [int])")

# we'll reset the layout later anyways
ax_new = fig.add_subplot(1,N+1,N+1, projection=self._projection)

axes = fig.axes
N = len(axes)
N = len(axes) + 1

ind = None
if self.axpos is not None:
rows, cols, ind = self.axpos
# we'll deal with this situation in the else below
pass
elif subplot_grid is None:
rows, cols = determine_grid(N)
elif (isinstance(subplot_grid, list) or isinstance(subplot_grid, tuple)) and len(subplot_grid)==2:
rows, cols = subplot_grid
else:
raise TypeError("subplot_grid must be None or tuple/list of length 2 (rows/cols)")

if ind is None:
if self.axpos is None:
# we'll reset the layout later anyways
ax_new = fig.add_subplot(rows,cols,N, projection=self._projection)
axes = fig.axes

for i,ax in enumerate(axes):
try:
ax.change_geometry(rows, cols, i+1)
except AttributeError:
# colorbars and sizebars won't be able to change geometry
pass
else:
ax_new.change_geometry(rows, cols, ind)
if len(self.axpos) == 3:
# then axpos is nrows, ncolumn, index
ax_new = fig.add_subplot(*self.axpos, projection=self._projection)
elif len(self.axpos) == 6:
# then axpos is nrows, ncols, indx, indy, widthx, widthy
ax_new = plt.subplot2grid(self.axpos[0:2], self.axpos[2:4], colspan=self.axpos[4], rowspan=self.axpos[5])
fig.add_axes(ax_new)

else:
raise NotImplementedError


ax = self._get_backend_object(ax_new)
self._backend_artists = []
Expand Down Expand Up @@ -1398,11 +1444,22 @@ def __init__(self, direction, axes, unit=None, pad=None, lim=[None, None], label

def __repr__(self):

return "<{} | limits: {} | type: {} | label: {}>".format(self.direction,
return "<{} | lim: {} | type: {} | label: {}>".format(self.direction,
self.lim,
self.unit.physical_type,
self.label)

@classmethod
def from_dict(cls, dict):
return cls(**dict)

def to_dict(self):
return {'direction': self.direction,
'unit': self.unit.to_string(),
'pad': self._pad,
'lim': common.arraytolistrecursive(self._lim),
'label': self._label}

@property
def unit(self):
"""
Expand Down Expand Up @@ -2126,6 +2183,10 @@ def value(self, value):

class AxView(AxArray):
def __init__(self, direction, axes, value):
if isinstance(value, dict):
direction = value.get('direction')
value = value.get('value')

self._value = value

super(AxView, self).__init__(direction, axes)
Expand All @@ -2134,6 +2195,14 @@ def __repr__(self):

return "<{} | >".format(self.direction)

@classmethod
def from_dict(cls, dict):
return cls(**dict)

def to_dict(self):
return {'direction': self.direction,
'value': common.arraytolistrecursive(self._value)}

@property
def value(self):
"""
Expand Down
Loading

0 comments on commit e3d2312

Please sign in to comment.