diff --git a/doc/api/index.rst b/doc/api/index.rst index ccb049f996a..a8cad413095 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -212,6 +212,7 @@ Class-style Parameters :toctree: generated Box + Pattern Enums ----- diff --git a/doc/techref/index.md b/doc/techref/index.md index 0380c229290..a17c19f868e 100644 --- a/doc/techref/index.md +++ b/doc/techref/index.md @@ -12,7 +12,6 @@ common_parameters.md projections.md fonts.md text_formatting.md -patterns.md encodings.md justification_codes.md environment_variables.md diff --git a/doc/techref/patterns.md b/doc/techref/patterns.md deleted file mode 100644 index ee53a5c87ee..00000000000 --- a/doc/techref/patterns.md +++ /dev/null @@ -1,25 +0,0 @@ -# Bit and Hachure Patterns - -PyGMT supports a variety of bit and hachure patterns that can be used to fill polygons. - -These patterns can be defined using the following syntax: - -**P**|**p**_pattern_[**+b**_color_][**+f**_color_][**+r**_dpi_] - -*pattern* can either be a number in the range 1-90 or the name of a 1-, 8-, or 24-bit -image raster file. The former will result in one of the 90 predefined 64x64 bit-patterns -provided by GMT (see the figure below). The latter allows the user to create customized, -repeating images using image raster files. - -By specifying uppercase **P** instead of **p** the image will be bit-reversed, i.e., -white and black areas will be interchanged (only applies to 1-bit images or predefined -bit-image patterns). For these patterns and other 1-bit images one may specify -alternative **b**ackground and **f**oreground colors (by appending **+b**_color_ and/or -**+f**_color_) that will replace the default white and black pixels, respectively. -Excluding *color* from a fore- or background specification yields a transparent image -where only the back- or foreground pixels will be painted. The **+r**_dpi_ modifier sets -the resolution in dpi. - -The image below shows the 90 predefined bit patterns that can be used in PyGMT. - -![](https://docs.generic-mapping-tools.org/6.5/_images/GMT_App_E.png) diff --git a/examples/gallery/symbols/patterns.py b/examples/gallery/symbols/patterns.py index a518ac7f034..f9896a9ee47 100644 --- a/examples/gallery/symbols/patterns.py +++ b/examples/gallery/symbols/patterns.py @@ -2,8 +2,8 @@ Bit and hachure patterns ======================== -In addition to colors, PyGMT also allows using bit and hachure patterns to fill -symbols, polygons, and other areas, via the ``fill`` parameter or similar parameters. +In addition to colors, PyGMT also allows using bit and hachure patterns to fill symbols, +polygons, and other areas, via the ``fill`` parameter or similar parameters. Example method parameters that support bit and hachure patterns include: @@ -19,49 +19,47 @@ ``uncertaintyfill`` - :meth:`pygmt.Figure.wiggle`: Anomalies via ``fillpositive`` and ``fillnegative`` -GMT provides 90 predefined patterns that can be used in PyGMT. The patterns are numbered -from 1 to 90, and can be colored and inverted. The resolution of the pattern -can be changed, and the background and foreground colors can be set. For a complete list -of available patterns and the full syntax to specify a pattern, refer to the -:doc:`/techref/patterns`. +GMT provides 90 predefined 1-bit patterns, which are numbered from 1 to 90. In addition, +custom 1-, 8-, or 24-bit image raster files can also be used as patterns. + +These patterns can be specified via the :class:`pygmt.params.Pattern` class. The +patterns can be customized with different resolution and different foreground and +background colors. The foreground and background colors can also be inverted. """ # %% import pygmt +from pygmt.params import Pattern # A list of patterns that will be demonstrated. -# To use a pattern as fill append "p" and the number of the desired pattern. -# By default, the pattern is plotted in black and white with a resolution of 300 dpi. +# By default, a pattern is plotted in black and white with a resolution of 300 dpi. patterns = [ - # Plot a hachted pattern via pattern number 8 - "p8", - # Plot a dotted pattern via pattern number 19 - "p19", - # Set the background color ("+b") to "red3" and the foreground color ("+f") to - # "lightgray" - "p19+bred3+flightbrown", - # Invert the pattern by using a capitalized "P" - "P19+bred3+flightbrown", - # Change the resolution ("+r") to 100 dpi - "p19+bred3+flightbrown+r100", - # Make the background transparent by not giving a color after "+b"; - # works analogous for the foreground - "p19+b+flightbrown+r100", + # Predefined 1-bit pattern 8. + Pattern(8), + # Predefined 1-bit pattern 19. + Pattern(19), + # Pattern 19 with custom background ("red3") and foreground ("lightbrown"). + Pattern(19, bgcolor="red3", fgcolor="lightbrown"), + # Invert the background and foreground. + Pattern(19, invert=True, bgcolor="red3", fgcolor="lightbrown"), + # Same as above, but with a 100 dpi resolution. + Pattern(19, bgcolor="red3", fgcolor="lightbrown", dpi=100), + # Same as above, but with a transparent background by setting bgcolor to "". + Pattern(19, bgcolor="", fgcolor="lightbrown", dpi=100), ] fig = pygmt.Figure() fig.basemap( region=[0, 10, 0, 12], - projection="X10c", + projection="X18c/10c", frame="rlbt+glightgray+tBit and Hachure Patterns", ) - y = 11 for pattern in patterns: # Plot a square with the pattern as fill. # The square has a size of 2 centimeters with a 1 point thick, black outline. - fig.plot(x=2, y=y, style="s2c", pen="1p,black", fill=pattern) + fig.plot(x=1, y=y, style="s2c", pen="1p,black", fill=pattern) # Add a description of the pattern. - fig.text(x=4, y=y, text=pattern, font="Courier-Bold", justify="ML") + fig.text(x=2, y=y, text=str(repr(pattern)), font="Courier-Bold", justify="ML") y -= 2 fig.show() diff --git a/examples/tutorials/advanced/cartesian_histograms.py b/examples/tutorials/advanced/cartesian_histograms.py index ad32abfc6e2..5f089227bc5 100644 --- a/examples/tutorials/advanced/cartesian_histograms.py +++ b/examples/tutorials/advanced/cartesian_histograms.py @@ -18,6 +18,7 @@ # Import the required packages import numpy as np import pygmt +from pygmt.params import Pattern # %% # Generate random data from a normal distribution: @@ -204,10 +205,8 @@ frame=["wSnE", "xaf10", "ya5f1+lCumulative counts"], data=data01, series=10, - # Use pattern ("p") number 8 as fill for the bars - # Set the background ("+b") to white [Default] - # Set the foreground ("+f") to black [Default] - fill="p8+bwhite+fblack", + # Fill bars with GMT pattern 8, with white background and black foreground. + fill=Pattern(8, bgcolor="white", fgcolor="black"), pen="1p,darkgray,solid", histtype=0, # Show cumulative counts diff --git a/examples/tutorials/advanced/focal_mechanisms.py b/examples/tutorials/advanced/focal_mechanisms.py index b44286b6397..c9ba743e904 100644 --- a/examples/tutorials/advanced/focal_mechanisms.py +++ b/examples/tutorials/advanced/focal_mechanisms.py @@ -18,6 +18,7 @@ # %% import pandas as pd import pygmt +from pygmt.params import Pattern # Set up arguments for basemap region = [-5, 5, -5, 5] @@ -99,10 +100,7 @@ # --------------------- # # Use the parameters ``compressionfill`` and ``extensionfill`` to fill the quadrants -# with different colors or patterns. Regarding patterns see the gallery example -# :doc:`Bit and hachure patterns ` and the Technical -# Reference :doc:`Bit and hachure patterns `. - +# with different colors or :class:`patterns `. fig = pygmt.Figure() fig.basemap(region=region, projection=projection, frame=frame) @@ -122,8 +120,8 @@ longitude=2, latitude=0, depth=0, - compressionfill="p8", - extensionfill="p31", + compressionfill=Pattern(8), + extensionfill=Pattern(31), outline=True, ) diff --git a/examples/tutorials/basics/polygons.py b/examples/tutorials/basics/polygons.py index 67144fdc680..dbd13775bdb 100644 --- a/examples/tutorials/basics/polygons.py +++ b/examples/tutorials/basics/polygons.py @@ -50,8 +50,8 @@ # %% # Use the ``fill`` parameter to fill the polygon with a color or -# :doc:`pattern `. Note, that there are no lines drawn between the -# data points by default if ``fill`` is used. Use the ``pen`` parameter to add an +# :class:`pattern `. 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() diff --git a/pygmt/params/__init__.py b/pygmt/params/__init__.py index f2904afba94..b80b921407a 100644 --- a/pygmt/params/__init__.py +++ b/pygmt/params/__init__.py @@ -3,3 +3,4 @@ """ from pygmt.params.box import Box +from pygmt.params.pattern import Pattern diff --git a/pygmt/params/pattern.py b/pygmt/params/pattern.py new file mode 100644 index 00000000000..33c83a569cd --- /dev/null +++ b/pygmt/params/pattern.py @@ -0,0 +1,111 @@ +""" +The Pattern class for specifying bit and hachure patterns. +""" + +import dataclasses + +from pygmt._typing import PathLike +from pygmt.alias import Alias +from pygmt.exceptions import GMTValueError +from pygmt.params.base import BaseParam + +__doctest_skip__ = ["Pattern"] + + +@dataclasses.dataclass(repr=False) +class Pattern(BaseParam): + """ + Class for specifying bit and hachure patterns. + + This class allows users to specify predefined bit-patterns or custom 1-, 8-, or + 24-bit image raster files to fill symbols and polygons in various PyGMT plotting + methods. The patterns can be customized with different resolution and different + foreground and background colors. The foreground and background colors can also be + inverted. + + GMT provides 90 predefined patterns that can be used in PyGMT. The patterns are + numbered from 1 to 90, and shown below: + + .. figure:: https://docs.generic-mapping-tools.org/6.5/_images/GMT_App_E.png + :alt: The 90 predefined bit-patterns provided with GMT + :width: 75% + :align: center + + Parameters + ---------- + pattern + The pattern to use. It can be specified in two forms: + + - An integer in the range of 1-90, corresponding to one of 90 predefined 64x64 + bit-patterns + - Name of a 1-, 8-, or 24-bit image raster file, to create customized, repeating + images using image raster files. + dpi + Resolution of the pattern in dots per inch (DPI) [Default is 300]. + bgcolor/fgcolor + The background/foreground color for predefined bit-patterns or 1-bit images. + Setting either to an empty string will yield a transparent background/foreground + where only the foreground/background pixels will be painted. [Default is white + for background and black for foreground]. + invert + If ``True``, the pattern will be bit-inverted, i.e., white and black areas will + be interchanged (only applies to predefined bit-patterns or 1-bit images). + + Examples + -------- + Draw a global map with land areas filled with pattern 15 in a light red background + and 200 dpi resolution: + + >>> import pygmt + >>> from pygmt.params import Pattern + >>> fig = pygmt.Figure() + >>> fig.coast( + ... region="g", + ... projection="H10c", + ... frame=True, + ... land=Pattern(15, bgcolor="lightred", dpi=200), + ... shorelines=True, + ... ) + >>> fig.show() + """ + + pattern: int | PathLike + dpi: int | None = None + bgcolor: str | None = None + fgcolor: str | None = None + invert: bool = False + + def _validate(self): + """ + Validate the parameters. + """ + # Integer pattern number must be in the range 1-90. + if isinstance(self.pattern, int) and not (1 <= self.pattern <= 90): + raise GMTValueError( + self.pattern, + description="pattern number", + reason=( + "Parameter 'pattern' must be an integer in the range 1-90 " + "or the name of a 1-, 8-, or 24-bit image raster file." + ), + ) + # fgcolor and bgcolor cannot both be empty. + if self.fgcolor == "" and self.bgcolor == "": + _value = f"{self.fgcolor=}, {self.bgcolor=}" + raise GMTValueError( + _value, + description="fgcolor and bgcolor", + reason="fgcolor and bgcolor cannot both be empty.", + ) + + @property + def _aliases(self): + """ + Aliases for the Pattern class. + """ + return [ + Alias(self.pattern, name="pattern", prefix="P" if self.invert else "p"), + Alias(self.bgcolor, name="bgcolor", prefix="+b"), + Alias(self.fgcolor, name="fgcolor", prefix="+f"), + Alias(self.dpi, name="dpi", prefix="+r"), + ] diff --git a/pygmt/tests/test_params_pattern.py b/pygmt/tests/test_params_pattern.py new file mode 100644 index 00000000000..713503bb008 --- /dev/null +++ b/pygmt/tests/test_params_pattern.py @@ -0,0 +1,51 @@ +""" +Test the Pattern class. +""" + +import pytest +from pygmt.exceptions import GMTValueError +from pygmt.params import Pattern + + +def test_pattern(): + """ + Test the Pattern class. + """ + assert str(Pattern(1)) == "p1" + assert str(Pattern(pattern=1)) == "p1" + + assert str(Pattern("pattern.png")) == "ppattern.png" + + assert str(Pattern(10, bgcolor="red")) == "p10+bred" + assert str(Pattern(20, fgcolor="blue")) == "p20+fblue" + assert str(Pattern(30, bgcolor="red", fgcolor="blue")) == "p30+bred+fblue" + assert str(Pattern(30, fgcolor="blue", bgcolor="")) == "p30+b+fblue" + assert str(Pattern(30, fgcolor="", bgcolor="red")) == "p30+bred+f" + + assert str(Pattern(40, dpi=300)) == "p40+r300" + + assert str(Pattern(50, invert=True)) == "P50" + + pattern = Pattern(90, invert=True, bgcolor="red", fgcolor="blue", dpi=300) + assert str(pattern) == "P90+bred+fblue+r300" + + pattern = Pattern("pattern.png", bgcolor="red", fgcolor="blue", dpi=300) + assert str(pattern) == "ppattern.png+bred+fblue+r300" + + +def test_pattern_invalid_pattern(): + """ + Test that an invalid pattern number raises a GMTValueError. + """ + with pytest.raises(GMTValueError): + _ = str(Pattern(0)) + with pytest.raises(GMTValueError): + _ = str(Pattern(91)) + + +def test_pattern_invalid_colors(): + """ + Test that both fgcolor and bgcolor cannot be empty strings. + """ + with pytest.raises(GMTValueError): + _ = str(Pattern(10, fgcolor="", bgcolor=""))