Skip to content

Commit 4868c8a

Browse files
yvonnefroehlichseismanmichaelgrundweiji14
authored
Figure.pygmtlogo: Initial implementation for the circular, colored PyGMT logo (#3849)
Co-authored-by: Dongdong Tian <seisman.info@gmail.com> Co-authored-by: Michael Grund <23025878+michaelgrund@users.noreply.github.com> Co-authored-by: Wei Ji <23487320+weiji14@users.noreply.github.com>
1 parent 400aac0 commit 4868c8a

5 files changed

Lines changed: 384 additions & 0 deletions

File tree

doc/api/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ Plotting figure elements
3535
Figure.logo
3636
Figure.magnetic_rose
3737
Figure.paragraph
38+
Figure.pygmtlogo
3839
Figure.scalebar
3940
Figure.solar
4041
Figure.text

pygmt/figure.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from pygmt.src.plot import plot as _plot
3131
from pygmt.src.plot3d import plot3d as _plot3d
3232
from pygmt.src.psconvert import psconvert as _psconvert
33+
from pygmt.src.pygmtlogo import pygmtlogo as _pygmtlogo
3334
from pygmt.src.rose import rose as _rose
3435
from pygmt.src.scalebar import scalebar as _scalebar
3536
from pygmt.src.shift_origin import shift_origin as _shift_origin
@@ -464,6 +465,7 @@ def _repr_html_(self) -> str:
464465
plot = _plot
465466
plot3d = _plot3d
466467
psconvert = _psconvert
468+
pygmtlogo = _pygmtlogo
467469
rose = _rose
468470
scalebar = _scalebar
469471
set_panel = _set_panel

pygmt/src/pygmtlogo.py

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
"""
2+
pygmtlogo - Plot the PyGMT logo.
3+
4+
The initial design of the logo is kindly provided by `@sfrooti <https://github.com/sfrooti>`_
5+
and consists of a visual and the wordmark "PyGMT".
6+
"""
7+
8+
from collections.abc import Sequence
9+
from typing import Literal
10+
11+
import numpy as np
12+
from pygmt._typing import AnchorCode, PathLike
13+
from pygmt.helpers import GMTTempFile, fmt_docstring
14+
from pygmt.params import Box, Position
15+
16+
__doctest_skip__ = ["pygmtlogo"]
17+
18+
19+
def _create_logo( # noqa: PLR0915
20+
shape: Literal["circle", "hexagon"] = "circle",
21+
theme: Literal["light", "dark"] = "light",
22+
wordmark: Literal["none", "horizontal", "vertical"] = "none",
23+
color: bool = True,
24+
figname: PathLike = "pygmt_logo.eps",
25+
debug: bool = False,
26+
):
27+
"""
28+
Create the PyGMT logo using PyGMT.
29+
"""
30+
from pygmt.figure import Figure # noqa: PLC0415
31+
32+
# Helpful definitions
33+
size = 4
34+
region = [-size, size] * 2
35+
proj = "x1c"
36+
# Rotation around z-axis by 30 degrees counter-clockwise placed in the center.
37+
perspective = "30+w0/0"
38+
39+
# Radii (make sure that r4-r5 == r2-r3)
40+
r0, r1, r2, r3, r4, r5 = size * np.array([128, 112, 75, 61, 53, 39]) / 128
41+
# Pen thicknesses
42+
thick_shape = r0 - r1 # for shape
43+
thick_gt = r4 - r5 # for letters G and T
44+
thick_m = r4 / 5 # for letter M
45+
thick_comp = thick_shape / 3 # for compass lines
46+
thick_gap = thick_shape / 4
47+
48+
# Define colors
49+
color_light = "white"
50+
color_dark = "gray20"
51+
52+
blue = "48/105/152" # Python blue
53+
yellow = "255/212/59" # Python yellow
54+
red = "238/86/52" # GMT red
55+
if not color:
56+
blue = yellow = red = color_dark
57+
if theme == "dark":
58+
blue = yellow = red = color_light
59+
60+
# Background and wordmark
61+
match theme:
62+
case "light":
63+
color_bg = color_light
64+
color_py = blue
65+
color_gmt = color_dark
66+
case "dark":
67+
color_bg = color_dark
68+
color_py = yellow
69+
color_gmt = color_light
70+
71+
# Define shape
72+
match shape:
73+
case "circle":
74+
symbol = "c"
75+
size_shape = r0 + r1
76+
hex_factor = 1.0
77+
case "hexagon":
78+
symbol = "h"
79+
size_shape = (r0 + 0.34) * 2
80+
hex_factor = 1.1
81+
82+
# Define wordmark
83+
font = "AvantGarde-Book"
84+
match wordmark:
85+
case "vertical":
86+
args_text_wm = {"x": 0, "y": -4.5, "justify": "CT", "font": f"2.5c,{font}"}
87+
case "horizontal":
88+
args_text_wm = {"x": 4.5, "y": 0.8, "justify": "LM", "font": f"8c,{font}"}
89+
90+
def _letter_g_coords():
91+
"""Coordinates for letter G."""
92+
outer_angles = np.deg2rad(np.arange(90, 361))
93+
inner_angles = outer_angles[::-1]
94+
offset = thick_gt / 2
95+
# Outer arc (r4)
96+
arc_outer_x, arc_outer_y = np.cos(outer_angles) * r4, np.sin(outer_angles) * r4
97+
# Connecting lines
98+
connector_x, connector_y = [r4, 0, 0, r5], [offset, offset, -offset, -offset]
99+
# Inner arc (r5)
100+
arc_inner_x, arc_inner_y = np.cos(inner_angles) * r5, np.sin(inner_angles) * r5
101+
# Combine all coordinates (outer arc, connectors, inner arc)
102+
g_x = np.concatenate([arc_outer_x, connector_x, arc_inner_x])
103+
g_y = np.concatenate([arc_outer_y, connector_y, arc_inner_y])
104+
return {"x": g_x, "y": g_y}
105+
106+
def _letter_m_coords():
107+
"""Coordinates for letter M."""
108+
# X-coordinates from left to right.
109+
x1 = thick_gap # Left edge of left vertical line of M.
110+
x5 = r4 # Right edge of right vertical line of M.
111+
x2 = x1 + thick_m # Right edge of left vertical line of M.
112+
x3 = (x1 + x5) / 2 # The middle of M.
113+
x4 = x5 - thick_m # Left edge of right vertical line of M.
114+
# Y-coordinates from bottom to top.
115+
y1 = thick_gt / 2 + thick_gap # Bottom of the letter M.
116+
y2 = r5 - thick_gt # Bottom of the middle peak of M.
117+
y3 = r5 # Top of the middle peak of M.
118+
y4 = r4 # Top of letter M.
119+
# X- and Y-coordinates of the letter M, starting from the left edge of the left
120+
# vertical line and going clockwise.
121+
m_x = [x1, x1, x2, x3, x4, x5, x5, x4, x4, x3, x2, x2]
122+
m_y = [y1, y4, y4, y3, y4, y4, y1, y1, y3, y2, y3, y1]
123+
return {"x": m_x, "y": m_y}
124+
125+
def _letter_t_coords():
126+
"""Coordinates for letter T."""
127+
outer_angles = np.deg2rad(np.arange(240, 300, 0.5))
128+
inner_angles = outer_angles[::-1]
129+
arc_outer_x, arc_outer_y = np.cos(outer_angles) * r2, np.sin(outer_angles) * r2
130+
arc_inner_x, arc_inner_y = np.cos(inner_angles) * r3, np.sin(inner_angles) * r3
131+
# The arrowhead is an equilateral triangle
132+
x0 = thick_gt / 2 # Extra half-width for arrow head
133+
y0 = 1.8 * x0 * np.sqrt(3) # Height for arrow head
134+
arrow_x = [-x0, -x0, -x0 * 2.0, 0, x0 * 2.0, x0, x0]
135+
arrow_y = [-r2, -r0 + y0, -r0 + y0, -r0, -r0 + y0, -r0 + y0, -r2]
136+
mask_left = arc_outer_x < -x0
137+
mask_right = arc_outer_x > x0
138+
t_x = np.concatenate(
139+
[arc_inner_x, arc_outer_x[mask_left], arrow_x, arc_outer_x[mask_right]]
140+
)
141+
t_y = np.concatenate(
142+
[arc_inner_y, arc_outer_y[mask_left], arrow_y, arc_outer_y[mask_right]]
143+
)
144+
# Ensure the same X-coordinate for the right edge of T and the middle of M.
145+
mask = np.abs(t_x) <= (thick_gap + r4) / 2
146+
return {"x": t_x[mask], "y": t_y[mask]}
147+
148+
def _bg_arrow_coords():
149+
"""Coordinates for the background arrow."""
150+
# x0, y0 is the same as in _letter_t_coords().
151+
x0 = thick_gt / 2
152+
y0 = 1.8 * x0 * np.sqrt(3)
153+
# The background arrow is thick_comp wider than the letter T.
154+
x1 = x0 + thick_comp / 2.0 # Half-width of the arrow tail
155+
x2 = 2 * x0 + thick_comp / np.sqrt(3) # Half-width of the arrow head
156+
157+
arrow_x = [-x1, -x1, -x2, -(x2 - 2 * x0), (x2 - 2 * x0), x2, x1, x1]
158+
arrow_y = [r0, -r0 + y0, -r0 + y0, -r0, -r0, -r0 + y0, -r0 + y0, r0]
159+
return {"x": arrow_x, "y": arrow_y}
160+
161+
def _compass_lines():
162+
"""Coordinates of compass lines."""
163+
sqrt2 = np.sqrt(2) / 2
164+
x1, x2, x3 = r0 * sqrt2, r3 * sqrt2, (r2 + (r3 - r4)) * sqrt2
165+
# Coordinates of vectors in the format of (x_start, y_start, x_end, y_end).
166+
return [
167+
(-r0 * hex_factor, 0, -r3, 0), # left horizontal
168+
(r3, 0, r0 * hex_factor, 0), # right horizontal
169+
(-x1, x1, -x2, x2), # upper left
170+
(-x1, -x1, -x2, -x2), # lower left
171+
(x1, x1, x3, x3), # upper right
172+
(x1, -x1, x2, -x2), # lower right
173+
]
174+
175+
def _vline_coords():
176+
"""
177+
Coordinates for the vertical line at the top.
178+
"""
179+
x0 = thick_gt / 2
180+
return {"x": [-x0, -x0, x0, x0], "y": [r0, r3, r3, r0]}
181+
182+
fig = Figure()
183+
fig.basemap(region=region, projection=proj, perspective=perspective, frame="none")
184+
185+
# Earth - circle / hexagon
186+
args_shape = {
187+
"style": f"{symbol}{size_shape}c",
188+
"perspective": True,
189+
"no_clip": True, # Needed for corners of hexagon shape
190+
}
191+
# Shape fill
192+
fig.plot(x=0, y=0, fill=color_bg, **args_shape)
193+
194+
# Compass lines
195+
fig.plot(
196+
data=_compass_lines(),
197+
pen=f"{thick_comp}c,{yellow}",
198+
style="v0c+s",
199+
perspective=True,
200+
no_clip=True,
201+
)
202+
203+
# Shape outline (over ends of compass lines for hexagon shape)
204+
fig.plot(x=0, y=0, pen=f"{thick_shape}c,{blue}", **args_shape)
205+
206+
# Arrow in background color (over shape outline but under letters)
207+
fig.plot(data=_bg_arrow_coords(), fill=color_bg, perspective=True)
208+
209+
# Letters G, M, and T
210+
fig.plot(data=_letter_g_coords(), fill=red, perspective=True)
211+
fig.plot(data=_letter_m_coords(), fill=red, perspective=True)
212+
fig.plot(data=_letter_t_coords(), fill=red, perspective=True)
213+
214+
# Upper vertical line
215+
fig.plot(data=_vline_coords(), fill=red, perspective=True)
216+
217+
# Outline around the shape for black and white color with dark theme
218+
if not color and theme == "dark":
219+
fig.plot(
220+
x=0,
221+
y=0,
222+
style=f"{symbol}{size_shape + thick_shape}c",
223+
pen=f"1p,{color_dark}",
224+
perspective=True,
225+
no_clip=True,
226+
)
227+
228+
# Add wordmark "PyGMT"
229+
if wordmark != "none":
230+
text_wm = f"@;{color_py};Py@;;@;{color_gmt};GMT@;;"
231+
fig.text(text=text_wm, no_clip=True, **args_text_wm)
232+
233+
# Helpful for implementing the logo; not included in the logo
234+
if debug:
235+
from pygmt import config # noqa: PLC0415
236+
237+
# Gridlines
238+
with config(MAP_FRAME_TYPE="inside", MAP_GRID_PEN="0.1p,gray30"):
239+
fig.basemap(frame="g1")
240+
# Circles for the different radii
241+
for r in [r0, r1, r2, r3, r4, r5]:
242+
fig.plot(x=0, y=0, style=f"c{2 * r}c", pen="0.3p,gray30")
243+
pen = "0.3p,gray30,2_2"
244+
fig.plot(x=0, y=0, style=f"c{2 * (r2 + (r3 - r4))}c", pen=pen)
245+
# Lines for letter M
246+
fig.hlines(y=[r4, r5], xmin=-3, pen=pen, perspective=True)
247+
fig.vlines(x=[r4, (thick_gap + r4) / 2], ymax=3, pen=pen, perspective=True)
248+
249+
fig.savefig(fname=figname)
250+
251+
252+
@fmt_docstring
253+
def pygmtlogo( # noqa: PLR0913
254+
self,
255+
shape: Literal["circle", "hexagon"] = "circle",
256+
theme: Literal["light", "dark"] = "light",
257+
wordmark: Literal["none", "horizontal", "vertical"] = "none",
258+
color: bool = True,
259+
width: float | str | None = None,
260+
height: float | str | None = None,
261+
position: Position | Sequence[float | str] | AnchorCode | None = None,
262+
box: Box | bool = False,
263+
verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"]
264+
| bool = False,
265+
panel: int | Sequence[int] | bool = False,
266+
perspective: float | Sequence[float] | str | bool = False,
267+
transparency: float | None = None,
268+
):
269+
"""
270+
Plot the PyGMT logo.
271+
272+
The design of the logo is kindly provided by `@sfrooti <https://github.com/sfrooti>`_
273+
and consists of a visual and the wordmark "PyGMT".
274+
275+
Parameters
276+
----------
277+
shape
278+
Shape of the visual logo. Use ``"circle"`` for a circle shape [Default] or
279+
``"hexagon"`` for a hexagon shape.
280+
theme
281+
Use ``"light"`` for light mode (i.e., a white background) [Default] and
282+
``"dark"`` for dark mode (i.e., a darkgray background).
283+
wordmark
284+
Add the wordmark "PyGMT" and adjust its orientation relative to the visual.
285+
Valid values are:
286+
287+
- ``"none"``: no wordmark [Default].
288+
- ``"horizontal"``: wordmark at the right side of the visual.
289+
- ``"vertical"``: wordmark below the visual.
290+
color
291+
``True`` for a color logo, and ``False`` for a black and white logo.
292+
position
293+
Position of the GMT logo on the plot. It can be specified in multiple ways:
294+
295+
- A :class:`pygmt.params.Position` object to fully control the reference point,
296+
anchor point, and offset.
297+
- A sequence of two values representing the x- and y-coordinates in plot
298+
coordinates, e.g., ``(1, 2)`` or ``("1c", "2c")``.
299+
- A :doc:`2-character justification code </techref/justification_codes>` for a
300+
position inside the plot, e.g., ``"TL"`` for Top Left corner inside the plot.
301+
302+
If not specified, defaults to the Bottom Left corner of the plot (position
303+
``(0, 0)`` with anchor ``"BL"``).
304+
width
305+
height
306+
Width or height of the PyGMT logo. Since the aspect ratio is fixed, only one of
307+
the two can be specified.
308+
box
309+
Draw a background box behind the logo. If set to ``True``, a simple rectangular
310+
box is drawn using :gmt-term:`MAP_FRAME_PEN`. To customize the box appearance,
311+
pass a :class:`pygmt.params.Box` object to control style, fill, pen, and other
312+
box properties.
313+
$verbose
314+
$panel
315+
$perspective
316+
$transparency
317+
318+
Examples
319+
--------
320+
>>> import pygmt
321+
322+
The simplest way to plot the PyGMT logo is to call the method without any arguments.
323+
324+
>>> fig = pygmt.Figure()
325+
>>> fig.pygmtlogo()
326+
>>> fig.show()
327+
328+
Plot the PyGMT logo with the wordmark "PyGMT" with a height of 1 centimeter at the
329+
right side in the Bottom Right corner on an existing basemap:
330+
331+
>>> fig = pygmt.Figure()
332+
>>> fig.basemap(region=[-90, -70, 0, 20], projection="M10c", frame=True)
333+
>>> fig.pygmtlogo(wordmark="horizontal", position="BR", height="1c")
334+
>>> fig.show()
335+
"""
336+
with GMTTempFile(suffix=".eps") as logofile:
337+
# Create logo file
338+
_create_logo(
339+
color=color,
340+
theme=theme,
341+
shape=shape,
342+
wordmark=wordmark,
343+
figname=logofile.name,
344+
)
345+
346+
# Add to existing Figure instance
347+
self.image(
348+
imagefile=logofile.name,
349+
position=position,
350+
width=width,
351+
height=height,
352+
box=box,
353+
verbose=verbose,
354+
panel=panel,
355+
perspective=perspective,
356+
transparency=transparency,
357+
)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
outs:
2+
- md5: 35c59c31c92f13c705a24933465ff551
3+
size: 14374
4+
hash: md5
5+
path: test_pygmtlogo.png

0 commit comments

Comments
 (0)