|
| 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 | + ) |
0 commit comments