Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logo-handling class and remote fetching of Ouranos logos #119

Merged
merged 23 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
554a719
Add a logo setting and fetching mechanism for Figanos
Zeitsperre Oct 5, 2023
038d086
typing
Zeitsperre Oct 5, 2023
0225db0
remove obsolete method
Zeitsperre Oct 5, 2023
0b1def1
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 5, 2023
724b5a6
nicer ui, automatic reloading
Zeitsperre Oct 6, 2023
022dc0d
implement logo class, remove ouranos logos
Zeitsperre Oct 6, 2023
d4934fd
simpler implementation, add figanos logo by default, add documentation
Zeitsperre Oct 6, 2023
b0bb4cc
update HISTORY.rst
Zeitsperre Oct 6, 2023
8137485
update HISTORY.rst
Zeitsperre Oct 6, 2023
ac0bfdc
Apply suggestions from code review
Zeitsperre Oct 10, 2023
ffe56d5
fix docs and better logic
Zeitsperre Oct 10, 2023
32b6d77
update jupyter documentation
Zeitsperre Oct 10, 2023
38ea662
address comments
Zeitsperre Oct 10, 2023
31b159b
set ouranos logo on default if default is `figanos_logo`
Zeitsperre Oct 10, 2023
fdd8c5f
image rescaling
Zeitsperre Oct 11, 2023
ffb5dcb
fix logo installation error
Zeitsperre Oct 11, 2023
daae932
add scikit-image
Zeitsperre Oct 11, 2023
162d00d
update HISTORY.rst
Zeitsperre Oct 11, 2023
214edb6
typo fix
Zeitsperre Oct 12, 2023
bdb885b
support scaling of vector graphics for logos, add xclim and cairosvg …
Zeitsperre Oct 13, 2023
d44c21b
allow passing Logos class to plot_logo
Zeitsperre Oct 13, 2023
01cf6f5
add svg-based logo, update changes
Zeitsperre Oct 17, 2023
35f2fe8
rerun notebook, skip execution on long-running cell
Zeitsperre Oct 17, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ Contributors to this version: Sarah-Claude Bourdeau-Goulet (:user:`Sarahclaude`)
New features and enhancements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* New function hatchmap (:pull:`107`).
* Support for translating figures. Activating a locale through xclim's ``metadata_locales`` option will try to use metadata saved by xclim or xscen in this locale and to translate common terms appearing in the figures. Figanos currently ships with french translations of those terms. (:pull:`109`, :issue:`64`).
* Support for translating figures. Activating a locale through xclim's ``metadata_locales`` option will try to use metadata saved by `xclim` or xscen in this locale and to translate common terms appearing in the figures. Figanos currently ships with French translations of those terms. (:pull:`109`, :issue:`64`).
* New ``figanos.Logos`` class added to manage and install logos stored in user's Home configuration directory. The ``figanos.utils.plot_logo`` function call signature has changed to support the new system. (:issue:`115`, :pull:`119`).

Internal changes
^^^^^^^^^^^^^^^^
* Clean up of the dependencies to remove the notebooks deps from the core deps.
* `figanos` now uses Trusted Publishing to publish the package on PyPI and TestPyPI. (:pull:`113`).
* The official Ouranos logos have been removed from the repository. They can now installed if required via the ``figanos.Logos.install_ouranos_logos`` class method. (:issue:`115`, :pull:`119`).
Zeitsperre marked this conversation as resolved.
Show resolved Hide resolved

0.2.0 (2023-06-19)
------------------
Expand Down
602 changes: 456 additions & 146 deletions docs/notebooks/figanos_docs.ipynb

Large diffs are not rendered by default.

62 changes: 60 additions & 2 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,67 @@
Usage
=====

To use figanos in a project::
Quickstart
~~~~~~~~~~
To use figanos in a project:

.. code-block:: python

import figanos.matplotlib as fg
fg.utils.set_mpl_style('ouranos')

fg.utils.set_mpl_style("ouranos")

The style can be applied to any matplotlib figures, even if they are not created with figanos.

Logo Management
~~~~~~~~~~~~~~~
Figanos stores logos for convenience so that they can be called by name when creating figures. On installation, the `default` logo will be set to the `figanos_logo.png` file. Files are saved in the user's home configuration folder (`XDG_CONFIG_HOME` on Linux), in the `figanos/logos` folder.

For users who are permitted to use the Ouranos logos, they can be installed with the following command. You only need to run this once when setting up a new environment with figanos.

.. code-block:: python

from figanos import Logos

logos = Logos()

logos.default # Returns the path to the default logo
# '/home/username/.config/figanos/logos/figanos_logo.png'

logos.install_ouranos_logos(permitted=True)
# "Ouranos logos installed at /home/username/.config/figanos/logos"

logos.installed() # Returns the installed logo names
# ['default',
# 'figanos_logo',
# 'ouranos_logo_horizontal_blanc',
# 'ouranos_logo_horizontal_couleur',
# 'ouranos_logo_horizontal_noir',
# 'ouranos_logo_vertical_blanc',
# 'ouranos_logo_vertical_couleur',
# 'ouranos_logo_vertical_noir']

Custom Logos
^^^^^^^^^^^^
Custom logos can also be installed via the `figanos.Logos().set_logo()`` class method.

The ``set_logo()`` method takes the following arguments:

.. code-block:: python

from figanos import Logos

logos = Logos().set_logo(
"/path/to/my/file.png", # Path to the logo file
"my_custom_logo", # Name of the logo, optional
# If no name is provided, the name will be the file name without the extension
)

logos.my_custom_logo # Returns the installed logo path

To change the default to an already-installed logo, simply call the `set_logo()` method with the logo.<option> and the name set as `default`. For example:

.. code-block:: python

# To set the default logo to the horizontal white Ouranos logo
logos.set_logo(logos.ouranos_logo_horizontal_blanc, "default")
2 changes: 2 additions & 0 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ dependencies:
- geopandas
- numpy
- pandas
- platformdirs
- pyyaml
- xarray
# To make the package and notebooks usable
- xclim >=0.38
Expand Down
1 change: 1 addition & 0 deletions figanos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
__version__ = "0.2.0"

from . import matplotlib
from ._logo import Logos
144 changes: 144 additions & 0 deletions figanos/_logo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import logging
import shutil
import urllib.parse
import urllib.request
import warnings
from pathlib import Path
from typing import Optional, Union

import platformdirs
import yaml

__all__ = ["Logos"]

LOGO_CONFIG_FILE = "logo_mapping.yaml"
OURANOS_LOGOS_URL = "https://raw.githubusercontent.com/Ouranosinc/.github/main/images/"
_figanos_logo = Path(__file__).parent / "data" / "figanos_logo.png"


class Logos:
r"""Class for managing logos to be used in graphics.

Attributes
----------
config : Path
The path to the folder where the logo configuration file is stored.
catalogue : Path
The path to the logo configuration file.
default : str
The path to the default logo.

Methods
-------
installed()
Retrieves a list of installed logos.
install_ouranos_logos(\*, permitted: bool = False)
Fetches and installs the Ouranos logos.
set_logo(path: Union[str, Path], name: str = None)
Sets the path and name to a logo file.
If no logos are already set, the first one will be set as the default.
reload_config()
Reloads the logo configuration from the YAML file.
"""

default = None

def __init__(self) -> None:
"""Constructor for the Logo class."""
self.config = (
Path(platformdirs.user_config_dir("figanos", ensure_exists=True)) / "logos"
)
self.catalogue = self.config / LOGO_CONFIG_FILE
self._logos = {}
self._setup()
self.reload_config()

if not self._logos.get("default"):
warnings.warn(f"Setting default logo to {_figanos_logo}")
self.set_logo(_figanos_logo)
self.set_logo(_figanos_logo, name="default")

def _setup(self) -> None:
if (
not self.catalogue.exists()
or yaml.safe_load(self.catalogue.read_text()) is None
):
if not self.catalogue.exists():
warnings.warn(
f"No logo configuration file found. Creating one at {self.catalogue}."
)
self.config.mkdir(parents=True, exist_ok=True)
with open(self.catalogue, "w") as f:
yaml.dump(dict(logos={}), f)

def __str__(self):
return f"{self.__getitem__('default')}"

def __repr__(self):
return f"{self._logos.items()}"

def __getitem__(self, name: str) -> Optional[str]:
"""Retrieve a logo path by its name."""
return self._logos.get(name, None)

def reload_config(self) -> None:
"""Reload the configuration from the YAML file."""
self._logos = yaml.safe_load(self.catalogue.read_text())["logos"]
for logo_name, logo_path in self._logos.items():
if not Path(logo_path).exists():
warnings.warn(f"Logo file {logo_name} not found at {logo_path}.")
setattr(self, logo_name, logo_path)

def installed(self) -> list:
"""Retrieve a list of installed logos."""
return list(self._logos.keys())

def set_logo(self, path: Union[str, Path], name: Optional[str] = None) -> None:
"""Copies the logo at a given path to the config folder and maps it to a given name in the logo config."""
_logo_mapping = yaml.safe_load(self.catalogue.read_text())["logos"]

logo_path = Path(path)
if logo_path.exists() and logo_path.is_file():
if name is None:
name = logo_path.stem
install_logo_path = self.config / logo_path.name

if not install_logo_path.exists():
shutil.copy(logo_path, install_logo_path)

logging.info("Setting %s logo to %s", name, install_logo_path)
_logo_mapping[name] = str(install_logo_path)
self.catalogue.write_text(yaml.dump(dict(logos=_logo_mapping)))
self.reload_config()

elif not logo_path.exists():
warnings.warn(f"Logo file {logo_path} not found. Not setting logo.")
elif not logo_path.is_file():
warnings.warn(f"Logo path {logo_path} is a folder. Not setting logo.")

def install_ouranos_logos(self, *, permitted: bool = False) -> None:
"""Fetches and installs the Ouranos logo.

The Ouranos logo is reserved for use by employees and project partners of Ouranos.

Parameters
----------
permitted : bool
Whether the user has permission to use the Ouranos logo.
"""
if permitted:
for orientation in ["horizontal", "vertical"]:
for colour in ["couleur", "blanc", "noir"]:
file = f"ouranos_logo_{orientation}_{colour}.png"
logo_url = urllib.parse.urljoin(OURANOS_LOGOS_URL, file)
try:
urllib.request.urlretrieve(logo_url, self.config / file)
self.set_logo(self.config / file)
except Exception as e:
logging.error(f"Error downloading or setting Ouranos logo: {e}")
print(f"Ouranos logos installed at: {self.config}.")
else:
warnings.warn(
"You have not indicated that you have permission to use the Ouranos logo. "
"If you do, please set the `permitted` argument to `True`."
)
Binary file added figanos/data/figanos_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed figanos/data/ouranos_logo.png
Binary file not shown.
Binary file removed figanos/data/ouranos_logo_25.png
Binary file not shown.
36 changes: 25 additions & 11 deletions figanos/matplotlib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
from xclim.core.options import METADATA_LOCALES
from xclim.core.options import OPTIONS as XC_OPTIONS

from .._logo import Logos

TERMS: dict = {}
"""
A translation directory for special terms to appear on the plots.
Expand Down Expand Up @@ -528,10 +530,10 @@ def plot_coords(
def plot_logo(
ax: matplotlib.axes.Axes,
loc: str | tuple[float, float] | int,
path_png: str | None = None,
offsetim_kw: None | dict = {"alpha": 1, "zoom": 0.5},
logo: str | pathlib.Path | None = None,
**offset_image_kwargs,
) -> matplotlib.axes.Axes:
"""Place logo of plot area.
r"""Place logo of plot area.

Parameters
----------
Expand All @@ -540,23 +542,35 @@ def plot_logo(
loc : string, int or tuple
Location of text, replicating https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.legend.html.
If a tuple, must be in axes coordinates.
path_png: str or None
Path to picture of logo, must be a png.
If none, Ouranos logo is used by default.
offsetim_kw: dict
Arugments to pass to matplotlib.offsetbox.OffsetImage().
logo : str, Path, dict, optional
A name (str) or Path to picture of logo, or a name of an already-installed logo.
If a Path is provided, the logo will be installed and accessible via the 'Path().name' of file.
The default logo is the Figanos logo. To install the Ouranos (or another) logo consult the Usage page.
Logos must be in 'png' format.
\*\*offset_image_kwargs
Arguments to pass to matplotlib.offsetbox.OffsetImage().

Returns
-------
matplotlib.axes.Axes
"""
if offset_image_kwargs is None:
offset_image_kwargs = {"alpha": 0.8, "zoom": 0.25}

logos = Logos()
if logo:
if isinstance(logo, pathlib.Path):
path_png = logo
path_png = logos[logo]
else:
path_png = logos.default
if path_png is None:
path_png = (
pathlib.Path(__file__).resolve().parents[1] / "data" / "ouranos_logo_25.png"
Zeitsperre marked this conversation as resolved.
Show resolved Hide resolved
raise ValueError(
"No logo found. Please install one with the figanos.Logos().install_logo() method."
)

image = mpl.pyplot.imread(path_png)
imagebox = mpl.offsetbox.OffsetImage(image, **offsetim_kw)
imagebox = mpl.offsetbox.OffsetImage(image, **offset_image_kwargs)
loc, box_a, ha, va = loc_mpl(loc)

ab = mpl.offsetbox.AnnotationBbox(
Expand Down
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
"matplotlib",
"numpy",
"pandas",
"xarray",
"platformdirs",
"pyyaml",
"seaborn",
"xarray",
]

test_requirements = ["pytest>=3"]
Expand Down