Repository: UXARRAY/uxarray

Issue:
Matplotlib Visualization Usage Example 

Discussion:
review-notebook-app[bot]: Check out this pull request on&nbsp; <a href="https://app.reviewnb.com/UXARRAY/uxarray/pull/844"><img align="absmiddle"  alt="ReviewNB" height="28" class="BotMessageButtonImage" src="https://raw.githubusercontent.com/ReviewNB/support/master/images/button_reviewnb.png"/></a> 

 See visual diffs & provide feedback on Jupyter Notebooks. 

---

 <i>Powered by <a href='https://www.reviewnb.com/?utm_source=gh'>ReviewNB</a></i>
github-actions[bot]: 
## ASV Benchmarking

<details>
  <summary>Benchmark Comparison Results</summary>

  
Benchmarks that have improved:

| Change   | Before [b986ef35]    | After [02f0ac02]    |   Ratio | Benchmark (Parameter)                                                                |
|----------|----------------------|---------------------|---------|--------------------------------------------------------------------------------------|
| -        | 389M                 | 347M                |    0.89 | mpas_ocean.Integrate.peakmem_integrate('480km')                                      |
| -        | 11.4±0.06s           | 347±3ms             |    0.03 | mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude') |
| -        | 11.3±0.02s           | 346±2ms             |    0.03 | mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include') |
| -        | 11.3±0.02s           | 349±2ms             |    0.03 | mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')   |
| -        | 715±4ms              | 23.3±0.8ms          |    0.03 | mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude') |
| -        | 726±6ms              | 21.9±0.2ms          |    0.03 | mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include') |
| -        | 722±2ms              | 21.9±0.2ms          |    0.03 | mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')   |

Benchmarks that have stayed the same:

| Change   | Before [b986ef35]    | After [02f0ac02]    |   Ratio | Benchmark (Parameter)                                                                                                                 |
|----------|----------------------|---------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------|
|          | 242M                 | 243M                |    1    | face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))    |
|          | 303M                 | 303M                |    1    | face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))  |
|          | 321M                 | 308M                |    0.96 | face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc')) |
|          | 318M                 | 304M                |    0.95 | face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))  |
|          | 14.1±0s              | 13.9±0.03s          |    0.99 | face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))       |
|          | 2.03±0.01s           | 2.07±0.02s          |    1.02 | face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))     |
|          | 19.5±0.01s           | 19.5±0.08s          |    1    | face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))    |
|          | 72.3±0.9ms           | 74.3±1ms            |    1.03 | face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))     |
|          | 1.64±0.01s           | 1.64±0.01s          |    1    | import.Imports.timeraw_import_uxarray                                                                                                 |
|          | 373M                 | 396M                |    1.06 | mpas_ocean.GeoDataFrame.peakmem_to_geodataframe('120km', False)                                                                       |
|          | 353M                 | 384M                |    1.09 | mpas_ocean.GeoDataFrame.peakmem_to_geodataframe('120km', True)                                                                        |
|          | 323M                 | 355M                |    1.1  | mpas_ocean.GeoDataFrame.peakmem_to_geodataframe('480km', False)                                                                       |
|          | 322M                 | 354M                |    1.1  | mpas_ocean.GeoDataFrame.peakmem_to_geodataframe('480km', True)                                                                        |
|          | 1.20±0.01s           | 1.20±0s             |    1    | mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)                                                                          |
|          | 55.0±0.6ms           | 56.9±0.5ms          |    1.03 | mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)                                                                           |
|          | 93.4±1ms             | 93.7±0.3ms          |    1    | mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)                                                                          |
|          | 5.12±0.1ms           | 5.39±0.09ms         |    1.05 | mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)                                                                           |
|          | 259M                 | 259M                |    1    | mpas_ocean.Gradient.peakmem_gradient('120km')                                                                                         |
|          | 236M                 | 238M                |    1.01 | mpas_ocean.Gradient.peakmem_gradient('480km')                                                                                         |
|          | 2.67±0.02ms          | 2.66±0.01ms         |    1    | mpas_ocean.Gradient.time_gradient('120km')                                                                                            |
|          | 282±1μs              | 286±2μs             |    1.01 | mpas_ocean.Gradient.time_gradient('480km')                                                                                            |
|          | 177±1ms              | 177±1ms             |    1    | mpas_ocean.Integrate.time_integrate('120km')                                                                                          |
|          | 12.0±0.08ms          | 11.9±0.08ms         |    1    | mpas_ocean.Integrate.time_integrate('480km')                                                                                          |
|          | 232M                 | 232M                |    1    | quad_hexagon.QuadHexagon.peakmem_open_dataset                                                                                         |
|          | 231M                 | 231M                |    1    | quad_hexagon.QuadHexagon.peakmem_open_grid                                                                                            |
|          | 6.61±0.03ms          | 6.67±0.04ms         |    1.01 | quad_hexagon.QuadHexagon.time_open_dataset                                                                                            |
|          | 5.63±0.02ms          | 5.63±0.01ms         |    1    | quad_hexagon.QuadHexagon.time_open_grid                                                                                               |

Benchmarks that have got worse:

| Change   | Before [b986ef35]    | After [02f0ac02]    |   Ratio | Benchmark (Parameter)                                              |
|----------|----------------------|---------------------|---------|--------------------------------------------------------------------|
| +        | 5.76±0.08μs          | 1.66±0.07ms         |  288.28 | mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km') |
| +        | 5.33±0.02μs          | 478±20μs            |   89.7  | mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km') |
| +        | 323M                 | 364M                |    1.13 | mpas_ocean.Integrate.peakmem_integrate('120km')                    |

</details>


Patch:
diff --git a/benchmarks/asv.conf.json b/benchmarks/asv.conf.json
index 26987cee..31a43921 100644
--- a/benchmarks/asv.conf.json
+++ b/benchmarks/asv.conf.json
@@ -54,7 +54,7 @@
     // If missing or the empty string, the tool will be automatically
     // determined by looking for tools on the PATH environment
     // variable.
-    "environment_type": "mamba",
+    "environment_type": "conda",
     "conda_channels": ["conda-forge"],
 
     // timeout in seconds for installing any dependencies in environment
diff --git a/benchmarks/mpas_ocean.py b/benchmarks/mpas_ocean.py
index e124386f..71972c5e 100644
--- a/benchmarks/mpas_ocean.py
+++ b/benchmarks/mpas_ocean.py
@@ -97,3 +97,17 @@ class ConnectivityConstruction:
 
     def time_n_nodes_per_face(self, resolution):
         self.uxds.uxgrid.n_nodes_per_face
+
+
+class MatplotlibConversion:
+    param_names = ['resolution', 'periodic_elements']
+    params = (['480km', '120km'], ['include', 'exclude', 'split'])
+
+    def setup(self, resolution, periodic_elements):
+        self.uxds = ux.open_dataset(file_path_dict[resolution][0], file_path_dict[resolution][1])
+
+    def teardown(self, resolution, periodic_elements):
+        del self.uxds
+
+    def time_dataarray_to_polycollection(self, resolution, periodic_elements):
+        self.uxds[data_var].to_polycollection()
diff --git a/ci/asv.yml b/ci/asv.yml
index 76be35ae..5fd70ebe 100644
--- a/ci/asv.yml
+++ b/ci/asv.yml
@@ -28,5 +28,5 @@ dependencies:
   - pip:
     - antimeridian
     - pyfma
-    - asv<0.6.2
+    - git+https://github.com/airspeed-velocity/asv
     - -e ../
diff --git a/ci/environment.yml b/ci/environment.yml
index 56b53aeb..9501c397 100644
--- a/ci/environment.yml
+++ b/ci/environment.yml
@@ -30,4 +30,5 @@ dependencies:
   - spatialpandas
   - xarray
   - pip:
+    - git+https://github.com/airspeed-velocity/asv
     - pyfma
diff --git a/docs/user-guide/mpl.ipynb b/docs/user-guide/mpl.ipynb
new file mode 100644
index 00000000..8dcac8c1
--- /dev/null
+++ b/docs/user-guide/mpl.ipynb
@@ -0,0 +1,313 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "2cea9b83a226284a",
+   "metadata": {},
+   "source": [
+    "# Plotting with Matplotlib\n",
+    "\n",
+    "While UXarray's plotting API is written around the [HoloViz](https://holoviz.org/) stack of packages, plotting directly with Matplotlib is supported through the conversion to a `LineCollection` or `PolyCollection` object. This user guide will cover:\n",
+    "* Converting a `Grid` to a `LineCollection`\n",
+    "* Converting a `UxDataArray` to a `PolyCollection`\n",
+    "* Using Geographic Projections & Elements\n",
+    "* Handling periodic elements along the antimeridian"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ad9cbbd19d0d65ff",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import uxarray as ux\n",
+    "import matplotlib.pyplot as plt\n",
+    "import cartopy.crs as ccrs"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "783a9518289edb21",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "base_path = \"../../test/meshfiles/ugrid/outCSne30/\"\n",
+    "grid_path = base_path + \"outCSne30.ug\"\n",
+    "data_path = base_path + \"outCSne30_vortex.nc\"\n",
+    "\n",
+    "uxds = ux.open_dataset(grid_path, data_path)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "379beeaa7e4d6e93",
+   "metadata": {},
+   "source": [
+    "## Visualize Grid Topology with `LineCollection`\n",
+    "\n",
+    "The [`Grid.to_linecollection()`](https://uxarray.readthedocs.io/en/latest/user_api/generated/uxarray.Grid.to_linecollection.html#) method can be used to convert a `Grid` instance into a [`matplotlib.collections.LineCollection`](https://matplotlib.org/stable/api/collections_api.html#matplotlib.collections.LineCollection) instance. It represents a collection of lines that represent the edges of an unstructured grid."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "a5026668490e2fe7",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "lc = uxds.uxgrid.to_linecollection()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "5122376c2a45b5f0",
+   "metadata": {},
+   "source": [
+    "Once we have converted our ``Grid`` to a ``LineCollection``, we can directly use Matplotlib."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "ba78190a3ff1f298",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# control the width of each edge\n",
+    "lc.set_linewidth(0.5)\n",
+    "\n",
+    "# set the color of each edge\n",
+    "lc.set_edgecolor(\"black\")\n",
+    "\n",
+    "fig, ax = plt.subplots(1, 1, figsize=(10, 5), constrained_layout=True)\n",
+    "\n",
+    "ax.set_xlim((-180, 180))\n",
+    "ax.set_ylim((-90, 90))\n",
+    "ax.add_collection(lc)\n",
+    "\n",
+    "plt.title(\"Line Collection Plot\")\n",
+    "plt.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "16942a05ab3e1144",
+   "metadata": {},
+   "source": [
+    "## Visualize Data with `PolyCollection`\n",
+    "\n",
+    "The [`Grid.to_polycollection()`](https://uxarray.readthedocs.io/en/latest/user_api/generated/uxarray.Grid.to_polycollection.html#) method can be used to convert a `UxDataArray` containing a face-centered data variable into a [`matplotlib.collections.PolyCollection`](https://matplotlib.org/stable/api/collections_api.html#matplotlib.collections.PolyCollection) instance. It represents a collection of polygons that represent the faces of an unstructured grid, shaded using the values of the face-centered data variable."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "1c331babce6e1750",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "pc = uxds[\"psi\"].to_polycollection()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "bd9182e2915c4414",
+   "metadata": {},
+   "source": [
+    "Just like with the ``LineCollection``, we can directly use Matplotlib to visualize our ``PolyCollection``"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "cec2edf9996c11a6",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "pc.set_antialiased(False)\n",
+    "\n",
+    "pc.set_cmap(\"plasma\")\n",
+    "\n",
+    "fig, ax = plt.subplots(1, 1, figsize=(10, 5), constrained_layout=True)\n",
+    "\n",
+    "ax.set_xlim((-180, 180))\n",
+    "ax.set_ylim((-90, 90))\n",
+    "ax.add_collection(pc)\n",
+    "\n",
+    "plt.title(\"Poly Collection Plot\")\n",
+    "plt.show()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "8cd9fce0ffba13e",
+   "metadata": {},
+   "source": [
+    "## Geographic Projections & Elements\n",
+    "\n",
+    "Both the `Grid.to_linecollection()` and `UxDataArray.to_polycollection()` methods accept an optional argument `projection` for setting a Cartopy projection. A full list of Cartopy projections can be found [here](https://scitools.org.uk/cartopy/docs/v0.15/crs/projections.html).\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "78bb71b8f927f14c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "projection = ccrs.Orthographic()\n",
+    "\n",
+    "pc = uxds[\"psi\"].to_polycollection(projection=projection, override=True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "2bf989e265cff66",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import cartopy.feature as cfeature\n",
+    "\n",
+    "pc.set_antialiased(False)\n",
+    "pc.set_cmap(\"plasma\")\n",
+    "\n",
+    "fig, ax = plt.subplots(\n",
+    "    1,\n",
+    "    1,\n",
+    "    figsize=(10, 5),\n",
+    "    facecolor=\"w\",\n",
+    "    constrained_layout=True,\n",
+    "    subplot_kw=dict(projection=projection),\n",
+    ")\n",
+    "\n",
+    "# add geographic features\n",
+    "ax.add_feature(cfeature.COASTLINE)\n",
+    "ax.add_feature(cfeature.BORDERS)\n",
+    "\n",
+    "ax.add_collection(pc)\n",
+    "ax.set_global()\n",
+    "plt.title(\"PolyCollection Plot with Projection & Features\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "c8510926d181ed7f",
+   "metadata": {},
+   "source": [
+    "## Handling Periodic Elements"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "fde7fafb87b9cc96",
+   "metadata": {},
+   "source": [
+    "### Global Data\n",
+    "\n",
+    "If your grid contains elements that cross the antimeridian, plotting them without any corrections will lead to artifacts, as can be observed in the first plot below. \n",
+    "\n",
+    "UXarray provides two ways of handling these elements:\n",
+    "- **Exclusion:** Elements will be excluded from the plot, with no other corrections being done, indicated by setting `periodic_elements='exclude'`, this is the default.\n",
+    "- **Splitting:** Each element is split into two across the antimeridian, indicated by setting `periodic_elements='split'`\n",
+    "\n",
+    "\n",
+    "```{warning}\n",
+    "Setting ``periodic_elements='split'`` will lead to roughly a 20 times perfromance hit compared to the other method, so it is suggested to only use this option for small grids.\n",
+    "```\n",
+    "\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "492dbde14343d38e",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "methods = [\"include\", \"exclude\", \"split\"]\n",
+    "poly_collections = [\n",
+    "    uxds[\"psi\"].to_polycollection(periodic_elements=method) for method in methods\n",
+    "]\n",
+    "\n",
+    "\n",
+    "fig, axes = plt.subplots(\n",
+    "    nrows=3, figsize=(20, 10), subplot_kw={\"projection\": ccrs.PlateCarree()}\n",
+    ")\n",
+    "\n",
+    "for ax, pc, method in zip(axes, poly_collections, methods):\n",
+    "    pc.set_linewidth(0)\n",
+    "    pc.set_cmap(\"plasma\")\n",
+    "    ax.set_xlim((-180, 180))\n",
+    "    pc.set_antialiased(False)\n",
+    "    ax.set_ylim((-90, 90))\n",
+    "    ax.add_collection(pc)\n",
+    "    ax.set_title(f\"periodic_elements='{method}'\")"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "d55137184d571f90",
+   "metadata": {},
+   "source": [
+    "### Regional Data\n",
+    "\n",
+    "If you grid doesn't contain any periodic elements, it is always suggested to keep ``periodic_elements='include'`` for the best perfromance, as there is no difference in the resulting plots."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "d03f11e44f9f608c",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "methods = [\"include\", \"exclude\", \"split\"]\n",
+    "poly_collections = [\n",
+    "    uxds[\"psi\"]\n",
+    "    .subset.bounding_circle((0, 0), 20)\n",
+    "    .to_polycollection(periodic_elements=method)\n",
+    "    for method in methods\n",
+    "]\n",
+    "\n",
+    "\n",
+    "fig, axes = plt.subplots(\n",
+    "    nrows=3, figsize=(10, 10), subplot_kw={\"projection\": ccrs.PlateCarree()}\n",
+    ")\n",
+    "\n",
+    "for ax, pc, method in zip(axes, poly_collections, methods):\n",
+    "    pc.set_linewidth(0)\n",
+    "    pc.set_cmap(\"plasma\")\n",
+    "    pc.set_antialiased(False)\n",
+    "    ax.set_xlim((-20, 20))\n",
+    "    ax.set_ylim((-20, 20))\n",
+    "    ax.add_collection(pc)\n",
+    "    ax.set_title(f\"periodic_elements='{method}'\")"
+   ]
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 2
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython2",
+   "version": "2.7.6"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/docs/userguide.rst b/docs/userguide.rst
index 8474c261..8d1f598b 100644
--- a/docs/userguide.rst
+++ b/docs/userguide.rst
@@ -31,8 +31,11 @@ These user guides provide detailed explanations of the core functionality in UXa
 `Data Structures <user-guide/data-structures.ipynb>`_
  Core data structures for working with unstructured grid and data files
 
-`Plotting <user-guide/plotting.ipynb>`_
- Visualize unstructured grid datasets
+`Plotting API <user-guide/plotting.ipynb>`_
+ Visualize unstructured grid datasets using UXarray's plotting api
+
+`Plotting with Matplotlib <user-guide/mpl.ipynb>`_
+ Use Matplotlib for plotting with PolyCollection and LineCollection
 
 `Advanced Plotting Techniques <user-guide/advanced-plotting.ipynb>`_
  Deeper dive into getting the most out of UXarray's plotting functionality
@@ -73,7 +76,8 @@ These user guides provide additional detail about specific features in UXarray.
    user-guide/grid-formats.rst
    user-guide/data-structures.ipynb
    user-guide/plotting.ipynb
-   advanced-plotting.ipynb
+   user-guide/mpl.ipynb
+   user-guide/advanced-plotting.ipynb
    user-guide/subset.ipynb
    user-guide/topological-aggregations.ipynb
    user-guide/area_calc.ipynb
diff --git a/uxarray/core/dataarray.py b/uxarray/core/dataarray.py
index a99defd1..4cd39ccd 100644
--- a/uxarray/core/dataarray.py
+++ b/uxarray/core/dataarray.py
@@ -15,6 +15,7 @@ from xarray.core.utils import UncachedAccessor
 
 from warnings import warn
 
+
 from uxarray.core.gradient import (
     _calculate_grad_on_edge_from_faces,
     _calculate_edge_face_difference,
@@ -28,6 +29,8 @@ from uxarray.core.aggregation import _uxda_grid_aggregate
 
 import warnings
 
+import cartopy.crs as ccrs
+
 
 class UxDataArray(xr.DataArray):
     """N-dimensional ``xarray.DataArray``-like array. Inherits from
@@ -181,28 +184,30 @@ class UxDataArray(xr.DataArray):
             )
 
     def to_polycollection(
-        self, override=False, cache=True, correct_antimeridian_polygons=True
+        self,
+        periodic_elements: Optional[str] = "exclude",
+        projection: Optional[ccrs.Projection] = None,
+        return_indices: Optional[bool] = False,
+        cache: Optional[bool] = True,
+        override: Optional[bool] = False,
     ):
-        """Constructs a ``matplotlib.collections.PolyCollection`` object with
-        polygons representing the geometry of the unstructured grid, with
-        polygons that cross the antimeridian split across the antimeridian.
+        """Converts a ``UxDataArray`` to a
+        ``matplotlib.collections.PolyCollection``, representing each face as a
+        polygon shaded with a face-centered data variable.
 
         Parameters
         ----------
-        override : bool
-            Flag to recompute the ``PolyCollection`` stored under the ``uxgrid`` if one is already cached
-        cache : bool
-            Flag to indicate if the computed ``PolyCollection`` stored under the ``uxgrid`` accessor should be cached
-        correct_antimeridian_polygons: bool, Optional
-            Parameter to select whether to correct and split antimeridian polygons
-
-        Returns
-        -------
-        poly_collection : matplotlib.collections.PolyCollection
-            The output `PolyCollection` of polygons representing the geometry of the unstructured grid paired with
-            a data variable.
+        periodic_elements: str
+            Method for handling elements that cross the antimeridian. One of ['include', 'exclude', 'split']
+        projection: ccrs.Projection
+            Cartopy geographic projection to use
+        return_indices: bool
+            Flag to indicate whether to return the indices of corrected polygons, if any exist
+        cache: bool
+            Flag to indicate whether to cache the computed PolyCollection
+        override: bool
+            Flag to indicate whether to override a cached PolyCollection, if it exists
         """
-
         # data is multidimensional, must be a 1D slice
         if self.values.ndim > 1:
             raise ValueError(
@@ -210,41 +215,31 @@ class UxDataArray(xr.DataArray):
                 f"for face-centered data."
             )
 
-        # face-centered data
-        if self.values.size == self.uxgrid.n_face:
-            (
-                poly_collection,
-                corrected_to_original_faces,
-            ) = self.uxgrid.to_polycollection(
-                override=override,
-                cache=cache,
-                correct_antimeridian_polygons=correct_antimeridian_polygons,
+        if self._face_centered():
+            poly_collection, corrected_to_original_faces = (
+                self.uxgrid.to_polycollection(
+                    override=override,
+                    cache=cache,
+                    periodic_elements=periodic_elements,
+                    return_indices=True,
+                    projection=projection,
+                )
             )
 
             # map data with antimeridian polygons
             if len(corrected_to_original_faces) > 0:
                 data = self.values[corrected_to_original_faces]
-
-            # no antimeridian polygons
             else:
                 data = self.values
 
             poly_collection.set_array(data)
-            return poly_collection, corrected_to_original_faces
-
-        # node-centered data
-        elif self.values.size == self.uxgrid.n_node:
-            raise ValueError(
-                f"Data Variable with size {self.values.size} mapped on the nodes of each polygon"
-                f"not supported yet."
-            )
 
-        # data not mapped to faces or nodes
+            if return_indices:
+                return poly_collection, corrected_to_original_faces
+            else:
+                return poly_collection
         else:
-            raise ValueError(
-                f"Data Variable with size {self.values.size} does not match the number of faces "
-                f"({self.uxgrid.n_face}."
-            )
+            raise ValueError("Data variable must be face centered.")
 
     def to_dataset(
         self,
diff --git a/uxarray/grid/connectivity.py b/uxarray/grid/connectivity.py
index 222c26f7..81a6119b 100644
--- a/uxarray/grid/connectivity.py
+++ b/uxarray/grid/connectivity.py
@@ -143,6 +143,7 @@ def _populate_n_nodes_per_face(grid):
     )
 
 
+@njit()
 def _build_n_nodes_per_face(face_nodes, n_face, n_max_face_nodes):
     """Constructs ``n_nodes_per_face``, which contains the number of non-fill-
     value nodes for each face in ``face_node_connectivity``"""
diff --git a/uxarray/grid/geometry.py b/uxarray/grid/geometry.py
index 55b7d1ea..d2c43e8f 100644
--- a/uxarray/grid/geometry.py
+++ b/uxarray/grid/geometry.py
@@ -10,6 +10,9 @@ import warnings
 import pandas as pd
 import xarray as xr
 
+import cartopy.crs as ccrs
+
+
 from numba import njit
 
 POLE_POINTS = {"North": np.array([0.0, 0.0, 1.0]), "South": np.array([0.0, 0.0, -1.0])}
@@ -56,6 +59,7 @@ def _build_polygon_shells(
     n_face,
     n_max_face_nodes,
     n_nodes_per_face,
+    projection=None,
 ):
     """Builds an array of polygon shells, which can be used with Shapely to
     construct polygons."""
@@ -63,6 +67,14 @@ def _build_polygon_shells(
         face_node_connectivity, n_face, n_max_face_nodes, n_nodes_per_face
     )
 
+    if projection is not None:
+        lonlat_proj = projection.transform_points(
+            ccrs.PlateCarree(), node_lon, node_lat
+        )
+
+        node_lon = lonlat_proj[:, 0]
+        node_lat = lonlat_proj[:, 1]
+
     polygon_shells = (
         np.array(
             [node_lon[closed_face_nodes], node_lat[closed_face_nodes]], dtype=np.float32
@@ -271,12 +283,17 @@ def _build_corrected_polygon_shells(polygon_shells):
     return corrected_polygon_shells, _corrected_shells_to_original_faces
 
 
-def _grid_to_matplotlib_polycollection(grid):
+def _grid_to_matplotlib_polycollection(grid, periodic_elements, projection=None):
     """Constructs and returns a ``matplotlib.collections.PolyCollection``"""
 
     # import optional dependencies
     from matplotlib.collections import PolyCollection
 
+    if periodic_elements == "split" and projection is not None:
+        raise ValueError(
+            "Projections are not supported when splitting periodic elements.'"
+        )
+
     polygon_shells = _build_polygon_shells(
         grid.node_lon.values,
         grid.node_lat.values,
@@ -284,21 +301,51 @@ def _grid_to_matplotlib_polycollection(grid):
         grid.n_face,
         grid.n_max_face_nodes,
         grid.n_nodes_per_face.values,
+        projection,
     )
 
-    (
-        corrected_polygon_shells,
-        corrected_to_original_faces,
-    ) = _build_corrected_polygon_shells(polygon_shells)
+    if periodic_elements == "exclude":
+        antimeridian_face_indices = grid.antimeridian_face_indices
+        shells_without_antimeridian = np.delete(
+            polygon_shells, antimeridian_face_indices, axis=0
+        )
+
+        corrected_to_original_faces = np.delete(
+            np.arange(grid.n_face), antimeridian_face_indices, axis=0
+        )
+
+        return PolyCollection(shells_without_antimeridian), corrected_to_original_faces
+
+    elif periodic_elements == "split":
+        # create polygon shells without projection
+        polygon_shells = _build_polygon_shells(
+            grid.node_lon.values,
+            grid.node_lat.values,
+            grid.face_node_connectivity.values,
+            grid.n_face,
+            grid.n_max_face_nodes,
+            grid.n_nodes_per_face.values,
+            projection=None,
+        )
 
-    return PolyCollection(corrected_polygon_shells), corrected_to_original_faces
+        # correct polygon shells without projection
+        (
+            corrected_polygon_shells,
+            corrected_to_original_faces,
+        ) = _build_corrected_polygon_shells(polygon_shells)
 
+        return PolyCollection(corrected_polygon_shells), corrected_to_original_faces
 
-def _grid_to_matplotlib_linecollection(grid):
+    else:
+        return PolyCollection(polygon_shells), []
+
+
+def _grid_to_matplotlib_linecollection(grid, periodic_elements):
     """Constructs and returns a ``matplotlib.collections.LineCollection``"""
 
     # import optional dependencies
     from matplotlib.collections import LineCollection
+    from shapely import polygons as Polygons
 
     polygon_shells = _build_polygon_shells(
         grid.node_lon.values,
@@ -309,10 +356,21 @@ def _grid_to_matplotlib_linecollection(grid):
         grid.n_nodes_per_face.values,
     )
 
-    # obtain corrected shapely polygons
-    polygons = _build_corrected_shapely_polygons(
-        polygon_shells, grid.antimeridian_face_indices
-    )
+    if periodic_elements == "exclude":
+        antimeridian_face_indices = grid.antimeridian_face_indices
+        shells_without_antimeridian = np.delete(
+            polygon_shells, antimeridian_face_indices, axis=0
+        )
+
+        polygons = Polygons(shells_without_antimeridian)
+
+    elif periodic_elements == "split":
+        # obtain corrected shapely polygons
+        polygons = _build_corrected_shapely_polygons(
+            polygon_shells, grid.antimeridian_face_indices
+        )
+    else:
+        polygons = Polygons(polygon_shells)
 
     # Convert polygons into lines
     lines = []
diff --git a/uxarray/grid/grid.py b/uxarray/grid/grid.py
index f80ebf10..c9b4577a 100644
--- a/uxarray/grid/grid.py
+++ b/uxarray/grid/grid.py
@@ -65,11 +65,14 @@ from uxarray.grid.validation import (
     _check_area,
 )
 
-
 from xarray.core.utils import UncachedAccessor
 
 from warnings import warn
 
+import cartopy.crs as ccrs
+
+import copy
+
 
 class Grid:
     """Represents a two-dimensional unstructured grid encoded following the
@@ -147,11 +150,16 @@ class Grid:
         # initialize attributes
         self._antimeridian_face_indices = None
 
-        # initialize cached data structures (visualization)
+        # initialize cached data structures and flags (visualization)
         self._gdf = None
         self._gdf_exclude_am = None
         self._poly_collection = None
+        self._poly_collection_periodic_state = None
+        self._poly_collection_projection_state = None
+        self._corrected_to_original_faces = None
         self._line_collection = None
+        self._line_collection_periodic_state = None
+        self._line_collection_projection_state = None
         self._raster_data_id = None
 
         # initialize cached data structures (nearest neighbor operations)
@@ -1227,73 +1235,110 @@ class Grid:
 
     def to_polycollection(
         self,
-        override: Optional[bool] = False,
+        periodic_elements: Optional[str] = "exclude",
+        projection: Optional[ccrs.Projection] = None,
+        return_indices: Optional[bool] = False,
         cache: Optional[bool] = True,
-        correct_antimeridian_polygons: Optional[bool] = True,
+        override: Optional[bool] = False,
     ):
-        """Constructs a ``matplotlib.collections.PolyCollection`` object with
-        polygons representing the geometry of the unstructured grid, with
-        polygons that cross the antimeridian split.
+        """Converts a ``Grid`` to a ``matplotlib.collections.PolyCollection``,
+        representing each face as a polygon.
 
         Parameters
         ----------
-        override : bool
-            Flag to recompute the ``PolyCollection`` if one is already cached
-        cache : bool
-            Flag to indicate if the computed ``PolyCollection`` should be cached
-
-        Returns
-        -------
-        polycollection : matplotlib.collections.PolyCollection
-            The output `PolyCollection` containing faces represented as polygons
-        corrected_to_original_faces: list
-            Original indices used to map the corrected polygon shells to their entries in face nodes
+        periodic_elements: str
+            Method for handling elements that cross the antimeridian. One of ['include', 'exclude', 'split']
+        projection: ccrs.Projection
+            Cartopy geographic projection to use
+        return_indices: bool
+            Flag to indicate whether to return the indices of corrected polygons, if any exist
+        cache: bool
+            Flag to indicate whether to cache the computed PolyCollection
+        override: bool
+            Flag to indicate whether to override a cached PolyCollection, if it exists
         """
 
-        # use cached polycollection
+        if periodic_elements not in ["include", "exclude", "split"]:
+            raise ValueError(
+                f"Invalid value for 'periodic_elements'. Expected one of ['include', 'exclude', 'split'] but received: {periodic_elements}"
+            )
+
+        if self._poly_collection is not None:
+            if (
+                self._poly_collection_periodic_state != periodic_elements
+                or self._poly_collection_projection_state != projection
+            ):
+                # cached PolyCollection has a different projection or periodic element handling method
+                override = True
+
         if self._poly_collection is not None and not override:
-            return self._poly_collection
+            # use cached PolyCollection
+            if return_indices:
+                return copy.deepcopy(
+                    self._poly_collection
+                ), self._corrected_to_original_faces
+            else:
+                return copy.deepcopy(self._poly_collection)
 
         (
             poly_collection,
             corrected_to_original_faces,
-        ) = _grid_to_matplotlib_polycollection(self)
+        ) = _grid_to_matplotlib_polycollection(self, periodic_elements, projection)
 
-        # cache computed polycollection
         if cache:
+            # cache PolyCollection, indices, and state
             self._poly_collection = poly_collection
+            self._corrected_to_original_faces = corrected_to_original_faces
+            self._poly_collection_periodic_state = periodic_elements
+            self._poly_collection_projection_state = projection
 
-        return poly_collection, corrected_to_original_faces
+        if return_indices:
+            return copy.deepcopy(poly_collection), corrected_to_original_faces
+        else:
+            return copy.deepcopy(poly_collection)
 
     def to_linecollection(
-        self, override: Optional[bool] = False, cache: Optional[bool] = True
+        self,
+        periodic_elements: Optional[str] = "exclude",
+        projection: Optional[ccrs.Projection] = None,
+        cache: Optional[bool] = True,
+        override: Optional[bool] = False,
     ):
-        """Constructs a ``matplotlib.collections.LineCollection`` object with
-        line segments representing the geometry of the unstructured grid,
-        corrected near the antimeridian.
+        """Converts a ``Grid`` to a ``matplotlib.collections.LineCollection``,
+        representing each edge as a line.
 
         Parameters
         ----------
-        override : bool
-            Flag to recompute the ``LineCollection`` if one is already cached
-        cache : bool
-            Flag to indicate if the computed ``LineCollection`` should be cached
-
-        Returns
-        -------
-        line_collection : matplotlib.collections.LineCollection
-            The output `LineCollection` containing faces represented as polygons
+        periodic_elements: str
+            Method for handling elements that cross the antimeridian. One of ['include', 'exclude', 'split']
+        projection: ccrs.Projection
+            Cartopy geographic projection to use
+        cache: bool
+            Flag to indicate whether to cache the computed PolyCollection
+        override: bool
+            Flag to indicate whether to override a cached PolyCollection, if it exists
         """
+        if periodic_elements not in ["include", "exclude", "split"]:
+            raise ValueError(
+                f"Invalid value for 'periodic_elements'. Expected one of ['include', 'exclude', 'split'] but received: {periodic_elements}"
+            )
+
+        if self._line_collection is not None:
+            if (
+                self._line_collection_periodic_state != periodic_elements
+                or self._line_collection_projection_state != projection
+            ):
+                override = True
 
-        # use cached line collection
-        if self._line_collection is not None and not override:
-            return self._line_collection
+            if not override:
+                return self._line_collection
 
-        line_collection = _grid_to_matplotlib_linecollection(self)
+        line_collection = _grid_to_matplotlib_linecollection(self, periodic_elements)
 
-        # cache computed line collection
         if cache:
             self._line_collection = line_collection
+            self._line_collection_periodic_state = periodic_elements
+            self._line_collection_projection_state = periodic_elements
 
         return line_collection
 


Test patch:
diff --git a/test/test_dataarray.py b/test/test_dataarray.py
index d3ea231b..9d040841 100644
--- a/test/test_dataarray.py
+++ b/test/test_dataarray.py
@@ -68,7 +68,7 @@ class TestGeometryConversions(TestCase):
             uxds_geoflow['v1'].to_polycollection()
 
         # grid conversion
-        pc_geoflow_grid, _ = uxds_geoflow.uxgrid.to_polycollection()
+        pc_geoflow_grid = uxds_geoflow.uxgrid.to_polycollection(periodic_elements='split')
 
         polygon_shells = _build_polygon_shells(
             uxds_geoflow.uxgrid.node_lon.values,
@@ -95,7 +95,7 @@ class TestGeometryConversions(TestCase):
         corrected_polygon_shells, _ = _build_corrected_polygon_shells(
             polygon_shells)
 
-        pc_geoflow_data, _ = uxds_ne30['psi'].to_polycollection()
+        pc_geoflow_data = uxds_ne30['psi'].to_polycollection(periodic_elements='split')
 
         assert len(pc_geoflow_data._paths) == len(corrected_polygon_shells)