diff --git a/CHANGELOG.md b/CHANGELOG.md
index 83e1dc6..4a435b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.1.4] - 2026-08-24
+
+### Fixed
+
+- **Scientific plot labels**: Pair plots and fitted plots preserve MathText labels and titles, including uncertainty headers such as `u$x (m)$`. If Matplotlib cannot parse a label or title, the affected plot automatically retries with readable plain text instead of failing.
+
## [1.1.3] - 2026-05-17
### Added
@@ -237,6 +243,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Initial 0.8.x release. See repository history and documentation for features and changes prior to 0.8.1.
[1.1.3]: https://github.com/DOKOS-TAYOS/RegressionLab/compare/v1.1.2...v1.1.3
+[1.1.4]: https://github.com/DOKOS-TAYOS/RegressionLab/compare/v1.1.3...v1.1.4
[1.1.2]: https://github.com/DOKOS-TAYOS/RegressionLab/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/DOKOS-TAYOS/RegressionLab/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/DOKOS-TAYOS/RegressionLab/compare/v1.0.0...v1.1.0
diff --git a/CITATION.cff b/CITATION.cff
index ff9b16a..8a321d9 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -7,7 +7,7 @@ authors:
given-names: Alejandro
email: alejandro.mata.ali@gmail.com
license: MIT
-version: 1.1.3
+version: 1.1.4
repository-code: https://github.com/DOKOS-TAYOS/RegressionLab
url: https://github.com/DOKOS-TAYOS/RegressionLab
abstract: >-
diff --git a/README.md b/README.md
index 74cfede..ecc5e47 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
[](https://www.python.org/)
[](LICENSE)
-[](https://github.com/DOKOS-TAYOS/RegressionLab)
+[](https://github.com/DOKOS-TAYOS/RegressionLab)
[](https://regressionlab.streamlit.app/)
[](https://scipy.org/)
[](https://numpy.org/)
@@ -212,7 +212,7 @@ For information about third-party libraries and their licenses, see
-**Version**: 1.1.3 • **Last Updated**: May 2026
+**Version**: 1.1.4 • **Last Updated**: August 2026
Made with ❤️ by [Alejandro Mata Ali](https://github.com/DOKOS-TAYOS)
diff --git a/docs/README.md b/docs/README.md
index 4fa529c..b3ae5bd 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -265,6 +265,6 @@ Documentation should be reviewed and updated:
---
-**Documentation Version**: 1.1.3
+**Documentation Version**: 1.1.4
**Last Updated**: February 2026
**Maintainer**: Alejandro Mata Ali
diff --git a/docs/api/config.md b/docs/api/config.md
index f88967f..8ab6151 100644
--- a/docs/api/config.md
+++ b/docs/api/config.md
@@ -21,7 +21,7 @@ Usage remains the same: import from `config` (e.g. `from config import PLOT_CONF
### Application Metadata
```python
-__version__ = "1.1.3"
+__version__ = "1.1.4"
__author__ = "Alejandro Mata Ali"
__email__ = "alejandro.mata.ali@gmail.com"
```
diff --git a/docs/index.md b/docs/index.md
index adb9897..c016a79 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -103,6 +103,6 @@ If you need assistance:
---
-**Version**: 1.1.3
+**Version**: 1.1.4
**Author**: Alejandro Mata Ali
**License**: MIT
diff --git a/pyproject.toml b/pyproject.toml
index 54fc789..66462e2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "regressionlab"
-version = "1.1.3"
+version = "1.1.4"
description = "Scientific curve fitting application with GUI"
readme = "README.md"
requires-python = ">=3.12"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/api/index.po b/sphinx-docs/locale/es/LC_MESSAGES/api/index.po
index 0cbadd9..ff29324 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/api/index.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/api/index.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/configuration.po b/sphinx-docs/locale/es/LC_MESSAGES/configuration.po
index 515c75c..b04b17c 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/configuration.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/configuration.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/contributing.po b/sphinx-docs/locale/es/LC_MESSAGES/contributing.po
index 229904a..c11f919 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/contributing.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/contributing.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/customization.po b/sphinx-docs/locale/es/LC_MESSAGES/customization.po
index ac2b789..c49ceff 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/customization.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/customization.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/extending.po b/sphinx-docs/locale/es/LC_MESSAGES/extending.po
index 17b1ebb..4df0973 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/extending.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/extending.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/index.po b/sphinx-docs/locale/es/LC_MESSAGES/index.po
index 1fcadca..e5a178e 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/index.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/index.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/installation.po b/sphinx-docs/locale/es/LC_MESSAGES/installation.po
index f407793..322e055 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/installation.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/installation.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/introduction.po b/sphinx-docs/locale/es/LC_MESSAGES/introduction.po
index 4fff2cd..579b015 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/introduction.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/introduction.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/license.po b/sphinx-docs/locale/es/LC_MESSAGES/license.po
index 7711266..55f17da 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/license.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/license.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/modules/core.po b/sphinx-docs/locale/es/LC_MESSAGES/modules/core.po
index 887b9a0..3e94ce9 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/modules/core.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/modules/core.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/modules/data_analysis.po b/sphinx-docs/locale/es/LC_MESSAGES/modules/data_analysis.po
index c290477..52bfb92 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/modules/data_analysis.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/modules/data_analysis.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/modules/fitting.po b/sphinx-docs/locale/es/LC_MESSAGES/modules/fitting.po
index 909c9f1..4ff6b0f 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/modules/fitting.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/modules/fitting.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/modules/frontend.po b/sphinx-docs/locale/es/LC_MESSAGES/modules/frontend.po
index 9d22cfe..cbdfc32 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/modules/frontend.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/modules/frontend.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/modules/loaders.po b/sphinx-docs/locale/es/LC_MESSAGES/modules/loaders.po
index 45ca032..2a905bb 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/modules/loaders.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/modules/loaders.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/modules/plotting.po b/sphinx-docs/locale/es/LC_MESSAGES/modules/plotting.po
index f8cc603..d575a3a 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/modules/plotting.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/modules/plotting.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/modules/streamlit_app.po b/sphinx-docs/locale/es/LC_MESSAGES/modules/streamlit_app.po
index 98dbfe4..77c7c63 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/modules/streamlit_app.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/modules/streamlit_app.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/modules/utils.po b/sphinx-docs/locale/es/LC_MESSAGES/modules/utils.po
index 797cdf9..bf519f5 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/modules/utils.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/modules/utils.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/streamlit-guide.po b/sphinx-docs/locale/es/LC_MESSAGES/streamlit-guide.po
index ee1bc10..4e38e6a 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/streamlit-guide.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/streamlit-guide.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/tkinter-guide.po b/sphinx-docs/locale/es/LC_MESSAGES/tkinter-guide.po
index 4bd98e7..026d51b 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/tkinter-guide.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/tkinter-guide.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/troubleshooting.po b/sphinx-docs/locale/es/LC_MESSAGES/troubleshooting.po
index bebf17b..a11bc59 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/troubleshooting.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/troubleshooting.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-13 19:57+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/locale/es/LC_MESSAGES/usage.po b/sphinx-docs/locale/es/LC_MESSAGES/usage.po
index 6cc50ee..e42aff2 100644
--- a/sphinx-docs/locale/es/LC_MESSAGES/usage.po
+++ b/sphinx-docs/locale/es/LC_MESSAGES/usage.po
@@ -7,7 +7,7 @@
#, fuzzy
msgid ""
msgstr ""
-"Project-Id-Version: RegressionLab 1.1.3\n"
+"Project-Id-Version: RegressionLab 1.1.4\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-14 18:22+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
diff --git a/sphinx-docs/source/conf.py b/sphinx-docs/source/conf.py
index ffd22ae..966122d 100644
--- a/sphinx-docs/source/conf.py
+++ b/sphinx-docs/source/conf.py
@@ -587,8 +587,8 @@ def add_filters(_app):
except ImportError:
pass # dotenv not required for documentation build
-release = "1.1.3"
-version = "1.1.3"
+release = "1.1.4"
+version = "1.1.4"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
diff --git a/sphinx-docs/source/index.rst b/sphinx-docs/source/index.rst
index 5fee0c9..3688d68 100644
--- a/sphinx-docs/source/index.rst
+++ b/sphinx-docs/source/index.rst
@@ -14,7 +14,7 @@
-
+
diff --git a/src/config/constants.py b/src/config/constants.py
index e6ca3b4..000d1ab 100644
--- a/src/config/constants.py
+++ b/src/config/constants.py
@@ -7,7 +7,7 @@
from config.equation_registry import get_equation_registry
# Application version number
-__version__ = "1.1.3"
+__version__ = "1.1.4"
# Application metadata
__author__ = "Alejandro Mata Ali"
diff --git a/src/plotting/plot_utils.py b/src/plotting/plot_utils.py
index 187b1f8..fb77860 100644
--- a/src/plotting/plot_utils.py
+++ b/src/plotting/plot_utils.py
@@ -7,6 +7,7 @@
# Standard library
from pathlib import Path
+import re
from typing import Any, Dict, List, Optional, Sequence, Tuple, Union
# Third-party packages
@@ -43,6 +44,80 @@
_PAIR_PLOT_CELL_INCHES_MIN = 1.4 # Minimum per cell so many variables stay readable
+def _format_plot_label(name: str) -> str:
+ """Return a MathText-safe label while preserving valid scientific notation."""
+ math_prefix = ""
+ if name.startswith("u$") and name.endswith("$") and name.count("$") == 2:
+ math_prefix = r"\mathrm{u}"
+ content = name[2:-1]
+ elif name.startswith("$") and name.endswith("$") and name.count("$") == 2:
+ content = name[1:-1]
+ else:
+ return name
+
+ match = re.fullmatch(r"(?P[^()]+?)\s*\((?P[^()]*)\)", content)
+ if match is None:
+ return f"${math_prefix}{content}$"
+
+ symbol = match.group("symbol").strip()
+ unit = match.group("unit").strip()
+ return rf"${math_prefix}{symbol}\;(\mathrm{{{unit}}})$"
+
+
+def _plain_plot_label(name: str) -> str:
+ """Return a readable label after MathText parsing has failed."""
+ return name.replace("$", "").replace("\\", "")
+
+
+def _apply_plain_pair_plot_labels(axes: Any, names: List[str]) -> None:
+ """Replace pair-plot labels with text that does not require MathText."""
+ for i, row in enumerate(axes):
+ for j, ax in enumerate(row):
+ if i == j:
+ for text in ax.texts:
+ text.set_text(_plain_plot_label(names[i]))
+ else:
+ ax.set_xlabel(_plain_plot_label(names[j]))
+ ax.set_ylabel(_plain_plot_label(names[i]))
+
+
+def _tight_layout_pair_plot(fig: Any, axes: Any, names: List[str]) -> None:
+ """Apply pair-plot layout, falling back to plain labels on MathText errors."""
+ try:
+ fig.tight_layout(pad=1.2, h_pad=1.0, w_pad=1.0)
+ except ValueError as error:
+ if "Parse" not in str(error):
+ raise
+ logger.warning("MathText failed in pair plot; retrying with plain labels.")
+ _apply_plain_pair_plot_labels(axes, names)
+ fig.tight_layout(pad=1.2, h_pad=1.0, w_pad=1.0)
+
+
+def _tight_layout_fit_plot(
+ fig: Any,
+ ax: Any,
+ x_name: str,
+ y_name: str,
+ fit_name: str,
+ *,
+ show_title: bool,
+ title_font: Any,
+ axis_font: Any,
+) -> None:
+ """Apply fit-plot layout, falling back to plain text on MathText errors."""
+ try:
+ fig.tight_layout()
+ except ValueError as error:
+ if "Parse" not in str(error):
+ raise
+ logger.warning("MathText failed in fit plot; retrying with plain labels.")
+ ax.set_xlabel(_plain_plot_label(x_name), fontproperties=axis_font)
+ ax.set_ylabel(_plain_plot_label(y_name), fontproperties=axis_font)
+ if show_title:
+ ax.set_title(_plain_plot_label(fit_name), fontproperties=title_font)
+ fig.tight_layout()
+
+
def _save_figure(
fig: Any,
save_path: Union[str, Path],
@@ -141,10 +216,11 @@ def create_pair_plots(
edgecolors=_PAIR_PLOT_SCATTER_EDGE,
linewidths=0.5,
)
- ax.set_xlabel(names[0], fontsize=10)
- ax.set_ylabel(names[0], fontsize=10)
+ label = _format_plot_label(names[0])
+ ax.set_xlabel(label, fontsize=10)
+ ax.set_ylabel(label, fontsize=10)
ax.tick_params(axis="both", labelsize=9)
- plt.tight_layout()
+ fig.tight_layout()
if output_path:
return _save_figure(fig, output_path, plot_config, log_saved=True)
return fig
@@ -164,7 +240,7 @@ def create_pair_plots(
ax.text(
0.5,
0.5,
- names[i],
+ _format_plot_label(names[i]),
ha="center",
va="center",
fontsize=min(10, max(8, 14 - n)),
@@ -185,11 +261,17 @@ def create_pair_plots(
linewidths=0.4,
)
ax.grid(True, linestyle="--", alpha=0.35)
- ax.set_xlabel(xcol, fontsize=min(9, max(7, 11 - n // 2)))
- ax.set_ylabel(ycol, fontsize=min(9, max(7, 11 - n // 2)))
+ ax.set_xlabel(
+ _format_plot_label(xcol),
+ fontsize=min(9, max(7, 11 - n // 2)),
+ )
+ ax.set_ylabel(
+ _format_plot_label(ycol),
+ fontsize=min(9, max(7, 11 - n // 2)),
+ )
ax.tick_params(axis="both", labelsize=8)
ax.set_aspect("auto")
- plt.tight_layout(pad=1.2, h_pad=1.0, w_pad=1.0)
+ _tight_layout_pair_plot(fig, axes, names)
if output_path:
return _save_figure(fig, output_path, plot_config, log_saved=True)
@@ -325,17 +407,26 @@ def create_plot(
logger.debug("Data plotted")
- ax.set_xlabel(x_name, fontproperties=fonta)
- ax.set_ylabel(y_name, fontproperties=fonta)
+ ax.set_xlabel(_format_plot_label(x_name), fontproperties=fonta)
+ ax.set_ylabel(_format_plot_label(y_name), fontproperties=fonta)
if plot_config.get("show_title", False):
- ax.set_title(fit_name, fontproperties=fontt)
+ ax.set_title(_format_plot_label(fit_name), fontproperties=fontt)
if plot_config.get("show_grid", False):
ax.grid(True, alpha=0.3)
ax.tick_params(axis="both", which="major", labelsize=font_config["tick_size"])
- plt.tight_layout()
+ _tight_layout_fit_plot(
+ fig,
+ ax,
+ x_name,
+ y_name,
+ fit_name,
+ show_title=plot_config.get("show_title", False),
+ title_font=fontt,
+ axis_font=fonta,
+ )
logger.debug("Saving plot to: %s", save_path)
return _save_figure(fig, save_path, plot_config, log_saved=True)
diff --git a/tests/test_plot_utils.py b/tests/test_plot_utils.py
new file mode 100644
index 0000000..db8de65
--- /dev/null
+++ b/tests/test_plot_utils.py
@@ -0,0 +1,111 @@
+"""Tests for plotting utilities."""
+
+from pathlib import Path
+
+import matplotlib
+import numpy as np
+import pandas as pd
+
+matplotlib.use("Agg")
+
+import matplotlib.pyplot as plt
+
+from config import PLOT_CONFIG
+from loaders import excel_reader, get_variable_names
+from plotting import create_pair_plots, create_plot
+
+
+def test_pair_plots_preserve_dollar_delimited_column_names_as_mathtext() -> None:
+ """Pair plots must retain scientific MathText column labels."""
+ data = pd.DataFrame(
+ {
+ "$x (m)$": [0.0, 1.0, 2.0],
+ "$y (m)$": [1.0, 2.0, 3.0],
+ }
+ )
+
+ figure = create_pair_plots(data, ["$x (m)$", "$y (m)$"])
+ figure.canvas.draw()
+
+ assert figure.axes[1].get_xlabel() == r"$y\;(\mathrm{m})$"
+ assert figure.axes[1].get_ylabel() == r"$x\;(\mathrm{m})$"
+
+ plt.close(figure)
+
+
+def test_pair_plots_format_uncertainty_mathtext_as_one_expression() -> None:
+ """Uncertainty prefixes must be included in the MathText expression."""
+ data = pd.DataFrame(
+ {
+ "$x (m)$": [0.0, 1.0, 2.0],
+ "u$x (m)$": [0.1, 0.1, 0.1],
+ }
+ )
+
+ figure = create_pair_plots(data, ["$x (m)$", "u$x (m)$"])
+ figure.canvas.draw()
+
+ assert figure.axes[1].get_xlabel() == r"$\mathrm{u}x\;(\mathrm{m})$"
+
+ plt.close(figure)
+
+
+def test_pair_plots_fall_back_to_plain_labels_when_mathtext_is_invalid() -> None:
+ """Invalid MathText labels must not prevent a pair plot from rendering."""
+ data = pd.DataFrame(
+ {
+ r"$x\notacommand$": [0.0, 1.0, 2.0],
+ "$y (m)$": [1.0, 2.0, 3.0],
+ }
+ )
+
+ figure = create_pair_plots(data, [r"$x\notacommand$", "$y (m)$"])
+ figure.canvas.draw()
+
+ assert figure.axes[1].get_ylabel() == "xnotacommand"
+
+ plt.close(figure)
+
+
+def test_pair_plots_render_mathtext_headers_from_all_input_workbooks() -> None:
+ """Every bundled workbook with MathText headers must render without errors."""
+ input_dir = Path(__file__).parent.parent / "input"
+
+ for workbook in sorted(input_dir.glob("*.xlsx")):
+ data = excel_reader(str(workbook))
+ variable_names = get_variable_names(data, filter_uncertainty=True)
+ figure = create_pair_plots(data, variable_names)
+ figure.canvas.draw()
+
+ if len(variable_names) > 1:
+ for label in (figure.axes[1].get_xlabel(), figure.axes[1].get_ylabel()):
+ assert (
+ label.startswith("$") and label.endswith("$")
+ if "$" in label
+ else True
+ )
+
+ plt.close(figure)
+
+
+def test_fit_plot_falls_back_to_plain_title_when_mathtext_is_invalid(
+ tmp_path: Path,
+) -> None:
+ """An invalid MathText title must not prevent a fitted plot from being saved."""
+ output_path = tmp_path / "fit.png"
+
+ result = create_plot(
+ x=np.array([0.0, 1.0, 2.0]),
+ y=np.array([0.0, 1.0, 2.0]),
+ ux=np.zeros(3),
+ uy=np.zeros(3),
+ y_fitted=np.array([0.0, 1.0, 2.0]),
+ fit_name=r"$x\notacommand$",
+ x_name="x(m)",
+ y_name="y(m)",
+ plot_config={**PLOT_CONFIG, "show_title": True},
+ output_path=output_path,
+ )
+
+ assert result == str(output_path)
+ assert output_path.exists()