diff --git a/doc/api/index.rst b/doc/api/index.rst index 25de6d44adf..e1895262909 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -33,6 +33,7 @@ Plotting map elements Figure.inset Figure.legend Figure.logo + Figure.pygmtlogo Figure.solar Figure.text Figure.timestamp diff --git a/examples/gallery/embellishments/pygmt_logo.py b/examples/gallery/embellishments/pygmt_logo.py new file mode 100644 index 00000000000..b9655035656 --- /dev/null +++ b/examples/gallery/embellishments/pygmt_logo.py @@ -0,0 +1,160 @@ +""" +PyGMT logo +========== +Beside the GMT logo, there is a separate PyGMT logo which can be plotted and added +to a figure using :meth:`pygmt.Figure.pygmtlogo`. The design of the logo itself is +kindly provided by `@sfrooti `_ and consists of a visual +and the wordmark "PyGMT". +The logo is available in circle and hexagon shape. It can be plotted using colors of +Python (blue and yellow) and GMT (red) or in black and white as well as in light or +dark mode. The wordmark can be added at the right side or bottom of the visual. +""" + +import pygmt + +fig = pygmt.Figure() +fig.pygmtlogo() +fig.show() + +# %% + +fig = pygmt.Figure() +fig.pygmtlogo(theme="dark", box="+ggray20") +fig.show() + +# %% + +fig = pygmt.Figure() +fig.basemap(region=[-5, 5, -5, 5], projection="X10c", frame=[1, "+gtan"]) + +fig.pygmtlogo(position="jTL+o0.2c+w4c", box="+gwhite+p1p,gray") +fig.pygmtlogo(shape="hexagon", position="jTR+o0.2c+w4c") + +fig.pygmtlogo(color=False, wordmark=False, position="jTL+o0.5c/2c+w1.5c", box=False) +fig.pygmtlogo( + color=False, + theme="dark", + shape="hexagon", + wordmark=False, + position="jTR+o0.5c/2c+w1.5c", + box=False, +) +fig.pygmtlogo(wordmark="vertical", position="jMC+w2c", box="+gwhite") + +fig.show() + +# %% +# All combinations + +i_plot = 0 + +fig = pygmt.Figure() + +for color in [True, False]: + for theme in ["light", "dark"]: + for shape in ["circle", "hexagon"]: + for wordmark in [False, True, "horizontal", "vertical"]: + for box in [False, True]: + if not box: + box_used = False + elif box: + if theme == "light": + box_used = "+gwhite" + elif theme == "dark": + box_used = "+ggray20" + # fig = pygmt.Figure() + fig.basemap( + region=[-1, 1, -1, 1], projection="X2.5c/3.5c", frame="+gtan" + ) + # fig.image("@needle.png", position="jMC+w2c", box=box_used) + fig.pygmtlogo( + color=color, + theme=theme, + shape=shape, + wordmark=wordmark, + position="jMC+w2c", + box=box_used, + ) + + fig.shift_origin(xshift="+w+0.5c") + n_hor = 8 + if i_plot in range(n_hor - 1, 100, n_hor): + fig.shift_origin( + xshift=f"-{(n_hor * 2.5 + n_hor * 0.5)}c", + yshift="-h-0.5c", + ) # n_hor*width + n_hor*xshift + + i_plot = i_plot + 1 +fig.show() + + +# %% +# All versions +# modified from +# https://github.com/GenericMappingTools/pygmt/pull/3849#issuecomment-2753372170 +# by @seisman + +fig = pygmt.Figure() + +# Logo without workmark. +fig.basemap(region=[0, 7, 0, 13], projection="x1c", frame="a1f1g1") +for x, y, theme in [(1, 3, "light"), (4, 3, "dark")]: + for color, shape in [ + (True, "circle"), + (True, "hexagon"), + (False, "circle"), + (False, "hexagon"), + ]: + fig.pygmtlogo( + color=color, + theme=theme, + shape=shape, + wordmark=False, + position=f"g{x}/{y}+jTL+w2c", + ) + y += 3 # noqa: PLW2901 + +fig.shift_origin(xshift=8) + +# Logo with vertical wordmark. +fig.basemap(region=[0, 7, 0, 13], projection="x1c", frame="a1f1g1") +for x, y, theme in [(1, 3, "light"), (4, 3, "dark")]: + for color, shape in [ + (True, "circle"), + (True, "hexagon"), + (False, "circle"), + (False, "hexagon"), + ]: + fig.pygmtlogo( + color=color, + theme=theme, + shape=shape, + wordmark="vertical", + position=f"g{x}/{y}+jTL+w2c", + ) + y += 3 # noqa: PLW2901 + +fig.shift_origin(xshift=8) + +# Logo with horizontal wordmark. +fig.basemap(region=[0, 20, 0, 13], projection="x1c", frame="a1f1g1") +for x, y, theme in [(1, 3, "light"), (11, 3, "dark")]: + for color, shape in [ + (True, "circle"), + (True, "hexagon"), + (False, "circle"), + (False, "hexagon"), + ]: + fig.pygmtlogo( + color=color, + theme=theme, + shape=shape, + wordmark="horizontal", + position=f"g{x}/{y}+jTL+w0/2c", + ) + y += 3 # noqa: PLW2901 + +fig.show(width=1000) + + +# sphinx_gallery_thumbnail_number = 3 diff --git a/pygmt/figure.py b/pygmt/figure.py index 5c5d4734ce6..26aad0557c6 100644 --- a/pygmt/figure.py +++ b/pygmt/figure.py @@ -427,6 +427,7 @@ def _repr_html_(self) -> str: plot, plot3d, psconvert, + pygmtlogo, rose, set_panel, shift_origin, diff --git a/pygmt/src/__init__.py b/pygmt/src/__init__.py index 8905124f917..93be835fffb 100644 --- a/pygmt/src/__init__.py +++ b/pygmt/src/__init__.py @@ -42,6 +42,7 @@ from pygmt.src.plot3d import plot3d from pygmt.src.project import project from pygmt.src.psconvert import psconvert +from pygmt.src.pygmtlogo import pygmtlogo from pygmt.src.rose import rose from pygmt.src.select import select from pygmt.src.shift_origin import shift_origin diff --git a/pygmt/src/pygmtlogo.py b/pygmt/src/pygmtlogo.py new file mode 100644 index 00000000000..dcc93d7158c --- /dev/null +++ b/pygmt/src/pygmtlogo.py @@ -0,0 +1,292 @@ +""" +pygmtlogo - Create and plot the PyGMT logo. +The design of the logo is kindly provided by `@sfrooti `_ +and consists of a visual and the wordmark "PyGMT". +""" + +from pathlib import Path + +import pygmt + + +def create_logo(color=True, theme="light", shape="circle", wordmark=True): # noqa: PLR0915 + """ + Create the PyGMT logo using PyGMT. + The design of the logo is kindly provided by `@sfrooti `_ + and consists of a visual and the wordmark "PyGMT". + + Parameters + ---------- + + color : bool + Set to ``True`` to use colors referring to Python (blue and yellow) and GMT + (red) [Default]. For ``False``, the logo is drawn in black and white. + theme : str + Use ``"light"`` for light mode (i.e., white background) [Default] and ``"dark"`` + for dark mode (i.e., darkgray [gray20] background). + shape : str + Shape of the visual. Use ``"circle"`` for a circle shape [Default] or + ``"hexagon"`` for a hexagon shape. + wordmark : bool, str + Add the wordmark "PyGMT" and adjust its orientation relative to the visual. + Set to ``True`` or ``"horizontal"``, to add the wordmark at the right side of + the visual [Default]. Use ``"vertical"`` to place the wordmark below the + visual and ``False`` to add no wordmark. + """ + + # ----------------------------------------------------------------------------- + # Helpful definitions + # ----------------------------------------------------------------------------- + size = 4 + region = [-size, size] * 2 + projection = "x1c" + + # Outer and inner radii of compass lines + r1, r2 = size * 0.625, size * 0.325 + + # Rotation around z (vertical) axis placed in the center + # Has to be applied to each plotting command, up on second call set to True + perspective = "30+w0/0" # Rotation by 30 degrees + + # ----------------------------------------------------------------------------- + # Define colors + # ----------------------------------------------------------------------------- + color_light = "white" + color_dark = "gray20" + + # visual + color_blue = "48/105/152" # Python blue + color_yellow = "255/212/59" # Python yellow + color_red = "238/86/52" # GMT red + if not color: + color_blue = color_yellow = color_red = color_dark + if theme == "dark": + color_blue = color_yellow = color_red = color_light + + # background and wordmark + color_bg = color_light + color_py = color_blue + color_gmt = color_dark + if theme == "dark": + color_bg = color_dark + color_py = color_yellow + color_gmt = color_light + + # ----------------------------------------------------------------------------- + # Define shape + # ----------------------------------------------------------------------------- + symbol = "c" # circle + diameter = 7.5 + diameter_add = 0.5 + if shape == "hexagon": + symbol = "h" # hexagon + diameter = 8.6 + diameter_add = 0.6 + + # ----------------------------------------------------------------------------- + # Define wordmark + # ----------------------------------------------------------------------------- + font = "AvantGarde-Book" + match wordmark: + case "vertical": + args_text_wm = { + "x": 0, + "y": -5, + "justify": "CT", + "font": f"2.5c,{font}", + } + case True | "horizontal": + args_text_wm = { + "x": 6, + "y": 0, + "justify": "LM", + "font": f"8c,{font}", + } + + # ----------------------------------------------------------------------------- + # Start plotting + # ----------------------------------------------------------------------------- + fig = pygmt.Figure() + + # ............................................................................. + # blue circle / hexagon for Earth + # ............................................................................. + fig.plot( + x=0, + y=0, + region=region, + projection=projection, + style=f"{symbol}{diameter}c", + pen=f"0.5c,{color_blue}", + fill=color_bg, + perspective=perspective, + no_clip=True, # needed for corners of hexagon shape + ) + + # ............................................................................. + # yellow lines for compass + # ............................................................................. + lines_yellow = [ + ([-size, size], [0, 0]), # horizontal line + ([-r1, -r2], [r1, r2]), # upper left + ([-r1, -r2], [-r1, -r2]), # lower left + ([r1, r2], [r1, r2]), # upper right + ([r1, r2], [-r1, -r2]), # lower right + ] + for x, y in lines_yellow: + fig.plot(x=x, y=y, pen=f"5p,{color_yellow}", perspective=True) + + # ............................................................................. + # letter G + # ............................................................................. + # horizontal red line + fig.plot(x=[0.1, 1.65], y=[0, 0], pen=f"12p,{color_red}", perspective=True) + # red ring sector + fig.plot(x=0, y=0, style="w3.3c/90/0+i2.35c", fill=color_red, perspective=True) + # space between yellow lines and ring sector + fig.plot(x=0, y=0, style="w3.7c/0/360+i3.3c", fill=color_bg, perspective=True) + # vertical yellow line + fig.plot(x=[0, 0], y=[-4, 4], pen=f"6p,{color_yellow}", perspective=True) + # cover yellow line in lower part of the ring sector + fig.plot(x=0, y=0, style="w3.3c/260/-80+i2.35c", fill=color_red, perspective=True) + + # ............................................................................. + # upper vertical red line + # ............................................................................. + # space between red line and blue circle / hexagon + fig.plot(x=[0, 0], y=[4, 3.0], pen=f"18p,{color_bg}", perspective=True) + # red line + fig.plot(x=[0, 0], y=[4, 1.9], pen=f"12p,{color_red}", perspective=True) + + # ............................................................................. + # letter M + # ............................................................................. + # space between letter M and yellow line at the right side + # fig.plot(x=[1.6, 1.6], y=[1.5, 1.775], pen=f"10p,{color_bg}") + fig.plot(x=[1.6, 1.6], y=[1.5, 2.0], pen=f"10p,{color_bg}", perspective=True) + + # polygon with small distance to horizontal line of letter G + # starting point: lower right corner of the left vertical line of letter M + # direction: clockwise + m_x1 = 0.33 - 0.33 / 2 - 0.06 + m_x2 = 1.54 + 0.33 / 2 - 0.06 # outer radius of letter G + m_x = [ + m_x1 + m_x2 / 5, # vertical left upwarts + m_x1, + m_x1, + m_x1 + m_x2 / 5, + m_x1 + (m_x2 - m_x1) / 2, # mid pick above + m_x2 - m_x2 / 5, # vertical right downwarts + m_x2, + m_x2, + m_x2 - m_x2 / 5, + m_x2 - m_x2 / 5, # right pick below + m_x1 + (m_x2 - m_x1) / 2, # mid pick below + m_x1 + m_x2 / 5, # left pick below + ] + m_y1 = 0.3 + m_y2 = 1.65 # outer radius of letter G + m_y = [ + m_y1, # vertical left upwarts + m_y1, + m_y2, + m_y2, + m_y2 - m_y2 / 4, # mid pick above + m_y2, # vertical right downwarts + m_y2, + m_y1, + m_y1, + m_y2 - m_y2 / 3, # right pick below + m_y2 - m_y2 / 2 - m_y2 / 18, # mid pick below + m_y2 - m_y2 / 3, # left pick below + ] + fig.plot(x=m_x, y=m_y, close=True, fill=color_red) + + # ............................................................................. + # letter T + # ............................................................................. + # red curved horizontal line + fig.plot(x=0, y=0, style="w4.6c/240/-60+i3.7c", fill=color_red, perspective=True) + # vertical endings of curved horizontal line + args_vert = {"y": [-1.5, -2.5], "pen": f"9p,{color_bg}", "perspective": True} + fig.plot(x=[-1.05, -1.05], **args_vert) + fig.plot(x=[1.05, 1.05], **args_vert) + # arrow head as inverse triangle with pen for space to blue circle / hexagon + fig.plot( + x=0, + y=-3.55, + style="i1.1c", + fill=color_red, + pen=f"3p,{color_bg}", + perspective=True, + ) + # arrow tail + fig.plot(x=[0, 0], y=[-2, -3.57], pen=f"12p,{color_red}", perspective=True) + + # outline around shape for black and white with dark theme (i.e., white shape) + if not color and theme == "dark": + fig.plot( + x=0, + y=0, + style=f"{symbol}{diameter + diameter_add}c", + pen=f"1p,{color_dark}", + perspective=True, + no_clip=True, + ) + + # ............................................................................. + # Add wordmark "PyGMT" + # ............................................................................. + if wordmark: + text_wm = f"@;{color_py};Py@;;@;{color_gmt};GMT@;;" + fig.text(text=text_wm, no_clip=True, **args_text_wm) + + # ............................................................................. + # Save + # ............................................................................. + fig_name_logo = "pygmt_logo" + fig.savefig(fname=f"{fig_name_logo}.eps") + + return fig_name_logo, color_bg + + +def pygmtlogo( # noqa: PLR0913 + self, + color=True, + theme="light", + shape="circle", + wordmark=True, + position=None, # -> use position parameter of Figure.image + box=None, # -> use box parameter of Figure.image + projection=None, + region=None, + verbose=None, + panel=None, + transparency=None, +): + """ + Plot the PyGMT logo. + """ + + # ----------------------------------------------------------------------------- + # Create logo file + # ----------------------------------------------------------------------------- + fig_name_logo, color_bg = create_logo( + color=color, theme=theme, shape=shape, wordmark=wordmark + ) + + # ----------------------------------------------------------------------------- + # Add to existing Figure instance + # ----------------------------------------------------------------------------- + self.image( + imagefile=f"{fig_name_logo}.eps", + position=position, + box=box, + projection=projection, + region=region, + verbose=verbose, + panel=panel, + transparency=transparency, + ) + + Path.unlink(f"{fig_name_logo}.eps") diff --git a/pygmt/tests/test_pygmtlogo.py b/pygmt/tests/test_pygmtlogo.py new file mode 100644 index 00000000000..a632ac13427 --- /dev/null +++ b/pygmt/tests/test_pygmtlogo.py @@ -0,0 +1,61 @@ +""" +Test Figure.pygmtlogo. +""" + +import pytest +from pygmt import Figure + + +@pytest.mark.benchmark +@pytest.mark.mpl_image_compare +def test_pylogo(): + """ + Plot the PyGMT logo using the default settings. + """ + fig = Figure() + fig.pygmtlogo() + return fig + + +@pytest.mark.mpl_image_compare +def test_pylogo_on_a_map(): + """ + Plot the PyGMT logo and adjust the position, offset, and size. + """ + fig = Figure() + fig.basemap(region=[-5, 5, -5, 5], projection="X10c", frame=1) + fig.pygmtlogo(position="jMC+o0.25c/0.25c+w7.5c", box=True) + return fig + + +@pytest.mark.mpl_image_compare +def test_pylogo_no_wordmark(): + """ + Plot the PyGMT logo without wordmark. + """ + fig = Figure() + fig.basemap(region=[-5, 5, -5, 5], projection="X10c", frame=1) + fig.pygmtlogo(wordmark=False) + return fig + + +@pytest.mark.mpl_image_compare +def test_pylogo_lightmode(): + """ + Plot the PyGMT logo in light mode. + """ + fig = Figure() + fig.basemap(region=[-5, 5, -5, 5], projection="X10c", frame=1) + fig.pygmtlogo(darkmode=False) + return fig + + +@pytest.mark.mpl_image_compare +def test_pylogo_vertical(): + """ + Plot the PyGMT logo with vertical orientation of the wordmark. + """ + fig = Figure() + fig.basemap(region=[-5, 5, -5, 5], projection="X10c", frame=1) + fig.pygmtlogo(orientation="vertical") + return fig