Repository: pyvista/pyvista

Issue:
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)

Discussion:
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
```


Patch:
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) +


Test patch:
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'))