Repository: pyvista/pyvista

Issue:
Export as svg?
How do I export a plot as a .svg rather than .png?

Thanks!

Discussion:
banesullivan: Note, this might need to be refactored to `export_graphic` considering this is actually an export method. This would also be in line with ParaView's convention that saving as an SVG is available under **Export Scene**
codecov-io: # [Codecov](https://codecov.io/gh/pyvista/pyvista/pull/331?src=pr&el=h1) Report
> Merging [#331](https://codecov.io/gh/pyvista/pyvista/pull/331?src=pr&el=desc) into [master](https://codecov.io/gh/pyvista/pyvista/commit/d4805249c937c08659e1f7a0c11f26c80aff231d?src=pr&el=desc) will **decrease** coverage by `0.24%`.
> The diff coverage is `15.38%`.

```diff
@@            Coverage Diff             @@
##           master     #331      +/-   ##
==========================================
- Coverage   84.49%   84.24%   -0.25%     
==========================================
  Files          30       30              
  Lines        7042     7065      +23     
==========================================
+ Hits         5950     5952       +2     
- Misses       1092     1113      +21
```


Patch:
diff --git a/docs/getting-started/what-is-a-mesh.rst b/docs/getting-started/what-is-a-mesh.rst
new file mode 100644
index 00000000..d8e25124
--- /dev/null
+++ b/docs/getting-started/what-is-a-mesh.rst
@@ -0,0 +1,188 @@
+.. _what_is_a_mesh:
+
+What is a Mesh?
+===============
+
+In PyVista, a mesh is any spatially referenced information and usually consists
+of geometrical representations of a surface or volume in 3D space.
+We commonly refer to any spatially referenced dataset as a mesh, so often the
+distinction between a mesh, a grid, and a volume can get fuzzy - but that does
+not matter in PyVista. If you have a dataset that is a surface mesh with 2D
+geometries like triangles, we call it a mesh and if you have a dataset with
+3D geometries like voxels, tetrahedrals, hexahedrons, etc., then we also call
+that a mesh! Why? Because it is simple that way.
+
+In all spatially referenced datasets, there lies an underlying mesh structure
+- the connectivity or geometry between nodes to define cells. Whether those
+cells are 2D or 3D is not always of importance and we've worked hard to make
+PyVista work for datasets of either or mixed geometries so that you as a user
+do not have to get get bogged down in the nuances.
+
+What is a Node?
+---------------
+
+Nodes are the vertices of the mesh - the XYZ coordinates of the the underlying
+structure. This aspect of a mesh is what is often worked with in Finite Element
+modeling. All PyVista datasets (meshes!) have nodes and sometimes, you can
+have a mesh that only has nodes - like a point cloud.
+
+For example, you can create a point cloud mesh using the
+:class:`pyvista.PolyData` class which is built for meshes that have 1D and 2D
+cell types (we'll get into what a cell is briefly).
+
+Let's start with a point cloud - this is a mesh type that only has vertices.
+You can create one by defining a 2D array XYZ coordinates like so:
+
+
+.. testcode:: python
+
+    import numpy as np
+    import pyvista as pv
+
+    nodes = np.random.rand(100, 3)
+    mesh = pv.PolyData(nodes)
+    mesh.plot(point_size=10, screenshot='random_nodes.png')
+
+
+.. image:: ../images/auto-generated/random_nodes.png
+
+
+But it's import to note that most meshes have some sort of connectivity between
+nodes such as this gridded mesh:
+
+.. testcode:: python
+
+    from pyvista import examples
+
+    mesh = examples.load_hexbeam()
+    bcpos = [(6.20, 3.00, 7.50),
+             (0.16, 0.13, 2.65),
+             (-0.28, 0.94, -0.21)]
+
+    p = pv.Plotter()
+    p.add_mesh(mesh, show_edges=True, color='white')
+    p.add_mesh(pv.PolyData(mesh.points), color='red',
+           point_size=10, render_points_as_spheres=True)
+    p.camera_position = [(6.20, 3.00, 7.50),
+                     (0.16, 0.13, 2.65),
+                     (-0.28, 0.94, -0.21)]
+    p.show(screenshot='beam_nodes.png')
+
+
+.. image:: ../images/auto-generated/beam_nodes.png
+
+
+Or this triangulated surface:
+
+.. testcode:: python
+
+    mesh = examples.download_bunny_coarse()
+
+    p = pv.Plotter()
+    p.add_mesh(mesh, show_edges=True, color='white')
+    p.add_mesh(pv.PolyData(mesh.points), color='red',
+           point_size=10, render_points_as_spheres=True)
+    p.camera_position = [(0.02, 0.30, 0.73),
+                     (0.02, 0.03, -0.022),
+                     (-0.03, 0.94, -0.34)]
+    p.show(screenshot='bunny_nodes.png')
+
+
+.. image:: ../images/auto-generated/bunny_nodes.png
+
+
+What is a Cell?
+---------------
+
+A cell is the geometry between nodes that defines the connectivity or topology
+of a mesh. In the examples above, cells are defined by the lines
+(edges colored in black) connecting nodes (colored in red).
+For example, a cell in the beam example is a a voxel defined by region
+between four nodes in that mesh:
+
+
+
+.. testcode:: python
+
+    mesh = examples.load_hexbeam()
+
+    p = pv.Plotter()
+    p.add_mesh(mesh, show_edges=True, color='white')
+    p.add_mesh(pv.PolyData(mesh.points), color='red',
+           point_size=10, render_points_as_spheres=True)
+
+    p.add_mesh(mesh.extract_cells(mesh.n_cells-1),
+               color='pink', edge_color='blue',
+               line_width=5, show_edges=True)
+
+    p.camera_position = [(6.20, 3.00, 7.50),
+                     (0.16, 0.13, 2.65),
+                     (-0.28, 0.94, -0.21)]
+    p.show(screenshot='beam_cell.png')
+
+
+.. image:: ../images/auto-generated/beam_cell.png
+
+
+Cells aren't limited to voxels, they could be a triangle between three nodes,
+a line between two nodes, or even a single node could be its own cell (but
+that's a special case).
+
+
+
+What are attributes?
+--------------------
+
+Attributes a data values that live on either the nodes or cells of a mesh. In
+PyVista, we work with both point data and cell data and allow easy access to
+data dictionaries to hold arrays for attributes that live either on all nodes
+or on all cells of a mesh. These attributes can be accessed by dictionaries
+attached to any PyVista mesh called ``.point_arrays`` or ``.cell_arrays``.
+
+
+Point data refers to arrays of values (scalars, vectors, etc.) that
+live on each node of the mesh. The order of this array is crucial! Each element
+in an attribute array must correspond to a node or cell in the mesh.
+Let's create some point data for the beam mesh.
+When plotting the values between nodes are interpolated across the cells.
+
+.. testcode:: python
+
+    mesh.point_arrays['my point values'] = np.arange(mesh.n_points)
+
+    mesh.plot(scalars='my point values', cpos=bcpos,
+              show_edges=True, screenshot='beam_point_data.png')
+
+
+.. image:: ../images/auto-generated/beam_point_data.png
+
+Cell data refers to arrays of values (scalars, vectors, etc.) that
+live on each cell of the mesh. That is the entire cell (2D face or 3D volume)
+has is assigned the value of that attribute.
+
+.. testcode:: python
+
+    mesh.cell_arrays['my cell values'] = np.arange(mesh.n_cells)
+
+    mesh.plot(scalars='my cell values', cpos=bcpos,
+              show_edges=True, screenshot='beam_cell_data.png')
+
+
+.. image:: ../images/auto-generated/beam_cell_data.png
+
+
+Here's a comparison of point data vs. cell data and how point data is
+interpolated across cells when mapping colors:
+
+.. testcode:: python
+
+    mesh = examples.load_uniform()
+
+    p = pv.Plotter(shape=(1,2))
+    p.add_mesh(mesh, scalars='Spatial Point Data', show_edges=True)
+    p.subplot(0,1)
+    p.add_mesh(mesh, scalars='Spatial Cell Data', show_edges=True)
+    p.show(screenshot='point_vs_cell_data.png')
+
+
+.. image:: ../images/auto-generated/point_vs_cell_data.png
diff --git a/docs/index.rst b/docs/index.rst
index 2a89f87e..06bea4e8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -166,6 +166,7 @@ for 3D visualization in our `external examples list <./external_examples.html>`_
    :hidden:
 
    getting-started/installation
+   getting-started/what-is-a-mesh
    getting-started/simple
    examples/index
    external_examples
diff --git a/pyvista/core/filters.py b/pyvista/core/filters.py
index 4acde0da..bebd9e99 100644
--- a/pyvista/core/filters.py
+++ b/pyvista/core/filters.py
@@ -663,7 +663,7 @@ class DataSetFilters(object):
 
     def contour(dataset, isosurfaces=10, scalars=None, compute_normals=False,
                 compute_gradients=False, compute_scalars=True, rng=None,
-                preference='point'):
+                preference='point', method='contour'):
         """Contours an input dataset by an array. ``isosurfaces`` can be an integer
         specifying the number of isosurfaces in the data range or an iterable set of
         values for explicitly setting the isosurfaces.
@@ -694,11 +694,23 @@ class DataSetFilters(object):
             When scalars is specified, this is the preferred scalar type to
             search for in the dataset.  Must be either ``'point'`` or ``'cell'``
 
+        method : str, optional
+            Specify to choose which vtk filter is used to create the contour.
+            Must be one of ``'contour'``, ``'marching_cubes'`` and
+            ``'flying_edges'``. Defaults to ``'contour'``.
+
         """
+        if method is None or method == 'contour':
+            alg = vtk.vtkContourFilter()
+        elif method == 'marching_cubes':
+            alg = vtk.vtkMarchingCubes()
+        elif method == 'flying_edges':
+            alg = vtk.vtkFlyingEdges3D()
+        else:
+            raise RuntimeError("Method '{}' is not supported".format(method))
         # Make sure the input has scalars to contour on
         if dataset.n_arrays < 1:
             raise AssertionError('Input dataset for the contour filter must have scalar data.')
-        alg = vtk.vtkContourFilter()
         alg.SetInputDataObject(dataset)
         alg.SetComputeNormals(compute_normals)
         alg.SetComputeGradients(compute_gradients)
diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py
index 55784a21..9c8c08fc 100644
--- a/pyvista/plotting/plotting.py
+++ b/pyvista/plotting/plotting.py
@@ -2831,7 +2831,8 @@ class BasePlotter(PickingHelper, WidgetHelper):
                          show_points=True, point_color=None, point_size=5,
                          name=None, shape_color='grey', shape='rounded_rect',
                          fill_shape=True, margin=3, shape_opacity=1.0,
-                         pickable=True, render_points_as_spheres=False, **kwargs):
+                         pickable=False, render_points_as_spheres=False,
+                         tolerance=0.001, **kwargs):
         """
         Creates a point actor with one label from list labels assigned to
         each point.
@@ -2904,6 +2905,12 @@ class BasePlotter(PickingHelper, WidgetHelper):
         shape_opacity : flaot
             The opacity of the shape between zero and one.
 
+        tolerance : float
+            a tolerance to use to determine whether a point label is visible.
+            A tolerance is usually required because the conversion from world
+            space to display space during rendering introduces numerical
+            round-off.
+
         Returns
         -------
         labelMapper : vtk.vtkvtkLabeledDataMapper
@@ -2946,10 +2953,15 @@ class BasePlotter(PickingHelper, WidgetHelper):
             vtklabels.InsertNextValue(str(item))
         vtkpoints.GetPointData().AddArray(vtklabels)
 
+        # Only show visible points
+        vis_points = vtk.vtkSelectVisiblePoints()
+        vis_points.SetInputData(vtkpoints)
+        vis_points.SetRenderer(self.renderer)
+        vis_points.SetTolerance(tolerance)
+
         # Create heirarchy
         hier = vtk.vtkPointSetToLabelHierarchy()
-        hier.SetInputData(vtkpoints)
-        # hier.SetOrientationArrayName('orientation')
+        hier.SetInputConnection(vis_points.GetOutputPort())
         hier.SetLabelArrayName('labels')
 
         # create label mapper
@@ -3067,7 +3079,6 @@ class BasePlotter(PickingHelper, WidgetHelper):
         """Internal helper for saving a NumPy image array"""
         if not image.size:
             raise Exception('Empty image.  Have you run plot() first?')
-
         # write screenshot to file
         supported_formats = [".png", ".jpeg", ".jpg", ".bmp", ".tif", ".tiff"]
         if isinstance(filename, str):
@@ -3075,12 +3086,46 @@ class BasePlotter(PickingHelper, WidgetHelper):
                 filename = os.path.join(pyvista.FIGURE_PATH, filename)
             if not any([filename.lower().endswith(ext) for ext in supported_formats]):
                 filename += ".png"
+            filename = os.path.abspath(os.path.expanduser(filename))
+            w = imageio.imwrite(filename, image)
             if not return_img:
-                return imageio.imwrite(filename, image)
-            imageio.imwrite(filename, image)
-
+                return w
         return image
 
+
+    def save_graphic(self, filename, title='PyVista Export', raster=True, painter=True):
+        """Save a screenshot of the rendering window as a graphic file:
+        '.svg', '.eps', '.ps', '.pdf', '.tex'
+        """
+        if not hasattr(self, 'ren_win'):
+            raise AttributeError('This plotter is closed and unable to save a screenshot.')
+        if isinstance(pyvista.FIGURE_PATH, str) and not os.path.isabs(filename):
+            filename = os.path.join(pyvista.FIGURE_PATH, filename)
+        filename = os.path.abspath(os.path.expanduser(filename))
+        extension = pyvista.fileio.get_ext(filename)
+        valid = ['.svg', '.eps', '.ps', '.pdf', '.tex']
+        if extension not in valid:
+            raise RuntimeError('Extension ({}) is an invalid choice. Valid options include: {}'.format(extension, ', '.join(valid)))
+        writer = vtk.vtkGL2PSExporter()
+        modes = {
+            '.svg': writer.SetFileFormatToSVG,
+            '.eps': writer.SetFileFormatToEPS,
+            '.ps': writer.SetFileFormatToPS,
+            '.pdf': writer.SetFileFormatToPDF,
+            '.tex': writer.SetFileFormatToTeX,
+        }
+        writer.CompressOff()
+        writer.SetFilePrefix(filename.replace(extension, ''))
+        writer.SetInput(self.ren_win)
+        modes[extension]()
+        writer.SetTitle(title)
+        writer.SetWrite3DPropsAsRasterImage(raster)
+        if painter:
+            writer.UsePainterSettings()
+        writer.Update()
+        return
+
+
     def screenshot(self, filename=None, transparent_background=None,
                    return_img=None, window_size=None):
         """
@@ -3130,7 +3175,7 @@ class BasePlotter(PickingHelper, WidgetHelper):
                 # Save last image
                 return self._save_image(self.last_image, filename, return_img)
             # Plotter hasn't been rendered or was improperly closed
-            raise AttributeError('This plotter is unable to save a screenshot.')
+            raise AttributeError('This plotter is closed and unable to save a screenshot.')
 
         if isinstance(self, Plotter):
             # TODO: we need a consistent rendering function


Test patch:
diff --git a/tests/test_filters.py b/tests/test_filters.py
index b25ae4d5..8bd67445 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -231,12 +231,13 @@ def test_wireframe_composite():
     output = COMPOSITE.wireframe()
     assert output.n_blocks == COMPOSITE.n_blocks
 
-
-def test_contour():
+@pytest.mark.parametrize('method', ['contour', 'marching_cubes',
+                                    'flying_edges'])
+def test_contour(method):
     dataset = examples.load_uniform()
-    iso = dataset.contour()
+    iso = dataset.contour(method=method)
     assert iso is not None
-    iso = dataset.contour(isosurfaces=[100, 300, 500])
+    iso = dataset.contour(isosurfaces=[100, 300, 500], method=method)
     assert iso is not None
     with pytest.raises(AssertionError):
         result = dataset.contour(scalars='Spatial Cell Data')
diff --git a/tests/test_plotting.py b/tests/test_plotting.py
index 92d8428e..572bc083 100644
--- a/tests/test_plotting.py
+++ b/tests/test_plotting.py
@@ -368,13 +368,15 @@ def test_box_axes():
 
 
 @pytest.mark.skipif(NO_PLOTTING, reason="Requires system to support plotting")
-def test_screenshot():
+def test_screenshot(tmpdir):
     plotter = pyvista.Plotter(off_screen=True)
     plotter.add_mesh(pyvista.Sphere())
     img = plotter.screenshot(transparent_background=True)
     assert np.any(img)
     img_again = plotter.screenshot()
     assert np.any(img_again)
+    filename = str(tmpdir.mkdir("tmpdir").join('export-graphic.svg'))
+    plotter.save_graphic(filename)
 
     # checking if plotter closes
     ref = proxy(plotter)