From fea96f4f8b219b6f4777156eff4680d324c2214d Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Apr 2026 08:36:03 +0800 Subject: [PATCH 1/4] Add subtitle support to Frame --- pygmt/params/frame.py | 9 ++++++++- pygmt/tests/test_params_frame.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 4286e7c2070..0100d2ef42c 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -91,6 +91,7 @@ class _Axes(BaseParam): axes: str | None = None title: str | None = None + subtitle: str | None = None fill: str | None = None @property @@ -99,6 +100,7 @@ def _aliases(self): Alias(self.axes, name="axes"), Alias(self.fill, name="fill", prefix="+g"), Alias(self.title, name="title", prefix="+t"), + Alias(self.subtitle, name="subtitle", prefix="+s"), ] @@ -187,6 +189,9 @@ class Frame(BaseParam): #: The title string centered above the plot frame [Default is no title]. title: str | None = None + #: The subtitle string centered below the plot title [Default is no subtitle]. + subtitle: str | None = None + #: Fill for the interior of the frame with a color or a pattern [Default is no #: fill]. fill: str | None = None @@ -236,7 +241,9 @@ def _validate(self): def _aliases(self): # _Axes() maps to an empty string, which becomes '-B' without arguments and is # invalid when combined with individual axis settings (e.g., '-B -Bxaf -Byaf'). - frame_settings = _Axes(axes=self.axes, title=self.title, fill=self.fill) + frame_settings = _Axes( + axes=self.axes, title=self.title, subtitle=self.subtitle, fill=self.fill + ) has_secondary_xy_axis = any([self.axis2, self.xaxis2, self.yaxis2]) return [ Alias(frame_settings) if str(frame_settings) else Alias(None), diff --git a/pygmt/tests/test_params_frame.py b/pygmt/tests/test_params_frame.py index e9ac05dae95..1aca7d11eb9 100644 --- a/pygmt/tests/test_params_frame.py +++ b/pygmt/tests/test_params_frame.py @@ -32,10 +32,16 @@ def test_params_frame_only(): """ assert str(Frame("WSen")) == "WSen" assert str(Frame(axes="WSEN", title="My Title")) == "WSEN+tMy Title" + assert str(Frame(axes="WSEN", subtitle="My Subtitle")) == "WSEN+sMy Subtitle" frame = str(Frame(axes="WSEN", title="My Title", fill="red")) assert frame == "WSEN+gred+tMy Title" + frame = str( + Frame(axes="WSEN", title="My Title", subtitle="My Subtitle", fill="red") + ) + assert frame == "WSEN+gred+tMy Title+sMy Subtitle" + def test_params_frame_axis(): """ @@ -47,9 +53,10 @@ def test_params_frame_axis(): frame = Frame( axes="WSEN", title="My Title", + subtitle="My Subtitle", axis=Axis(annot=True, tick=True, grid=True, label="LABEL"), ) - assert list(frame) == ["WSEN+tMy Title", "afg+lLABEL"] + assert list(frame) == ["WSEN+tMy Title+sMy Subtitle", "afg+lLABEL"] frame = Frame( axes="WSEN", From 24daaae08e02e217bc46720c8cefa4cd7e522a9a Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Apr 2026 08:50:40 +0800 Subject: [PATCH 2/4] Require title when Frame subtitle is set --- pygmt/params/frame.py | 5 +++++ pygmt/tests/test_params_frame.py | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 0100d2ef42c..8be6af5022c 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -222,6 +222,11 @@ def _validate(self): """ Validate the parameters of the Frame class. """ + if self.subtitle is not None and self.title is None: + raise GMTParameterError( + required="title", + reason="The 'subtitle' parameter requires 'title' to be set.", + ) if self.axis is not None and any( [self.xaxis, self.yaxis, self.xaxis2, self.yaxis2] ): diff --git a/pygmt/tests/test_params_frame.py b/pygmt/tests/test_params_frame.py index 1aca7d11eb9..ab58804b862 100644 --- a/pygmt/tests/test_params_frame.py +++ b/pygmt/tests/test_params_frame.py @@ -32,7 +32,6 @@ def test_params_frame_only(): """ assert str(Frame("WSen")) == "WSen" assert str(Frame(axes="WSEN", title="My Title")) == "WSEN+tMy Title" - assert str(Frame(axes="WSEN", subtitle="My Subtitle")) == "WSEN+sMy Subtitle" frame = str(Frame(axes="WSEN", title="My Title", fill="red")) assert frame == "WSEN+gred+tMy Title" @@ -127,6 +126,9 @@ def test_params_frame_invalid_axis_combinations(): """ Test that invalid combinations of uniform and individual axis settings fail. """ + with pytest.raises(GMTParameterError, match="subtitle.*requires 'title'"): + Frame(subtitle="My Subtitle") + with pytest.raises(GMTParameterError, match="Either 'axis' or"): Frame(axis=Axis(annot=1), xaxis=Axis(annot=2)) From c025f077ead1da11c4dd6ba16a6512f2da84353c Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Apr 2026 08:55:17 +0800 Subject: [PATCH 3/4] Improve docstrings and tests --- pygmt/params/frame.py | 5 +++-- pygmt/tests/test_params_frame.py | 12 +++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 8be6af5022c..3829253185b 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -189,7 +189,8 @@ class Frame(BaseParam): #: The title string centered above the plot frame [Default is no title]. title: str | None = None - #: The subtitle string centered below the plot title [Default is no subtitle]. + #: The subtitle string centered below the plot title [Requires ``title``. Default is + #: no subtitle]. subtitle: str | None = None #: Fill for the interior of the frame with a color or a pattern [Default is no @@ -225,7 +226,7 @@ def _validate(self): if self.subtitle is not None and self.title is None: raise GMTParameterError( required="title", - reason="The 'subtitle' parameter requires 'title' to be set.", + reason="The 'subtitle' attribute requires 'title' to be set.", ) if self.axis is not None and any( [self.xaxis, self.yaxis, self.xaxis2, self.yaxis2] diff --git a/pygmt/tests/test_params_frame.py b/pygmt/tests/test_params_frame.py index ab58804b862..6411b626d0d 100644 --- a/pygmt/tests/test_params_frame.py +++ b/pygmt/tests/test_params_frame.py @@ -33,13 +33,11 @@ def test_params_frame_only(): assert str(Frame("WSen")) == "WSen" assert str(Frame(axes="WSEN", title="My Title")) == "WSEN+tMy Title" - frame = str(Frame(axes="WSEN", title="My Title", fill="red")) - assert frame == "WSEN+gred+tMy Title" + frame = Frame(axes="WSEN", title="My Title", fill="red") + assert str(frame) == "WSEN+gred+tMy Title" - frame = str( - Frame(axes="WSEN", title="My Title", subtitle="My Subtitle", fill="red") - ) - assert frame == "WSEN+gred+tMy Title+sMy Subtitle" + frame = Frame(axes="WSEN", title="My Title", subtitle="My Subtitle", fill="red") + assert str(frame) == "WSEN+gred+tMy Title+sMy Subtitle" def test_params_frame_axis(): @@ -126,7 +124,7 @@ def test_params_frame_invalid_axis_combinations(): """ Test that invalid combinations of uniform and individual axis settings fail. """ - with pytest.raises(GMTParameterError, match="subtitle.*requires 'title'"): + with pytest.raises(GMTParameterError, match="requires 'title'"): Frame(subtitle="My Subtitle") with pytest.raises(GMTParameterError, match="Either 'axis' or"): From 55c186315527fd9c2f409028cc12ef973b332bef Mon Sep 17 00:00:00 2001 From: Dongdong Tian Date: Thu, 16 Apr 2026 16:53:15 +0800 Subject: [PATCH 4/4] Update pygmt/params/frame.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Yvonne Fröhlich <94163266+yvonnefroehlich@users.noreply.github.com> --- pygmt/params/frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pygmt/params/frame.py b/pygmt/params/frame.py index 3829253185b..fe0a7329b16 100644 --- a/pygmt/params/frame.py +++ b/pygmt/params/frame.py @@ -189,8 +189,8 @@ class Frame(BaseParam): #: The title string centered above the plot frame [Default is no title]. title: str | None = None - #: The subtitle string centered below the plot title [Requires ``title``. Default is - #: no subtitle]. + #: The subtitle string centered below the plot title. Requires ``title`` to be set. + #: [Default is no subtitle]. subtitle: str | None = None #: Fill for the interior of the frame with a color or a pattern [Default is no