pep8speaks: Hello @untzag! Thanks for submitting the PR.



 - In the file [`WrightTools/artists/_base.py`](https://github.com/wright-group/WrightTools/blob/46fb51978262bd10092c6c65be63276bed5a8944/WrightTools/artists/_base.py), following are the PEP8 issues :

> [Line 69:27](https://github.com/wright-group/WrightTools/blob/46fb51978262bd10092c6c65be63276bed5a8944/WrightTools/artists/_base.py#L69): [E225](https://duckduckgo.com/?q=pep8%20E225) missing whitespace around operator
> [Line 560:1](https://github.com/wright-group/WrightTools/blob/46fb51978262bd10092c6c65be63276bed5a8944/WrightTools/artists/_base.py#L560): [E402](https://duckduckgo.com/?q=pep8%20E402) module level import not at top of file
















diff --git a/WrightTools/_dataset.py b/WrightTools/_dataset.py
index d0e0059..52efb9e 100644
--- a/WrightTools/_dataset.py
+++ b/WrightTools/_dataset.py
@@ -117,6 +117,14 @@ class Dataset(h5py.Dataset):
         out += ' {0}'.format(self.shape)
         return out
 
+    @property
+    def full(self):
+        arr = self[:]
+        for i in range(arr.ndim):
+            if arr.shape[i] == 1:
+                arr = np.repeat(arr, self.parent.shape[i], axis=i)
+        return arr
+
     @property
     def fullpath(self):
         """Full path: file and internal structure."""
@@ -142,6 +150,11 @@ class Dataset(h5py.Dataset):
         """Parent."""
         return self._parent
 
+    @property
+    def points(self):
+        """Squeezed array."""
+        return np.squeeze(self[:])
+
     @property
     def units(self):
         """Units."""
diff --git a/WrightTools/artists/_base.py b/WrightTools/artists/_base.py
index 75623b1..774c6ad 100644
--- a/WrightTools/artists/_base.py
+++ b/WrightTools/artists/_base.py
@@ -64,9 +64,9 @@ class Axes(matplotlib.axes.Axes):
         if autolabel in ['xy', 'both', 'x'] and not xlabel:
             xlabel = data.axes[0].label
         if autolabel in ['xy', 'both', 'y'] and not ylabel:
-            if data.dimensionality == 1:
+            if data.ndim == 1:
                 ylabel = data.channels[channel_index].label
-            elif data.dimensionality == 2:
+            elif data.ndim == 2:
                 ylabel = data.axes[1].label
         # apply
         if xlabel:
@@ -175,8 +175,8 @@ class Axes(matplotlib.axes.Axes):
         # unpack data object, if given
         if isinstance(args[0], Data):
             data = args.pop(0)
-            if not data.dimensionality == 2:
-                raise wt_exceptions.DimensionalityError(2, data.dimensionality)
+            if not data.ndim == 2:
+                raise wt_exceptions.DimensionalityError(2, data.ndim)
             # arrays
             channel_index = wt_kit.get_index(data.channel_names, channel)
             signed = data.channels[channel_index].signed
@@ -245,8 +245,8 @@ class Axes(matplotlib.axes.Axes):
         # unpack data object, if given
         if isinstance(args[0], Data):
             data = args.pop(0)
-            if not data.dimensionality == 2:
-                raise wt_exceptions.DimensionalityError(2, data.dimensionality)
+            if not data.ndim == 2:
+                raise wt_exceptions.DimensionalityError(2, data.ndim)
             # arrays
             channel_index = wt_kit.get_index(data.channel_names, channel)
             xi = data.axes[0].full
@@ -359,8 +359,8 @@ class Axes(matplotlib.axes.Axes):
         # unpack data object, if given
         if isinstance(args[0], Data):
             data = args.pop(0)
-            if not data.dimensionality == 2:
-                raise wt_exceptions.DimensionalityError(2, data.dimensionality)
+            if not data.ndim == 2:
+                raise wt_exceptions.DimensionalityError(2, data.ndim)
             # arrays
             channel_index = wt_kit.get_index(data.channel_names, channel)
             xi = data.axes[0].full
@@ -426,8 +426,8 @@ class Axes(matplotlib.axes.Axes):
         if hasattr(args[0], 'id'):  # TODO: replace once class comparison works...
             data = args.pop(0)
             channel = kwargs.pop('channel', 0)
-            if not data.dimensionality == 1:
-                raise wt_exceptions.DimensionalityError(1, data.dimensionality)
+            if not data.ndim == 1:
+                raise wt_exceptions.DimensionalityError(1, data.ndim)
             # arrays
             channel_index = wt_kit.get_index(data.channel_names, channel)
             xi = data.axes[0][:]
diff --git a/WrightTools/artists/_helpers.py b/WrightTools/artists/_helpers.py
index 92fcbba..190371f 100644
--- a/WrightTools/artists/_helpers.py
+++ b/WrightTools/artists/_helpers.py
@@ -464,7 +464,7 @@ def pcolor_helper(xi, yi, zi):
         xi.shape = (xi.size, 1)
     if yi.ndim == 1:
         yi.shape = (1, yi.size)
-    shape = wt_kit.joint_shape([xi, yi])
+    shape = wt_kit.joint_shape(xi, yi)
     # full
 
     def full(arr):
diff --git a/WrightTools/data/_axis.py b/WrightTools/data/_axis.py
index 3a6cc10..2553e96 100644
--- a/WrightTools/data/_axis.py
+++ b/WrightTools/data/_axis.py
@@ -126,7 +126,7 @@ class Axis(object):
     @property
     def shape(self):
         """Shape."""
-        return wt_kit.joint_shape(self.variables)
+        return wt_kit.joint_shape(*self.variables)
 
     @property
     def size(self):
@@ -146,8 +146,8 @@ class Axis(object):
         except (AssertionError, AttributeError):
             pattern = '|'.join(map(re.escape, operators))
             keys = re.split(pattern, self.expression)
-            self._variables = [self.parent.variables[self.parent.variable_names.index(key)]
-                               for key in keys]
+            indices = [self.parent.variable_names.index(key) for key in keys]
+            self._variables = [self.parent.variables[i] for i in indices]
         finally:
             return self._variables
 
diff --git a/WrightTools/data/_data.py b/WrightTools/data/_data.py
index 7ca0912..962afbf 100644
--- a/WrightTools/data/_data.py
+++ b/WrightTools/data/_data.py
@@ -57,8 +57,6 @@ class Data(Group):
         self._on_axes_updated()
         # the following are populated if not already recorded
         self.channel_names
-        self.constant_names
-        self.kind
         self.source
         self.variable_names
 
@@ -97,28 +95,11 @@ class Data(Group):
         """Channels."""
         return tuple(self[n] for n in self.channel_names)
 
-    @property
-    def constant_names(self):
-        """Constant names."""
-        if 'constant_names' not in self.attrs.keys():
-            self.attrs['constant_names'] = np.array([], dtype='S')
-        return tuple(s.decode() for s in self.attrs['constant_names'])
-
-    @property
-    def constants(self):
-        """Constants."""
-        return tuple(self[n] for n in self.constant_names)
-
     @property
     def datasets(self):
         """Datasets."""
         return tuple(v for _, v in self.items() if isinstance(v, h5py.Dataset))
 
-    @property
-    def dimensionality(self):
-        """Get dimensionality of Data object."""
-        return len(self._axes)
-
     @property
     def info(self):
         """Retrieve info dictionary about a Data object."""
@@ -156,7 +137,7 @@ class Data(Group):
         try:
             assert self._shape is not None
         except (AssertionError, AttributeError):
-            self._shape = wt_kit.joint_shape(self.variables)
+            self._shape = wt_kit.joint_shape(*self.variables)
         finally:
             return self._shape
 
@@ -334,7 +315,7 @@ class Data(Group):
         kept = args + list(at.keys())
         kept_axes = [self._axes[self.axis_expressions.index(a)] for a in kept]
         removed_axes = [a for a in self._axes if a not in kept_axes]
-        removed_shape = wt_kit.joint_shape(removed_axes)
+        removed_shape = wt_kit.joint_shape(*removed_axes)
         if removed_shape == ():
             removed_shape = (1,) * self.ndim
         # iterate
@@ -447,12 +428,12 @@ class Data(Group):
         --------
         Axis.convert
             Convert a single axis object to compatable units. Call on an
-            axis object in data.axes or data.constants.
+            axis object in data.axes.
         """
         # get kind of units
         units_kind = wt_units.kind(destination_units)
         # apply to all compatible axes
-        for axis in self.axes + self.constants:
+        for axis in self.axes:
             if axis.units_kind == units_kind:
                 axis.convert(destination_units, convert_variables=convert_variables)
                 if verbose:
@@ -700,82 +681,99 @@ class Data(Group):
         if verbose:
             print('channel {0} leveled along axis {1}'.format(channel.natural_name, axis))
 
-    def map_axis(self, axis, points, input_units='same', edge_tolerance=0., verbose=True):
+    def map_variable(self, variable, points, input_units='same', *, name=None, parent=None,
+                     verbose=True):
         """Map points of an axis to new points using linear interpolation.
 
         Out-of-bounds points are written nan.
 
         Parameters
         ----------
-        axis : int or str
-            The axis to map onto.
-        points : 1D array-like or int
+        variable : string
+            The variable to map onto.
+        points : array-like or int
             If array, the new points. If int, new points will have the same
             limits, with int defining the number of evenly spaced points
             between.
         input_units : str (optional)
             The units of the new points. Default is same, which assumes
             the new points have the same units as the axis.
-        edge_tolerance : float (optional)
-            Axis edge points that are within this amount of the new edge
-            points are coerced to the new edge points before interpolation.
-            Default is 0.
+        name : string (optional)
+            The name of the new data object. If None, generated from
+            natural_name. Default is None.
+        parent : WrightTools.Collection (optional)
+            Parent of new data object. If None, data is made at root of a
+            new temporary file.
         verbose : bool (optional)
             Toggle talkback. Default is True.
+
+        Returns
+        -------
+        WrightTools.Data
+            New data object.
         """
-        raise NotImplementedError
-        # get axis index --------------------------------------------------------------------------
-        if isinstance(axis, int):
-            axis_index = axis
-        elif isinstance(axis, str):
-            axis_index = self.axis_names.index(axis)
-        else:
-            raise TypeError("axis: expected {int, str}, got %s" % type(axis))
-        axis = self._axes[axis_index]
-        # get points ------------------------------------------------------------------------------
+        # get variable index
+        variable_index = wt_kit.get_index(self.variable_names, variable)
+        variable = self.variables[variable_index]
+        # get points
         if isinstance(points, int):
-            points = np.linspace(axis[0], axis[-1], points)
-            input_units = 'same'
-        else:
-            points = np.array(points)
-        # transform points to axis units ----------------------------------------------------------
+            points = np.linspace(variable.min(), variable.max(), points)
+        points = np.array(points)
+        # points dimensionality
+        if points.ndim < variable.ndim:
+            for i, d in enumerate(variable.shape):
+                if d == 1:
+                    points = np.expand_dims(points, axis=i)
+        # convert points
         if input_units == 'same':
             pass
         else:
-            points = wt_units.converter(points, input_units, axis.units)
-        # points must be ascending ----------------------------------------------------------------
-        flipped = np.zeros(len(self._axes), dtype=np.bool)
-        for i in range(len(self._axes)):
-            if self._axes[i][0] > self._axes[i][-1]:
-                self.flip(i)
-                flipped[i] = True
-        # handle edge tolerance -------------------------------------------------------------------
-        for index in [0, -1]:
-            old = axis[index]
-            new = points[index]
-            if new - edge_tolerance < old < new + edge_tolerance:
-                axis[index] = new
-        # interpn data ----------------------------------------------------------------------------
-        old_points = [a[:] for a in self._axes]
-        new_points = [a[:] if a is not axis else points for a in self._axes]
-        if len(self._axes) == 1:
-            for channel in self.channels:
-                function = scipy.interpolate.interp1d(self._axes[0][:], channel[:])
-                channel[:] = function(new_points[0])
+            points = wt_units.converter(points, input_units, variable.units)
+        # construct new data object
+        special = ['name', 'axes', 'channel_names', 'variable_names']
+        kwargs = {k: v for k, v in self.attrs.items() if k not in special}
+        if name is None:
+            name = '{0}_{1}_mapped'.format(self.natural_name, variable.natural_name)
+        kwargs['name'] = name
+        kwargs['parent'] = parent
+        out = Data(**kwargs)
+        # mapped variable
+        values = points
+        out.create_variable(values=values, **variable.attrs)
+        # orthogonal variables
+        for v in self.variables:
+            if wt_kit.orthogonal(v.shape, variable.shape):
+                out.create_variable(values=v[:], **v.attrs)
+        out.transform(*self.axis_expressions)
+        # interpolate
+        if self.ndim == 1:
+
+            def interpolate(dataset, points):
+                function = scipy.interpolate.interp1d(variable[:], dataset[:])
+                return function(points)
+
         else:
-            xi = tuple(np.meshgrid(*new_points, indexing='ij'))
-            for channel in self.channels:
-                values = channel[:]
-                channel[:] = scipy.interpolate.interpn(old_points, values, xi,
-                                                       method='linear',
-                                                       bounds_error=False,
-                                                       fill_value=np.nan)
-        # cleanup ---------------------------------------------------------------------------------
-        for i in range(len(self._axes)):
-            if not i == axis_index:
-                if flipped[i]:
-                    self.flip(i)
-        axis[:] = points
+            pts = np.array([a.full.flatten() for a in self.axes]).T
+            out_pts = np.array([a.full.flatten() for a in out.axes]).T
+
+            def interpolate(dataset, points):
+                values = dataset.full.flatten()
+                function = scipy.interpolate.LinearNDInterpolator(pts, values, rescale=True)
+                new = function(out_pts)
+                new.shape = out.shape
+                return new
+
+        for v in self.variables:
+            if v.natural_name not in out.variable_names:
+                out.create_variable(values=interpolate(v, points), **v.attrs)
+        out.variable_names = self.variable_names  # enforce old order
+        out._variables = None  # force regeneration of variables @property
+        for channel in self.channels:
+            out.create_channel(values=interpolate(channel, points), **channel.attrs)
+        # finish
+        if verbose:
+            print('data mapped from {0} to {1}'.format(self.shape, out.shape))
+        return out
 
     def offset(self, points, offsets, along, offset_axis,
                units='same', offset_units='same', mode='valid',
@@ -1078,7 +1076,7 @@ class Data(Group):
         Uses the share_nans method found in wt.kit.
         """
         def f(_, s, channels):
-            outs = wt_kit.share_nans([c[s] for c in channels])
+            outs = wt_kit.share_nans(*[c[s] for c in channels])
             for c, o in zip(channels, outs):
                 c[s] = o
 
diff --git a/WrightTools/kit/_array.py b/WrightTools/kit/_array.py
index 9a4cadb..66cfb11 100644
--- a/WrightTools/kit/_array.py
+++ b/WrightTools/kit/_array.py
@@ -12,8 +12,16 @@ from .. import exceptions as wt_exceptions
 # --- define --------------------------------------------------------------------------------------
 
 
-__all__ = ['closest_pair', 'diff', 'fft', 'joint_shape', 'remove_nans_1D', 'share_nans',
-           'smooth_1D', 'unique', 'valid_index']
+__all__ = ['closest_pair',
+           'diff',
+           'fft',
+           'joint_shape',
+           'orthogonal',
+           'remove_nans_1D',
+           'share_nans',
+           'smooth_1D',
+           'unique',
+           'valid_index']
 
 
 # --- functions -----------------------------------------------------------------------------------
@@ -144,59 +152,78 @@ def fft(xi, yi, axis=0):
     return xi, yi
 
 
-def joint_shape(arrs):
-    """Given a list of arrays, return the joint shape.
+def joint_shape(*args):
+    """Given a set of arrays, return the joint shape.
 
     Parameters
     ----------
-    arrs : list of array-like
-        Input arrays.
+    args : array-likes
 
     Returns
     -------
     tuple of int
         Joint shape.
     """
-    if len(arrs) == 0:
+    if len(args) == 0:
         return ()
     shape = []
-    shapes = [a.shape for a in arrs]
-    ndim = arrs[0].ndim
+    shapes = [a.shape for a in args]
+    ndim = args[0].ndim
     for i in range(ndim):
         shape.append(max([s[i] for s in shapes]))
     return tuple(shape)
 
 
-def remove_nans_1D(arrs):
-    """Remove nans in a list of 1D arrays.
+def orthogonal(*args):
+    """Determine if a set of arrays are orthogonal.
+
+    Parameters
+    ----------
+    args : array-likes or array shapes
+
+    Returns
+    -------
+    bool
+        Array orthogonality condition.
+    """
+    for i, arg in enumerate(args):
+        if hasattr(arg, 'shape'):
+            args[i] = arg.shape
+    for s in zip(*args):
+        if np.product(s) != max(s):
+            return False
+    return True
+
+
+def remove_nans_1D(*args):
+    """Remove nans in a set of 1D arrays.
 
     Removes indicies in all arrays if any array is nan at that index.
     All input arrays must have the same size.
 
     Parameters
     ----------
-    arrs : list of 1D arrays
-        The arrays to remove nans from
+    args : 1D arrays
 
     Returns
     -------
-    list
-        List of 1D arrays in same order as given, with nan indicies removed.
+    tuple
+        Tuple of 1D arrays in same order as given, with nan indicies removed.
     """
     # find all indicies to keep
     bads = np.array([])
-    for arr in arrs:
+    for arr in args:
         bad = np.array(np.where(np.isnan(arr))).flatten()
         bads = np.hstack((bad, bads))
-    if hasattr(arrs, 'shape') and len(arrs.shape) == 1:
-        goods = [i for i in np.arange(arrs.shape[0]) if i not in bads]
+    if hasattr(args, 'shape') and len(args.shape) == 1:
+        goods = [i for i in np.arange(args.shape[0]) if i not in bads]
     else:
-        goods = [i for i in np.arange(len(arrs[0])) if i not in bads]
+        goods = [i for i in np.arange(len(args[0])) if i not in bads]
     # apply
-    return [a[goods] for a in arrs]
+    return tuple(a[goods] for a in args)
 
 
-def share_nans(arrs):
+def share_nans(*arrs):
     """Take a list of nD arrays and return a new list of nD arrays.
 
     The new list is in the same order as the old list.
@@ -205,8 +232,7 @@ def share_nans(arrs):
 
     Parameters
     ----------
-    arrs : list of nD arrays
-        The arrays to syncronize nans from
+    *arrs : nD arrays.
 
     Returns
     -------
@@ -216,7 +242,7 @@ def share_nans(arrs):
     nans = np.zeros((arrs[0].shape))
     for arr in arrs:
         nans *= arr
-    return [a + nans for a in arrs]
+    return tuple([a + nans for a in arrs])
 
 
 def smooth_1D(arr, n=10):
diff --git a/docs/data.rst b/docs/data.rst
index 76455be..365e49a 100644
--- a/docs/data.rst
+++ b/docs/data.rst
@@ -40,50 +40,50 @@ It is possible to create data objects directly in special circumstances, as show
    def my_resonance(xi, yi, intensity=1, FWHM=500, x0=7000):
        def single(arr, intensity=intensity, FWHM=FWHM, x0=x0):
            return intensity*(0.5*FWHM)**2/((xi-x0)**2+(0.5*FWHM)**2)
-       return single(xi)[:, None] * single(yi)[None, :]
-   xi = np.linspace(6000, 8000, 75)
-   yi = np.linspace(6000, 8000, 75)
+       return single(xi) * single(yi)
+   xi = np.linspace(6000, 8000, 75)[:, None]
+   yi = np.linspace(6000, 8000, 75)[None, :]
    zi = my_resonance(xi, yi)
    # package into data object
    data = wt.Data(name='example')
-   xi.shape = (75, 1)
-   data.add_variable(name='w1', units='wn', values=xi)
-   yi.shape = (1, 75)
-   data.add_variable(name='w2', units='wn', values=yi)
-   data.add_channel(name='signal', values=zi)
-   data.transform(['w1', 'w2'])
+   data.create_variable(name='w1', units='wn', values=xi)
+   data.create_variable(name='w2', units='wn', values=yi)
+   data.create_channel(name='signal', values=zi)
+   data.transform('w1', 'w2')
 
 Structure & properties
 ----------------------
 
 So what is a data object anyway?
 To put it simply, ``Data`` is a collection of ``Axis`` and ``Channel`` objects.
+``Axis`` objects are composed of ``Variable`` objects.
 
 ===============  ============================
-attribute        contains
+attribute        tuple of...
 ---------------  ----------------------------
 data.axes        wt.data.Axis objects
 data.channels    wt.data.Channel objects
+data.variables   wt.data.Variable objects
 ===============  ============================
 
+See also `Data.axis_expressions`, `Data.channel_names` and `Data.variable_names`.
+
 Axis
 ````
 
 Axes are the coordinates of the dataset. They have the following key attributes:
 
-===============  ==========================================================
-attribute        description
----------------  ----------------------------------------------------------
-axis.label       LaTeX-formatted label, appropriate for plotting
-axis.min         coordinates minimum, in current units
-axis.max         coordinates maximum, in current units
-axis.name        axis name
-axis.points      coordinates array, in current units
-axis.units       current axis units (change with ``axis.convert``)
-===============  ==========================================================
-
-Axes can also be constants (data.constants), in which case they contain a single value in points.
-This is crucial for keeping track of low dimensional data within a high dimensional experimental space.
+=================  ==========================================================
+attribute          description
+-----------------  ----------------------------------------------------------
+axis.label         LaTeX-formatted label, appropriate for plotting
+axis.min()         coordinates minimum, in current units
+axis.max()         coordinates maximum, in current units
+axis.natural_name  axis name
+axis.units         current axis units (change with ``axis.convert``)
+axis.variables     component variables
+axis.expression    expression
+=================  ==========================================================
 
 Channel
 ```````
@@ -94,13 +94,12 @@ Channels contain the n-dimensional data itself. They have the following key attr
 attribute        description
 ---------------  ----------------------------------------------------------
 channel.label    LaTeX-formatted label, appropriate for plotting
-channel.mag      channel magnitude (furthest deviation from null)
-channel.max      channel maximum
-channel.min      channel minimum
+channel.mag()    channel magnitude (furthest deviation from null)
+channel.max()    channel maximum
+channel.min()    channel minimum
 channel.name     channel name
 channel.null     channel null (value of zero signal)
 channel.signed   flag to indicate if channel is signed
-channel.values   n-dimensional array
 ===============  ==========================================================
 
 Data
@@ -121,15 +120,10 @@ The natural syntax is recommended, as it tends to result in more readable code.
    >>> data.pyro2 == data.channels[2]
    True
 
-The order of the ``data.axes`` list is crucial, as the coordinate arrays must be kept aligned with the shape of the corresponding n-dimensional data arrays.
-
-In contrast, the order of ``data.channels`` is arbitrary.
+The order of axes and channels is arbitrary.
 However many methods within WrightTools operate on the zero-indexed channel by default.
 For this reason, you can bring your favorite channel to zero-index using :meth:`~WrightTools.data.Data.bring_to_front`.
 
-At many points throughout WrightTools you will need to refer to a particular axis or channel.
-In such a case, you can always refer by name (string) or index (integer).
-
 Units aware & interpolation ready
 ---------------------------------
 
@@ -144,12 +138,10 @@ Here we list some of the capabilities that are enabled by this behavior.
 ==================================================  ================================================================================
 method                                              description
 --------------------------------------------------  --------------------------------------------------------------------------------
-:meth:`~WrightTools.data.Data.divide`               divide one channel by another, interpolating the divisor
 :meth:`~WrightTools.data.Data.heal`                 use interpolation to guess the value of NaNs within a channel
 :meth:`~WrightTools.data.join`                      join together multiple data objects, accounting for dimensionality and overlap
-:meth:`~WrightTools.data.Data.map_axis`             re-map axis coordinates
+:meth:`~WrightTools.data.Data.map_variable`         re-map data coordinates
 :meth:`~WrightTools.data.Data.offset`               offset one axis based on another
-:meth:`~WrightTools.data.Data.subtract`             subtract one channel from another, interpolating the subtrahend
 ==================================================  ================================================================================
 
 Dimensionality without the cursing
@@ -169,7 +161,6 @@ method                                              description
 :meth:`~WrightTools.data.Data.chop`                 chop data into a list of lower dimensional data
 :meth:`~WrightTools.data.Data.collapse`             destroy one dimension of data using a mathematical strategy
 :meth:`~WrightTools.data.Data.split`                split data at a series of coordinates, without reducing dimensionality
-:meth:`~WrightTools.data.Data.transpose`            change the order of data axes
 ==================================================  ================================================================================
 
 WrightTools seamlessly handles dimensionality throughout.
@@ -186,9 +177,7 @@ A selection of important methods follows.
 method                                              description
 --------------------------------------------------  --------------------------------------------------------------------------------
 :meth:`~WrightTools.data.Data.clip`                 clip values outside of a given range
-:meth:`~WrightTools.data.Data.dOD`                  transform into dOD units
 :meth:`~WrightTools.data.Data.level`                level the edge of data along a certain axis
-:meth:`~WrightTools.data.Data.m`                    apply m-factor corrections [#carlson1989]_
 :meth:`~WrightTools.data.Data.normalize`            normalize a channel such that mag --> 1 and null --> 0
 :meth:`~WrightTools.data.Data.revert`               revert the data object to an earlier state
 :meth:`~WrightTools.data.Data.scale`                apply a scaling to a channel, such as square root or log
@@ -198,14 +187,6 @@ method                                              description
 ==================================================  ================================================================================
 
 .. _JASCO: https://jascoinc.com/products/spectroscopy/
-
 .. _NISE: https://github.com/wright-group/NISE
-
 .. _PyCMDS: https://github.com/wright-group/PyCMDS
-
 .. _Shimadzu: http://www.ssi.shimadzu.com/products/productgroup.cfm?subcatlink=uvvisspectro
-
-.. [#carlson1989] **Absorption and Coherent Interference Effects in Multiply Resonant Four-Wave Mixing Spectroscopy**
-                  Roger J. Carlson, and John C. Wright
-                  *Applied Spectroscopy* **1989** 43, 1195--1208
-                  `doi:10.1366/0003702894203408 <http://dx.doi.org/10.1366/0003702894203408>`_
diff --git a/docs/index.rst b/docs/index.rst
index 2f3561d..b9334ef 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -38,10 +38,10 @@ Contents
    wt5
    data
    artists
-   fit
+   .. fit
    kit
    units
-   diagrams
+   .. diagrams
    datasets
    contributing 
    api/modules
diff --git a/docs/install.rst b/docs/install.rst
index 9d39933..9edd8d3 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -9,7 +9,7 @@ conda-forge
 Conda_ is a multilingual package/environment manager.
 It seamlessly handles non-Python library dependencies which many scientific Python tools rely upon.
 Conda is reccomended, especially for Windows users.
-If you don't have Python yet, start by `installing Anaconda`_
+If you don't have Python yet, start by `installing Anaconda`_ or `miniconda`_.
 
 `conda-forge`_ is a community-driven conda channel. `conda-forge contains a WrightTools feedstock`_.
 
@@ -44,5 +44,6 @@ To upgrade:
 .. _installing Anaconda: https://www.continuum.io/downloads
 .. _conda-forge: https://conda-forge.org/
 .. _conda-forge contains a WrightTools feedstock: https://github.com/conda-forge/wrighttools-feedstock
+.. _miniconda: https://conda.io/miniconda.html
 .. _pip: https://pypi.python.org/pypi/pip
 .. _WrightTools is hosted on PyPI: https://pypi.org/project/WrightTools/
diff --git a/docs/units.rst b/docs/units.rst
index e906dbb..7e01952 100644
--- a/docs/units.rst
+++ b/docs/units.rst
@@ -12,7 +12,7 @@ You can use it directly, if you wish.
    >>> wt.units.converter(2., 'eV', 'nm')
    620.0
 
-This same units system enables the units-aware properties throughout WrightTools, as in ``Axis`` and ``Curve``.
+This same units system enables the units-aware properties throughout WrightTools.
 
 In WrightTools, units are organized into kinds.
 It is always possible to convert between units of the same kind, and never possible to convert between kinds.

recover map_axis
diff --git a/tests/data/map_axis.py b/tests/data/map_variable.py
similarity index 53%
rename from tests/data/map_axis.py
rename to tests/data/map_variable.py
index a5ff8a6..c9d1119 100644
--- a/tests/data/map_axis.py
+++ b/tests/data/map_variable.py
@@ -1,11 +1,9 @@
-"""test map_axis"""
+"""Test map_variable."""
 
 
 # --- import --------------------------------------------------------------------------------------
 
 
-import pytest
-
 import numpy as np
 
 import WrightTools as wt
@@ -15,35 +13,30 @@ from WrightTools import datasets
 # --- test ----------------------------------------------------------------------------------------
 
 
-@pytest.mark.skip()
 def test_array():
     p = datasets.JASCO.PbSe_batch_1
     data = wt.data.from_JASCO(p)
     assert data.shape == (1801,)
     new = np.linspace(6000, 8000, 55)
-    data.map_axis(0, new, 'wn')
+    mapped = data.map_variable('energy', new, 'wn')
     assert data.axes[0][:].all() == new.all()
     data.close()
 
 
-@pytest.mark.skip()
-def test_edge_tolerance():
-    ps = datasets.KENT.LDS821_TRSF
-    data = wt.data.from_KENT(ps, ignore=['wm', 'd1', 'd2'])
-    new = np.linspace(1250, 1600, 101)
-    data.map_axis('w2', new, edge_tolerance=1)
-    assert data.w2[:].all() == new.all()
-    assert not np.isnan(data.channels[0][:]).any()
-    data.close()
-
-
-@pytest.mark.skip()
 def test_int():
     p = datasets.PyCMDS.wm_w2_w1_000
     data = wt.data.from_PyCMDS(p)
     assert data.shape == (35, 11, 11)
-    data.map_axis(1, 5)
-    assert data.shape == (35, 5, 11)
-    data.map_axis(2, 25)
-    assert data.shape == (35, 5, 25)
+    mapped = data.map_variable('w2', 5)
+    assert mapped.shape == (35, 5, 11)
+    mapped = data.map_variable('w1', 25)
+    assert mapped.shape == (35, 11, 25)
     data.close()
+
+
+# --- run -----------------------------------------------------------------------------------------
+
+
+if __name__ == '__main__':
+    test_array()
+    test_int()
diff --git a/tests/data/split.py b/tests/data/split.py
index c6bea9a..6c280d9 100644
--- a/tests/data/split.py
+++ b/tests/data/split.py
@@ -118,7 +118,7 @@ def test_split_axis_name():
     a.close()
 
 
-@pytest.mark.skip()  # constants required
+@pytest.mark.skip()
 def test_split_constant():
     p = datasets.PyCMDS.wm_w2_w1_000
     a = wt.data.from_PyCMDS(p)
diff --git a/tests/kit/joint_shape.py b/tests/kit/joint_shape.py
index a9afe81..60b85f5 100644
--- a/tests/kit/joint_shape.py
+++ b/tests/kit/joint_shape.py
@@ -15,11 +15,11 @@ import WrightTools as wt
 def test_5x7():
     arr1 = np.empty((5, 1))
     arr2 = np.empty((1, 7))
-    assert wt.kit.joint_shape([arr1, arr2]) == (5, 7)
+    assert wt.kit.joint_shape(arr1, arr2) == (5, 7)
 
 
 def test_3x4x5():
     arr1 = np.empty((1, 4, 1))
     arr2 = np.empty((3, 1, 5))
     arr3 = np.empty((1, 1, 1))
-    assert wt.kit.joint_shape([arr1, arr2, arr3]) == (3, 4, 5)
+    assert wt.kit.joint_shape(arr1, arr2, arr3) == (3, 4, 5)
diff --git a/tests/kit/remove_nans_1D.py b/tests/kit/remove_nans_1D.py
index 29b90f8..31d15ab 100644
--- a/tests/kit/remove_nans_1D.py
+++ b/tests/kit/remove_nans_1D.py
@@ -15,13 +15,13 @@ import WrightTools as wt
 def test_simple():
     arr = np.arange(-4, 6, dtype=float)
     arr[arr < 0] = np.nan
-    assert wt.kit.remove_nans_1D([arr])[0].all() == np.arange(0, 6, dtype=float).all()
+    assert wt.kit.remove_nans_1D(arr)[0].all() == np.arange(0, 6, dtype=float).all()
 
 
 def test_list():
     arrs = [np.random.random(21) for _ in range(5)]
     arrs[0][0] = np.nan
     arrs[1][-1] = np.nan
-    arrs = wt.kit.remove_nans_1D(arrs)
+    arrs = wt.kit.remove_nans_1D(*arrs)
     for arr in arrs:
         assert arr.size == 19
diff --git a/tests/kit/share_nans.py b/tests/kit/share_nans.py
index 730a298..40b78d3 100644
--- a/tests/kit/share_nans.py
+++ b/tests/kit/share_nans.py
@@ -15,6 +15,13 @@ import WrightTools as wt
 def test_5():
     arrs = [np.random.random(5) for _ in range(12)]
     arrs[2][2] = np.nan
-    arrs = wt.kit.share_nans(arrs)
+    arrs = wt.kit.share_nans(*arrs)
     for arr in arrs:
         assert np.isnan(arr[2])
+
+
+# --- run -----------------------------------------------------------------------------------------
+
+
+if __name__ == '__main__':
+    test_5()

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest pytest-cov sphinx sphinx-gallery sphinx-rtd-theme
apt-get update apt-get install -y libfreetype6-dev hdf5-tools libhdf5-dev libopenblas-dev
3.5
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
appdirs==1.4.4
attrs==22.2.0
Babel==2.11.0
cached-property==1.5.2
certifi==2021.5.30
charset-normalizer==2.0.12
coverage==6.2
cycler==0.11.0
docutils==0.18.1
h5py==3.1.0
idna==3.10
imageio==2.15.0
imagesize==1.4.1
importlib-metadata==4.8.3
iniconfig==1.1.1
Jinja2==3.0.3
kiwisolver==1.3.1
MarkupSafe==2.0.1
matplotlib==3.3.4
numexpr==2.8.1
numpy==1.19.5
packaging==21.3
Pillow==8.4.0
pluggy==1.0.0
py==1.11.0
Pygments==2.14.0
pyparsing==3.1.4
pytest==7.0.1
pytest-cov==4.0.0
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.27.1
scipy==1.5.4
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinx-gallery==0.10.0
sphinx-rtd-theme==2.0.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jquery==4.1
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tidy_headers==1.0.3
tomli==1.2.3
typing_extensions==4.1.1
urllib3==1.26.20
-e git+https://github.com/wright-group/WrightTools.git@592649ce55c9fa7847325c9e9b15b320a38f1389#egg=WrightTools
zipp==3.6.0

name: WrightTools
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - appdirs==1.4.4
      - attrs==22.2.0
      - babel==2.11.0
      - cached-property==1.5.2
      - charset-normalizer==2.0.12
      - coverage==6.2
      - cycler==0.11.0
      - docutils==0.18.1
      - h5py==3.1.0
      - idna==3.10
      - imageio==2.15.0
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - jinja2==3.0.3
      - kiwisolver==1.3.1
      - markupsafe==2.0.1
      - matplotlib==3.3.4
      - numexpr==2.8.1
      - numpy==1.19.5
      - packaging==21.3
      - pillow==8.4.0
      - pluggy==1.0.0
      - py==1.11.0
      - pygments==2.14.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - pytest-cov==4.0.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.27.1
      - scipy==1.5.4
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinx-gallery==0.10.0
      - sphinx-rtd-theme==2.0.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jquery==4.1
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tidy-headers==1.0.3
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - urllib3==1.26.20
      - zipp==3.6.0
prefix: /opt/conda/envs/WrightTools

tests/kit/joint_shape.py::test_5x7 tests/kit/joint_shape.py::test_3x4x5 tests/kit/remove_nans_1D.py::test_simple tests/kit/remove_nans_1D.py::test_list tests/kit/share_nans.py::test_5
tests/data/map_variable.py::test_array tests/data/map_variable.py::test_int
WrightTools/kit/_array.py WrightTools/artists/_base.py docs/units.rst WrightTools/_dataset.py WrightTools/artists/_helpers.py docs/data.rst docs/install.rst WrightTools/data/_axis.py docs/index.rst WrightTools/data/_data.py
WrightTools/kit/_array.py WrightTools/artists/_base.py docs/units.rst WrightTools/_dataset.py WrightTools/artists/_helpers.py docs/data.rst docs/install.rst WrightTools/data/_axis.py docs/index.rst WrightTools/data/_data.py