diff --git a/pygmt/src/legend.py b/pygmt/src/legend.py index 47901676aac..5b6fb0fcd4a 100644 --- a/pygmt/src/legend.py +++ b/pygmt/src/legend.py @@ -3,12 +3,13 @@ """ import io +from collections.abc import Sequence from typing import Literal -from pygmt._typing import PathLike -from pygmt.alias import AliasSystem +from pygmt._typing import AnchorCode, PathLike +from pygmt.alias import Alias, AliasSystem from pygmt.clib import Session -from pygmt.exceptions import GMTTypeError +from pygmt.exceptions import GMTInvalidInput, GMTTypeError from pygmt.helpers import ( build_arg_list, data_kind, @@ -20,18 +21,21 @@ @fmt_docstring -@use_alias( - R="region", - D="position", - F="box", - p="perspective", -) +@use_alias(R="region", D="position", F="box", p="perspective") @kwargs_to_strings(R="sequence", p="sequence") -def legend( +def legend( # noqa: PLR0913 self, spec: PathLike | io.StringIO | None = None, projection=None, - position="JTR+jTR+o0.2c", + position: Sequence[float | str] | AnchorCode | None = None, + position_type: Literal[ + "mapcoords", "boxcoords", "plotcoords", "inside", "outside" + ] = "plotcoords", + anchor: AnchorCode | None = None, + anchor_offset: Sequence[float | str] | None = None, + width=None, + height=None, + spacing=None, box="+gwhite+p1p", verbose: Literal["quiet", "error", "warning", "timing", "info", "compat", "debug"] | bool = False, @@ -69,14 +73,51 @@ def legend( See :gmt-docs:`legend.html` for the definition of the legend specification. {projection} {region} - position : str - [**g**\|\ **j**\|\ **J**\|\ **n**\|\ **x**]\ *refpoint*\ - **+w**\ *width*\ [/*height*]\ [**+j**\ *justify*]\ [**+l**\ *spacing*]\ - [**+o**\ *dx*\ [/*dy*]]. - Define the reference point on the map for the - legend. By default, uses **JTR**\ **+jTR**\ **+o**\ 0.2c which - places the legend at the top-right corner inside the map frame, with a - 0.2 cm offset. + position/position_type + Specify the reference point on the map for the directional rose. The reference + point can be specified in five different ways, which is selected by the + **position_type** parameter. The actual reference point is then given by the + coordinates or code specified by the **position** parameter. + + The **position_type** parameter can be one of the following: + + - ``"mapcoords"``: **position** is given as (*longitude*, *latitude*) in map + coordinates. + - ``"boxcoords"``: **position** is given as (*nx*, *ny*) in normalized + coordinates, i.e., fractional coordinates between 0 and 1 in both the x and y + directions. For example, (0, 0) is the lower-left corner and (1, 1) is the + upper-right corner of the plot bounding box. + - ``"plotcoords"``: **position** is given as (x, y) in plot coordinates, i.e., + the distances in inches, centimeters, or points from the lower left plot + origin. + - ``"inside"`` or ``"outside"``: **position** is one of the nine + :doc:`2-character justification codes `, meaning + placing the reference point at specific locations, either inside or outside + the plot bounding box. + anchor + Anchor point of the directional rose, specified by one of the + :doc:`2-character justification codes `. + The default value depends on the **position_type** parameter. + + - ``position_type="inside"``: **anchor** defaults to the same as **position**. + - ``position_type="outside"``: **anchor** defaults to the mirror opposite of + **position**. + - Otherwise, **anchor** defaults to ``"MC"`` (middle center). + anchor_offset + *offset* or (*offset_x*, *offset_y*). + Offset the anchor point by *offset_x* and *offset_y*. If a single value *offset* + is given, *offset_y* = *offset_x* = *offset*. + width/height + Width and height of the legend box in plot coordinates (inches, cm, etc.). If + unit is % (percentage) then width as computed as that fraction of the map width. + If height is given as percentage then then height is recomputed as that fraction + of the legend width (not map height). Note: If **width** is not given then the + width is computed within the Postscript code. Currently, this is only possible + if just legend codes D, H, L, S, or V are used and that the number of symbol + columns (N) is 1. If **height** is zero or not given then we estimate height + based the expected vertical extent of the items to be placed. + spacing + Line spacing factor in units of the current font size [Default is 1.1]. box : bool or str [**+c**\ *clearances*][**+g**\ *fill*][**+i**\ [[*gap*/]\ *pen*]]\ [**+p**\ [*pen*]][**+r**\ [*radius*]][**+s**\ [[*dx*/*dy*/][*shade*]]]. @@ -104,7 +145,31 @@ def legend( type(spec), reason="Only one legend specification file is allowed." ) - aliasdict = AliasSystem().add_common( + if height is not None and width is None: + msg = "Parameter 'width' must be given if 'height' is specified." + raise GMTInvalidInput(msg) + + _dimension = (width, height) if height is not None else width + aliasdict = AliasSystem( + D=[ + Alias( + position_type, + name="position_type", + mapping={ + "mapcoords": "g", + "boxcoords": "n", + "plotcoords": "x", + "inside": "j", + "outside": "J", + }, + ), + Alias(position, name="position", sep="/", size=2), + Alias(anchor, name="anchor", prefix="+j"), + Alias(anchor_offset, name="anchor_offset", prefix="+o", sep="/", size=2), + Alias(_dimension, name="width/height", prefix="+w"), + Alias(spacing, name="spacing", prefix="+l"), + ], + ).add_common( J=projection, V=verbose, c=panel,