Repository: GenericMappingTools/pygmt

Issue:
Add a tutorial for plotting polygons
We already have a tutorial for plotting lines (https://www.pygmt.org/dev/tutorials/basics/lines.html), but still we don't have a tutorial for plotting polygons with different outlines and fills.

Discussion:
seisman: A little off-topic: what do you think about updating the existing "Plotting lines" tutorial to make it more eye-catching? Currently, the tutorial just plots lines with two or more data points. I feel a more complicated line/curve like sine/cosine functions are better.


yvonnefroehlich: > A little off-topic: what do you think about updating the existing "Plotting lines" tutorial to make it more eye-catching? Currently, the tutorial just plots lines with two or more data points. I feel a more complicated line/curve like sine/cosine functions are better.

Will look and work on this after AGU.

Patch:
diff --git a/examples/tutorials/basics/polygons.py b/examples/tutorials/basics/polygons.py
new file mode 100644
index 00000000..6b3b4d37
--- /dev/null
+++ b/examples/tutorials/basics/polygons.py
@@ -0,0 +1,100 @@
+"""
+Plotting polygons
+=================
+
+Plotting polygons is handled by the :meth:`pygmt.Figure.plot` method.
+
+This tutorial focuses on input data given as NumPy arrays. Besides NumPy arrays,
+array-like objects are supported. Here, a polygon is a closed shape defined by a series
+of data points with x and y coordinates, connected by line segments, with the start and
+end points being identical. For plotting a :class:`geopandas.GeoDataFrame` object with
+polygon geometries, e.g., to create a choropleth map, see the gallery example
+:doc:`Choropleth map </gallery/maps/choropleth_map>`.
+"""
+
+# %%
+import numpy as np
+import pygmt
+
+# %%
+# Plot polygons
+# -------------
+#
+# Set up sample data points as NumPy arrays for the x and y values.
+
+x = np.array([-2, 1, 3, 0, -4, -2])
+y = np.array([-3, -1, 1, 3, 2, -3])
+
+# %%
+# Create a Cartesian plot via the :meth:`pygmt.Figure.basemap` method. Pass arrays to
+# the ``x`` and ``y`` parameters of the :meth:`pygmt.Figure.plot` method. Without
+# further adjustments, lines are drawn between the data points. By default, the lines
+# are 0.25-points thick, black, and solid. In this example, the data points are chosen
+# to make the lines form a polygon.
+
+fig = pygmt.Figure()
+fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame=True)
+fig.plot(x=x, y=y)
+fig.show()
+
+# %%
+# The ``pen`` parameter can be used to adjust the lines or outline of the polygon. The
+# argument passed to ``pen`` is one string with the comma-separated optional values
+# *width*,\ *color*,\ *style*.
+
+fig = pygmt.Figure()
+fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame=True)
+# Use a 2-points thick, darkred, dashed outline
+fig.plot(x=x, y=y, pen="2p,darkred,dashed")
+fig.show()
+
+# %%
+# Use the ``fill`` parameter to fill the polygon with a color or
+# :doc:`pattern </techref/patterns>`. Note, that there are no lines drawn between the
+# data points by default if ``fill`` is used. Use the ``pen`` parameter to add an
+# outline around the polygon.
+
+fig = pygmt.Figure()
+fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame=True)
+# Fill the polygon with color "orange"
+fig.plot(x=x, y=y, fill="orange")
+fig.show()
+
+
+# %%
+# Close polygons
+# --------------
+#
+# Set up sample data points as NumPy array for the x and y values. Now, the data points
+# do not form a polygon.
+
+x = np.array([-2, 1, 3, 0, -4])
+y = np.array([-3, -1, 1, 3, 2])
+
+# %%
+# The ``close`` parameter can be used to force the polygon to be closed.
+
+fig = pygmt.Figure()
+fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame=True)
+fig.plot(x=x, y=y, pen=True)
+
+fig.shift_origin(xshift="w+1c")
+
+fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame=True)
+fig.plot(x=x, y=y, pen=True, close=True)
+fig.show()
+
+# %%
+# When using the ``fill`` parameter, the polygon is automatically closed.
+
+fig = pygmt.Figure()
+fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame=True)
+fig.plot(x=x, y=y, pen=True)
+
+fig.shift_origin(xshift="w+1c")
+
+fig.basemap(region=[-5, 5, -5, 5], projection="X5c", frame=True)
+fig.plot(x=x, y=y, pen=True, fill="orange")
+fig.show()
+
+# sphinx_gallery_thumbnail_number = 5
diff --git a/pygmt/helpers/utils.py b/pygmt/helpers/utils.py
index 89459a78..ef51fe2e 100644
--- a/pygmt/helpers/utils.py
+++ b/pygmt/helpers/utils.py
@@ -12,6 +12,7 @@ import sys
 import time
 import webbrowser
 from collections.abc import Iterable, Mapping, Sequence
+from pathlib import Path
 from typing import Any, Literal
 
 import xarray as xr
@@ -187,7 +188,8 @@ def _check_encoding(argstr: str) -> Encoding:
         charset["ZapfDingbats"].values()
     )
     for encoding in ["ISOLatin1+"] + [f"ISO-8859-{i}" for i in range(1, 17) if i != 12]:
-        if all(c in (set(charset[encoding].values()) | adobe_chars) for c in argstr):
+        chars = set(charset[encoding].values()) | adobe_chars
+        if all(c in chars for c in argstr):
             return encoding  # type: ignore[return-value]
     # Return the "ISOLatin1+" encoding if the string contains characters from multiple
     # charset encodings or contains characters that are not in any charset encoding.
@@ -580,7 +582,7 @@ def launch_external_viewer(fname: str, waiting: float = 0):
         case "win32":  # Windows
             os.startfile(fname)  # type:ignore[attr-defined] # noqa: S606
         case _:  # Fall back to the browser if can't recognize the operating system.
-            webbrowser.open_new_tab(f"file://{fname}")
+            webbrowser.open_new_tab(f"file://{Path(fname).resolve()}")
     if waiting > 0:
         # Preview images will be deleted when a GMT modern-mode session ends, but the
         # external viewer program may take a few seconds to open the images.


Test patch:
diff --git a/pygmt/tests/test_helpers.py b/pygmt/tests/test_helpers.py
index 717708f2..92013d3f 100644
--- a/pygmt/tests/test_helpers.py
+++ b/pygmt/tests/test_helpers.py
@@ -194,13 +194,16 @@ def test_launch_external_viewer_win32():
         mock_startfile.assert_called_once_with("preview.png")
 
 
-def test_launch_external_viewer_unknown_os():
+@pytest.mark.parametrize("fname", ["preview.png", "/full/path/to/preview.png"])
+def test_launch_external_viewer_unknown_os(fname):
     """
     Test that launch_external_viewer uses the webbrowser module as a fallback.
     """
     with (
-        patch("webbrowser.open_new_tab") as mock_open,
         patch("sys.platform", "unknown"),
+        patch("webbrowser.open_new_tab") as mock_open,
     ):
-        launch_external_viewer("preview.png")
-        mock_open.assert_called_once_with("file://preview.png")
+        launch_external_viewer(fname)
+        fullpath = Path(fname).resolve()
+        assert fullpath.is_absolute()
+        mock_open.assert_called_once_with(f"file://{fullpath}")