codecov-io: # [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=h1) Report
> Merging [#274](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=desc) into [master](https://codecov.io/gh/pyvista/pyvista/commit/1b5aa3966673fb9362170a4eba0132b1966eb13d?src=pr&el=desc) will **decrease** coverage by `0.3%`.
> The diff coverage is `64.28%`.

[![Impacted file tree graph](https://codecov.io/gh/pyvista/pyvista/pull/274/graphs/tree.svg?width=650&token=KzeS9JHDaQ&height=150&src=pr)](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #274      +/-   ##
==========================================
- Coverage   83.44%   83.14%   -0.31%     
==========================================
  Files          23       31       +8     
  Lines        6489     6871     +382     
==========================================
+ Hits         5415     5713     +298     
- Misses       1074     1158      +84
```


| [Impacted Files](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pyvista/utilities/sphinx\_gallery.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS91dGlsaXRpZXMvc3BoaW54X2dhbGxlcnkucHk=) | `28.57% <ø> (ø)` | |
| [pyvista/plotting/export\_vtkjs.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS9wbG90dGluZy9leHBvcnRfdnRranMucHk=) | `87.66% <ø> (ø)` | |
| [pyvista/plotting/colors.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS9wbG90dGluZy9jb2xvcnMucHk=) | `89.18% <ø> (ø)` | |
| [pyvista/utilities/errors.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS91dGlsaXRpZXMvZXJyb3JzLnB5) | `60.65% <ø> (ø)` | |
| [pyvista/utilities/parametric\_objects.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS91dGlsaXRpZXMvcGFyYW1ldHJpY19vYmplY3RzLnB5) | `84.7% <100%> (ø)` | |
| [pyvista/plotting/ipy\_tools.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS9wbG90dGluZy9pcHlfdG9vbHMucHk=) | `76.47% <100%> (ø)` | |
| [pyvista/core/composite.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS9jb3JlL2NvbXBvc2l0ZS5weQ==) | `100% <100%> (ø)` | |
| [pyvista/utilities/features.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS91dGlsaXRpZXMvZmVhdHVyZXMucHk=) | `65.62% <100%> (ø)` | |
| [pyvista/examples/downloads.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS9leGFtcGxlcy9kb3dubG9hZHMucHk=) | `51.23% <100%> (+5.78%)` | :arrow_up: |
| [pyvista/utilities/geometric\_objects.py](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree#diff-cHl2aXN0YS91dGlsaXRpZXMvZ2VvbWV0cmljX29iamVjdHMucHk=) | `86.52% <100%> (ø)` | |
| ... and [34 more](https://codecov.io/gh/pyvista/pyvista/pull/274/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=footer). Last update [1b5aa39...37a97b2](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

codecov-io: # [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=h1) Report
> Merging [#274](https://codecov.io/gh/pyvista/pyvista/pull/274?src=pr&el=desc) into [master](https://codecov.io/gh/pyvista/pyvista/commit/d7e791f36572a06e3da403482b12461390b631e4?src=pr&el=desc) will **decrease** coverage by `1.42%`.
> The diff coverage is `41.55%`.

```diff
@@            Coverage Diff             @@
##           master     #274      +/-   ##
==========================================
- Coverage   84.55%   83.12%   -1.43%     
==========================================
  Files          30       31       +1     
  Lines        6714     7208     +494     
==========================================
+ Hits         5677     5992     +315     
- Misses       1037     1216     +179
```

diff --git a/.travis.yml b/.travis.yml
index 5964dc99..224b9cc1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -171,6 +171,7 @@ deploy:
     skip_cleanup: true
     on:
       condition: ($TRAVIS_PYTHON_VERSION == 3.7) && ($TRAVIS_REPO_SLUG == 'pyvista/pyvista') && ($TRAVIS_BRANCH == 'master') && ($DOCS_TRIGGER == 'BANE_RULES' && ($DEPLOY_DOCS == 'true'))
+      tags: true
   - provider: pypi
     user: akaszynski
     distributions: sdist
diff --git a/docs/utilities/utilities.rst b/docs/utilities/utilities.rst
index f09b85f5..3669c1fa 100644
--- a/docs/utilities/utilities.rst
+++ b/docs/utilities/utilities.rst
@@ -15,7 +15,7 @@ Object Conversions
 
 .. autofunction:: pyvista.wrap
 
-.. autofunction:: pyvista.is_pyvista_obj
+.. autofunction:: pyvista.is_pyvista_dataset
 
 .. autofunction:: pyvista.image_to_texture
 
@@ -49,7 +49,7 @@ Mesh Creation
 Array Access
 ~~~~~~~~~~~~
 
-.. autofunction:: pyvista.get_scalar
+.. autofunction:: pyvista.get_array
 
 .. autofunction:: pyvista.convert_array
 
diff --git a/examples/02-plot/opacity.py b/examples/02-plot/opacity.py
index 0d123987..78d50e56 100644
--- a/examples/02-plot/opacity.py
+++ b/examples/02-plot/opacity.py
@@ -127,7 +127,7 @@ p.show()
 
 model = examples.download_model_with_variance()
 contours = model.contour(10, scalars='Temperature')
-print(contours.scalar_names)
+print(contours.array_names)
 
 ###############################################################################
 # Make sure to flag ``use_transparency=True`` since we want areas of high
diff --git a/pyvista/core/__init__.py b/pyvista/core/__init__.py
index eee767c9..77bfb649 100644
--- a/pyvista/core/__init__.py
+++ b/pyvista/core/__init__.py
@@ -2,4 +2,5 @@ from .common import Common
 from .composite import MultiBlock
 from .filters import CompositeFilters, DataSetFilters, PolyDataFilters, UnstructuredGridFilters
 from .grid import Grid, RectilinearGrid, UniformGrid
+from .objects import Table
 from .pointset import PointGrid, PolyData, StructuredGrid, UnstructuredGrid
diff --git a/pyvista/core/common.py b/pyvista/core/common.py
index 434dab22..a659e06d 100644
--- a/pyvista/core/common.py
+++ b/pyvista/core/common.py
@@ -12,8 +12,8 @@ from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy
 
 import pyvista
 from pyvista.utilities import (CELL_DATA_FIELD, FIELD_DATA_FIELD,
-                               POINT_DATA_FIELD, convert_array, get_scalar,
-                               is_pyvista_obj, parse_field_choice,
+                               POINT_DATA_FIELD, convert_array, get_array,
+                               is_pyvista_dataset, parse_field_choice,
                                raise_not_matching, vtk_bit_array_to_char)
 
 from .filters import DataSetFilters
@@ -25,22 +25,274 @@ log.setLevel('CRITICAL')
 DEFAULT_VECTOR_KEY = '_vectors'
 
 
-class Common(DataSetFilters, object):
-    """ Methods in common to grid and surface objects"""
+
+class DataObject(object):
+    """ Methods common to all wrapped data objects """
+    def __init__(self, *args, **kwargs):
+        self._field_bool_array_names = []
+
+
+    def __new__(cls, *args, **kwargs):
+        if cls is DataObject:
+            raise TypeError("pyvista.DataObject is an abstract class and may not be instantiated.")
+        return object.__new__(cls, *args, **kwargs)
+
+
+    def save(self, filename, binary=True):
+         """
+         Writes this mesh to a file.
+
+         Parameters
+         ----------
+         filename : str
+             Filename of mesh to be written.  File type is inferred from
+             the extension of the filename unless overridden with
+             ftype.
+
+         binary : bool, optional
+             Writes the file as binary when True and ASCII when False.
+
+         Notes
+         -----
+         Binary files write much faster than ASCII and have a smaller
+         file size.
+         """
+         raise NotImplementedError('{} mesh type does not have a save method.'.format(type(self)))
+
+
+    def get_data_range(self, arr=None, preference='field'):
+        """Get the non-NaN min and max of a named scalar array
+
+        Parameters
+        ----------
+        arr : str, np.ndarray, optional
+            The name of the array to get the range. If None, the active scalar
+            is used
+
+        preference : str, optional
+            When scalars is specified, this is the perfered scalar type to
+            search for in the dataset.  Must be either ``'point'``, ``'cell'``,
+            or ``'field'``.
+
+        """
+        raise NotImplementedError('{} mesh type does not have a `get_data_range` method.'.format(type(self)))
+
+
+    def _get_attrs(self):
+        """An internal helper for the representation methods"""
+        raise NotImplementedError
+
+
+    def head(self, display=True, html=None):
+        """Return the header stats of this dataset. If in IPython, this will
+        be formatted to HTML. Otherwise returns a console friendly string"""
+        # Generate the output
+        if html:
+            fmt = ""
+            # HTML version
+            fmt += "\n"
+            fmt += "<table>\n"
+            fmt += "<tr><th>{}</th><th>Information</th></tr>\n".format(type(self).__name__)
+            row = "<tr><td>{}</td><td>{}</td></tr>\n"
+            # now make a call on the object to get its attributes as a list of len 2 tuples
+            for attr in self._get_attrs():
+                try:
+                    fmt += row.format(attr[0], attr[2].format(*attr[1]))
+                except:
+                    fmt += row.format(attr[0], attr[2].format(attr[1]))
+            if hasattr(self, 'n_arrays'):
+                fmt += row.format('N Arrays', self.n_arrays)
+            fmt += "</table>\n"
+            fmt += "\n"
+            if display:
+                from IPython.display import display, HTML
+                display(HTML(fmt))
+                return
+            return fmt
+        # Otherwise return a string that is Python console friendly
+        fmt = "{} ({})\n".format(type(self).__name__, hex(id(self)))
+        # now make a call on the object to get its attributes as a list of len 2 tuples
+        row = "  {}:\t{}\n"
+        for attr in self._get_attrs():
+            try:
+                fmt += row.format(attr[0], attr[2].format(*attr[1]))
+            except:
+                fmt += row.format(attr[0], attr[2].format(attr[1]))
+        if hasattr(self, 'n_arrays'):
+            fmt += row.format('N Arrays', self.n_arrays)
+        return fmt
+
+
+    def _repr_html_(self):
+        """A pretty representation for Jupyter notebooks that includes header
+        details and information about all scalar arrays"""
+        raise NotImplemented
+
+
+    def copy_meta_from(self, ido):
+        """Copies pyvista meta data onto this object from another object"""
+        pass
+
+
+    def copy(self, deep=True):
+        """
+        Returns a copy of the object
+
+        Parameters
+        ----------
+        deep : bool, optional
+            When True makes a full copy of the object.
+
+        Returns
+        -------
+        newobject : same as input
+           Deep or shallow copy of the input.
+        """
+        thistype = type(self)
+        newobject = thistype()
+        if deep:
+            newobject.DeepCopy(self)
+        else:
+            newobject.ShallowCopy(self)
+        newobject.copy_meta_from(self)
+        return newobject
+
+
+    def _field_array(self, name=None):
+        """
+        Returns field scalars of a vtk object
+
+        Parameters
+        ----------
+        name : str
+            Name of field scalars to retrive.
+
+        Returns
+        -------
+        scalars : np.ndarray
+            Numpy array of scalars
+
+        """
+        if name is None:
+            raise RuntimeError('Must specify an array to fetch.')
+        vtkarr = self.GetFieldData().GetAbstractArray(name)
+        if vtkarr is None:
+            raise AssertionError('({}) is not a valid field scalar array'.format(name))
+
+        # numpy does not support bit array data types
+        if isinstance(vtkarr, vtk.vtkBitArray):
+            vtkarr = vtk_bit_array_to_char(vtkarr)
+            if name not in self._point_bool_array_names:
+                self._field_bool_array_names.append(name)
+
+        array = convert_array(vtkarr)
+        if array.dtype == np.uint8 and name in self._field_bool_array_names:
+            array = array.view(np.bool)
+        return array
+
+
+    def _add_field_array(self, scalars, name, deep=True):
+        """
+        Adds field scalars to the mesh
+
+        Parameters
+        ----------
+        scalars : numpy.ndarray
+            Numpy array of scalars.  Does not have to match number of points or
+            numbers of cells.
+
+        name : str
+            Name of field scalars to add.
+
+        deep : bool, optional
+            Does not copy scalars when False.  A reference to the scalars
+            must be kept to avoid a segfault.
+
+        """
+        if scalars is None:
+            raise TypeError('Empty array unable to be added')
+
+        if not isinstance(scalars, np.ndarray):
+            scalars = np.array(scalars)
+
+        # need to track which arrays are boolean as all boolean arrays
+        # must be stored as uint8
+        if scalars.dtype == np.bool:
+            scalars = scalars.view(np.uint8)
+            if name not in self._field_bool_array_names:
+                self._field_bool_array_names.append(name)
+
+        if not scalars.flags.c_contiguous:
+            scalars = np.ascontiguousarray(scalars)
+
+        vtkarr = convert_array(scalars, deep=deep)
+        vtkarr.SetName(name)
+        self.GetFieldData().AddArray(vtkarr)
+
+
+    def _add_field_scalar(self, scalars, name, set_active=False, deep=True):
+        """DEPRECATED: Please use `_add_field_array`"""
+        warnings.warn('Deprecation Warning: `_add_field_scalar` is now `_add_field_array`', RuntimeWarning)
+        return self._add_field_array(scalars, name, set_active=set_active, deep=deep)
+
+
+    def add_field_array(self, scalars, name, deep=True):
+        self._add_field_array(scalars, name, deep=deep)
+
+
+    @property
+    def field_arrays(self):
+        """ Returns all field arrays """
+        fdata = self.GetFieldData()
+        narr = fdata.GetNumberOfArrays()
+
+        # just return if unmodified
+        if hasattr(self, '_field_arrays'):
+            keys = list(self._field_arrays.keys())
+            if narr == len(keys):
+                return self._field_arrays
+
+        # dictionary with callbacks
+        self._field_arrays = FieldScalarsDict(self)
+
+        for i in range(narr):
+            name = fdata.GetArrayName(i)
+            if name is None or len(name) < 1:
+                name = 'Field Array {}'.format(i)
+                fdata.GetAbstractArray(i).SetName(name)
+            self._field_arrays[name] = self._field_array(name)
+
+        self._field_arrays.enable_callback()
+        return self._field_arrays
+
+
+    def clear_field_arrays(self):
+        """ removes all field arrays """
+        keys = self.field_arrays.keys()
+        for key in keys:
+            self._remove_array(FIELD_DATA_FIELD, key)
+
+
+
+class Common(DataSetFilters, DataObject):
+    """ Methods in common to spatially referenced objects"""
 
     # Simply bind pyvista.plotting.plot to the object
     plot = pyvista.plot
 
+
     def __new__(cls, *args, **kwargs):
         if cls is Common:
             raise TypeError("pyvista.Common is an abstract class and may not be instantiated.")
         return object.__new__(cls, *args, **kwargs)
 
+
     def __init__(self, *args, **kwargs):
+        super(Common, self).__init__()
         self.references = []
         self._point_bool_array_names = []
         self._cell_bool_array_names = []
-        self._field_bool_array_names = []
+
 
     @property
     def active_scalar_info(self):
@@ -90,12 +342,13 @@ class Common(DataSetFilters, object):
 
         return self._active_scalar_info
 
+
     @property
     def active_vectors_info(self):
         """Return the active scalar's field and name: [field, name]"""
         if not hasattr(self, '_active_vectors_info'):
             # Sometimes, precomputed normals aren't set as active
-            if 'Normals' in self.scalar_names:
+            if 'Normals' in self.array_names:
                 self.set_active_vectors('Normals')
             else:
                 self._active_vectors_info = [POINT_DATA_FIELD, None] # field and name
@@ -112,6 +365,7 @@ class Common(DataSetFilters, object):
 
         return self._active_vectors_info
 
+
     @property
     def active_vectors(self):
         """The active vectors array"""
@@ -122,26 +376,31 @@ class Common(DataSetFilters, object):
             if field is CELL_DATA_FIELD:
                 return self.cell_arrays[name]
 
+
     @property
     def active_vectors_name(self):
         """The name of the active vectors array"""
         return self.active_vectors_info[1]
 
+
     @active_vectors_name.setter
     def active_vectors_name(self, name):
         """Set the name of the active vector"""
         return self.set_active_vectors(name)
 
+
     @property
     def active_scalar_name(self):
         """Returns the active scalar's name"""
         return self.active_scalar_info[1]
 
+
     @active_scalar_name.setter
     def active_scalar_name(self, name):
         """Set the name of the active scalar"""
         return self.set_active_scalar(name)
 
+
     @property
     def points(self):
         """ returns a pointer to the points as a numpy object """
@@ -149,6 +408,7 @@ class Common(DataSetFilters, object):
         arr = vtk_to_numpy(vtk_data)
         return pyvista_ndarray(arr, vtk_data)
 
+
     @points.setter
     def points(self, points):
         """ set points without copying """
@@ -159,6 +419,7 @@ class Common(DataSetFilters, object):
         self.GetPoints().Modified()
         self.Modified()
 
+
     @property
     def arrows(self):
         """
@@ -177,11 +438,13 @@ class Common(DataSetFilters, object):
         name = self.active_vectors_name
         return self.glyph(scale=name, orient=name)
 
+
     @property
     def vectors(self):
         """ Returns active vectors """
         return self.active_vectors
 
+
     @vectors.setter
     def vectors(self, array):
         """ Sets the active vector  """
@@ -195,6 +458,7 @@ class Common(DataSetFilters, object):
         self.point_arrays[DEFAULT_VECTOR_KEY] = array
         self.active_vectors_name = DEFAULT_VECTOR_KEY
 
+
     @property
     def t_coords(self):
         """The active texture coordinates on the points"""
@@ -202,6 +466,7 @@ class Common(DataSetFilters, object):
             return vtk_to_numpy(self.GetPointData().GetTCoords())
         return None
 
+
     @t_coords.setter
     def t_coords(self, t_coords):
         """Set the array to use as the texture coordinates"""
@@ -222,6 +487,7 @@ class Common(DataSetFilters, object):
         self.GetPointData().Modified()
         return
 
+
     @property
     def textures(self):
         """A dictionary to hold ``vtk.vtkTexture`` objects that can be
@@ -232,11 +498,13 @@ class Common(DataSetFilters, object):
             self._textures = {}
         return self._textures
 
+
     def clear_textures(self):
         """Clear the textures from this mesh"""
         if hasattr(self, '_textures'):
             del self._textures
 
+
     def _activate_texture(mesh, name):
         """Grab a texture and update the active texture coordinates. This makes
         sure to not destroy old texture coordinates
@@ -270,16 +538,17 @@ class Common(DataSetFilters, object):
         else:
             # Be sure to reset the tcoords if present
             # Grab old coordinates
-            if name in mesh.scalar_names:
+            if name in mesh.array_names:
                 old_tcoord = mesh.GetPointData().GetTCoords()
                 mesh.GetPointData().SetTCoords(mesh.GetPointData().GetAbstractArray(name))
                 mesh.GetPointData().AddArray(old_tcoord)
                 mesh.Modified()
         return texture
 
+
     def set_active_scalar(self, name, preference='cell'):
         """Finds the scalar by name and appropriately sets it as active"""
-        _, field = get_scalar(self, name, preference=preference, info=True)
+        _, field = get_array(self, name, preference=preference, info=True)
         self._last_active_scalar_name = self.active_scalar_info[1]
         if field == POINT_DATA_FIELD:
             self.GetPointData().SetActiveScalars(name)
@@ -289,9 +558,10 @@ class Common(DataSetFilters, object):
             raise RuntimeError('Data field ({}) not useable'.format(field))
         self._active_scalar_info = [field, name]
 
+
     def set_active_vectors(self, name, preference='point'):
         """Finds the vectors by name and appropriately sets it as active"""
-        _, field = get_scalar(self, name, preference=preference, info=True)
+        _, field = get_array(self, name, preference=preference, info=True)
         if field == POINT_DATA_FIELD:
             self.GetPointData().SetActiveVectors(name)
         elif field == CELL_DATA_FIELD:
@@ -300,9 +570,10 @@ class Common(DataSetFilters, object):
             raise RuntimeError('Data field ({}) not useable'.format(field))
         self._active_vectors_info = [field, name]
 
+
     def rename_scalar(self, old_name, new_name, preference='cell'):
         """Changes array name by searching for the array then renaming it"""
-        _, field = get_scalar(self, old_name, preference=preference, info=True)
+        _, field = get_array(self, old_name, preference=preference, info=True)
         if field == POINT_DATA_FIELD:
             self.point_arrays[new_name] = self.point_arrays.pop(old_name)
         elif field == CELL_DATA_FIELD:
@@ -314,6 +585,7 @@ class Common(DataSetFilters, object):
         if self.active_scalar_info[1] == old_name:
             self.set_active_scalar(new_name, preference=field)
 
+
     @property
     def active_scalar(self):
         """Returns the active scalar as an array"""
@@ -321,11 +593,12 @@ class Common(DataSetFilters, object):
         if name is None:
             return None
         if field == POINT_DATA_FIELD:
-            return self._point_scalar(name)
+            return self._point_array(name)
         elif field == CELL_DATA_FIELD:
-            return self._cell_scalar(name)
+            return self._cell_array(name)
 
-    def _point_scalar(self, name=None):
+
+    def _point_array(self, name=None):
         """
         Returns point scalars of a vtk object
 
@@ -343,119 +616,38 @@ class Common(DataSetFilters, object):
         if name is None:
             # use active scalar array
             field, name = self.active_scalar_info
-            if field != POINT_DATA_FIELD:
-                raise RuntimeError('Must specify an array to fetch.')
-        vtkarr = self.GetPointData().GetAbstractArray(name)
-        if vtkarr is None:
-            raise AssertionError('({}) is not a point scalar'.format(name))
-
-        # numpy does not support bit array data types
-        if isinstance(vtkarr, vtk.vtkBitArray):
-            vtkarr = vtk_bit_array_to_char(vtkarr)
-            if name not in self._point_bool_array_names:
-                self._point_bool_array_names.append(name)
-
-        array = convert_array(vtkarr)
-        if array.dtype == np.uint8 and name in self._point_bool_array_names:
-            array = array.view(np.bool)
-        return array
-
-    def _add_point_scalar(self, scalars, name, set_active=False, deep=True):
-        """
-        Adds point scalars to the mesh
-
-        Parameters
-        ----------
-        scalars : numpy.ndarray
-            Numpy array of scalars.  Must match number of points.
-
-        name : str
-            Name of point scalars to add.
-
-        set_active : bool, optional
-            Sets the scalars to the active plotting scalars.  Default False.
-
-        deep : bool, optional
-            Does not copy scalars when False.  A reference to the scalars
-            must be kept to avoid a segfault.
-
-        """
-        if scalars is None:
-            raise TypeError('Empty array unable to be added')
-
-        if not isinstance(scalars, np.ndarray):
-            scalars = np.array(scalars)
-
-        if scalars.shape[0] != self.n_points:
-            raise Exception('Number of scalars must match the number of ' +
-                            'points')
-
-        # need to track which arrays are boolean as all boolean arrays
-        # must be stored as uint8
-        if scalars.dtype == np.bool:
-            scalars = scalars.view(np.uint8)
-            if name not in self._point_bool_array_names:
-                self._point_bool_array_names.append(name)
-
-        if not scalars.flags.c_contiguous:
-            scalars = np.ascontiguousarray(scalars)
-
-        vtkarr = convert_array(scalars, deep=deep)
-        vtkarr.SetName(name)
-        self.GetPointData().AddArray(vtkarr)
-        if set_active or self.active_scalar_info[1] is None:
-            self.GetPointData().SetActiveScalars(name)
-            self._active_scalar_info = [POINT_DATA_FIELD, name]
-
-    def points_to_double(self):
-        """ Makes points double precision """
-        if self.points.dtype != np.double:
-            self.points = self.points.astype(np.double)
-
-    def _field_scalar(self, name=None):
-        """
-        Returns field scalars of a vtk object
-
-        Parameters
-        ----------
-        name : str
-            Name of field scalars to retrive.
-
-        Returns
-        -------
-        scalars : np.ndarray
-            Numpy array of scalars
-
-        """
-        if name is None:
-            raise RuntimeError('Must specify an array to fetch.')
-        vtkarr = self.GetFieldData().GetAbstractArray(name)
+            if field != POINT_DATA_FIELD:
+                raise RuntimeError('Must specify an array to fetch.')
+        vtkarr = self.GetPointData().GetAbstractArray(name)
         if vtkarr is None:
-            raise AssertionError('({}) is not a valid field scalar array'.format(name))
+            raise AssertionError('({}) is not a point scalar'.format(name))
 
         # numpy does not support bit array data types
         if isinstance(vtkarr, vtk.vtkBitArray):
             vtkarr = vtk_bit_array_to_char(vtkarr)
             if name not in self._point_bool_array_names:
-                self._field_bool_array_names.append(name)
+                self._point_bool_array_names.append(name)
 
         array = convert_array(vtkarr)
-        if array.dtype == np.uint8 and name in self._field_bool_array_names:
+        if array.dtype == np.uint8 and name in self._point_bool_array_names:
             array = array.view(np.bool)
         return array
 
-    def _add_field_scalar(self, scalars, name, deep=True):
+
+    def _add_point_array(self, scalars, name, set_active=False, deep=True):
         """
-        Adds field scalars to the mesh
+        Adds point scalars to the mesh
 
         Parameters
         ----------
         scalars : numpy.ndarray
-            Numpy array of scalars.  Does not have to match number of points or
-            numbers of cells.
+            Numpy array of scalars.  Must match number of points.
 
         name : str
-            Name of field scalars to add.
+            Name of point scalars to add.
+
+        set_active : bool, optional
+            Sets the scalars to the active plotting scalars.  Default False.
 
         deep : bool, optional
             Does not copy scalars when False.  A reference to the scalars
@@ -468,22 +660,66 @@ class Common(DataSetFilters, object):
         if not isinstance(scalars, np.ndarray):
             scalars = np.array(scalars)
 
+        if scalars.shape[0] != self.n_points:
+            raise Exception('Number of scalars must match the number of ' +
+                            'points')
+
         # need to track which arrays are boolean as all boolean arrays
         # must be stored as uint8
         if scalars.dtype == np.bool:
             scalars = scalars.view(np.uint8)
-            if name not in self._field_bool_array_names:
-                self._field_bool_array_names.append(name)
+            if name not in self._point_bool_array_names:
+                self._point_bool_array_names.append(name)
 
         if not scalars.flags.c_contiguous:
             scalars = np.ascontiguousarray(scalars)
 
         vtkarr = convert_array(scalars, deep=deep)
         vtkarr.SetName(name)
-        self.GetFieldData().AddArray(vtkarr)
+        self.GetPointData().AddArray(vtkarr)
+        if set_active or self.active_scalar_info[1] is None:
+            self.GetPointData().SetActiveScalars(name)
+            self._active_scalar_info = [POINT_DATA_FIELD, name]
+
+
+    def _add_point_scalar(self, scalars, name, set_active=False, deep=True):
+        """DEPRECATED: Please use `_add_point_array`"""
+        warnings.warn('Deprecation Warning: `_add_point_scalar` is now `_add_point_array`', RuntimeWarning)
+        return self._add_point_array(scalars, name, set_active=set_active, deep=deep)
+
+
+    def get_data_range(self, arr=None, preference='cell'):
+        """Get the non-NaN min and max of a named scalar array
+
+        Parameters
+        ----------
+        arr : str, np.ndarray, optional
+            The name of the array to get the range. If None, the active scalar
+            is used
+
+        preference : str, optional
+            When scalars is specified, this is the perfered scalar type to
+            search for in the dataset.  Must be either ``'point'``, ``'cell'``,
+            or ``'field'``.
+
+        """
+        if arr is None:
+            # use active scalar array
+            _, arr = self.active_scalar_info
+        if isinstance(arr, str):
+            arr = get_array(self, arr, preference=preference)
+        # If array has no tuples return a NaN range
+        if arr is None or arr.size == 0 or not np.issubdtype(arr.dtype, np.number):
+            return (np.nan, np.nan)
+        # Use the array range
+        return np.nanmin(arr), np.nanmax(arr)
+
+
+    def points_to_double(self):
+        """ Makes points double precision """
+        if self.points.dtype != np.double:
+            self.points = self.points.astype(np.double)
 
-    def add_field_array(self, scalars, name, deep=True):
-        self._add_field_scalar(scalars, name, deep=deep)
 
     def rotate_x(self, angle):
         """
@@ -497,6 +733,7 @@ class Common(DataSetFilters, object):
         """
         axis_rotation(self.points, angle, inplace=True, axis='x')
 
+
     def rotate_y(self, angle):
         """
         Rotates mesh about the y-axis.
@@ -509,6 +746,7 @@ class Common(DataSetFilters, object):
         """
         axis_rotation(self.points, angle, inplace=True, axis='y')
 
+
     def rotate_z(self, angle):
         """
         Rotates mesh about the z-axis.
@@ -521,6 +759,7 @@ class Common(DataSetFilters, object):
         """
         axis_rotation(self.points, angle, inplace=True, axis='z')
 
+
     def translate(self, xyz):
         """
         Translates the mesh.
@@ -533,6 +772,7 @@ class Common(DataSetFilters, object):
         """
         self.points += np.asarray(xyz)
 
+
     def transform(self, trans):
         """
         Compute a transformation in place using a 4x4 transform.
@@ -566,7 +806,8 @@ class Common(DataSetFilters, object):
         self.points[:, 1] = y
         self.points[:, 2] = z
 
-    def _cell_scalar(self, name=None):
+
+    def _cell_array(self, name=None):
         """
         Returns the cell scalars of a vtk object
 
@@ -602,7 +843,8 @@ class Common(DataSetFilters, object):
             array = array.view(np.bool)
         return array
 
-    def _add_cell_scalar(self, scalars, name, set_active=False, deep=True):
+
+    def _add_cell_array(self, scalars, name, set_active=False, deep=True):
         """
         Adds cell scalars to the vtk object.
 
@@ -645,6 +887,13 @@ class Common(DataSetFilters, object):
             self.GetCellData().SetActiveScalars(name)
             self._active_scalar_info = [CELL_DATA_FIELD, name]
 
+
+    def _add_cell_scalar(self, scalars, name, set_active=False, deep=True):
+        """DEPRECATED: Please use `_add_cell_array`"""
+        warnings.warn('Deprecation Warning: `_add_cell_scalar` is now `_add_cell_array`', RuntimeWarning)
+        return self._add_cell_array(scalars, name, set_active=set_active, deep=deep)
+
+
     def copy_meta_from(self, ido):
         """Copies pyvista meta data onto this object from another object"""
         self._active_scalar_info = ido.active_scalar_info
@@ -652,29 +901,6 @@ class Common(DataSetFilters, object):
         if hasattr(ido, '_textures'):
             self._textures = ido._textures
 
-    def copy(self, deep=True):
-        """
-        Returns a copy of the object
-
-        Parameters
-        ----------
-        deep : bool, optional
-            When True makes a full copy of the object.
-
-        Returns
-        -------
-        newobject : same as input
-           Deep or shallow copy of the input.
-        """
-        thistype = type(self)
-        newobject = thistype()
-        if deep:
-            newobject.DeepCopy(self)
-        else:
-            newobject.ShallowCopy(self)
-        newobject.copy_meta_from(self)
-        return newobject
-
 
     @property
     def point_arrays(self):
@@ -700,37 +926,12 @@ class Common(DataSetFilters, object):
             if name is None or len(name) < 1:
                 name = 'Point Array {}'.format(i)
                 pdata.GetAbstractArray(i).SetName(name)
-            self._point_arrays[name] = self._point_scalar(name)
+            self._point_arrays[name] = self._point_array(name)
 
         self._point_arrays.enable_callback()
         return self._point_arrays
 
 
-    @property
-    def field_arrays(self):
-        """ Returns all field arrays """
-        fdata = self.GetFieldData()
-        narr = fdata.GetNumberOfArrays()
-
-        # just return if unmodified
-        if hasattr(self, '_field_arrays'):
-            keys = list(self._field_arrays.keys())
-            if narr == len(keys):
-                return self._field_arrays
-
-        # dictionary with callbacks
-        self._field_arrays = FieldScalarsDict(self)
-
-        for i in range(narr):
-            name = fdata.GetArrayName(i)
-            if name is None or len(name) < 1:
-                name = 'Field Array {}'.format(i)
-                fdata.GetAbstractArray(i).SetName(name)
-            self._field_arrays[name] = self._field_scalar(name)
-
-        self._field_arrays.enable_callback()
-        return self._field_arrays
-
 
     def _remove_array(self, field, key):
         """internal helper to remove a single array by name from each field"""
@@ -752,17 +953,13 @@ class Common(DataSetFilters, object):
         for key in keys:
             self._remove_array(POINT_DATA_FIELD, key)
 
+
     def clear_cell_arrays(self):
         """ removes all cell arrays """
         keys = self.cell_arrays.keys()
         for key in keys:
             self._remove_array(CELL_DATA_FIELD, key)
 
-    def clear_field_arrays(self):
-        """ removes all field arrays """
-        keys = self.field_arrays.keys()
-        for key in keys:
-            self._remove_array(FIELD_DATA_FIELD, key)
 
     def clear_arrays(self):
         """ removes all arrays from point/cell/field data """
@@ -795,31 +992,36 @@ class Common(DataSetFilters, object):
             if name is None or len(name) < 1:
                 name = 'Cell Array {}'.format(i)
                 cdata.GetAbstractArray(i).SetName(name)
-            self._cell_arrays[name] = self._cell_scalar(name)
+            self._cell_arrays[name] = self._cell_array(name)
 
         self._cell_arrays.enable_callback()
         return self._cell_arrays
 
+
     @property
     def n_points(self):
         """The number of points in the entire dataset"""
         return self.GetNumberOfPoints()
 
+
     @property
     def n_cells(self):
         """The number of cells in the entire dataset"""
         return self.GetNumberOfCells()
 
+
     @property
     def number_of_points(self):  # pragma: no cover
         """ returns the number of points """
         return self.GetNumberOfPoints()
 
+
     @property
     def number_of_cells(self):  # pragma: no cover
         """ returns the number of cells """
         return self.GetNumberOfCells()
 
+
     @property
     def bounds(self):
         """
@@ -828,22 +1030,35 @@ class Common(DataSetFilters, object):
         """
         return list(self.GetBounds())
 
+
     @property
     def length(self):
         """the length of the diagonal of the bounding box"""
         return self.GetLength()
 
+
     @property
     def center(self):
         """ Center of the bounding box """
         return list(self.GetCenter())
 
+
     @property
     def extent(self):
         """ The range of the bounding box """
         if hasattr(self, 'GetExtent'):
             return list(self.GetExtent())
 
+
+    @extent.setter
+    def extent(self, extent):
+        """ The range of the bounding box """
+        if hasattr(self, 'SetExtent'):
+            return self.SetExtent(extent)
+        else:
+            raise AttributeError('This mesh type does not handle extents.')
+
+
     @property
     def volume(self):
         """
@@ -858,35 +1073,10 @@ class Common(DataSetFilters, object):
         sizes = self.compute_cell_sizes(length=False, area=False, volume=True)
         return np.sum(sizes.cell_arrays['Volume'])
 
-    def get_data_range(self, arr=None, preference='cell'):
-        """Get the non-NaN min and max of a named scalar array
-
-        Parameters
-        ----------
-        arr : str, np.ndarray, optional
-            The name of the array to get the range. If None, the active scalar
-            is used
-
-        preference : str, optional
-            When scalars is specified, this is the perfered scalar type to
-            search for in the dataset.  Must be either ``'point'``, ``'cell'``,
-            or ``'field'``.
-
-        """
-        if arr is None:
-            # use active scalar array
-            _, arr = self.active_scalar_info
-        if isinstance(arr, str):
-            arr = get_scalar(self, arr, preference=preference)
-        # If array has no tuples return a NaN range
-        if arr is None or arr.size == 0 or not np.issubdtype(arr.dtype, np.number):
-            return (np.nan, np.nan)
-        # Use the array range
-        return np.nanmin(arr), np.nanmax(arr)
 
-    def get_scalar(self, name, preference='cell', info=False):
+    def get_array(self, name, preference='cell', info=False):
         """ Searches both point, cell and field data for an array """
-        return get_scalar(self, name, preference=preference, info=info)
+        return get_array(self, name, preference=preference, info=info)
 
 
     def __getitem__(self, index):
@@ -898,7 +1088,8 @@ class Common(DataSetFilters, object):
             preference = 'cell'
         else:
             raise KeyError('Index ({}) not understood. Index must be a string name or a tuple of string name and string preference.'.format(index))
-        return self.get_scalar(name, preference=preference, info=False)
+        return self.get_array(name, preference=preference, info=False)
+
 
     def __setitem__(self, name, scalars):
         """Add/set an array in the point_arrays, or cell_arrays depending on the
@@ -922,6 +1113,7 @@ class Common(DataSetFilters, object):
             raise_not_matching(scalars, self)
         return
 
+
     @property
     def n_arrays(self):
         """The number of scalar arrays present in the dataset"""
@@ -929,14 +1121,16 @@ class Common(DataSetFilters, object):
                self.GetCellData().GetNumberOfArrays() + \
                self.GetFieldData().GetNumberOfArrays()
 
+
     @property
     def n_scalars(self):
         """DEPRECATED: Please use `n_arrays`"""
         warnings.warn('Deprecation Warning: `n_scalars` is now `n_arrays`', RuntimeWarning)
         return self.n_arrays
 
+
     @property
-    def scalar_names(self):
+    def array_names(self):
         """A list of scalar names for the dataset. This makes
         sure to put the active scalar's name first in the list."""
         names = []
@@ -954,6 +1148,13 @@ class Common(DataSetFilters, object):
         return names
 
 
+    @property
+    def scalar_names(self):
+        """DEPRECATED: Please use `array_names`"""
+        warnings.warn('Deprecation Warning: `scalar_names` is now `array_names`', RuntimeWarning)
+        return self.array_names
+
+
     def _get_attrs(self):
         """An internal helper for the representation methods"""
         attrs = []
@@ -969,44 +1170,6 @@ class Common(DataSetFilters, object):
         return attrs
 
 
-    def head(self, display=True, html=None):
-        """Return the header stats of this dataset. If in IPython, this will
-        be formatted to HTML. Otherwise returns a console friendly string"""
-        # Generate the output
-        if html:
-            fmt = ""
-            # HTML version
-            fmt += "\n"
-            fmt += "<table>\n"
-            fmt += "<tr><th>{}</th><th>Information</th></tr>\n".format(type(self).__name__)
-            row = "<tr><td>{}</td><td>{}</td></tr>\n"
-            # now make a call on the object to get its attributes as a list of len 2 tuples
-            for attr in self._get_attrs():
-                try:
-                    fmt += row.format(attr[0], attr[2].format(*attr[1]))
-                except:
-                    fmt += row.format(attr[0], attr[2].format(attr[1]))
-            fmt += row.format('N Arrays', self.n_arrays)
-            fmt += "</table>\n"
-            fmt += "\n"
-            if display:
-                from IPython.display import display, HTML
-                display(HTML(fmt))
-                return
-            return fmt
-        # Otherwise return a string that is Python console friendly
-        fmt = "{} ({})\n".format(type(self).__name__, hex(id(self)))
-        # now make a call on the object to get its attributes as a list of len 2 tuples
-        row = "  {}:\t{}\n"
-        for attr in self._get_attrs():
-            try:
-                fmt += row.format(attr[0], attr[2].format(*attr[1]))
-            except:
-                fmt += row.format(attr[0], attr[2].format(attr[1]))
-        fmt += row.format('N Arrays', self.n_arrays)
-        return fmt
-
-
     def _repr_html_(self):
         """A pretty representation for Jupyter notebooks that includes header
         details and information about all scalar arrays"""
@@ -1057,10 +1220,12 @@ class Common(DataSetFilters, object):
         """Object representation"""
         return self.head(display=False, html=False)
 
+
     def __str__(self):
         """Object string representation"""
         return self.head(display=False, html=False)
 
+
     def overwrite(self, mesh):
         """
         Overwrites this mesh inplace with the new mesh's geometries and data
@@ -1072,9 +1237,10 @@ class Common(DataSetFilters, object):
 
         """
         self.DeepCopy(mesh)
-        if is_pyvista_obj(mesh):
+        if is_pyvista_dataset(mesh):
             self.copy_meta_from(mesh)
 
+
     def cast_to_unstructured_grid(self):
         """Get a new representation of this object as an
         :class:`pyvista.UnstructuredGrid`
@@ -1084,6 +1250,7 @@ class Common(DataSetFilters, object):
         alg.Update()
         return pyvista.filters._get_output(alg)
 
+
     @property
     def quality(self):
         """
@@ -1116,27 +1283,6 @@ class Common(DataSetFilters, object):
         return pyansys.CellQuality(dataset)
 
 
-    def save(self, filename, binary=True):
-         """
-         Writes this mesh to a file.
-
-         Parameters
-         ----------
-         filename : str
-             Filename of mesh to be written.  File type is inferred from
-             the extension of the filename unless overridden with
-             ftype.
-
-         binary : bool, optional
-             Writes the file as binary when True and ASCII when False.
-
-         Notes
-         -----
-         Binary files write much faster than ASCII and have a smaller
-         file size.
-         """
-         raise NotImplementedError('{} mesh type does not have a save method.'.format(type(self)))
-
 
 class _ScalarsDict(dict):
     """Internal helper for scalars dictionaries"""
@@ -1147,23 +1293,30 @@ class _ScalarsDict(dict):
         self.remover = None
         self.modifier = None
 
+
     def enable_callback(self):
         """Enable callbacks to be set True"""
         self.callback_enabled = True
 
+
+    def adder(self, scalars, name, set_active=False, deep=True):
+        raise NotImplementedError()
+
+
     def pop(self, key):
         """Get and remove an element by key name"""
         arr = dict.pop(self, key).copy()
         self.remover(key)
         return arr
 
+
     def update(self, data):
         """
         Update this dictionary with th key-value pairs from a given
         dictionary
         """
-        if not isinstance(data, dict):
-            raise TypeError('Data to update must be in a dictionary.')
+        if not isinstance(data, (dict, pyvista.Table)):
+            raise TypeError('Data to update must be in a dictionary or PyVista Table.')
         for k, v in data.items():
             arr = np.array(v)
             try:
@@ -1172,13 +1325,17 @@ class _ScalarsDict(dict):
                 logging.warning("Values under key ({}) not supported by VTK".format(k))
         return
 
+
     def __setitem__(self, key, val):
         """ overridden to assure data is contigious """
+        if isinstance(val, (list, tuple)):
+            val = np.array(val)
         if self.callback_enabled:
             self.adder(val, key, deep=False)
         dict.__setitem__(self, key, val)
         self.modifier()
 
+
     def __delitem__(self, key):
         """Remove item by key name"""
         self.remover(key)
@@ -1190,14 +1347,14 @@ class CellScalarsDict(_ScalarsDict):
     Updates internal cell data when an array is added or removed from
     the dictionary.
     """
-
     def __init__(self, data):
         _ScalarsDict.__init__(self, data)
         self.remover = lambda key: self.data._remove_array(CELL_DATA_FIELD, key)
         self.modifier = lambda *args: self.data.GetCellData().Modified()
 
+
     def adder(self, scalars, name, set_active=False, deep=True):
-        self.data._add_cell_scalar(scalars, name, set_active=False, deep=deep)
+        self.data._add_cell_array(scalars, name, set_active=False, deep=deep)
 
 
 class PointScalarsDict(_ScalarsDict):
@@ -1205,28 +1362,29 @@ class PointScalarsDict(_ScalarsDict):
     Updates internal point data when an array is added or removed from
     the dictionary.
     """
-
     def __init__(self, data):
         _ScalarsDict.__init__(self, data)
         self.remover = lambda key: self.data._remove_array(POINT_DATA_FIELD, key)
         self.modifier = lambda *args: self.data.GetPointData().Modified()
 
+
     def adder(self, scalars, name, set_active=False, deep=True):
-        self.data._add_point_scalar(scalars, name, set_active=False, deep=deep)
+        self.data._add_point_array(scalars, name, set_active=False, deep=deep)
+
 
 class FieldScalarsDict(_ScalarsDict):
     """
     Updates internal field data when an array is added or removed from
     the dictionary.
     """
-
     def __init__(self, data):
         _ScalarsDict.__init__(self, data)
         self.remover = lambda key: self.data._remove_array(FIELD_DATA_FIELD, key)
         self.modifier = lambda *args: self.data.GetFieldData().Modified()
 
+
     def adder(self, scalars, name, set_active=False, deep=True):
-        self.data._add_field_scalar(scalars, name, deep=deep)
+        self.data._add_field_array(scalars, name, deep=deep)
 
 
 def axis_rotation(points, angle, inplace=False, deg=True, axis='z'):
diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py
index 9b69a1f6..8edb3dab 100644
--- a/pyvista/core/composite.py
+++ b/pyvista/core/composite.py
@@ -13,15 +13,16 @@ from vtk import vtkMultiBlockDataSet
 
 import pyvista
 from pyvista import plot
-from pyvista.utilities import get_scalar, is_pyvista_obj, wrap
+from pyvista.utilities import get_array, is_pyvista_dataset, wrap
 
+from .common import DataObject
 from .filters import CompositeFilters
 
 log = logging.getLogger(__name__)
 log.setLevel('CRITICAL')
 
 
-class MultiBlock(vtkMultiBlockDataSet, CompositeFilters):
+class MultiBlock(vtkMultiBlockDataSet, CompositeFilters, DataObject):
     """
     A composite class to hold many data sets which can be iterated over.
     This wraps/extends the ``vtkMultiBlockDataSet`` class in VTK so that we can
@@ -65,7 +66,7 @@ class MultiBlock(vtkMultiBlockDataSet, CompositeFilters):
         datasets. This is perfrom inplace"""
         for i in range(self.n_blocks):
             block = self.GetBlock(i)
-            if not is_pyvista_obj(block):
+            if not is_pyvista_dataset(block):
                 self.SetBlock(i, pyvista.wrap(block))
         return
 
@@ -213,7 +214,7 @@ class MultiBlock(vtkMultiBlockDataSet, CompositeFilters):
             if data is None:
                 continue
             # get the scalar if availble
-            arr = get_scalar(data, name)
+            arr = get_array(data, name)
             if arr is None or not np.issubdtype(arr.dtype, np.number):
                 continue
             tmi, tma = np.nanmin(arr), np.nanmax(arr)
@@ -244,7 +245,7 @@ class MultiBlock(vtkMultiBlockDataSet, CompositeFilters):
         data = self.GetBlock(index)
         if data is None:
             return data
-        if data is not None and not is_pyvista_obj(data):
+        if data is not None and not is_pyvista_dataset(data):
             data = wrap(data)
         if data not in self.refs:
             self.refs.append(data)
@@ -312,7 +313,7 @@ class MultiBlock(vtkMultiBlockDataSet, CompositeFilters):
             name = index
         else:
             i, name = index, None
-        if data is not None and not is_pyvista_obj(data):
+        if data is not None and not is_pyvista_dataset(data):
             data = wrap(data)
         if i == -1:
             self.append(data)
diff --git a/pyvista/core/filters.py b/pyvista/core/filters.py
index 5586fefa..fb36d825 100644
--- a/pyvista/core/filters.py
+++ b/pyvista/core/filters.py
@@ -33,7 +33,7 @@ from vtk.util.numpy_support import (numpy_to_vtk, numpy_to_vtkIdTypeArray,
 
 import pyvista
 from pyvista.utilities import (CELL_DATA_FIELD, POINT_DATA_FIELD,
-                               generate_plane, get_scalar, is_inside_bounds,
+                               generate_plane, get_array, is_inside_bounds,
                                wrap)
 
 NORMALS = {
@@ -201,7 +201,7 @@ class DataSetFilters(object):
             output clipped mesh.
         """
         if not isinstance(surface, vtk.vtkPolyData):
-            surface = DataSetFilters.extract_surface(surface)
+            surface = DataSetFilters.extract_geometry(surface)
         function = vtk.vtkImplicitPolyDataDistance()
         function.SetInput(surface)
         if compute_distance:
@@ -439,7 +439,7 @@ class DataSetFilters(object):
         # set the scalaras to threshold on
         if scalars is None:
             field, scalars = dataset.active_scalar_info
-        arr, field = get_scalar(dataset, scalars, preference=preference, info=True)
+        arr, field = get_array(dataset, scalars, preference=preference, info=True)
 
         if arr is None:
             raise AssertionError('No arrays present to threshold.')
@@ -717,7 +717,7 @@ class DataSetFilters(object):
         if scalars is None:
             field, scalars = dataset.active_scalar_info
         else:
-            _, field = get_scalar(dataset, scalars, preference=preference, info=True)
+            _, field = get_array(dataset, scalars, preference=preference, info=True)
         # NOTE: only point data is allowed? well cells works but seems buggy?
         if field != pyvista.POINT_DATA_FIELD:
             raise AssertionError('Contour filter only works on Point data. Array ({}) is in the Cell data.'.format(scalars))
@@ -1001,7 +1001,7 @@ class DataSetFilters(object):
         """
         if scalars is None:
             field, scalars = dataset.active_scalar_info
-        arr, field = get_scalar(dataset, scalars, preference='point', info=True)
+        arr, field = get_array(dataset, scalars, preference='point', info=True)
         if field != pyvista.POINT_DATA_FIELD:
             raise AssertionError('Dataset can only by warped by a point data array.')
         scale_factor = kwargs.get('scale_factor', None)
@@ -1538,7 +1538,7 @@ class DataSetFilters(object):
         # Get variable of interest
         if scalars is None:
             field, scalars = dataset.active_scalar_info
-        values = sampled.get_scalar(scalars)
+        values = sampled.get_array(scalars)
         distance = sampled['Distance']
 
         # Remainder of the is plotting
@@ -1936,9 +1936,9 @@ class DataSetFilters(object):
         try:
             # Set user specified quality measure
             measure_setters[quality_measure]()
-        except KeyError:
-            options = ', '.join(["'{}'".format(s) for s in measure_setters.keys()])
-            raise KeyError('Cell quality type ({}) not available. Options are: {}'.format(options))
+        except (KeyError, IndexError):
+            options = ', '.join(["'{}'".format(s) for s in list(measure_setters.keys())])
+            raise KeyError('Cell quality type ({}) not available. Options are: {}'.format(quality_measure, options))
         alg.SetInputData(dataset)
         alg.SetUndefinedQuality(null_value)
         alg.Update()
@@ -1964,7 +1964,7 @@ class DataSetFilters(object):
             field, scalars = dataset.active_scalar_info
         if not isinstance(scalars, str):
             raise TypeError('Scalar array must be given as a string name')
-        _, field = dataset.get_scalar(scalars, preference=preference, info=True)
+        _, field = dataset.get_array(scalars, preference=preference, info=True)
         # args: (idx, port, connection, field, name)
         alg.SetInputArrayToProcess(0, 0, 0, field, scalars)
         alg.SetInputData(dataset)
@@ -2534,7 +2534,7 @@ class PolyDataFilters(DataSetFilters):
         if scalars is not None:
             if not isinstance(scalars, str):
                 raise TypeError('Scalar array must be given as a string name')
-            _, field = poly_data.get_scalar(scalars, preference=preference, info=True)
+            _, field = poly_data.get_array(scalars, preference=preference, info=True)
             # args: (idx, port, connection, field, name)
             tube.SetInputArrayToProcess(0, 0, 0, field, scalars)
             tube.SetVaryRadiusToVaryRadiusByScalar()
diff --git a/pyvista/core/objects.py b/pyvista/core/objects.py
new file mode 100644
index 00000000..3eb3f12c
--- /dev/null
+++ b/pyvista/core/objects.py
@@ -0,0 +1,404 @@
+"""This module provides wrappers for vtkDataObjects: data onjects without any
+sort of spatial reference.
+"""
+import collections
+
+import numpy as np
+import vtk
+
+import pyvista
+from pyvista.utilities import (ROW_DATA_FIELD, convert_array, get_array,
+                               parse_field_choice, raise_not_matching,
+                               row_array)
+
+from .common import DataObject, _ScalarsDict
+
+try:
+    import pandas as pd
+except ImportError:
+    pd = None
+
+
+
+
+
+
+class Table(vtk.vtkTable, DataObject):
+    """Wrapper for the ``vtkTable`` class. Create by passing a 2D NumPy array
+    of shape (``n_rows`` by ``n_columns``) or from a dictionary containing
+    NumPy arrays.
+
+    Example
+    -------
+    >>> import pyvista as pv
+    >>> import numpy as np
+    >>> arrays = np.random.rand(100, 3)
+    >>> table = pv.Table(arrays)
+
+    """
+    def __init__(self, *args, **kwargs):
+
+        if len(args) == 1:
+            if isinstance(args[0], vtk.vtkTable):
+                deep = kwargs.get('deep', True)
+                if deep:
+                    self.DeepCopy(args[0])
+                else:
+                    self.ShallowCopy(args[0])
+            elif isinstance(args[0], np.ndarray):
+                self._from_arrays(args[0])
+            elif isinstance(args[0], dict):
+                self._from_dict(args[0])
+            elif pd is not None and isinstance(args[0], pd.DataFrame):
+                self._from_pandas(args[0])
+            else:
+                raise TypeError('Table unable to be made from ({})'.format(type(args[0])))
+
+
+        self._row_bool_array_names = []
+
+
+    def _from_arrays(self, arrays):
+        if not arrays.ndim == 2:
+            raise AssertionError('Only 2D arrays are supported by Tables.')
+        np_table = arrays.T
+        for i, array in enumerate(np_table):
+            self.row_arrays['Array {}'.format(i)] = array
+        return
+
+
+    def _from_dict(self, array_dict):
+        for array in array_dict.values():
+            if not isinstance(array, (np.ndarray)) and array.ndim < 3:
+                raise RuntimeError('Dictionaty must contain only NumPy arrays with maximum of 2D.')
+        for name, array in array_dict.items():
+            self.row_arrays[name] = array
+        return
+
+
+    def _from_pandas(self, data_frame):
+        for name in data_frame.keys():
+            self.row_arrays[name] = data_frame[name]
+        return
+
+
+    @property
+    def n_rows(self):
+        return self.GetNumberOfRows()
+
+
+    @n_rows.setter
+    def n_rows(self, n):
+        self.SetNumberOfRows(n)
+
+
+    @property
+    def n_columns(self):
+        return self.GetNumberOfColumns()
+
+
+    @property
+    def n_arrays(self):
+        return self.n_columns
+
+
+    def _row_array(self, name=None):
+        """
+        Returns row scalars of a vtk object
+
+        Parameters
+        ----------
+        name : str
+            Name of row scalars to retrieve.
+
+        Returns
+        -------
+        scalars : np.ndarray
+            Numpy array of scalars
+
+        """
+        if name is None:
+            # use first array
+            name = self.GetRowData().GetArrayName(0)
+            if name is None:
+                raise RuntimeError('No arrays present to fetch.')
+        vtkarr = self.GetRowData().GetAbstractArray(name)
+        if vtkarr is None:
+            raise AssertionError('({}) is not a row scalar'.format(name))
+
+        # numpy does not support bit array data types
+        if isinstance(vtkarr, vtk.vtkBitArray):
+            vtkarr = vtk_bit_array_to_char(vtkarr)
+            if name not in self._row_bool_array_names:
+                self._row_bool_array_names.append(name)
+
+        array = convert_array(vtkarr)
+        if array.dtype == np.uint8 and name in self._row_bool_array_names:
+            array = array.view(np.bool)
+        return array
+
+
+    @property
+    def row_arrays(self):
+        """ Returns the all row arrays """
+        pdata = self.GetRowData()
+        narr = pdata.GetNumberOfArrays()
+
+        # Update data if necessary
+        if hasattr(self, '_row_arrays'):
+            keys = list(self._row_arrays.keys())
+            if narr == len(keys):
+                if keys:
+                    if self._row_arrays[keys[0]].shape[0] == self.n_rows:
+                        return self._row_arrays
+                else:
+                    return self._row_arrays
+
+        # dictionary with callbacks
+        self._row_arrays = RowScalarsDict(self)
+
+        for i in range(narr):
+            name = pdata.GetArrayName(i)
+            self._row_arrays[name] = self._row_array(name)
+
+        self._row_arrays.enable_callback()
+        return self._row_arrays
+
+
+    def keys(self):
+        return list(self.row_arrays.keys())
+
+
+    def items(self):
+        return self.row_arrays.items()
+
+
+    def values(self):
+        return self.row_arrays.values()
+
+
+    def update(self, data):
+        self.row_arrays.update(data)
+
+
+    def pop(self, name):
+        """Pops off an array by the specified name"""
+        return self.row_arrays.pop(name)
+
+
+    def _add_row_array(self, scalars, name, deep=True):
+        """
+        Adds scalars to the vtk object.
+
+        Parameters
+        ----------
+        scalars : numpy.ndarray
+            Numpy array of scalars.  Must match number of points.
+
+        name : str
+            Name of point scalars to add.
+
+        deep : bool, optional
+            Does not copy scalars when False.  A reference to the scalars
+            must be kept to avoid a segfault.
+
+        """
+        if scalars is None:
+            raise TypeError('Empty array unable to be added')
+
+        if not isinstance(scalars, np.ndarray):
+            scalars = np.array(scalars)
+
+        if self.n_rows == 0 or self.n_columns == 0:
+            self.n_rows = scalars.shape[0]
+        elif scalars.shape[0] != self.n_rows:
+            raise Exception('Number of scalars must match the number of rows (%d)'
+                            % self.n_rows)
+
+        if not scalars.flags.c_contiguous:
+            scalars = np.ascontiguousarray(scalars)
+        if scalars.dtype == np.bool:
+            scalars = scalars.view(np.uint8)
+            self._row_bool_array_names.append(name)
+
+        vtkarr = convert_array(scalars, deep=deep)
+        vtkarr.SetName(name)
+        self.AddColumn(vtkarr)
+
+
+
+    def __getitem__(self, index):
+        """ Searches row data for an array """
+        if isinstance(index, str):
+            name = index
+        elif isinstance(index, int):
+            name = self.GetRowData().GetArrayName(index)
+        else:
+            raise KeyError('Index ({}) not understood. Index must be a string name or a tuple of string name and string preference.'.format(index))
+        return row_array(self, name)
+
+
+    def get(self, index):
+        """Get an array by its name"""
+        return self[index]
+
+
+    def __setitem__(self, name, scalars):
+        """Add/set an array in the row_arrays"""
+        if scalars is None:
+            raise TypeError('Empty array unable to be added')
+        if not isinstance(scalars, np.ndarray):
+            scalars = np.array(scalars)
+        self.row_arrays[name] = scalars
+
+
+    def _remove_array(self, field, key):
+        """internal helper to remove a single array by name from each field"""
+        field = parse_field_choice(field)
+        if field == ROW_DATA_FIELD:
+            self.GetRowData().RemoveArray(key)
+        else:
+            raise NotImplementedError('Not able to remove arrays from the ({}) data fiedl'.format(field))
+        return
+
+
+    def __delitem__(self, name):
+        """Removes an array by the specified name"""
+        del self.row_arrays[name]
+
+
+    def __iter__(self):
+        """The iterator across all arrays"""
+        self._iter_n = 0
+        return self
+
+
+    def next(self):
+        """Get the next block from the iterator"""
+        if self._iter_n < self.n_arrays:
+            result = self[self._iter_n]
+            self._iter_n += 1
+            return result
+        else:
+            raise StopIteration
+
+
+    __next__ = next
+
+
+    def _get_attrs(self):
+        """An internal helper for the representation methods"""
+        attrs = []
+        attrs.append(("N Rows", self.n_rows, "{}"))
+        return attrs
+
+
+    def _repr_html_(self):
+        """A pretty representation for Jupyter notebooks that includes header
+        details and information about all scalar arrays"""
+        fmt = ""
+        if self.n_arrays > 0:
+            fmt += "<table>"
+            fmt += "<tr><th>Header</th><th>Data Arrays</th></tr>"
+            fmt += "<tr><td>"
+        # Get the header info
+        fmt += self.head(display=False, html=True)
+        # Fill out scalar arrays
+        if self.n_arrays > 0:
+            fmt += "</td><td>"
+            fmt += "\n"
+            fmt += "<table>\n"
+            titles = ["Name", "Type", "N Comp", "Min", "Max"]
+            fmt += "<tr>" + "".join(["<th>{}</th>".format(t) for t in titles]) + "</tr>\n"
+            row = "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n"
+            row = "<tr>" + "".join(["<td>{}</td>" for i in range(len(titles))]) + "</tr>\n"
+
+            def format_array(key):
+                """internal helper to foramt array information for printing"""
+                arr = row_array(self, key)
+                dl, dh = self.get_data_range(key)
+                dl = pyvista.FLOAT_FORMAT.format(dl)
+                dh = pyvista.FLOAT_FORMAT.format(dh)
+                if arr.ndim > 1:
+                    ncomp = arr.shape[1]
+                else:
+                    ncomp = 1
+                return row.format(key, arr.dtype, ncomp, dl, dh)
+
+            for i in range(self.n_arrays):
+                key = self.GetRowData().GetArrayName(i)
+                fmt += format_array(key)
+
+            fmt += "</table>\n"
+            fmt += "\n"
+            fmt += "</td></tr> </table>"
+        return fmt
+
+
+    def __repr__(self):
+        """Object representation"""
+        return self.head(display=False, html=False)
+
+
+    def __str__(self):
+        """Object string representation"""
+        return self.head(display=False, html=False)
+
+
+    def to_pandas(self):
+        """Create a Pandas DataFrame from this Table"""
+        if pd is None:
+            raise ImportError('You must have Pandas installed.')
+        data_frame = pd.DataFrame()
+        for name, array in self.items():
+            data_frame[name] = array
+        return data_frame
+
+
+    def save(self, *args, **kwargs):
+        raise NotImplementedError("Please use the `to_pandas` method and"
+                " harness Pandas' wonderful file IO methods.")
+
+
+    def get_data_range(self, arr=None, preference='row'):
+        """Get the non-NaN min and max of a named scalar array
+
+        Parameters
+        ----------
+        arr : str, np.ndarray, optional
+            The name of the array to get the range. If None, the active scalar
+            is used
+
+        preference : str, optional
+            When scalars is specified, this is the perfered scalar type to
+            search for in the dataset.  Must be either ``'row'`` or
+            ``'field'``.
+
+        """
+        if arr is None:
+            # use the first array in the row data
+            self.GetRowData().GetArrayName(0)
+        if isinstance(arr, str):
+            arr = get_array(self, arr, preference=preference)
+        # If array has no tuples return a NaN range
+        if arr is None or arr.size == 0 or not np.issubdtype(arr.dtype, np.number):
+            return (np.nan, np.nan)
+        # Use the array range
+        return np.nanmin(arr), np.nanmax(arr)
+
+
+
+class RowScalarsDict(_ScalarsDict):
+    """
+    Updates internal row data when an array is added or removed from
+    the dictionary.
+    """
+
+    def __init__(self, data):
+        _ScalarsDict.__init__(self, data)
+        self.remover = lambda key: self.data._remove_array(ROW_DATA_FIELD, key)
+        self.modifier = lambda *args: self.data.GetRowData().Modified()
+
+
+    def adder(self, scalars, name, set_active=False, deep=True):
+        self.data._add_row_array(scalars, name, deep=deep)
diff --git a/pyvista/core/pointset.py b/pyvista/core/pointset.py
index 7ca8fd2a..a5450cdf 100644
--- a/pyvista/core/pointset.py
+++ b/pyvista/core/pointset.py
@@ -17,7 +17,7 @@ from vtk.util.numpy_support import (numpy_to_vtk, numpy_to_vtkIdTypeArray,
                                     vtk_to_numpy)
 
 import pyvista
-from pyvista.utilities import generate_plane, get_scalar
+from pyvista.utilities import generate_plane, get_array
 
 from .common import Common
 from .filters import _get_output, PolyDataFilters, UnstructuredGridFilters
diff --git a/pyvista/plotting/helpers.py b/pyvista/plotting/helpers.py
index 14f45a96..03e8066f 100644
--- a/pyvista/plotting/helpers.py
+++ b/pyvista/plotting/helpers.py
@@ -192,7 +192,7 @@ def plot_compare_four(data_a, data_b, data_c, data_d, disply_kwargs=None,
             p.subplot(i, j)
             p.add_mesh(datasets[i][j], **disply_kwargs)
             p.add_text(labels[i][j])
-            if is_pyvista_obj(outline):
+            if is_pyvista_dataset(outline):
                 p.add_mesh(outline, color=outline_color)
             if camera_position is not None:
                 p.camera_position = camera_position
diff --git a/pyvista/plotting/ipy_tools.py b/pyvista/plotting/ipy_tools.py
index 72828198..87210f79 100644
--- a/pyvista/plotting/ipy_tools.py
+++ b/pyvista/plotting/ipy_tools.py
@@ -17,7 +17,7 @@ import numpy as np
 import scooby
 
 import pyvista
-from pyvista.utilities import is_pyvista_obj, wrap
+from pyvista.utilities import is_pyvista_dataset, wrap
 
 from .qt_plotting import BackgroundPlotter
 
@@ -82,10 +82,10 @@ class InteractiveTool(object):
         if not scooby.in_ipython() or not IPY_AVAILABLE:
             logging.warning('Interactive plotting tools require IPython and the ``ipywidgets`` package.')
         # Check the input dataset to make sure its compatible
-        if not is_pyvista_obj(dataset):
+        if not is_pyvista_dataset(dataset):
             dataset = wrap(dataset)
-            if not is_pyvista_obj(dataset):
-                raise RuntimeError('Object not supported for plotting in pyvista.')
+            if not is_pyvista_dataset(dataset):
+                raise TypeError('Object type ({}) not supported for plotting in PyVista.'.format(type(dataset)))
 
         # Make the input/output of this tool available
         self.input_dataset = dataset
@@ -132,7 +132,7 @@ class InteractiveTool(object):
         self.tool(default_params=default_params, **kwargs)
 
 
-    def _get_scalar_names(self, limit=None):
+    def _get_array_names(self, limit=None):
         """Only give scalar options that have a varying range"""
         names = []
         if limit == 'point':
@@ -140,9 +140,9 @@ class InteractiveTool(object):
         elif limit == 'cell':
             inpnames = list(self.input_dataset.cell_arrays.keys())
         else:
-            inpnames = self.input_dataset.scalar_names
+            inpnames = self.input_dataset.array_names
         for name in inpnames:
-            arr = self.input_dataset.get_scalar(name)
+            arr = self.input_dataset.get_array(name)
             rng = self.input_dataset.get_data_range(name)
             if arr is not None and arr.size > 0 and (rng[1]-rng[0] > 0.0):
                 names.append(name)
@@ -331,7 +331,7 @@ class OrthogonalSlicer(InteractiveTool):
 
         # Create/display the widgets
         return interact(update, x=xsl, y=ysl, z=zsl,
-                        scalars=self._get_scalar_names())
+                        scalars=self._get_array_names())
 
 
 class ManySlicesAlongAxis(InteractiveTool):
@@ -405,7 +405,7 @@ class ManySlicesAlongAxis(InteractiveTool):
         del axes[idx]
         axes.insert(0, axis)
         return interact(update, n=nsl, axis=axes,
-                        scalars=self._get_scalar_names())
+                        scalars=self._get_array_names())
 
 
 class Threshold(InteractiveTool):
@@ -510,7 +510,7 @@ class Threshold(InteractiveTool):
 
 
         # Create/display the widgets
-        scalars = self._get_scalar_names()
+        scalars = self._get_array_names()
         name = default_params.get("scalars", scalars[0])
         idx = scalars.index(name)
         del scalars[idx]
@@ -599,7 +599,7 @@ class Clip(InteractiveTool):
         # Create/display the widgets
         self._last_normal = 'x'
         return interact(update, location=locsl, normal=axchoices, invert=True,
-                        scalars=self._get_scalar_names())
+                        scalars=self._get_array_names())
 
 
 
@@ -694,7 +694,7 @@ class Isocontour(InteractiveTool):
 
         # Create/display the widgets
         # NOTE: Contour filter can only contour by point data
-        scalars = self._get_scalar_names(limit='point')
+        scalars = self._get_array_names(limit='point')
         name = default_params.get("scalars", scalars[0])
         idx = scalars.index(name)
         del scalars[idx]
diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py
index c2b4b66b..00d4c449 100644
--- a/pyvista/plotting/plotting.py
+++ b/pyvista/plotting/plotting.py
@@ -14,8 +14,8 @@ import vtk
 from vtk.util import numpy_support as VN
 
 import pyvista
-from pyvista.utilities import (convert_array, convert_string_array, get_scalar,
-                               is_pyvista_obj, numpy_to_texture,
+from pyvista.utilities import (convert_array, convert_string_array, get_array,
+                               is_pyvista_dataset, numpy_to_texture,
                                raise_not_matching, wrap)
 
 from .colors import get_cmap_safe
@@ -569,8 +569,10 @@ class BasePlotter(object):
             VTK actor of the mesh.
         """
         # Convert the VTK data object to a pyvista wrapped object if neccessary
-        if not is_pyvista_obj(mesh):
+        if not is_pyvista_dataset(mesh):
             mesh = wrap(mesh)
+            if not is_pyvista_dataset(mesh):
+                raise TypeError('Object type ({}) not supported for plotting in PyVista.'.format(type(mesh)))
 
         ##### Parse arguments to be used for all meshes #####
 
@@ -649,9 +651,9 @@ class BasePlotter(object):
                 # Get a good name to use
                 next_name = '{}-{}'.format(name, idx)
                 # Get the data object
-                if not is_pyvista_obj(mesh[idx]):
+                if not is_pyvista_dataset(mesh[idx]):
                     data = wrap(mesh.GetBlock(idx))
-                    if not is_pyvista_obj(mesh[idx]):
+                    if not is_pyvista_dataset(mesh[idx]):
                         continue # move on if we can't plot it
                 else:
                     data = mesh.GetBlock(idx)
@@ -660,7 +662,7 @@ class BasePlotter(object):
                     # or it could have zeros points (be empty) after filtering
                     continue
                 # Now check that scalars is available for this dataset
-                if isinstance(data, vtk.vtkMultiBlockDataSet) or get_scalar(data, scalars) is None:
+                if isinstance(data, vtk.vtkMultiBlockDataSet) or get_array(data, scalars) is None:
                     ts = None
                 else:
                     ts = scalars
@@ -733,7 +735,7 @@ class BasePlotter(object):
         if isinstance(scalars, str):
             self.mapper.SetArrayName(scalars)
             original_scalar_name = scalars
-            scalars = get_scalar(mesh, scalars,
+            scalars = get_array(mesh, scalars,
                     preference=kwargs.get('preference', 'cell'), err=True)
             if stitle is None:
                 stitle = original_scalar_name
@@ -763,7 +765,7 @@ class BasePlotter(object):
         if isinstance(opacity, str):
             try:
                 # Get array from mesh
-                opacity = get_scalar(mesh, opacity,
+                opacity = get_array(mesh, opacity,
                         preference=kwargs.get('preference', 'cell'), err=True)
                 opacity = normalize(opacity)
                 _custom_opac = True
@@ -842,10 +844,10 @@ class BasePlotter(object):
             def prepare_mapper(scalars):
                 # Scalar interpolation approach
                 if scalars.shape[0] == mesh.n_points:
-                    self.mesh._add_point_scalar(scalars, title, set_active)
+                    self.mesh._add_point_array(scalars, title, set_active)
                     self.mapper.SetScalarModeToUsePointData()
                 elif scalars.shape[0] == mesh.n_cells:
-                    self.mesh._add_cell_scalar(scalars, title, set_active)
+                    self.mesh._add_cell_array(scalars, title, set_active)
                     self.mapper.SetScalarModeToUseCellData()
                 else:
                     raise_not_matching(scalars, mesh)
@@ -1137,7 +1139,7 @@ class BasePlotter(object):
             show_scalar_bar = rcParams['show_scalar_bar']
 
         # Convert the VTK data object to a pyvista wrapped object if neccessary
-        if not is_pyvista_obj(volume):
+        if not is_pyvista_dataset(volume):
             if isinstance(volume, np.ndarray):
                 volume = wrap(volume)
                 if resolution is None:
@@ -1147,6 +1149,8 @@ class BasePlotter(object):
                 volume.spacing = resolution
             else:
                 volume = wrap(volume)
+                if not is_pyvista_dataset(volume):
+                    raise TypeError('Object type ({}) not supported for plotting in PyVista.'.format(type(volume)))
         else:
             # HACK: Make a copy so the original object is not altered
             volume = volume.copy()
@@ -1208,7 +1212,7 @@ class BasePlotter(object):
         set_active = False
         if isinstance(scalars, str):
             title = scalars
-            scalars = get_scalar(volume, scalars,
+            scalars = get_array(volume, scalars,
                     preference=kwargs.get('preference', 'point'), err=True)
             if stitle is None:
                 stitle = title
@@ -1241,10 +1245,10 @@ class BasePlotter(object):
 
         # Scalar interpolation approach
         if scalars.shape[0] == volume.n_points:
-            volume._add_point_scalar(scalars, title, set_active)
+            volume._add_point_array(scalars, title, set_active)
             self.mapper.SetScalarModeToUsePointData()
         elif scalars.shape[0] == volume.n_cells:
-            volume._add_cell_scalar(scalars, title, set_active)
+            volume._add_cell_array(scalars, title, set_active)
             self.mapper.SetScalarModeToUseCellData()
         else:
             raise_not_matching(scalars, volume)
@@ -2252,7 +2256,7 @@ class BasePlotter(object):
 
         if isinstance(scalars, str):
             # Grab scalar array if name given
-            scalars = get_scalar(mesh, scalars)
+            scalars = get_array(mesh, scalars)
 
         if scalars is None:
             if render:
@@ -2713,7 +2717,7 @@ class BasePlotter(object):
 
         if isinstance(points, np.ndarray):
             vtkpoints = pyvista.PolyData(points) # Cast to poly data
-        elif is_pyvista_obj(points):
+        elif is_pyvista_dataset(points):
             vtkpoints = pyvista.PolyData(points.points)
             if isinstance(labels, str):
                 labels = points.point_arrays[labels].astype(str)
@@ -2795,7 +2799,7 @@ class BasePlotter(object):
         fmt : str
             String formatter used to format numerical data
         """
-        if not is_pyvista_obj(points):
+        if not is_pyvista_dataset(points):
             raise TypeError('input points must be a pyvista dataset, not: {}'.format(type(points)))
         if not isinstance(labels, str):
             raise TypeError('labels must be a string name of the scalar array to use')
@@ -3327,7 +3331,7 @@ class BasePlotter(object):
             viewup = rcParams['camera']['viewup']
         if path is None:
             path = self.generate_orbital_path(viewup=viewup)
-        if not is_pyvista_obj(path):
+        if not is_pyvista_dataset(path):
             path = pyvista.PolyData(path)
         points = path.points
 
diff --git a/pyvista/utilities/utilities.py b/pyvista/utilities/utilities.py
index 43d8790a..feec94f4 100644
--- a/pyvista/utilities/utilities.py
+++ b/pyvista/utilities/utilities.py
@@ -20,6 +20,7 @@ from .fileio import get_ext, get_reader, standard_reader_routine
 POINT_DATA_FIELD = 0
 CELL_DATA_FIELD = 1
 FIELD_DATA_FIELD = 2
+ROW_DATA_FIELD = 6
 
 
 def get_vtk_type(typ):
@@ -121,7 +122,7 @@ def convert_array(arr, name=None, deep=0, array_type=None):
 
 
 
-def is_pyvista_obj(obj):
+def is_pyvista_dataset(obj):
     """ Return True if the Object is a PyVista wrapped dataset """
     return isinstance(obj, (pyvista.Common, pyvista.MultiBlock))
 
@@ -141,6 +142,10 @@ def cell_scalar(mesh, name):
     vtkarr = mesh.GetCellData().GetAbstractArray(name)
     return convert_array(vtkarr)
 
+def row_array(data_object, name):
+    """ Returns cell scalars of a vtk object """
+    vtkarr = data_object.GetRowData().GetAbstractArray(name)
+    return convert_array(vtkarr)
 
 def parse_field_choice(field):
     if isinstance(field, str):
@@ -151,6 +156,8 @@ def parse_field_choice(field):
             field = POINT_DATA_FIELD
         elif field in ['field', 'f', 'fields']:
             field = FIELD_DATA_FIELD
+        elif field in ['row', 'r',]:
+            field = ROW_DATA_FIELD
         else:
             raise RuntimeError('Data field ({}) not supported.'.format(field))
     elif isinstance(field, int):
@@ -160,7 +167,7 @@ def parse_field_choice(field):
     return field
 
 
-def get_scalar(mesh, name, preference='cell', info=False, err=False):
+def get_array(mesh, name, preference='cell', info=False, err=False):
     """ Searches point, cell and field data for an array
 
     Parameters
@@ -180,6 +187,15 @@ def get_scalar(mesh, name, preference='cell', info=False, err=False):
         Boolean to control whether to throw an error if array is not present.
 
     """
+    if isinstance(mesh, vtk.vtkTable):
+        arr = row_array(mesh, name)
+        if arr is None and err:
+            raise KeyError('Data scalar ({}) not present in this dataset.'.format(name))
+        field = ROW_DATA_FIELD
+        if info:
+            return arr, field
+        return arr
+
     parr = point_scalar(mesh, name)
     carr = cell_scalar(mesh, name)
     farr = field_scalar(mesh, name)
@@ -348,6 +364,7 @@ def wrap(vtkdataset):
         'vtkImageData' : pyvista.UniformGrid,
         'vtkStructuredPoints' : pyvista.UniformGrid,
         'vtkMultiBlockDataSet' : pyvista.MultiBlock,
+        'vtkTable' : pyvista.Table,
         # 'vtkParametricSpline' : pyvista.Spline,
         }
     # Otherwise, we assume a VTK data object was passed
@@ -445,6 +462,10 @@ def fit_plane_to_points(points, return_meta=False):
 
 
 def raise_not_matching(scalars, mesh):
+    if isinstance(mesh, vtk.vtkTable):
+        raise Exception('Number of scalars ({})'.format(scalars.size) +
+                        'must match number of rows ' +
+                        '({}).'.format(mesh.n_rows) )
     raise Exception('Number of scalars ({})'.format(scalars.size) +
                     'must match either the number of points ' +
                     '({}) '.format(mesh.n_points) +

vtkTable wrapper
I'm mulling over whether or not to implement a `vtkTable` wrapper in `vtki` and I'm curious about other's thoughts.

The `vtkTable` is a very common dataset I use and I have some functions in [PVGeo](https://github.com/OpenGeoVis/PVGeo) that will convert them back and forth to Pandas `DataFrame` objects (basically just tables with an awesome interface). 

My hesitations are:

1. `vtkTable`s are subclasses of `vtkDataObject`s not `vtkDataSet`s unlike everything else in `vtki`. This means we'd have to set up a new/separate module for DataObject wrappers.
2. We'd have to add Pandas as a dependency to the project (or maybe just an extra)
3. What would the interface look like? Would we build out a Pandas `DataFrame` with callbacks to the VTK data object?
4. Is this even worth doing considering it will be entirely different than everything else in `vtki` (as in it will be quite time consuming to get right)
diff --git a/tests/test_common.py b/tests/test_common.py
index df52bbfd..2ba8b680 100644
--- a/tests/test_common.py
+++ b/tests/test_common.py
@@ -20,7 +20,7 @@ def test_point_arrays():
 
     orig_value = grid.point_arrays[key][0]/1.0
     grid.point_arrays[key][0] += 1
-    assert orig_value == grid._point_scalar(key)[0] -1
+    assert orig_value == grid._point_array(key)[0] -1
 
     del grid.point_arrays[key]
     assert key not in grid.point_arrays
@@ -33,6 +33,10 @@ def test_point_arrays():
     grid.clear_point_arrays()
     assert len(grid.point_arrays.keys()) == 0
 
+    grid.point_arrays['list'] = np.arange(grid.n_points).tolist()
+    assert isinstance(grid.point_arrays['list'], np.ndarray)
+    assert np.allclose(grid.point_arrays['list'], np.arange(grid.n_points))
+
 
 def test_point_arrays_bad_value():
     grid = GRID.copy()
@@ -61,6 +65,10 @@ def test_cell_arrays():
 
     assert np.allclose(grid[key], np.arange(grid.n_cells))
 
+    grid.cell_arrays['list'] = np.arange(grid.n_cells).tolist()
+    assert isinstance(grid.cell_arrays['list'], np.ndarray)
+    assert np.allclose(grid.cell_arrays['list'], np.arange(grid.n_cells))
+
 
 def test_cell_arrays_bad_value():
     grid = GRID.copy()
@@ -74,19 +82,28 @@ def test_cell_arrays_bad_value():
 def test_field_arrays():
     key = 'test_array_field'
     grid = GRID.copy()
-    # Add array of lenght not equal to n_cells or n_points
+    # Add array of length not equal to n_cells or n_points
     n = grid.n_cells // 3
     grid.field_arrays[key] = np.arange(n)
     assert key in grid.field_arrays
     assert np.allclose(grid.field_arrays[key], np.arange(n))
+    assert np.allclose(grid[key], np.arange(n))
 
     orig_value = grid.field_arrays[key][0]/1.0
     grid.field_arrays[key][0] += 1
     assert orig_value == grid.field_arrays[key][0] -1
 
+    assert key in grid.array_names
+
     del grid.field_arrays[key]
     assert key not in grid.field_arrays
 
+    grid.field_arrays['list'] = np.arange(n).tolist()
+    assert isinstance(grid.field_arrays['list'], np.ndarray)
+    assert np.allclose(grid.field_arrays['list'], np.arange(n))
+
+
+
 
 def test_field_arrays_bad_value():
     grid = GRID.copy()
@@ -218,8 +235,8 @@ def test_points_np_bool():
     grid.point_arrays['bool_arr'] = bool_arr
     bool_arr[:] = True
     assert grid.point_arrays['bool_arr'].all()
-    assert grid._point_scalar('bool_arr').all()
-    assert grid._point_scalar('bool_arr').dtype == np.bool
+    assert grid._point_array('bool_arr').all()
+    assert grid._point_array('bool_arr').dtype == np.bool
 
 
 def test_cells_np_bool():
@@ -228,8 +245,18 @@ def test_cells_np_bool():
     grid.cell_arrays['bool_arr'] = bool_arr
     bool_arr[:] = True
     assert grid.cell_arrays['bool_arr'].all()
-    assert grid._cell_scalar('bool_arr').all()
-    assert grid._cell_scalar('bool_arr').dtype == np.bool
+    assert grid._cell_array('bool_arr').all()
+    assert grid._cell_array('bool_arr').dtype == np.bool
+
+
+def test_field_np_bool():
+    grid = GRID.copy()
+    bool_arr = np.zeros(grid.n_cells // 3, np.bool)
+    grid.field_arrays['bool_arr'] = bool_arr
+    bool_arr[:] = True
+    assert grid.field_arrays['bool_arr'].all()
+    assert grid._field_array('bool_arr').all()
+    assert grid._field_array('bool_arr').dtype == np.bool
 
 
 def test_cells_uint8():
@@ -248,6 +275,15 @@ def test_points_uint8():
     assert np.allclose(grid.point_arrays['arr'], np.arange(grid.n_points))
 
 
+def test_field_uint8():
+    grid = GRID.copy()
+    n = grid.n_points//3
+    arr = np.zeros(n, np.uint8)
+    grid.field_arrays['arr'] = arr
+    arr[:] = np.arange(n)
+    assert np.allclose(grid.field_arrays['arr'], np.arange(n))
+
+
 def test_bitarray_points():
     grid = GRID.copy()
     n = grid.n_points
@@ -280,6 +316,22 @@ def test_bitarray_cells():
     assert np.allclose(grid.cell_arrays['bint_arr'], np_array)
 
 
+def test_bitarray_field():
+    grid = GRID.copy()
+    n = grid.n_cells // 3
+    vtk_array = vtk.vtkBitArray()
+    np_array = np.empty(n, np.bool)
+    vtk_array.SetNumberOfTuples(n)
+    vtk_array.SetName('bint_arr')
+    for i in range(n):
+        value = i%2
+        vtk_array.SetValue(i, value)
+        np_array[i] = value
+
+    grid.GetFieldData().AddArray(vtk_array)
+    assert np.allclose(grid.field_arrays['bint_arr'], np_array)
+
+
 def test_html_repr():
     """
     This just tests to make sure no errors are thrown on the HTML
@@ -450,16 +502,27 @@ def test_rename_scalar_cell():
     assert old_name not in grid.cell_arrays
 
 
+def test_rename_scalar_field():
+    grid = GRID.copy()
+    grid.field_arrays['fieldfoo'] = np.array([8, 6, 7])
+    field_keys = list(grid.field_arrays.keys())
+    old_name = field_keys[0]
+    new_name = 'cell changed'
+    grid.rename_scalar(old_name, new_name)
+    assert new_name in grid.field_arrays
+    assert old_name not in grid.field_arrays
+
+
 def test_change_name_fail():
     grid = GRID.copy()
     with pytest.raises(RuntimeError):
         grid.rename_scalar('not a key', '')
 
 
-def test_get_cell_scalar_fail():
+def test_get_cell_array_fail():
     sphere = pyvista.Sphere()
     with pytest.raises(RuntimeError):
-        sphere._cell_scalar(name=None)
+        sphere._cell_array(name=None)
 
 
 def test_extent():
@@ -531,16 +594,39 @@ def test_scalars_dict_update():
         'rand' : np.random.random(mesh.n_points)
     }
     mesh.point_arrays.update(arrays)
-    assert 'foo' in mesh.scalar_names
-    assert 'rand' in mesh.scalar_names
+    assert 'foo' in mesh.array_names
+    assert 'rand' in mesh.array_names
+    assert len(mesh.point_arrays) == n + 2
+
+    # Test update from Table
+    table = pyvista.Table(arrays)
+    mesh = examples.load_uniform()
+    mesh.point_arrays.update(table)
+    assert 'foo' in mesh.array_names
+    assert 'rand' in mesh.array_names
     assert len(mesh.point_arrays) == n + 2
 
 
 def test_hanlde_array_with_null_name():
     poly = pyvista.PolyData()
-    # Add array with no name
+    # Add point array with no name
     poly.GetPointData().AddArray(pyvista.convert_array(np.array([])))
     html = poly._repr_html_()
     assert html is not None
     pdata = poly.point_arrays
     assert pdata is not None
+    assert len(pdata) == 1
+    # Add cell array with no name
+    poly.GetCellData().AddArray(pyvista.convert_array(np.array([])))
+    html = poly._repr_html_()
+    assert html is not None
+    cdata = poly.cell_arrays
+    assert cdata is not None
+    assert len(cdata) == 1
+    # Add field array with no name
+    poly.GetFieldData().AddArray(pyvista.convert_array(np.array([5, 6])))
+    html = poly._repr_html_()
+    assert html is not None
+    fdata = poly.field_arrays
+    assert fdata is not None
+    assert len(fdata) == 1
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 4c0cb8cb..19af6b9c 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -54,6 +54,9 @@ def test_clip_surface():
     dataset = pyvista.RectilinearGrid(xx, yy, zz)
     clipped = dataset.clip_surface(surface, invert=False)
     assert clipped.n_points < dataset.n_points
+    clipped = dataset.clip_surface(surface, invert=False, compute_distance=True)
+    assert clipped.n_points < dataset.n_points
+    assert 'implicit_distance' in clipped.array_names
 
 
 def test_slice_filter():
@@ -210,7 +213,7 @@ def test_elevation():
     dataset = examples.load_uniform()
     # Test default params
     elev = dataset.elevation()
-    assert 'Elevation' in elev.scalar_names
+    assert 'Elevation' in elev.array_names
     assert 'Elevation' == elev.active_scalar_name
     assert elev.get_data_range() == (dataset.bounds[4], dataset.bounds[5])
     # test vector args
@@ -218,21 +221,21 @@ def test_elevation():
     t = list(c) # cast so it doesnt point to `c`
     t[2] = dataset.bounds[-1]
     elev = dataset.elevation(low_point=c, high_point=t)
-    assert 'Elevation' in elev.scalar_names
+    assert 'Elevation' in elev.array_names
     assert 'Elevation' == elev.active_scalar_name
     assert elev.get_data_range() == (dataset.center[2], dataset.bounds[5])
     # Test not setting active
     elev = dataset.elevation(set_active=False)
-    assert 'Elevation' in elev.scalar_names
+    assert 'Elevation' in elev.array_names
     assert 'Elevation' != elev.active_scalar_name
     # Set use a range by scalar name
     elev = dataset.elevation(scalar_range='Spatial Point Data')
-    assert 'Elevation' in elev.scalar_names
+    assert 'Elevation' in elev.array_names
     assert 'Elevation' == elev.active_scalar_name
     assert dataset.get_data_range('Spatial Point Data') == (elev.get_data_range('Elevation'))
     # Set use a user defined range
     elev = dataset.elevation(scalar_range=[1.0, 100.0])
-    assert 'Elevation' in elev.scalar_names
+    assert 'Elevation' in elev.array_names
     assert 'Elevation' == elev.active_scalar_name
     assert elev.get_data_range('Elevation') == (1.0, 100.0)
     # test errors
@@ -255,10 +258,10 @@ def test_texture_map_to_plane():
     point_v = (bnds[0], bnds[3], bnds[4])
     out = dataset.texture_map_to_plane(origin=origin, point_u=point_u, point_v=point_v)
     assert isinstance(out, type(dataset))
-    assert 'Texture Coordinates' in out.scalar_names
+    assert 'Texture Coordinates' in out.array_names
     # FINAL: Test in place modifiacation
     dataset.texture_map_to_plane(inplace=True)
-    assert 'Texture Coordinates' in dataset.scalar_names
+    assert 'Texture Coordinates' in dataset.array_names
 
 
 
@@ -267,8 +270,8 @@ def test_compute_cell_sizes():
         result = dataset.compute_cell_sizes()
         assert result is not None
         assert isinstance(result, type(dataset))
-        assert 'Area' in result.scalar_names
-        assert 'Volume' in result.scalar_names
+        assert 'Area' in result.array_names
+        assert 'Volume' in result.array_names
     # Test the volume property
     grid = pyvista.UniformGrid((10,10,10))
     volume = float(np.prod(np.array(grid.dimensions) - 1))
@@ -386,7 +389,11 @@ def test_resample():
     data_to_probe = examples.load_uniform()
     result = mesh.sample(data_to_probe)
     name = 'Spatial Point Data'
-    assert name in result.scalar_names
+    assert name in result.array_names
+    assert isinstance(result, type(mesh))
+    result = mesh.sample(data_to_probe, tolerance=1.0)
+    name = 'Spatial Point Data'
+    assert name in result.array_names
     assert isinstance(result, type(mesh))
 
 
@@ -458,7 +465,7 @@ def test_select_enclosed_points():
     surf = pyvista.Sphere(center=mesh.center, radius=mesh.length/2.)
     result = mesh.select_enclosed_points(surf)
     assert isinstance(result, type(mesh))
-    assert 'SelectedPoints' in result.scalar_names
+    assert 'SelectedPoints' in result.array_names
     assert result.n_arrays == mesh.n_arrays + 1
     # Now check non-closed surface
     mesh = pyvista.ParametricEllipsoid(0.2, 0.7, 0.7, )
@@ -466,7 +473,7 @@ def test_select_enclosed_points():
     surf.rotate_x(90)
     result = mesh.select_enclosed_points(surf, check_surface=False)
     assert isinstance(result, type(mesh))
-    assert 'SelectedPoints' in result.scalar_names
+    assert 'SelectedPoints' in result.array_names
     assert result.n_arrays == mesh.n_arrays + 1
     with pytest.raises(RuntimeError):
         result = mesh.select_enclosed_points(surf, check_surface=True)
@@ -494,12 +501,14 @@ def test_merge_general():
 def test_compute_cell_quality():
     mesh = pyvista.ParametricEllipsoid().decimate(0.8)
     qual = mesh.compute_cell_quality()
-    assert 'CellQuality' in qual.scalar_names
+    assert 'CellQuality' in qual.array_names
+    with pytest.raises(KeyError):
+        qual = mesh.compute_cell_quality(quality_measure='foo')
 
 
 def test_compute_gradients():
     mesh = examples.load_random_hills()
     grad = mesh.compute_gradient()
-    assert 'gradient' in grad.scalar_names
+    assert 'gradient' in grad.array_names
     assert np.shape(grad['gradient'])[0] == mesh.n_points
     assert np.shape(grad['gradient'])[1] == 3
diff --git a/tests/test_grid.py b/tests/test_grid.py
index b58a5751..fa5860b5 100644
--- a/tests/test_grid.py
+++ b/tests/test_grid.py
@@ -276,6 +276,7 @@ def test_create_uniform_grid_from_specs():
     dims = [10, 10, 10]
     grid = pyvista.UniformGrid(dims) # Using default spacing and origin
     assert grid.dimensions == [10, 10, 10]
+    assert grid.extent == [0, 9, 0, 9, 0, 9]
     assert grid.origin == [0.0, 0.0, 0.0]
     assert grid.spacing == [1.0, 1.0, 1.0]
     spacing = [2, 1, 5]
diff --git a/tests/test_objects.py b/tests/test_objects.py
new file mode 100644
index 00000000..4129d498
--- /dev/null
+++ b/tests/test_objects.py
@@ -0,0 +1,196 @@
+"""
+Tests for non-spatially referenced ojects
+"""
+import sys
+
+import numpy as np
+import pytest
+import vtk
+
+import pyvista
+from pyvista import examples
+
+try:
+    import pandas as pd
+except ImportError:
+    pd = None
+
+
+
+def test_table_init(tmpdir):
+    """Save some delimited text to a file and read it"""
+    filename = str(tmpdir.mkdir("tmpdir").join('tmp.%s' % 'csv'))
+    nr, nc = 50, 3
+    arrays = np.random.rand(nr, nc)
+
+    # Create from 2D array
+    table = pyvista.Table(arrays)
+    assert table.n_rows == nr
+    assert table.n_columns == nc
+    assert table.n_arrays == nc
+
+    assert len(table.row_arrays) == nc
+    for i in range(nc):
+        assert np.allclose(arrays[:,i], table[i])
+
+
+    # create from dictionary
+    array_dict = {}
+    for i in range(nc):
+        array_dict['foo{}'.format(i)] = arrays[:, i].copy()
+    table = pyvista.Table(array_dict)
+    assert table.n_rows == nr
+    assert table.n_columns == nc
+
+    assert len(table.row_arrays) == nc
+    for i in range(nc):
+        assert np.allclose(arrays[:,i], table['foo{}'.format(i)])
+
+    dataset = examples.load_hexbeam()
+    array_dict = dataset.point_arrays
+    table = pyvista.Table(array_dict)
+    assert table.n_rows == dataset.n_points
+    assert table.n_columns == len(array_dict)
+
+    assert len(table.row_arrays) == len(array_dict)
+    for name in table.keys():
+        assert np.allclose(dataset[name], table[name])
+
+    # Create from vtkTable object
+    h = '\t'.join(['a{}'.format(i) for i in range(nc)])
+    np.savetxt(filename, arrays, delimiter='\t', header=h, comments='')
+
+    reader = vtk.vtkDelimitedTextReader()
+    reader.SetFileName(filename)
+    reader.DetectNumericColumnsOn()
+    reader.SetFieldDelimiterCharacters('\t')
+    reader.SetHaveHeaders(True)
+    reader.Update()
+
+    # Test init
+    table = pyvista.Table(reader.GetOutput())
+    assert isinstance(table, vtk.vtkTable)
+    assert isinstance(table, pyvista.Table)
+
+    # Test wrap
+    table = pyvista.wrap(reader.GetOutput())
+    assert isinstance(table, vtk.vtkTable)
+    assert isinstance(table, pyvista.Table)
+
+    assert table.n_rows == nr
+    assert table.n_columns == nc
+
+    assert len(table.row_arrays) == nc
+    for i in range(nc):
+        assert np.allclose(arrays[:,i], table[i])
+
+    return
+
+
+
+def test_table_row_arrays():
+    nr, nc = 50, 3
+    arrays = np.random.rand(nr, nc)
+    table = pyvista.Table()
+    for i in range(nc):
+        table['foo{}'.format(i)] = arrays[:, i]
+    assert table.n_columns == nc
+    assert table.n_rows == nr
+    for i in range(nc):
+        assert np.allclose(table['foo{}'.format(i)],  arrays[:, i])
+    # Multi component
+    table = pyvista.Table()
+    table['multi'] = arrays
+    assert table.n_columns == 1
+    assert table.n_rows == nr
+    assert np.allclose(table[0], arrays)
+    assert np.allclose(table['multi'], arrays)
+    del table['multi']
+    assert table.n_columns == 0
+
+    dataset = examples.load_hexbeam()
+    array_dict = dataset.point_arrays
+    # Test dict methods
+    table = pyvista.Table()
+    table.update(array_dict)
+    assert table.n_rows == dataset.n_points
+    assert table.n_columns == len(array_dict)
+
+    assert len(table.row_arrays) == len(array_dict)
+    for name in table.keys():
+        assert np.allclose(dataset[name], table[name])
+
+    for i, array in enumerate(table.values()):
+        name = table.keys()[i]
+        assert np.allclose(dataset[name], array)
+
+    for name, array in table.items():
+        assert np.allclose(dataset[name], array)
+
+    n = table.n_arrays
+    array = table.pop(table.keys()[0])
+    assert isinstance(array, np.ndarray)
+    assert table.n_arrays == n-1
+    array = table.get(table.keys()[0])
+    assert isinstance(array, np.ndarray)
+    assert table.n_arrays == n-1
+
+    del table[table.keys()[0]]
+    assert table.n_arrays == n-2
+
+    return
+
+
+def test_table_row_np_bool():
+    n = 50
+    table = pyvista.Table()
+    bool_arr = np.zeros(n, np.bool)
+    table.row_arrays['bool_arr'] = bool_arr
+    bool_arr[:] = True
+    assert table.row_arrays['bool_arr'].all()
+    assert table._row_array('bool_arr').all()
+    assert table._row_array('bool_arr').dtype == np.bool
+
+
+def test_table_row_uint8():
+    n = 50
+    table = pyvista.Table()
+    arr = np.zeros(n, np.uint8)
+    table.row_arrays['arr'] = arr
+    arr[:] = np.arange(n)
+    assert np.allclose(table.row_arrays['arr'], np.arange(n))
+
+
+def test_table_repr():
+    nr, nc = 50, 3
+    arrays = np.random.rand(nr, nc)
+    table = pyvista.Table(arrays)
+    text = table._repr_html_()
+    assert isinstance(text, str)
+    text = table.__repr__()
+    assert isinstance(text, str)
+    text = table.__str__()
+    assert isinstance(text, str)
+
+
+@pytest.mark.skipif(pd is None, reason="Requires Pandas")
+def test_table_pandas():
+    nr, nc = 50, 3
+    arrays = np.random.rand(nr, nc)
+    df = pd.DataFrame()
+    for i in range(nc):
+        df['foo{}'.format(i)] = arrays[:, i].copy()
+    table = pyvista.Table(df)
+    assert table.n_rows == nr
+    assert table.n_columns == nc
+    for i in range(nc):
+        assert np.allclose(table.row_arrays['foo{}'.format(i)], arrays[:, i])
+    assert df.equals(table.to_pandas())
+
+
+def test_table_iter():
+    nr, nc = 50, 3
+    arrays = np.random.rand(nr, nc)
+    table = pyvista.Table(arrays)
+    for i, array in enumerate(table):
+        assert np.allclose(array, arrays[:, i])
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
index bf06c33a..16d30996 100644
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -303,7 +303,7 @@ def test_update():
 
 
 @pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting")
-def test_plot_cell_scalars():
+def test_plot_cell_arrays():
     plotter = pyvista.Plotter(off_screen=OFF_SCREEN)
     scalars = np.arange(sphere.n_faces)
     plotter.add_mesh(sphere, interpolate_before_map=True, scalars=scalars,
@@ -410,7 +410,7 @@ def test_multi_block_plot():
     multi.append(examples.load_rectilinear())
     uni = examples.load_uniform()
     arr = np.random.rand(uni.n_cells)
-    uni._add_cell_scalar(arr, 'Random Data')
+    uni._add_cell_array(arr, 'Random Data')
     multi.append(uni)
     # And now add a data set without the desired array and a NULL component
     multi[3] = examples.load_airplane()
@@ -778,3 +778,14 @@ def test_plot_string_array():
     p = pyvista.Plotter(off_screen=OFF_SCREEN)
     p.add_mesh(mesh, scalars='labels')
     p.show()
+
+
+@pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting")
+def test_fail_plot_table():
+    """Make sure tables cannot be plotted"""
+    table = pyvista.Table(np.random.rand(50, 3))
+    with pytest.raises(TypeError):
+        pyvista.plot(table)
+    with pytest.raises(TypeError):
+        plotter = pyvista.Plotter(off_screen=OFF_SCREEN)
+        plotter.add_mesh(table)
diff --git a/tests/test_utilities.py b/tests/test_utilities.py
index 362dbdae..aeeb8c57 100644
--- a/tests/test_utilities.py
+++ b/tests/test_utilities.py
@@ -60,24 +60,24 @@ def test_read(tmpdir):
         data = pyvista.read('this_file_totally_does_not_exist.vtk')
 
 
-def test_get_scalar():
+def test_get_array():
     grid = pyvista.UnstructuredGrid(ex.hexbeamfile)
     # add array to both point/cell data with same name
     carr = np.random.rand(grid.n_cells)
-    grid._add_cell_scalar(carr, 'test_data')
+    grid._add_cell_array(carr, 'test_data')
     parr = np.random.rand(grid.n_points)
-    grid._add_point_scalar(parr, 'test_data')
+    grid._add_point_array(parr, 'test_data')
     # add other data
     oarr = np.random.rand(grid.n_points)
-    grid._add_point_scalar(oarr, 'other')
+    grid._add_point_array(oarr, 'other')
     farr = np.random.rand(grid.n_points * grid.n_cells)
-    grid._add_field_scalar(farr, 'field_data')
-    assert np.allclose(carr, utilities.get_scalar(grid, 'test_data', preference='cell'))
-    assert np.allclose(parr, utilities.get_scalar(grid, 'test_data', preference='point'))
-    assert np.allclose(oarr, utilities.get_scalar(grid, 'other'))
-    assert None == utilities.get_scalar(grid, 'foo')
-    assert utilities.get_scalar(grid, 'test_data', preference='field') is None
-    assert np.allclose(farr, utilities.get_scalar(grid, 'field_data', preference='field'))
+    grid._add_field_array(farr, 'field_data')
+    assert np.allclose(carr, utilities.get_array(grid, 'test_data', preference='cell'))
+    assert np.allclose(parr, utilities.get_array(grid, 'test_data', preference='point'))
+    assert np.allclose(oarr, utilities.get_array(grid, 'other'))
+    assert None == utilities.get_array(grid, 'foo')
+    assert utilities.get_array(grid, 'test_data', preference='field') is None
+    assert np.allclose(farr, utilities.get_array(grid, 'field_data', preference='field'))
 
 
 

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y libgl1-mesa-dev xvfb
3.7
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
appdirs==1.4.4
attrs==24.2.0
backcall==0.2.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
cmocean==2.0
codecov==2.1.13
colorcet==3.1.0
comm==0.1.4
coverage==7.2.7
cycler==0.11.0
debugpy==1.7.0
decorator==5.1.1
entrypoints==0.4
exceptiongroup==1.2.2
fastjsonschema==2.21.1
fonttools==4.38.0
idna==3.10
imageio==2.31.2
imageio-ffmpeg==0.5.1
importlib-metadata==6.7.0
importlib-resources==5.12.0
iniconfig==2.0.0
ipykernel==6.16.2
ipython==7.34.0
ipywidgets==8.1.5
jedi==0.19.2
jsonschema==4.17.3
jupyter_client==7.4.9
jupyter_core==4.12.0
jupyterlab_widgets==3.0.13
kiwisolver==1.4.5
matplotlib==3.5.3
matplotlib-inline==0.1.6
nbformat==5.8.0
nbval==0.11.0
nest-asyncio==1.6.0
numpy==1.21.6
packaging==24.0
parso==0.8.4
pexpect==4.9.0
pickleshare==0.7.5
Pillow==9.5.0
pkgutil_resolve_name==1.3.10
pluggy==1.2.0
prompt_toolkit==3.0.48
psutil==7.0.0
ptyprocess==0.7.0
Pygments==2.17.2
pyparsing==3.1.4
PyQt5==5.11.3
PyQt5_sip==4.19.19
pyrsistent==0.19.3
pytest==7.4.4
pytest-cov==4.1.0
pytest-memprof==0.2.0
pytest-qt==4.2.0
python-dateutil==2.9.0.post0
-e git+https://github.com/pyvista/pyvista.git@b7a336abc412474a99f5a70522fd41cdb68343af#egg=pyvista
pyzmq==26.2.1
requests==2.31.0
scooby==0.7.3
six==1.17.0
tomli==2.0.1
tornado==6.2
traitlets==5.9.0
typing_extensions==4.7.1
urllib3==2.0.7
vtk==9.3.1
wcwidth==0.2.13
widgetsnbextension==4.0.13
zipp==3.15.0

name: pyvista
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=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - 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=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - appdirs==1.4.4
      - attrs==24.2.0
      - backcall==0.2.0
      - charset-normalizer==3.4.1
      - cmocean==2.0
      - codecov==2.1.13
      - colorcet==3.1.0
      - comm==0.1.4
      - coverage==7.2.7
      - cycler==0.11.0
      - debugpy==1.7.0
      - decorator==5.1.1
      - entrypoints==0.4
      - exceptiongroup==1.2.2
      - fastjsonschema==2.21.1
      - fonttools==4.38.0
      - idna==3.10
      - imageio==2.31.2
      - imageio-ffmpeg==0.5.1
      - importlib-metadata==6.7.0
      - importlib-resources==5.12.0
      - iniconfig==2.0.0
      - ipykernel==6.16.2
      - ipython==7.34.0
      - ipywidgets==8.1.5
      - jedi==0.19.2
      - jsonschema==4.17.3
      - jupyter-client==7.4.9
      - jupyter-core==4.12.0
      - jupyterlab-widgets==3.0.13
      - kiwisolver==1.4.5
      - matplotlib==3.5.3
      - matplotlib-inline==0.1.6
      - nbformat==5.8.0
      - nbval==0.11.0
      - nest-asyncio==1.6.0
      - numpy==1.21.6
      - packaging==24.0
      - parso==0.8.4
      - pexpect==4.9.0
      - pickleshare==0.7.5
      - pillow==9.5.0
      - pkgutil-resolve-name==1.3.10
      - pluggy==1.2.0
      - prompt-toolkit==3.0.48
      - psutil==7.0.0
      - ptyprocess==0.7.0
      - pygments==2.17.2
      - pyparsing==3.1.4
      - pyqt5==5.11.3
      - pyqt5-sip==4.19.19
      - pyrsistent==0.19.3
      - pytest==7.4.4
      - pytest-cov==4.1.0
      - pytest-memprof==0.2.0
      - pytest-qt==4.2.0
      - python-dateutil==2.9.0.post0
      - pyzmq==26.2.1
      - requests==2.31.0
      - scooby==0.7.3
      - six==1.17.0
      - tomli==2.0.1
      - tornado==6.2
      - traitlets==5.9.0
      - typing-extensions==4.7.1
      - urllib3==2.0.7
      - vtk==9.3.1
      - wcwidth==0.2.13
      - widgetsnbextension==4.0.13
      - zipp==3.15.0
prefix: /opt/conda/envs/pyvista

tests/test_common.py::test_point_arrays tests/test_common.py::test_cell_arrays tests/test_common.py::test_field_arrays tests/test_common.py::test_points_np_bool tests/test_common.py::test_cells_np_bool tests/test_common.py::test_field_np_bool tests/test_common.py::test_get_cell_array_fail tests/test_common.py::test_scalars_dict_update tests/test_filters.py::test_clip_surface tests/test_filters.py::test_elevation tests/test_filters.py::test_texture_map_to_plane tests/test_filters.py::test_compute_cell_sizes tests/test_filters.py::test_resample tests/test_filters.py::test_select_enclosed_points tests/test_filters.py::test_compute_gradients tests/test_objects.py::test_table_init tests/test_objects.py::test_table_row_arrays tests/test_objects.py::test_table_row_np_bool tests/test_objects.py::test_table_row_uint8 tests/test_objects.py::test_table_repr tests/test_objects.py::test_table_iter tests/test_utilities.py::test_get_array
tests/test_common.py::test_string_arrays tests/test_filters.py::test_threshold tests/test_filters.py::test_threshold_percent tests/test_filters.py::test_outline tests/test_filters.py::test_split_and_connectivity tests/test_filters.py::test_delaunay_3d tests/test_filters.py::test_smooth tests/test_filters.py::test_merge_general tests/test_filters.py::test_compute_cell_quality tests/test_grid.py::test_init_from_arrays
tests/test_common.py::test_point_arrays_bad_value tests/test_common.py::test_cell_arrays_bad_value tests/test_common.py::test_field_arrays_bad_value tests/test_common.py::test_copy tests/test_common.py::test_transform tests/test_common.py::test_transform_errors tests/test_common.py::test_translate tests/test_common.py::test_rotate_x tests/test_common.py::test_rotate_y tests/test_common.py::test_rotate_z tests/test_common.py::test_make_points_double tests/test_common.py::test_invalid_points tests/test_common.py::test_cells_uint8 tests/test_common.py::test_points_uint8 tests/test_common.py::test_field_uint8 tests/test_common.py::test_bitarray_points tests/test_common.py::test_bitarray_cells tests/test_common.py::test_bitarray_field tests/test_common.py::test_html_repr tests/test_common.py::test_print_repr tests/test_common.py::test_texture tests/test_common.py::test_invalid_vector tests/test_common.py::test_no_t_coords tests/test_common.py::test_no_arrows tests/test_common.py::test_arrows tests/test_common.py::test_set_active_vectors_name tests/test_common.py::test_set_active_scalars_name tests/test_common.py::test_set_t_coords tests/test_common.py::test_activate_texture_none tests/test_common.py::test_set_active_vectors_fail tests/test_common.py::test_set_active_scalars tests/test_common.py::test_set_active_scalar_name tests/test_common.py::test_rename_scalar_point tests/test_common.py::test_rename_scalar_cell tests/test_common.py::test_rename_scalar_field tests/test_common.py::test_change_name_fail tests/test_common.py::test_extent tests/test_common.py::test_axis_rotation_invalid tests/test_common.py::test_axis_rotation_not_inplace tests/test_common.py::test_bad_instantiation tests/test_common.py::test_clear_arrays tests/test_common.py::test_hanlde_array_with_null_name tests/test_filters.py::test_clip_filter tests/test_filters.py::test_clip_box tests/test_filters.py::test_slice_filter tests/test_filters.py::test_slice_orthogonal_filter tests/test_filters.py::test_slice_along_axis tests/test_filters.py::test_outline_corners tests/test_filters.py::test_extract_geometry tests/test_filters.py::test_wireframe tests/test_filters.py::test_contour tests/test_filters.py::test_cell_centers tests/test_filters.py::test_glyph tests/test_filters.py::test_warp_by_scalar tests/test_filters.py::test_cell_data_to_point_data tests/test_filters.py::test_point_data_to_cell_data tests/test_filters.py::test_triangulate tests/test_filters.py::test_streamlines tests/test_filters.py::test_plot_over_line tests/test_filters.py::test_slice_along_line tests/test_filters.py::test_interpolate tests/test_filters.py::test_decimate_boundary tests/test_grid.py::test_volume tests/test_grid.py::test_init_from_structured tests/test_grid.py::test_init_from_unstructured tests/test_grid.py::test_init_bad_input tests/test_grid.py::test_surface_indices tests/test_grid.py::test_extract_edges tests/test_grid.py::test_save[vtu-True] tests/test_grid.py::test_save[vtu-False] tests/test_grid.py::test_save[vtk-True] tests/test_grid.py::test_save[vtk-False] tests/test_grid.py::test_init_bad_filename tests/test_grid.py::test_save_bad_extension tests/test_grid.py::test_linear_copy tests/test_grid.py::test_extract_cells tests/test_grid.py::test_merge tests/test_grid.py::test_merge_not_main tests/test_grid.py::test_merge_list tests/test_grid.py::test_init_structured tests/test_grid.py::test_invalid_init_structured tests/test_grid.py::test_save_structured[vts-True] tests/test_grid.py::test_save_structured[vts-False] tests/test_grid.py::test_save_structured[vtk-True] tests/test_grid.py::test_save_structured[vtk-False] tests/test_grid.py::test_load_structured_bad_filename tests/test_grid.py::test_create_rectilinear_grid_from_specs tests/test_grid.py::test_create_rectilinear_grid_from_file tests/test_grid.py::test_read_rectilinear_grid_from_file tests/test_grid.py::test_create_uniform_grid_from_specs tests/test_grid.py::test_uniform_setters tests/test_grid.py::test_create_uniform_grid_from_file tests/test_grid.py::test_read_uniform_grid_from_file tests/test_grid.py::test_cast_uniform_to_structured tests/test_grid.py::test_save_rectilinear[vtr-True] tests/test_grid.py::test_save_rectilinear[vtr-False] tests/test_grid.py::test_save_rectilinear[vtk-True] tests/test_grid.py::test_save_rectilinear[vtk-False] tests/test_grid.py::test_save_uniform[vti-True] tests/test_grid.py::test_save_uniform[vti-False] tests/test_grid.py::test_save_uniform[vtk-True] tests/test_grid.py::test_save_uniform[vtk-False] tests/test_grid.py::test_grid_points tests/test_grid.py::test_grid_extract_selection_points tests/test_plotting.py::test_invalid_color tests/test_plotting.py::test_invalid_font tests/test_plotting.py::test_themes tests/test_plotting.py::test_opacity_transfer_functions tests/test_utilities.py::test_createvectorpolydata_error tests/test_utilities.py::test_createvectorpolydata_1D tests/test_utilities.py::test_createvectorpolydata tests/test_utilities.py::test_read tests/test_utilities.py::test_is_inside_bounds tests/test_utilities.py::test_get_sg_image_scraper tests/test_utilities.py::test_voxelize tests/test_utilities.py::test_report
examples/02-plot/opacity.py docs/utilities/utilities.rst pyvista/core/common.py pyvista/plotting/helpers.py pyvista/core/filters.py .travis.yml pyvista/core/__init__.py pyvista/plotting/plotting.py pyvista/core/pointset.py pyvista/core/composite.py pyvista/utilities/utilities.py pyvista/core/objects.py pyvista/plotting/ipy_tools.py
examples/02-plot/opacity.py docs/utilities/utilities.rst pyvista/core/common.py pyvista/plotting/helpers.py pyvista/core/filters.py .travis.yml pyvista/core/__init__.py pyvista/plotting/plotting.py pyvista/core/pointset.py pyvista/core/composite.py pyvista/utilities/utilities.py pyvista/core/objects.py pyvista/plotting/ipy_tools.py