diff --git a/.gitignore b/.gitignore index d424f75d..5538e722 100644 --- a/.gitignore +++ b/.gitignore @@ -66,8 +66,8 @@ instance/ # Sphinx documentation docs/_build/ -docs/apidoc/modules.rst docs/apidoc/figanos*.rst +docs/apidoc/modules.rst # PyBuilder target/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fe04fa2d..1edc5032 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,8 @@ repos: hooks: - id: ruff args: [ '--fix' ] - # - id: ruff-format + - id: ruff-format + exclude: ^src/figanos - repo: https://github.com/pycqa/flake8 rev: 7.1.1 hooks: @@ -66,7 +67,10 @@ repos: hooks: - id: nbstripout files: '.ipynb' - args: [ '--extra-keys=metadata.kernelspec' ] + args: [ + '--extra-keys="metadata.kernelspec"', + '--keep-metadata-keys="cell.metadata.nbsphinx"' + ] - repo: https://github.com/adrienverge/yamllint.git rev: v1.35.1 hooks: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5e1d5b16..26c093b0 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,7 @@ Changelog `Unreleased `_ (latest) -------------------------------------------------------------- -Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Marco Braun (:user:`vindelico`), Pascal Bourgault (:user:`aulemahal`), Sarah-Claude Bourdeau-Goulet (:user:`Sarahclaude`), Éric Dupuis (:user:`coxipi`), Juliette Lavoie (:user:`juliettelavoie`) +Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Marco Braun (:user:`vindelico`), Pascal Bourgault (:user:`aulemahal`), Sarah-Claude Bourdeau-Goulet (:user:`Sarahclaude`), Éric Dupuis (:user:`coxipi`), Juliette Lavoie (:user:`juliettelavoie`). New features and enhancements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -18,6 +18,8 @@ New features and enhancements * A new optional way to organize points in a ``fg.taylordiagram`` with `colors_key`, `markers_key` : DataArrays with a common dimension value or a common attribute are grouped with the same color/marker (:pull:`214`). * Heatmap (``fg.matplotlib.heatmap``) now supports `row,col` arguments in `plot_kw`, allowing to plot a grid of heatmaps. (:issue:`208`, :pull:`219`). * New function ``fg.matplotlib.triheatmap`` (:pull:`199`). +* Reorganized the documentation and add gallery (:issue:`278`, :issue:`274`, :issue:`202`, :pull:`279`). +* Added a new `pooch`-based mechanism for fetching and caching testing data used in the notebooks (``fg.pitou().fetch()``). (:pull:`279`). Breaking changes ^^^^^^^^^^^^^^^^ diff --git a/docs/_static/_gallery/basic_map.png b/docs/_static/_gallery/basic_map.png new file mode 100644 index 00000000..6b999b71 Binary files /dev/null and b/docs/_static/_gallery/basic_map.png differ diff --git a/docs/_static/_gallery/basic_timeseries.png b/docs/_static/_gallery/basic_timeseries.png new file mode 100644 index 00000000..27691565 Binary files /dev/null and b/docs/_static/_gallery/basic_timeseries.png differ diff --git a/docs/_static/_gallery/ensemble_timeseries.png b/docs/_static/_gallery/ensemble_timeseries.png new file mode 100644 index 00000000..4d25b227 Binary files /dev/null and b/docs/_static/_gallery/ensemble_timeseries.png differ diff --git a/docs/_static/_gallery/gdf2_map.png b/docs/_static/_gallery/gdf2_map.png new file mode 100644 index 00000000..1352538c Binary files /dev/null and b/docs/_static/_gallery/gdf2_map.png differ diff --git a/docs/_static/_gallery/gdf_map.png b/docs/_static/_gallery/gdf_map.png new file mode 100644 index 00000000..cc70a9b1 Binary files /dev/null and b/docs/_static/_gallery/gdf_map.png differ diff --git a/docs/_static/_gallery/hatch_map.png b/docs/_static/_gallery/hatch_map.png new file mode 100644 index 00000000..cb6be7f7 Binary files /dev/null and b/docs/_static/_gallery/hatch_map.png differ diff --git a/docs/_static/_gallery/heatmap.png b/docs/_static/_gallery/heatmap.png new file mode 100644 index 00000000..14c407b6 Binary files /dev/null and b/docs/_static/_gallery/heatmap.png differ diff --git a/docs/_static/_gallery/logo.png b/docs/_static/_gallery/logo.png new file mode 100644 index 00000000..48fcfeb5 Binary files /dev/null and b/docs/_static/_gallery/logo.png differ diff --git a/docs/_static/_gallery/multiple.png b/docs/_static/_gallery/multiple.png new file mode 100644 index 00000000..b4601d94 Binary files /dev/null and b/docs/_static/_gallery/multiple.png differ diff --git a/docs/_static/_gallery/partition.png b/docs/_static/_gallery/partition.png new file mode 100644 index 00000000..07698c33 Binary files /dev/null and b/docs/_static/_gallery/partition.png differ diff --git a/docs/_static/_gallery/station+grid_map.png b/docs/_static/_gallery/station+grid_map.png new file mode 100644 index 00000000..1d0b411b Binary files /dev/null and b/docs/_static/_gallery/station+grid_map.png differ diff --git a/docs/_static/_gallery/station_map.png b/docs/_static/_gallery/station_map.png new file mode 100644 index 00000000..81c148ae Binary files /dev/null and b/docs/_static/_gallery/station_map.png differ diff --git a/docs/_static/_gallery/stripes.png b/docs/_static/_gallery/stripes.png new file mode 100644 index 00000000..4923bd2c Binary files /dev/null and b/docs/_static/_gallery/stripes.png differ diff --git a/docs/_static/_gallery/taylor.png b/docs/_static/_gallery/taylor.png new file mode 100644 index 00000000..e31652af Binary files /dev/null and b/docs/_static/_gallery/taylor.png differ diff --git a/docs/_static/_gallery/triangle1.png b/docs/_static/_gallery/triangle1.png new file mode 100644 index 00000000..a27a7615 Binary files /dev/null and b/docs/_static/_gallery/triangle1.png differ diff --git a/docs/_static/_gallery/triangle2.png b/docs/_static/_gallery/triangle2.png new file mode 100644 index 00000000..b1d60a81 Binary files /dev/null and b/docs/_static/_gallery/triangle2.png differ diff --git a/docs/_static/_gallery/violin.png b/docs/_static/_gallery/violin.png new file mode 100644 index 00000000..ecb08fed Binary files /dev/null and b/docs/_static/_gallery/violin.png differ diff --git a/docs/conf.py b/docs/conf.py index 53191297..ac847341 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,7 +68,7 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a dictionary of suffix: filetype -source_suffix = {'.rst': 'restructuredtext'} +source_suffix = {".rst": "restructuredtext"} # The master toctree document. master_doc = "index" @@ -129,8 +129,7 @@ # so a file named "default.css" will overwrite the builtin "default.css". if not os.path.exists("_static"): os.makedirs("_static") -html_static_path = ['_static'] - +html_static_path = ["_static"] # -- Options for HTMLHelp output --------------------------------------- diff --git a/docs/gallery.rst b/docs/gallery.rst new file mode 100644 index 00000000..2df03140 --- /dev/null +++ b/docs/gallery.rst @@ -0,0 +1,90 @@ +Gallery +======== + +Here is a gallery of examples of `figanos`. +Go to the examples to see how to generate these figures. + +Timeseries +^^^^^^^^^^^ +Full code in the `Timeseries notebook `_. + +.. image:: _static/_gallery/basic_timeseries.png + :width: 40% + :target: notebooks/figanos_timeseries.html#Basic-timeseries + +.. image:: _static/_gallery/ensemble_timeseries.png + :width: 40% + :target: notebooks/figanos_timeseries.html#Ensembles + +Maps +^^^^ +Full code in the `Maps notebook `_. + +.. image:: _static/_gallery/basic_map.png + :width: 30% + :target: notebooks/figanos_maps.html#Gridded-Data-on-Maps + +.. image:: _static/_gallery/station_map.png + :width: 30% + :target: notebooks/figanos_maps.html#Station-Data-on-Maps + +.. image:: _static/_gallery/station+grid_map.png + :width: 30% + :target: notebooks/figanos_maps.html#Station-Data-on-Maps + +.. image:: _static/_gallery/hatch_map.png + :width: 30% + :target: notebooks/figanos_maps.html#Hatching-on-Maps + +.. image:: _static/_gallery/gdf_map.png + :width: 30% + :target: notebooks/figanos_maps.html#GeoDataFrame-on-Maps + +.. image:: _static/_gallery/gdf2_map.png + :width: 30% + :target: notebooks/figanos_maps.html#GeoDataFrame-on-Maps + +Miscellaneous +^^^^^^^^^^^^^ +Full code in the `Miscellaneous notebook `_. + +.. image:: _static/_gallery/stripes.png + :width: 30% + :target: notebooks/figanos_misc.html#Climate-Stripes + +.. image:: _static/_gallery/violin.png + :width: 30% + :target: notebooks/figanos_misc.html#Violin-Plots + +.. image:: _static/_gallery/heatmap.png + :width: 30% + :target: notebooks/figanos_misc.html#Heatmaps + +.. image:: _static/_gallery/triangle1.png + :width: 30% + :target: notebooks/figanos_misc.html#Triangle-heatmaps + +.. image:: _static/_gallery/triangle2.png + :width: 30% + :target: notebooks/figanos_misc.html#Triangle-Heatmaps + +.. image:: _static/_gallery/taylor.png + :width: 30% + :target: notebooks/figanos_misc.html#Taylor-Diagrams + +.. image:: _static/_gallery/partition.png + :width: 30% + :target: notebooks/figanos_misc.html#Partition-plots + +.. image:: _static/_gallery/logo.png + :width: 30% + :target: notebooks/figanos_misc.html#Logos + +Multiple plots +^^^^^^^^^^^^^^ + +Full code in the `Multiple plots notebook `_. + +.. image:: _static/_gallery/multiple.png + :width: 50% + :target: notebooks/figanos_multiplots.html#Maps diff --git a/docs/index.rst b/docs/index.rst index 04a854a8..7a9aa085 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,8 +3,23 @@ Welcome to figanos's documentation! Figanos: Tool to create **fig**\ ures\ in\ the Our\ **anos**\ style. +Overview +^^^^^^^^ + +Figanos is a dictionary-based function interface that wraps `Matplotlib `_ and `Xarray `_ plotting functions to create common climate data plots. Its inputs are most commonly xarray DataArrays or Datasets, and it is best used when these arrays are the output of workflows incorporating `Xscen `_ and/or `Xclim `_. Style-wise, the plots follow the general guidelines offered by the `IPCC visual style guide 2022 `_, but aim to create a look that could be distinctively associated with `Ouranos `_. + + +The following features are included in the package: + +* Automatically recognizes some common data structures (e.g. climate ensembles) using variable and coordinate names and creates the appropriate plots. +* Automatically links attributes from xarray objects to plot elements (title, axes), with customization options. +* Automatically assigns colors to some common variables and, following the IPCC visual guidelines. +* Provides options to visually enhance the plots, and includes a default style to ensure coherence when creating multiple plots. +* Returns a `matplotlib axes object `_ that is fully customizeable through matplotlib functions and methods. + + Need help? -========== +^^^^^^^^^^ * Ouranos employees can ask questions on the Ouranos private StackOverflow where you can tag subjects and people. (https://stackoverflow.com/c/ouranos/questions ). * Potential bugs in figanos can be reported as an issue here: https://github.com/Ouranosinc/figanos/issues . * Problems with data on Ouranos' servers can be reported as an issue here: https://github.com/Ouranosinc/miranda/issues @@ -16,9 +31,8 @@ Need help? installation usage - notebooks/figanos_docs - notebooks/figanos_colours - notebooks/figanos_multiplots + gallery + notebooks/index api contributing releasing diff --git a/docs/notebooks/figanos_colours.ipynb b/docs/notebooks/figanos_colours.ipynb deleted file mode 100644 index 92584629..00000000 --- a/docs/notebooks/figanos_colours.ipynb +++ /dev/null @@ -1,138 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Colours of Figanos" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "On this page, we present Figanos colours and colormaps, following the [IPCC visual style guide](https://www.ipcc.ch/site/assets/uploads/2022/09/IPCC_AR6_WGI_VisualStyleGuide_2022.pdf)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "from figanos import data\n", - "import figanos.matplotlib as fg\n", - "\n", - "import matplotlib as mpl\n", - "import numpy as np\n", - "import json\n", - "from matplotlib.patches import Rectangle\n", - "from pathlib import Path\n", - "from matplotlib import pyplot as plt\n", - "\n", - "fg.utils.set_mpl_style('ouranos')\n" - ] - }, - { - "cell_type": "markdown", - "id": "3", - "metadata": {}, - "source": [ - "## Colormaps\n", - "Figanos tries to guess the colormap based on keywords in the attributes of the data and the `divergent` argument. Users can also pass the name of a colormap (see options below) directly to the `cmap` argument. If you want to suggest a new keyword, create an [issue on the GitHub repository](https://github.com/Ouranosinc/figanos/issues).\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4", - "metadata": {}, - "outputs": [], - "source": [ - "with data().joinpath(\"ipcc_colors\").joinpath(\"variable_groups.json\").open(encoding=\"utf-8\") as f:\n", - " var_dict = json.load(f)\n", - "\n", - "for f in sorted(data().joinpath(\"ipcc_colors/continuous_colormaps_rgb_0-255\").glob(\"*\")):\n", - " name=Path(f).name.replace('.txt','')\n", - " cmap = fg.utils.create_cmap(filename=name)\n", - " fig = plt.figure()\n", - " ax = fig.add_axes([0.05, 0.80, 0.9, 0.1])\n", - " cb = mpl.colorbar.ColorbarBase(ax, orientation='horizontal',\n", - " cmap=cmap)\n", - " cb.outline.set_visible(False)\n", - " cb.ax.set_xticklabels([]);\n", - " split=name.split(\"_\")\n", - " var = split[0]+(split[2] if len(split)==3 else '')\n", - " kw= [k for k,v in var_dict.items() if v ==var]\n", - " #plt.title(f\"name: {name} \\n keywords: {kw}\", wrap=True)\n", - " plt.figtext(.5,.95 + (0.04 *int(len(kw)/10)),f\"name: {name}\", fontsize=15, ha='center')\n", - " plt.figtext(.5,.91,f\"keywords: {kw}\",fontsize=10,ha='center', wrap=True)" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "## Colours" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6", - "metadata": { - "pycharm": { - "name": "#%%\n" - } - }, - "outputs": [], - "source": [ - "color_dict = fg.utils.categorical_colors()\n", - "\n", - "keys = np.array(list(color_dict.keys()))\n", - "keys = np.array_split(keys, 3)\n", - "\n", - "\n", - "fig, ax = plt.subplots(figsize=(8,10))\n", - "ax.set_ylim(-25,3)\n", - "ax.set_xlim(0,12)\n", - "ax.set_axis_off()\n", - "for colorlist, x in zip(keys, [1, 5.5, 10]):\n", - " for y in np.arange(len(colorlist)):\n", - " ax.text(x, -y, colorlist[y],\n", - " va='bottom', ha='left',\n", - " backgroundcolor='white',\n", - " weight='normal', color='k'\n", - " )\n", - " ax.add_patch(\n", - " Rectangle(xy=(x-1, -y),width=0.5,height=0.5,\n", - " facecolor=color_dict[colorlist[y]],\n", - " edgecolor='0.8')\n", - " )" - ] - } - ], - "metadata": { - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.16" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/notebooks/figanos_docs.ipynb b/docs/notebooks/figanos_docs.ipynb deleted file mode 100644 index 4a3de66c..00000000 --- a/docs/notebooks/figanos_docs.ipynb +++ /dev/null @@ -1,1286 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "0", - "metadata": {}, - "source": [ - "# Getting started\n", - "Figanos enables the creation of common climate data plots.\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "id": "1", - "metadata": {}, - "source": [ - "## Overview\n", - "\n", - "Figanos is a dictionary-based function interface that wraps [Matplotlib](https://matplotlib.org/) and [Xarray](https://docs.xarray.dev/en/stable/) plotting functions to create common climate data plots. Its inputs are most commonly xarray DataArrays or Datasets, and it is best used when these arrays are the output of workflows incorporating [Xscen](https://github.com/Ouranosinc/xscen) and/or [Xclim](https://xclim.readthedocs.io/en/stable/). Style-wise, the plots follow the general guidelines offered by the [IPCC visual style guide 2022](https://www.ipcc.ch/site/assets/uploads/2022/09/IPCC_AR6_WGI_VisualStyleGuide_2022.pdf), but aim to create a look that could be distinctively associated with [Ouranos](https://www.ouranos.ca/en). The [Figanos Github repository](https://github.com/Ouranosinc/figanos) hosts the files needed to install the package, as well as the dependencies and requirements.\n", - "\n", - "Figanos currently includes the following functions:\n", - "\n", - "1. **timeseries()**: Creates time series as line plots.\n", - "2. **gridmap()**: Plots gridded georeferenced data on a map.\n", - "3. **hatchmap()**: Plots hatched areas on a map.\n", - "4. **scattermap()**: Make a scatter plot of georeferenced data on a map.\n", - "5. **gdfmap()**: Plots geometries (through a GeoDataFrame) on a map.\n", - "6. **stripes()**: Create climate stripe diagrams.\n", - "7. **violin()**: Create seaborn violin plots with extra options.\n", - "8. **heatmap()**: Create seaborn heatmaps with extra options.\n", - "9. **taylordiagram()**: Create Taylor diagram.\n", - "\n", - "The following features are also included in the package:\n", - "\n", - "* Automatically recognizes some common data structures (e.g. climate ensembles) using variable and coordinate names and creates the appropriate plots.\n", - "* Automatically links attributes from xarray objects to plot elements (title, axes), with customization options.\n", - "* Automatically assigns colors to some common variables and, following the IPCC visual guidelines.\n", - "* Provides options to visually enhance the plots, and includes a default style to ensure coherence when creating multiple plots.\n", - "* Returns a [matplotlib axes object](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html#matplotlib.axes.Axes) that is fully customizeable through matplotlib functions and methods.\n" - ] - }, - { - "cell_type": "markdown", - "id": "2", - "metadata": {}, - "source": [ - "## Preparing the data\n", - "\n", - "Figanos only accepts [xarray DataArrays or Datasets](https://docs.xarray.dev/en/stable/user-guide/data-structures.html) as data inputs. As a general rule, figanos functions will not accomplish any data processing or cleaning tasks - the object(s) passed to the functions should therefore only contain the data that will appear on the graph and the metadata that supports it.\n", - "\n", - "To create, for instance, a time series plot from a NetCDF file, the following preparation steps have to be taken:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3", - "metadata": {}, - "outputs": [], - "source": [ - "from __future__ import annotations\n", - "\n", - "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "# import necessary libraries\n", - "import xarray as xr\n", - "import matplotlib.pyplot as plt\n", - "import matplotlib\n", - "import numpy as np\n", - "import cartopy.crs as ccrs\n", - "import xclim as xc\n", - "from xclim import sdba\n", - "\n", - "import figanos.matplotlib as fg\n", - "from figanos import Logos" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "4", - "metadata": {}, - "outputs": [], - "source": [ - "# create xarray object from a NetCDF\n", - "url = 'https://pavics.ouranos.ca//twitcher/ows/proxy/thredds/dodsC/birdhouse/disk2/cccs_portal/indices/Final/BCCAQv2_CMIP6/tx_max/YS/ssp585/ensemble_percentiles/tx_max_ann_BCCAQ2v2+ANUSPLIN300_historical+ssp585_1950-2100_30ymean_percentiles.nc'\n", - "opened = xr.open_dataset(url, decode_timedelta=False)\n", - "\n", - "# select a location\n", - "ds_time = opened.isel(lon=500, lat=250)\n", - "# select only the variables containing the data to be plotted\n", - "ds_time = ds_time[['tx_max_p50', 'tx_max_p10', 'tx_max_p90']]\n", - "\n", - "ds_time" - ] - }, - { - "cell_type": "markdown", - "id": "5", - "metadata": {}, - "source": [ - "## Using the Ouranos stylesheet\n", - "\n", - "Most parameters affecting the style of plots can be set through matplotlib stylesheets. Figanos includes custom stylesheets that can be accessed through the set_mpl_style() function. Paths to your own stylesheets ('.mplstyle' extension) can also be passed to this function. To use the built-in matplotlib styles, use `mpl.style.use()`.\n", - "\n", - "The currently available stylesheets are as follows:\n", - "\n", - "* ouranos: General stylesheet, including default colors.\n", - "* transparent: Adds transparency to the styles (fully transparent figure background and 30% opacity on the axes).\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6", - "metadata": {}, - "outputs": [], - "source": [ - "# use ouranos style\n", - "fg.utils.set_mpl_style('ouranos')\n", - "\n", - "#setup notebook\n", - "%matplotlib inline\n", - "%config InlineBackend.print_figure_kwargs = {'bbox_inches':'tight'}" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7", - "metadata": {}, - "outputs": [], - "source": [ - "#display the cycler colors\n", - "from matplotlib.patches import Rectangle\n", - "\n", - "style_colors = matplotlib.rcParams[\"axes.prop_cycle\"].by_key()[\"color\"]\n", - "\n", - "fig, ax = plt.subplots(figsize=(10,3))\n", - "for color, x in zip(style_colors, np.arange(0, len(style_colors)*2, 2)):\n", - " ax.add_patch(\n", - " Rectangle(xy=(x, 1),width=0.8,height=0.5,\n", - " facecolor=color)\n", - " )\n", - " ax.text(x, 0.5, str(color), color=color)\n", - "\n", - "ax.set_ylim(0,2)\n", - "ax.set_xlim(0,14)\n", - "ax.set_aspect('equal')\n", - "ax.set_axis_off()" - ] - }, - { - "cell_type": "markdown", - "id": "8", - "metadata": {}, - "source": [ - "## Timeseries\n", - "\n", - "The [**timeseries()**](#timeseries) function accepts DataArrays or Datasets. When only one object is passed to the function, using a dictionary is optional. Selecting one of the variables from our Dataset creates a DataArray (one line)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9", - "metadata": {}, - "outputs": [], - "source": [ - "fg.timeseries(ds_time.tx_max_p50)" - ] - }, - { - "cell_type": "markdown", - "id": "10", - "metadata": {}, - "source": [ - "### Using the dictionary interface" - ] - }, - { - "cell_type": "markdown", - "id": "11", - "metadata": {}, - "source": [ - "\n", - "The main elements of a plot are dependent on four arguments, each accepting dictionaries:\n", - "\n", - "1. `data` : a dictionary containing the Xarray objects and their respective keys, used as labels on the plot.\n", - "2. `use_attrs`: a dictionary linking attributes from the Xarray object to plot text elements.\n", - "3. `fig_kw`: a dictionary to pass arguments to the `plt.figure()` instance.\n", - "4. `plot_kw` : a dictionary using the same keys as `data` to pass arguments to the underlying plotting function, in this case [matplotlib.axes.Axes.plot](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html).\n", - "\n", - "When labels are passed in `data`, any 'label' argument passed in `plot_kw` will be ignored." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "12", - "metadata": {}, - "outputs": [], - "source": [ - "my_data = {'50th percentile': ds_time.tx_max_p50, '90th percentile': ds_time.tx_max_p90}\n", - "my_attrs = {'ylabel': 'standard_name'} # will look for an attribute 'standard name' in the first entry of my_data\n", - "plot_kws = {'90th percentile': {'linestyle': '--'}}\n", - "\n", - "fg.timeseries(my_data,\n", - " use_attrs = my_attrs,\n", - " plot_kw = plot_kws,\n", - " show_lat_lon=False\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "13", - "metadata": {}, - "source": [ - "### Customizing plots\n", - "\n", - "Plots created with Figanos can be customized in two different ways:\n", - "\n", - "1. By using the built-in options through arguments (e.g. changing the type of legend with the `legend` arg).\n", - "2. By creating a Matplotlib Axes class and using its methods (e.g. setting a new title with `ax.set_title()`).\n", - "\n", - "Both of these types of customization are demonstrated below. In some cases, both methods can achieve the same result." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "14", - "metadata": {}, - "outputs": [], - "source": [ - "ax = fg.timeseries(my_data, show_lat_lon=\"upper left\", legend='edge') # fun legend option, moved latitude and longitude tag\n", - "ax.set_title('Custom Title', loc='left') #when the title is left aligned, the \"loc=left\" argument must be used.\n", - " # to remove a title, use ax.set_title('')\n", - "ax.set_xlabel('Custom xlabel')\n", - "ax.set_ylabel('Custom ylabel')\n", - "ax.grid(False) # removing the gridlines\n", - "ax.set_yticks([300,310]) # Custom yticks" - ] - }, - { - "cell_type": "markdown", - "id": "15", - "metadata": {}, - "source": [ - "#### Logos\n", - "\n", - "Logos can also be added to plots if desired using the `fignos.utils.plot_logo()` function. This function requires that logos are passed as `pathlib.Path` objects or installed and called by their name (as `str`).\n", - "\n", - "Figanos offers the `Logos()` convenience class for setup and management of logos so that they can be reused as needed. `Logos` can be used to set default logos as well as install custom logos, if desired. Logo files are saved to the user's config folder so that they can be reused.\n", - "\n", - "By default, the `figanos_logo.png` is installed on initialization, while the Ouranos set of logos can be installed if desired.\n", - "\n", - "For more information on logos, see the [Logos](../usage.rst#logo-management) documentation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16", - "metadata": {}, - "outputs": [], - "source": [ - "# Installing the default logos\n", - "l = Logos()\n", - "print(f\"Default logo is found at: {l.default}.\")\n", - "\n", - "# Installing the Ouranos logos\n", - "l.install_ouranos_logos(permitted=True)\n", - "\n", - "# Show all installed logos\n", - "l.installed()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "17", - "metadata": {}, - "outputs": [], - "source": [ - "# To set a new default logo we can simply use an existing entry\n", - "l.set_logo(l.logo_ouranos_horizontal_couleur, \"default\")\n", - "print(f\"Default logo is found at: {l.default}\")\n", - "l.set_logo(l.logo_ouranos_vertical_couleur, \"my_custom_logo\")\n", - "print(f\"my_custom_logo installed at: {l.my_custom_logo}.\")\n", - "\n", - "# Show all installed logos\n", - "l.installed()" - ] - }, - { - "cell_type": "markdown", - "id": "18", - "metadata": {}, - "source": [ - "The required arguments for `fignos.utils.plot_logo()` are a matplotlib axis (`ax`), a location (`loc`) string describing the position of the logo (ex: 'lower left', 'upper right', 'center').\n", - "\n", - "The `logo` argument is optional but will accept either a fixed location on disk (as `pathlib.Path`) or a `str` that is mapped to an already-installed logo (e.g. 'my_custom_logo'; If not set, the 'default' logo will be used).\n", - "\n", - "Either the `height` or `width` arguments can be used to resize the logo; If both are provided, only one will be used to resize the logo. This behaviour will change if `keep_ratio=False` is passed.\n", - "\n", - "The function can also accept keyword arguments that are passed directly to `matplotlib.offsetbox.OffsetImage()`, such as `alpha` (transparency)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "19", - "metadata": {}, - "outputs": [], - "source": [ - "ax = fg.timeseries(my_data, show_lat_lon=\"upper left\", legend='edge')\n", - "\n", - "# Plotting with the default logo\n", - "# fg.utils.plot_logo(ax, loc='lower right', alpha=0.8, width=120)\n", - "\n", - "# Plotting with a custom logo, resized with pixels\n", - "fg.utils.plot_logo(\n", - " ax,\n", - " logo=\"my_custom_logo\",\n", - " loc='lower right',\n", - " width=100,\n", - " alpha=0.8,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "20", - "metadata": {}, - "source": [ - "#### Translation\n", - "Figanos can automatically use translated version of the attributes to populate the plot. It also knows a few translations of usual terms, for the moment only in French." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "21", - "metadata": {}, - "outputs": [], - "source": [ - "# Populate the example data with french attributes\n", - "ds_time.tx_max_p50.attrs.update(\n", - " description_fr=\"Moyenne 30 ans du Maximum annuel de la température maximale quotidienne, 50e centile de l'ensemble.\",\n", - " long_name_fr=\"Moyenne 30 ans du Maximum de la température maximale quotidienne\"\n", - ")\n", - "with xc.set_options(metadata_locales=['fr']):\n", - " fg.timeseries(ds_time.tx_max_p50)" - ] - }, - { - "cell_type": "markdown", - "id": "22", - "metadata": {}, - "source": [ - "### Line plots with Datasets\n", - "\n", - "When Datasets are passed to the timeseries function, certain names and data configurations will be recognized and will result in certain kinds of plots.\n", - "\n", - "| Dataset configuration | Resulting plot | Notes |\n", - "|:----------:|:--------------:|:----------------:|\n", - "|Variables contain a substring of the format \"\\_pNN\", where N are numbers|Shaded line graph with the central line being the middle percentile|\n", - "|Contains a dimension named \"percentiles\"|Shaded line graph with the central line being the middle percentile| Behaviour is shared with DataArrays containing the same dimension.|\n", - "|Variables contain \"min\" and \"max\" and \"mean\" (can be capitalized) |Shaded line graph with the central line being the mean|\n", - "|Contains a dimension named \"realization\"|Line graph with one line per realization | When plot_kw is specified, all realizations within the Dataset will share one style. Behaviour is shared with DataArrays containing the same dimension.|\n", - "|Any other Dataset| Line graph with one line per variable||\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "23", - "metadata": {}, - "outputs": [], - "source": [ - "# Use 'median' as a key to make it the line label in the legend.\n", - "# legend='full' will create a legend entry for the shaded area\n", - "fg.plot.timeseries({'median': ds_time}, legend='full', show_lat_lon=False)" - ] - }, - { - "cell_type": "markdown", - "id": "24", - "metadata": {}, - "source": [ - "Whenever multiple lines are plotted from a single Dataset, their legend label will be the concatenation of the Dataset name (its key in the `data` arg.) and the name of the variables or coordinates from which the data is taken, unless the Dataset is passed to the function without a dictionary. When all lines from a Dataset have the same appearance, only the Dataset label will be shown." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "25", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a Dataset with different names as to not trigger the shaded line plot\n", - "ds_mod = ds_time.copy()\n", - "ds_mod = ds_mod.rename({'tx_max_p50': 'var1','tx_max_p10': 'var2','tx_max_p90': 'var3'})\n", - "\n", - "fg.timeseries({'ds':ds_mod}, show_lat_lon=True)" - ] - }, - { - "cell_type": "markdown", - "id": "26", - "metadata": {}, - "source": [ - "### Keyword - colour association\n", - "\n", - "Following the IPCC visual style guidelines and the practices of many other climate organizations, some scenarios (RCPs, SSPs), models and projects (CMIPs) are associated with specific colors. These colours can be implemented in timeseries() through the keys of the `data` argument. If a formulation of such scenarios or model names is found in a key, the corresponding line will be given the appropriate colour. For scenarios, alternative formats such as _ssp585_ or _rcp45_ are also accepted instead of the more formal _SSP5-8.5_ on _RCP4.5_. Model names do not currently have this flexibility. If multiple matching substrings exist, the following order of priority will dictate which colour is used:\n", - "\n", - "1. SSP scenarios\n", - "2. RCP scenarios\n", - "3. Model names\n", - "4. CMIP5 or CMIP6\n", - "\n", - "A list of the accepted substrings and colors is shown on the page Colours of Figanos." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "27", - "metadata": {}, - "outputs": [], - "source": [ - "# Create fake scenarios\n", - "data = {'tasmax_ssp434': ds_time,\n", - " 'tasmax_ssp245': ds_time.copy()-10,\n", - " 'tasmax_ssp585': ds_time.copy()+10}\n", - "\n", - "fg.timeseries(data=data, legend='edge', show_lat_lon=False)\n" - ] - }, - { - "cell_type": "markdown", - "id": "28", - "metadata": {}, - "source": [ - "## Gridded Data on Maps\n", - "\n", - "The gridmap function plots gridded data onto maps built using [Cartopy](https://scitools.org.uk/cartopy/docs/latest/) along with xarray plotting functions. The main arguments of the timeseries() functions are also found in gridmap(), but new ones are introduced to handle map projections and colormap/colorbar options.\n", - "\n", - "By default, the Lambert Conformal conic projection is used for the basemaps. The projection can be changed using the `projection` argument. The available projections [can be found here](https://scitools.org.uk/cartopy/docs/latest/reference/projections.html#cartopy-projections). The `transform` argument should be used to specify the data coordinate system. If a transform is not provided, figanos will look for dimensions named 'lat' and 'lon' or 'rlat' and 'rlon' and return the `ccrs.PlateCaree()` or `ccrs.RotatedPole()` transforms, respectively.\n", - "\n", - "Features can also be added to the map by passing the names of the [cartopy pre-defined features](https://scitools.org.uk/cartopy/docs/v0.14/matplotlib/feature_interface.html) in a list via the `features` argument (case-insensitively). A nested dictionary can also be passed to `features` in order to apply modifiers to these features, for instance `features = {'coastline': {'scale': '50m', 'color':'grey'}}`.\n", - "\n", - "The gridmap() function only accepts one object in its `data` argument, inside a dictionary or not. Datasets are accepted, but only their first variable will be plotted." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "29", - "metadata": {}, - "outputs": [], - "source": [ - "# Select a time and slicing our starting Dataset\n", - "ds_space = opened[['tx_max_p50']].isel(time=0).sel(lat=slice(40,65), lon=slice(-90,-55))\n", - "\n", - "# Define our spatial projection.\n", - "projection = ccrs.LambertConformal()\n", - "\n", - "fg.gridmap(ds_space, projection = projection, features = ['coastline','ocean'], frame = True, show_time = 'lower left')" - ] - }, - { - "cell_type": "markdown", - "id": "30", - "metadata": {}, - "source": [ - "### Colormaps and colorbars\n", - "\n", - "The colormap used to display the plots with gridmap() is directly dependent on three arguments:\n", - "\n", - "* `cmap` accepts colormap objects or strings.\n", - "\n", - "* `divergent` dictates whether or not the colormap will be sequential or divergent. If a number (integer of float) is provided, it becomes the center of the colormap. The default central value is 0.\n", - "\n", - "* `levels=N` will create a discrete colormap of N levels. Otherwise, the colormap will be continuous.\n", - "\n", - " By default ( if `cmap=None`), figanos will look for certain variable names in the attributes of the DataArray (`da.name` and `da.history`, in this order) and return a colormap corresponding to the 'group' of this variable, following the [IPCC visual style guide's scheme](https://www.ipcc.ch/site/assets/uploads/2022/09/IPCC_AR6_WGI_VisualStyleGuide_2022.pdf) (see page 11). The groups are displayed in the table below.\n", - "\n", - "|Variable Group|Matching strings|\n", - "|:------------:|:--------------:|\n", - "| Temperature (temp) | _tas, tasmin, tasmax, tdps, tg, tn, tx_|\n", - "|Precipitation (prec) |_pr, prc, hurs, huss, rain,
precip, precipitation, humidity, evapotranspiration_|\n", - "|Wind (wind) |_sfcWind, ua, uas, vas_|\n", - "|Cryosphere (cryo) |_snw, snd, prsn, siconc, ice_|\n", - "\n", - "Note: The strings shown above will not be recognized as variables if they are part of a longer word, for example 'tas' in 'fantastic'.\n", - "\n", - "The colormaps are built from RGB data be found in the [IPCC-WG1 Github repository](https://github.com/IPCC-WG1/colormaps). When none of the variables names match a group, or when multiple matches are found, the function resorts to the ['Batlow' colormap](https://www.fabiocrameri.ch/batlow/).\n", - "\n", - "Strings passed to these arguments can either be names of matplotlib colormaps or names of the IPCC-prescribed colormaps, such as 'temp_div'(divergent colormap for temperature variables) or 'prec_seq.txt' (sequential colormap for precipitation-related variables). Any colormap specified as a string can be reversed by adding '_r' to the end of the string.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "31", - "metadata": {}, - "outputs": [], - "source": [ - "# Change the name of our DataArray for one that includes 'pr' (precipitation) - this is still the same temperature data\n", - "da_pr = ds_space.tx_max_p50.copy()\n", - "da_pr.name = 'pr_max_p50'\n", - "\n", - "# Create a diverging colormap with 8 levels, centered at 300\n", - "ax = fg.gridmap(da_pr, projection=projection, divergent=300, levels=8, plot_kw={'cbar_kwargs':{'label':'precipitation'}})\n", - "ax.set_title('This is still temperature data,\\nbut let\\'s pretend.')" - ] - }, - { - "cell_type": "markdown", - "id": "32", - "metadata": {}, - "source": [ - "**Note**: Using the `levels` argument will result in a colormap that is split evenly across the span of the data, without consideration for how 'nice' the intervals are (i.e. the boundaries of the different colors will often fall on numbers with some decimals, that might be totally significant to an audience). To obtain 'nice' intervals, it is possible to use the `levels` argument in `plot_kw`. This might however, and often, result in the number of levels not being exactly the one that is specified. Using both arguments is not recommended." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "33", - "metadata": {}, - "outputs": [], - "source": [ - "# Create the same map, with 'nice' levels.\n", - "ax = fg.gridmap(da_pr, projection=projection, divergent=300,\n", - " plot_kw={'levels':8, 'cbar_kwargs':{'label':None}}, show_time=(0.85, 0.8))\n", - "ax.set_title('This cmap has 6 levels instead of 8,\\nbut aren\\'t they nice?')" - ] - }, - { - "cell_type": "markdown", - "id": "34", - "metadata": {}, - "source": [ - "It is also possible to specify your own levels by passing a list to `plot_kw['levels']." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "35", - "metadata": {}, - "outputs": [], - "source": [ - "ax = fg.plot.gridmap(da_pr, plot_kw={'levels':[290,294,298,302], 'cbar_kwargs':{'label':None}})\n", - "ax.set_title('Custom levels')\n", - "fg.utils.plot_logo(ax, loc=(0, 0.85), **{'zoom': 0.08})" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "36", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a custom colour map (refer to https://matplotlib.org/stable/tutorials/colors/colormap-manipulation.html#directly-creating-a-segmented-colormap-from-a-list)\n", - "from matplotlib.colors import LinearSegmentedColormap\n", - "custom_colors =[\"darkorange\", \"gold\", \"lawngreen\", \"lightseagreen\"]\n", - "custom_cmap = LinearSegmentedColormap.from_list(\"mycmap\", custom_colors)\n", - "ax = fg.gridmap(da_pr, projection=projection, divergent=300, cmap=custom_cmap,\n", - " plot_kw={'levels':8, 'cbar_kwargs':{'label':None}}, show_time=(0.85, 0.8))\n", - "ax.set_title('Custom cmap')" - ] - }, - { - "cell_type": "markdown", - "id": "37", - "metadata": {}, - "source": [ - "### pcolormesh vs contourf\n", - "\n", - "By default, xarray plots two-dimensional DataArrays using the matplotlib pcolormesh function (see [xarray.plot.pcolormesh](https://docs.xarray.dev/en/stable/generated/xarray.plot.pcolormesh.html#xarray.plot.pcolormesh)). The `contourf` argument in gridmap allows the user to use [xarray.plot.contourf](https://docs.xarray.dev/en/stable/generated/xarray.plot.contourf.html?highlight=xarray.plot.contourf) function instead. This also implies the key-value pairs passed in `plot_kw` are passed to these functions.\n", - "\n", - "At a large scales, both of these functions create practically equivalent plots. However, their inner workings are inheritely different, and these different ways of plotting data become apparent at small scales.\n", - "\n", - "When using contourf, passing a value in `levels` is equivalent to passing it in `plot_kw['levels']`, meaning the number of levels on the plot might not be exactly the specified value." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "38", - "metadata": {}, - "outputs": [], - "source": [ - "zoomed = ds_space['tx_max_p50'].sel(lat=slice(44,46), lon=slice(-65,-60))\n", - "\n", - "fig, axs = plt.subplots(1,2, figsize=(10,6), subplot_kw= {'projection': ccrs.LambertConformal()})\n", - "fg.gridmap(ax = axs[0], data=zoomed, contourf=False,plot_kw={'levels':10, 'add_colorbar':False})\n", - "axs[0].set_title('pcolormesh')\n", - "fg.gridmap(ax = axs[1],data=zoomed, contourf=True, plot_kw={'levels':10, 'cbar_kwargs':{'shrink':0.5, 'label':None}})\n", - "axs[1].set_title('contourf')" - ] - }, - { - "cell_type": "markdown", - "id": "39", - "metadata": {}, - "source": [ - "## Station Data on Maps\n", - "\n", - "Data that is georeferenced by coordinates (e.g. latitude and longitude) but is not on a grid can be plotted using the scattermap function. This function is practically identical to `gridmap()`, but introduces some new arguments (see examples below). The function essentially builds a basemap using cartopy and calls `plt.scatter()` to plot the data." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "40", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a fictional observational dataset from scratch\n", - "names = ['station_' + str(i) for i in np.arange(10)]\n", - "lat = 45 + np.random.rand(10)*3\n", - "lon = np.linspace(-76,-70, 10)\n", - "tas = 20 + np.random.rand(10)*7\n", - "tas[9] = np.nan\n", - "yrs = (10+30 * np.random.rand(10))\n", - "yrs[0] = np.nan\n", - "\n", - "attrs = {'units': 'degC', 'standard_name': 'air_temperature', 'long_name': 'Near-Surface Daily Maximum Air Temperature'}\n", - "\n", - "tas = xr.DataArray(data=tas,\n", - " coords={\n", - " 'station': names,\n", - " 'lat':('station', lat),\n", - " 'lon': ('station', lon),\n", - " 'years': ('station', yrs),\n", - " },\n", - " dims=['station'],\n", - " attrs=attrs)\n", - "tas.name = 'tas'\n", - "tas = tas.to_dataset()\n", - "tas.attrs[\"description\"] = \"Observations\"\n", - "\n", - "# Set nice features\n", - "features = {\"land\": {\"color\": \"#f0f0f0\"},\n", - " \"rivers\": {\"edgecolor\": \"#cfd3d4\"},\n", - " \"lakes\": {\"facecolor\": \"#cfd3d4\"},\n", - " \"coastline\": {\"edgecolor\": \"black\"},\n", - "}\n", - "\n", - "# Plot\n", - "ax =fg.scattermap(tas,\n", - " transform=ccrs.PlateCarree(),\n", - " sizes ='years',\n", - " size_range=(15, 100),\n", - " divergent=23.5,\n", - " features=features,\n", - " plot_kw={\n", - " \"xlim\": (-78,-68),\n", - " \"ylim\": (43,50),\n", - " \"edgecolor\": \"black\",\n", - " },\n", - " fig_kw={'figsize': (9,6)},\n", - " legend_kw={'loc': 'lower left',\n", - " 'title': 'Number of years of data'},\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "41", - "metadata": {}, - "source": [ - "It is possible to plot observations on top of gridded data by calling both `gridmap()` and `scattermap()` and fixing the colormap limits (`vmin` and `vmax`), like demonstrated below." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "42", - "metadata": {}, - "outputs": [], - "source": [ - "# defining our limits\n", - "vmin= 20\n", - "vmax= 35\n", - "\n", - "# plotting the gridded data\n", - "ax = fg.gridmap(ds_space-273.15,\n", - " plot_kw={'vmin': vmin, 'vmax': vmax, 'add_colorbar': False},\n", - " features=['coastline','ocean'],\n", - " show_time='lower right'\n", - " )\n", - "ax.set_extent([-76.5, -69, 44.5, 52], crs=ccrs.PlateCarree()) # equivalent to set_xlim and set_ylim for projections\n", - "\n", - "# plotting the observations\n", - "fg.scattermap(tas,\n", - " ax=ax,\n", - " transform=ccrs.PlateCarree(),\n", - " plot_kw={'vmin': vmin,\n", - " 'vmax': vmax,\n", - " 'edgecolor':'grey'}\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "43", - "metadata": {}, - "source": [ - "## Plotting hatched areas\n", - "The hatchmap function plots hatches on top of a map. It is a thin wrap around the [plt.contourf()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.contourf.html) function, with very similar functionality to `gridmap()` and similar data arguments to `timeseries()`. It can be overlayed on top of a map created with `gridmap()` as shown below. hatchmap can also be used with [plt.contourf()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.contourf.html) levels in plot_kw." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "44", - "metadata": { - "keep_output": true, - "nbsphinx": { - "execute": "never" - } - }, - "outputs": [], - "source": [ - "from xclim import ensembles\n", - "\n", - "urls = ['https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.1/NorESM1-M_rcp85_prcptot_monthly.nc',\n", - " 'https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.1/MPI-ESM-LR_rcp85_prcptot_monthly.nc',\n", - " 'https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.1/IPSL-CM5B-LR_rcp85_prcptot_monthly.nc',\n", - " ]\n", - "ens = ensembles.create_ensemble(urls)\n", - "fut = ens.sel(time=slice(\"2020\", \"2050\")).prcptot\n", - "ref = ens.sel(time=slice(\"1990\", \"2020\")).prcptot\n", - "chng_f= ensembles.robustness_fractions(\n", - " fut, ref, test=\"threshold\", abs_thresh=2\n", - ").changed\n", - "sup_8 = chng_f.where(chng_f>0.8)\n", - "inf_5 = chng_f.where(chng_f<0.5)\n", - "\n", - "ens_stats = ensembles.ensemble_mean_std_max_min(ens)\n", - "\n", - "ax = fg.gridmap(ens_stats.prcptot_mean.mean(dim='time', keep_attrs='True'), features = ['coastline','ocean'], frame = True)\n", - "\n", - "fg.hatchmap({'Over 0.8': sup_8, 'Under 0.5': inf_5}, ax=ax,\n", - " plot_kw={'Over 0.8': {'hatches': '*'}},\n", - " features = ['coastline','ocean'], frame = True,\n", - " legend_kw={'title': 'Ensemble change'})\n", - "ax.set_title('Ensemble plot - hatchmap and gridmap')" - ] - }, - { - "cell_type": "markdown", - "id": "45", - "metadata": {}, - "source": [ - "## GeoDataFrame on Maps\n", - "\n", - "The gdfmap function plots geometries contained in a GeoPandas [GeoDataFrame](https://geopandas.org/en/stable/docs/user_guide/data_structures.html#geodataframe) on maps. It is a thin wrap around the [GeoDataFrame.plot()](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.plot.html#geopandas.GeoDataFrame.plot) method, with verys similar functionality to `gridmap()` and most of the same features.\n", - "\n", - "To use this function, the data to be linked to the colormap has to be included in the GeoDataFrame. Its name (as a string) must be passed to the `df_col` argument. Like described above, if the `cmap` argument is `None`, the function will look for common variable names in the name of this column, and use an appropriate colormap if a match is found." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "46", - "metadata": {}, - "outputs": [], - "source": [ - "import geopandas as gpd\n", - "qc_bound = gpd.read_file(\"https://pavics.ouranos.ca/geoserver/public/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=public%3Aquebec_admin_boundaries&maxFeatures=50&outputFormat=application%2Fjson\")\n", - "qc_bound['pr']=qc_bound['RES_CO_REG'].astype(float) # create fake precipitation data\n", - "\n", - "ax = fg.gdfmap(qc_bound,\n", - " 'pr',\n", - " levels = 16,\n", - " plot_kw = {'legend_kwds': {'label': 'Fake precipitation (fake units)'}}\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "47", - "metadata": {}, - "source": [ - "Projections can be used like in `gridmap()`, although some of the Cartopy projections might lead to unexpected results due to the interaction between Cartopy and GeoPandas, especially when the whole globe is plotted.\n", - "\n", - "Also note that the colorbar parameters have to be accessed through the `legend_kwds` argument of [GeoDataFrame.plot()](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.plot.html#geopandas.GeoDataFrame.plot)." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "48", - "metadata": {}, - "outputs": [], - "source": [ - "r=gpd.read_file('https://www.donneesquebec.ca/recherche/dataset/11a317d0-97a2-4896-85b5-4cb26ccf5dc6/resource/4c6fe152-8c82-4d36-a8e0-9b584b9cde18/download/cours-eau-v3r.json')\n", - "ax = fg.gdfmap(r,\n", - " 'OBJECTID',\n", - " cmap = 'cool',\n", - " projection = ccrs.LambertConformal(),\n", - " features = {'ocean': {'color':'#a2bdeb'}},\n", - " plot_kw = {'legend_kwds':{'orientation': 'vertical'}},\n", - " frame=True\n", - " )\n", - "ax.set_title(\"Waterways of Trois-Rivières\")" - ] - }, - { - "cell_type": "markdown", - "id": "49", - "metadata": {}, - "source": [ - "## Climate Stripes\n", - "\n", - "Climate stripe diagrams are a way to present the relative change of climate variables or indicators over time, in a simple and aesthetically-oriented manner. Figanos creates such plots through the stripes function.\n", - "\n", - "While the vast majority of these diagrams will show the yearly change of a variable relative to a reference point, `stripes()` will adjust the size of the stripes to fill the figure to accomodate datasets with time intervals greater than a year.\n", - "\n", - "The function accepts DataArrays, one-variable Datasets, and a dictionary containing scenarios (DataArrays or Datasets) to be stacked. The plot will be divided in as many sub-axes as there are entries in the dictionary. Normally, these scenarios would contain identical data up to a certain year, where the scenarios diverge; the `divide` argument should be used to create an axis separation at this point of divergence." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "50", - "metadata": {}, - "outputs": [], - "source": [ - "# Create two datasets of mean annual temperature relative to the 1981-2010 period\n", - "url1 = 'https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.3/MPI-ESM-LR_rcp85_tx_mean_annual.nc'\n", - "rcp85 = xr.open_dataset(url1, decode_timedelta=False)\n", - "rcp85 = rcp85.sel(lon=-73, lat=46, method='nearest')\n", - "rcp85_deltas = rcp85 - rcp85.sel(time=slice(\"1981\",\"2010\")).mean(dim='time')\n", - "rcp85_deltas.tx_mean_annual.attrs['long_name'] = 'Mean annual daily max temp relative to 1981-2010'\n", - "rcp85_deltas.tx_mean_annual.attrs['units'] = 'K'\n", - "\n", - "url2 = 'https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.3/MPI-ESM-LR_rcp45_tx_mean_annual.nc'\n", - "rcp45 = xr.open_dataset(url2, decode_timedelta=False)\n", - "rcp45 = rcp45.sel(lon=-73, lat=46, method='nearest')\n", - "rcp45_deltas = rcp45 - rcp45.sel(time=slice(\"1981\",\"2010\")).mean(dim='time')\n", - "rcp45_deltas.tx_mean_annual.attrs['long_name'] = 'Annual mean of daily max temp relative to 1981-2010'\n", - "rcp45_deltas.tx_mean_annual.attrs['units'] = 'K'\n", - "\n", - "# Plot\n", - "fg.stripes({'rcp45': rcp45_deltas, 'rcp85': rcp85_deltas}, divide=2006)" - ] - }, - { - "cell_type": "markdown", - "id": "51", - "metadata": {}, - "source": [ - "Like most of the other functions, `stripes()` will attempt to find a colormap that is appropriate for the data variables." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "52", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a similar dataset with precipitation data\n", - "url3 = 'https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.3/MPI-ESM-LR_rcp85_precip_accumulation_annual.nc'\n", - "prec = xr.open_dataset(url3, decode_timedelta=False)\n", - "prec = prec.sel(lon=-73, lat=46, method='nearest')\n", - "prec_deltas = prec - prec.sel(time=slice(\"1981\",\"2010\")).mean(dim='time')\n", - "prec_deltas.precip_accumulation_annual.attrs['long_name'] = 'Total annual precipitation change relative to 1981-2010'\n", - "prec_deltas.precip_accumulation_annual.attrs['units'] = 'mm'\n", - "\n", - "ax = fg.stripes(prec_deltas)\n", - "ax.set_title('Precipitation')" - ] - }, - { - "cell_type": "markdown", - "id": "53", - "metadata": {}, - "source": [ - "## Violin Plots\n", - "\n", - "Violin plots are a practical tool for visualizing the statistical distribution of data in an ensemble, combining a box plot with a kernel density plot. The violin function wraps Seaborn's [violinplot](https://seaborn.pydata.org/generated/seaborn.violinplot.html#seaborn.violinplot) function to directly accept xarray objects, and incorporates other figanos features. The `data` argument can be a DataArray (one \"violin\"), a Dataset (as many \"violins\" as there are variables in the Dataset), or a dictionary of either types. In the case of a dictionary, its keys will become the \"violin\" labels.\n", - "\n", - "As with other functions, when `use_attrs` is passed and `data` is a dictionary, attributes from the first dictionary entry will be put on the plot." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "54", - "metadata": {}, - "outputs": [], - "source": [ - "fg.violin(ds_time, use_attrs={'title':'description'})" - ] - }, - { - "cell_type": "markdown", - "id": "55", - "metadata": {}, - "source": [ - "The optional `color` argument combines the Seaborn function's `color` and `palette` arguments. A single color or a list of colors can be passed. Integers can be passed instead of strings to refer to colors of the currently used stylesheet. If the list of colors is shorter than the number of variables on the plot, the colors are repeated." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "56", - "metadata": {}, - "outputs": [], - "source": [ - "my_data = {'p10': ds_time.tx_max_p10,'p50': ds_time.tx_max_p50, 'p90': ds_time.tx_max_p90}\n", - "\n", - "ax = fg.violin(my_data, plot_kw={'orient':'h'}, color=[3, 'purple', '#78bf84'])" - ] - }, - { - "cell_type": "markdown", - "id": "57", - "metadata": {}, - "source": [ - "## Heatmaps\n", - "\n", - "Similarly to violin plots, the heatmap function wraps Seaborn's [heatmap](https://seaborn.pydata.org/generated/seaborn.heatmap.html) function to directly accept xarray objects, and incorporates other figanos features. The `data` argument can be a DataArray, a Dataset, or a dictionary of either types and of length=1. There is no real benefit to using a dictionary, but it is accepted to be coherent with other functions in the package." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "58", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a diagnostics Dataset from scratch\n", - "improvement = np.random.rand(7,7)\n", - "diagnostics = xr.DataArray(data=improvement,\n", - " coords=dict(realization=['model1', 'model2', 'model3', 'model4', 'model5', 'model6', 'model7'],\n", - " properties=['aca_pr', 'aca_tasmax', 'aca_tasmin', 'corr_tasmax_pr',\n", - " 'corr_tasmax_tasmin', 'mean_tasmax', 'mean_pr']))\n", - "\n", - "diagnostics.attrs['long_name'] = \"% of improved grid cells\"\n", - "\n", - "# Plot a heatmap\n", - "fg.heatmap(diagnostics, divergent=0.5, plot_kw={'vmin': 0, 'linecolor': 'w', 'linewidth':1.5})\n" - ] - }, - { - "cell_type": "markdown", - "id": "59", - "metadata": {}, - "source": [ - "In order to produce realiable results, the xarray object passed to `heatmap()` has to have only two dimensions. Under the hood, the function converts the DataArray containing the data to a pandas DataFrame before plotting it. Using `transpose=True` swaps the x and y axes.\n", - "\n", - "The colorbar kwargs are accessible through the nesting of `cbar_kws` in `plot_kw`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "60", - "metadata": {}, - "outputs": [], - "source": [ - "ax = fg.heatmap(diagnostics,\n", - " transpose=True,\n", - " cmap='bwr_r',\n", - " divergent=0.5,\n", - " plot_kw={'cbar_kws':{'label': 'Proportion of cells improved'}, 'annot':True}\n", - " )\n", - "\n", - "# Remove the grid labels\n", - "ax.set_xlabel(\"\")\n", - "ax.set_ylabel(\"\")" - ] - }, - { - "cell_type": "markdown", - "id": "61", - "metadata": {}, - "source": [ - "## Triangle heatmaps\n", - "\n", - "The `triheatmap` function is based on the matplotlib function [tripcolor](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.tripcolor.html). It can create a heatmap with 2 or 4 triangles in each square of the heatmap.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "62", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a fake data\n", - "da = xr.DataArray(data=np.random.rand(2,3,4),\n", - " coords=dict(realization=['A', 'B'],\n", - " method=['a','b', 'c'],\n", - " experiment=['ssp126','ssp245','ssp370','ssp585'],\n", - " ))\n", - "da.name='pr' # to guess the cmap\n", - "# will be automatically detected for the cbar label\n", - "da.attrs['long_name']= 'precipitation' \n", - "da.attrs['units']= 'mm'\n", - "\n", - "# Plot a heatmap\n", - "fg.triheatmap(da,\n", - " z='experiment', # which dimension should be represented by triangles\n", - " divergent=True, # for the cmap\n", - " cbar='unique', # only show one cbar\n", - " plot_kw={'vmin':-1, 'vmax':1} # we are only showing the 1st cbar, so make sure the cbar of each triangle is the same\n", - " )" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "63", - "metadata": {}, - "outputs": [], - "source": [ - "# Create a fake data\n", - "da = xr.DataArray(data=np.random.rand(4,3,2),\n", - " coords=dict(realization=['A', 'B', 'C','D'],\n", - " method=['a','b', 'c'],\n", - " season=['DJF','JJA'],\n", - " ))\n", - "da.attrs['description']= \"La plus belle saison de ma vie\"\n", - "\n", - "# Plot a heatmap\n", - "fg.triheatmap(da,\n", - " z='season',\n", - " cbar='each', # show a cbar per triangle\n", - " use_attrs={'title':'description'},\n", - " cbar_kw=[{'label':'winter'},{'label':'summer'}], # Use a list to change the cbar associated with each triangle type (upper or lower)\n", - " plot_kw=[{'cmap':'winter'},{'cmap':'summer'}]) # Use a list to change each triangle type (upper or lower)" - ] - }, - { - "cell_type": "markdown", - "id": "64", - "metadata": {}, - "source": [ - "## Taylor Diagrams\n", - "\n", - "Taylor diagrams are a useful way to compare simulation datasets to a reference dataset. They allow to graphically represent the standard deviation of both the simulation and reference datasets, the correlation between both, and the root mean squared error (a function of the two previous statistical properties).\n", - "\n", - "The taylordiagram function creates each point on the Taylor diagram from an object created using `xclim.sdba.measures.taylordiagram`, as illustrated below.\n", - "\n", - "### Important Notes\n", - "* The structure of the matplotlib axes being different than in the other figanos functions, this funcion does not have an `ax` argument, and creates its own figure.\n", - "* To change the axis labels, use the `std_label` and `corr_label` arguments, rather than the `ax.set_xlabel()` method.\n", - "* Dataset with negative correlations with the reference dataset will not be plotted.\n", - "* To modify the appearance of the reference point (on the x-axis), use the keyword 'reference' in `plot_kw`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "65", - "metadata": {}, - "outputs": [], - "source": [ - "da_ref = ds_time['tx_max_p50']\n", - "\n", - "# Toy data with same mean as `da_ref` & modify deviations with trigonometric functions\n", - "homogenous_ref_mean = xr.full_like(da_ref, da_ref.mean(dim=\"time\"))\n", - "simd = {}\n", - "for i, f_trig in enumerate([np.cos, lambda x: np.cos(x)**2, np.tan]):\n", - " da = homogenous_ref_mean + f_trig(da_ref.values)\n", - " da.attrs[\"units\"] = da_ref.attrs[\"units\"]\n", - " simd[f\"model{i}\"] = sdba.measures.taylordiagram(sim=da, ref=da_ref)\n", - "\n", - "fg.taylordiagram(simd, std_range=(0, 1.3), contours=5, contours_kw={'colors': 'green'}, plot_kw={'reference': {'marker':'*'}})\n" - ] - }, - { - "cell_type": "markdown", - "id": "66", - "metadata": {}, - "source": [ - "### Normalized taylor diagram\n", - "\n", - "If we normalize the standard deviation of our measures, many Taylor diagrams with difference references can be combined in a single plot. In the following example, we have datasets with two variables (`tasmax, pr`) and three location coordinates. For each location (3) and variable (2), a Taylordiagram measure is computed. Each set of correlation and standard deviation is then plotted. " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "67", - "metadata": {}, - "outputs": [], - "source": [ - "from xclim.testing.utils import nimbus\n", - "\n", - "n = nimbus()\n", - "\n", - "ref = n.fetch(\"sdba/ahccd_1950-2013.nc\")\n", - "ds_ref = xr.open_dataset(ref)\n", - "sim = n.fetch(\"sdba/nrcan_1950-2013.nc\")\n", - "ds_sim = xr.open_dataset(sim)\n", - "\n", - "for v in ds_ref.data_vars: \n", - " ds_sim[v] = xc.core.units.convert_units_to(ds_sim[v], ds_ref[v], context=\"hydro\")\n", - "\n", - "# Here, we have three locations, two variables. We stack variables to convert from\n", - "# a Dataset to a DataArray.\n", - "da_ref = sdba.stack_variables(ds_ref)\n", - "da_sim = sdba.stack_variables(ds_sim)\n", - "\n", - "# Each location/variable will have its own set of taylor parameters\n", - "out = sdba.measures.taylordiagram(ref=da_ref, sim=da_sim, dim=\"time\")\n", - "\n", - "# If we normalize the taylor diagrams, they can be compared on the same plot\n", - "out[{\"taylor_param\":[0,1]}] = out[{\"taylor_param\":[0,1]}]/out[{\"taylor_param\":0}]\n", - "\n", - "# in xclim >= 0.50.0 : Normalization can be done when computing taylordiagram measure\n", - "# out = sdba.measures.taylordiagram(ref=da_ref, sim=da_sim, dim=\"time\", normalize=True)\n", - "\n", - "# The `markers_key` and `colors_key` are used to separate between two different features. \n", - "# Here, the type of marker is used to distinguish between locations, and the color \n", - "# distinguishes between variables. If those parameters are not specified, then each \n", - "# pair (location, multivar) has simply its own color.\n", - "fg.taylordiagram(out, markers_key=\"location\", colors_key=\"multivar\", ref_std_line = True)\n" - ] - }, - { - "cell_type": "markdown", - "id": "68", - "metadata": {}, - "source": [ - "## Partition plots\n", - "\n", - "Partition plots show the fraction of uncertainty associated with different components.\n", - "Xclim has a few different [partition functions](https://xclim.readthedocs.io/en/stable/api.html#uncertainty-partitioning).\n", - "\n", - "This tutorial is a reproduction of [xclim's documentation](https://xclim.readthedocs.io/en/stable/notebooks/partitioning.html).\n", - "\n", - "
\n", - "\n", - "Note that you could also use the [xscen library](https://xscen.readthedocs.io/en/latest/index.html) to build and ensemble from a catalog with `xscen.ensembles.build_partition_data`.\n", - "\n", - "
" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "69", - "metadata": {}, - "outputs": [], - "source": [ - "# Fetch data\n", - "import pandas as pd\n", - "\n", - "import xclim.ensembles\n", - "\n", - "# The directory in the Atlas repo where the data is stored\n", - "# host = \"https://github.com/IPCC-WG1/Atlas/raw/main/datasets-aggregated-regionally/data/CMIP6/CMIP6_tas_land/\"\n", - "host = \"https://raw.githubusercontent.com/IPCC-WG1/Atlas/main/datasets-aggregated-regionally/data/CMIP6/CMIP6_tas_land/\"\n", - "\n", - "# The file pattern, e.g. CMIP6_ACCESS-CM2_ssp245_r1i1p1f1.csv\n", - "pat = \"CMIP6_{model}_{scenario}_{member}.csv\"\n", - "\n", - "# Here we'll download data only for a very small demo sample of models and scenarios.\n", - "\n", - "# Download data for a few models and scenarios.\n", - "models = [\"ACCESS-CM2\", \"CMCC-CM2-SR5\", \"CanESM5\"]\n", - "members = [\"r1i1p1f1\", \"r1i1p1f1\", \"r1i1p1f1\"]\n", - "scenarios = [\"ssp245\", \"ssp370\", \"ssp585\"]\n", - "\n", - "# Create the input ensemble.\n", - "data = []\n", - "for model, member in zip(models, members):\n", - " for scenario in scenarios:\n", - " url = host + pat.format(model=model, scenario=scenario, member=member)\n", - "\n", - " # Fetch data using pandas\n", - " df = pd.read_csv(url, index_col=0, comment=\"#\", parse_dates=True)[\"world\"]\n", - " # Convert to a DataArray, complete with coordinates.\n", - " da = (\n", - " xr.DataArray(df)\n", - " .expand_dims(model=[model], scenario=[scenario])\n", - " .rename(date=\"time\")\n", - " )\n", - " data.append(da)\n", - "\n", - "# Combine DataArrays from the different models and scenarios into one.\n", - "ens_mon = xr.combine_by_coords(data)[\"world\"]\n", - "\n", - "# Then resample the monthly time series at the annual frequency\n", - "ens = ens_mon.resample(time=\"Y\").mean()" - ] - }, - { - "cell_type": "markdown", - "id": "70", - "metadata": {}, - "source": [ - "Compute uncertainties with xclim and use `fractional_uncertainty` to have the right format to plot." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "71", - "metadata": {}, - "outputs": [], - "source": [ - "# compute uncertainty\n", - "mean, uncertainties = xclim.ensembles.hawkins_sutton(ens, baseline=(\"2016\", \"2030\"))\n", - "#frac= xc.ensembles.fractional_uncertainty(uncertainties)\n", - "\n", - "#FIXME: xc.ensembles.fractional_uncertainty has not been released yet. Until until it is released, here it is.\n", - "def fractional_uncertainty(u: xr.DataArray):\n", - " \"\"\"Return the fractional uncertainty.\n", - "\n", - " Parameters\n", - " ----------\n", - " u : xr.DataArray\n", - " Array with uncertainty components along the `uncertainty` dimension.\n", - "\n", - " Returns\n", - " -------\n", - " xr.DataArray\n", - " Fractional, or relative uncertainty with respect to the total uncertainty.\n", - " \"\"\"\n", - " uncertainty = u / u.sel(uncertainty=\"total\") * 100\n", - " uncertainty.attrs.update(u.attrs)\n", - " uncertainty.attrs[\"long_name\"] = \"Fraction of total variance\"\n", - " uncertainty.attrs[\"units\"] = \"%\"\n", - " return uncertainty\n", - "\n", - "frac= fractional_uncertainty(uncertainties)\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "72", - "metadata": {}, - "outputs": [], - "source": [ - "# plot\n", - "fg.partition(\n", - " frac,\n", - " start_year='2016', # change the x-axis\n", - " show_num=True, # put the number of element of each uncertainty source in the legend FIXME: will only appear after xclim releases 0.48\n", - " fill_kw={\n", - " \"variability\": {'color':\"#DC551A\"},\n", - " \"model\": {'color':\"#2B2B8B\"},\n", - " \"scenario\": {'color':\"#275620\"},},\n", - " line_kw={'lw':2}\n", - ")\n" - ] - } - ], - "metadata": { - "celltoolbar": "Edit Metadata", - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/notebooks/figanos_maps.ipynb b/docs/notebooks/figanos_maps.ipynb new file mode 100644 index 00000000..7639d17f --- /dev/null +++ b/docs/notebooks/figanos_maps.ipynb @@ -0,0 +1,620 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Maps\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# setup notebook\n", + "%config InlineBackend.print_figure_kwargs = {'bbox_inches':'tight'}\n", + "from __future__ import annotations\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import xarray as xr\n", + "import figanos.matplotlib as fg\n", + "import cartopy.crs as ccrs\n", + "from matplotlib import pyplot as plt\n", + "import numpy as np\n", + "\n", + "fg.utils.set_mpl_style(\"ouranos\")\n", + "\n", + "# load dataset\n", + "url = \"https://pavics.ouranos.ca//twitcher/ows/proxy/thredds/dodsC/birdhouse/disk2/cccs_portal/indices/Final/BCCAQv2_CMIP6/tx_max/YS/ssp585/ensemble_percentiles/tx_max_ann_BCCAQ2v2+ANUSPLIN300_historical+ssp585_1950-2100_30ymean_percentiles.nc\"\n", + "opened = xr.open_dataset(url, decode_timedelta=False)\n", + "ds_space = opened[[\"tx_max_p50\"]].isel(time=0).sel(lat=slice(40, 65), lon=slice(-90, -55))" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Gridded Data on Maps\n", + "\n", + "The gridmap function plots gridded data onto maps built using [Cartopy](https://scitools.org.uk/cartopy/docs/latest/) along with xarray plotting functions.\n", + "\n", + "Visit the timeseries notebook to learn the basic functions of figanos. The main arguments of the timeseries() functions are also found in gridmap(), but new ones are introduced to handle map projections and colormap/colorbar options.\n", + "\n", + "By default, the Lambert Conformal conic projection is used for the basemaps. The projection can be changed using the `projection` argument. The available projections [can be found here](https://scitools.org.uk/cartopy/docs/latest/reference/projections.html#cartopy-projections). The `transform` argument should be used to specify the data coordinate system. If a transform is not provided, figanos will look for dimensions named 'lat' and 'lon' or 'rlat' and 'rlon' and return the `ccrs.PlateCaree()` or `ccrs.RotatedPole()` transforms, respectively.\n", + "\n", + "Features can also be added to the map by passing the names of the [cartopy pre-defined features](https://scitools.org.uk/cartopy/docs/v0.14/matplotlib/feature_interface.html) in a list via the `features` argument (case-insensitively). A nested dictionary can also be passed to `features` in order to apply modifiers to these features, for instance `features = {'coastline': {'scale': '50m', 'color':'grey'}}`.\n", + "\n", + "The gridmap() function only accepts one object in its `data` argument, inside a dictionary or not. Datasets are accepted, but only their first variable will be plotted." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "fg.gridmap(\n", + " ds_space,\n", + " features=[\"coastline\", \"ocean\"],\n", + " frame=True,\n", + " show_time=\"lower left\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/basic_map.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "### Colormaps and colorbars\n", + "\n", + "The colormap used to display the plots with gridmap() is directly dependent on three arguments:\n", + "\n", + "* `cmap` accepts colormap objects or strings. Strings passed can either be names of matplotlib colormaps or names of the IPCC-prescribed colormaps (see cell below). The colormaps are built from RGB data found in the [IPCC-WG1 GitHub repository](https://github.com/IPCC-WG1/colormaps). Any colormap specified as a string can be reversed by adding '_r' to the end of the string.\n", + "\n", + "* `divergent` dictates whether the colormap will be sequential or divergent. If a number (integer or float) is provided, it becomes the center of the colormap. The default central value is 0.\n", + "\n", + "* `levels=N` will create a discrete colormap of N levels. Otherwise, the colormap will be continuous.\n", + "\n", + " By default, if `cmap=None`, figanos will look for certain variable names in the attributes of the DataArray (`da.name` and `da.history`, in this order) and return a colormap corresponding to the 'group' of this variable, following the [IPCC visual style guide's scheme](https://www.ipcc.ch/site/assets/uploads/2022/09/IPCC_AR6_WGI_VisualStyleGuide_2022.pdf) (see page 11). The groups are displayed in the table below.\n", + "\n", + "|Variable Group|Matching strings|\n", + "|:------------:|:--------------:|\n", + "| Temperature (temp) | _tas, tasmin, tasmax, tdps, tg, tn, tx_|\n", + "|Precipitation (prec) |_pr, prc, hurs, huss, rain,
precip, precipitation, humidity, evapotranspiration_|\n", + "|Wind (wind) |_sfcWind, ua, uas, vas_|\n", + "|Cryosphere (cryo) |_snw, snd, prsn, siconc, ice_|\n", + "\n", + "Note: The strings shown above will not be recognized as variables if they are part of a longer word, for example, 'tas' in 'fantastic'.\n", + "\n", + " When none of the variables names match a group, or when multiple matches are found, the function resorts to the ['Batlow' colormap](https://www.fabiocrameri.ch/batlow/).\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "from figanos import data\n", + "import json\n", + "from pathlib import Path\n", + "import matplotlib\n", + "\n", + "with data().joinpath(\"ipcc_colors\").joinpath(\"variable_groups.json\").open(encoding=\"utf-8\") as f:\n", + " var_dict = json.load(f)\n", + "\n", + "for f in sorted(data().joinpath(\"ipcc_colors/continuous_colormaps_rgb_0-255\").glob(\"*\")):\n", + " name = Path(f).name.replace(\".txt\", \"\")\n", + " cmap = fg.utils.create_cmap(filename=name)\n", + " fig = plt.figure()\n", + " ax = fig.add_axes([0.05, 0.80, 0.9, 0.1])\n", + " cb = matplotlib.colorbar.ColorbarBase(ax, orientation=\"horizontal\", cmap=cmap)\n", + " cb.outline.set_visible(False)\n", + " cb.ax.set_xticklabels([])\n", + " split = name.split(\"_\")\n", + " var = split[0] + (split[2] if len(split) == 3 else \"\")\n", + " kw = [k for k, v in var_dict.items() if v == var]\n", + " # plt.title(f\"name: {name} \\n keywords: {kw}\", wrap=True)\n", + " plt.figtext(\n", + " 0.5,\n", + " 0.95 + (0.04 * int(len(kw) / 10)),\n", + " f\"name: {name}\",\n", + " fontsize=15,\n", + " ha=\"center\",\n", + " )\n", + " plt.figtext(0.5, 0.91, f\"keywords: {kw}\", fontsize=10, ha=\"center\", wrap=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "# Change the name of our DataArray for one that includes 'pr' (precipitation) - this is still the same temperature data\n", + "da_pr = ds_space.tx_max_p50.copy()\n", + "da_pr.name = \"pr_max_p50\"\n", + "\n", + "# Create a diverging colormap with 8 levels, centered at 300\n", + "ax = fg.gridmap(\n", + " da_pr,\n", + " divergent=300,\n", + " levels=8,\n", + " plot_kw={\"cbar_kwargs\": {\"label\": \"precipitation\"}},\n", + ")\n", + "ax.set_title(\"This is still temperature data,\\nbut let's pretend.\")" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "**Note**: Using the `levels` argument will result in a colormap that is split evenly across the span of the data, without consideration for how 'nice' the intervals are (i.e. the boundaries of the different colors will often fall on numbers with some decimals, that might be totally significant to an audience). To obtain 'nice' intervals, it is possible to use the `levels` argument in `plot_kw`. This might however, and often, result in the number of levels not being exactly the one that is specified. Using both arguments is not recommended." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "# Create the same map, with 'nice' levels.\n", + "ax = fg.gridmap(\n", + " da_pr,\n", + " divergent=300,\n", + " plot_kw={\"levels\": 8, \"cbar_kwargs\": {\"label\": None}},\n", + " show_time=(0.85, 0.8),\n", + ")\n", + "ax.set_title(\"This cmap has 6 levels instead of 8,\\nbut aren't they nice?\")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "It is also possible to specify your own levels by passing a list to `plot_kw['levels']." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "ax = fg.plot.gridmap(\n", + " da_pr,\n", + " plot_kw={\"levels\": [290, 294, 298, 302], \"cbar_kwargs\": {\"label\": None}},\n", + ")\n", + "ax.set_title(\"Custom levels\")\n", + "fg.utils.plot_logo(ax, loc=(0, 0.85), **{\"zoom\": 0.08})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a custom colour map (refer to https://matplotlib.org/stable/tutorials/colors/colormap-manipulation.html#directly-creating-a-segmented-colormap-from-a-list)\n", + "from matplotlib.colors import LinearSegmentedColormap\n", + "\n", + "custom_colors = [\"darkorange\", \"gold\", \"lawngreen\", \"lightseagreen\"]\n", + "custom_cmap = LinearSegmentedColormap.from_list(\"mycmap\", custom_colors)\n", + "ax = fg.gridmap(\n", + " da_pr,\n", + " divergent=300,\n", + " cmap=custom_cmap,\n", + " plot_kw={\"levels\": 8, \"cbar_kwargs\": {\"label\": None}},\n", + " show_time=(0.85, 0.8),\n", + ")\n", + "ax.set_title(\"Custom cmap\")" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "### pcolormesh vs contourf\n", + "\n", + "By default, xarray plots two-dimensional DataArrays using the matplotlib pcolormesh function (see [xarray.plot.pcolormesh](https://docs.xarray.dev/en/stable/generated/xarray.plot.pcolormesh.html#xarray.plot.pcolormesh)). The `contourf` argument in gridmap allows the user to use [xarray.plot.contourf](https://docs.xarray.dev/en/stable/generated/xarray.plot.contourf.html?highlight=xarray.plot.contourf) function instead. This also implies the key-value pairs passed in `plot_kw` are passed to these functions.\n", + "\n", + "At large scales, both of these functions create practically equivalent plots. However, their inner workings are inherently different, and these different ways of plotting data become apparent at small scales.\n", + "\n", + "When using `contourf`, passing a value in `levels` is equivalent to passing it in `plot_kw['levels']`, meaning the number of levels on the plot might not be exactly the specified value." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "zoomed = ds_space[\"tx_max_p50\"].sel(lat=slice(44, 46), lon=slice(-65, -60))\n", + "\n", + "fig, axs = plt.subplots(1, 2, figsize=(10, 6), subplot_kw={\"projection\": ccrs.LambertConformal()})\n", + "fg.gridmap(\n", + " ax=axs[0],\n", + " data=zoomed,\n", + " contourf=False,\n", + " plot_kw={\"levels\": 10, \"add_colorbar\": False},\n", + ")\n", + "axs[0].set_title(\"pcolormesh\")\n", + "fg.gridmap(\n", + " ax=axs[1],\n", + " data=zoomed,\n", + " contourf=True,\n", + " plot_kw={\"levels\": 10, \"cbar_kwargs\": {\"shrink\": 0.5, \"label\": None}},\n", + ")\n", + "axs[1].set_title(\"contourf\")" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## Station Data on Maps\n", + "\n", + "Data that is georeferenced by coordinates (e.g. latitude and longitude) but is not on a grid can be plotted using the scattermap function. This function is practically identical to `gridmap()`, but introduces some new arguments (see examples below). The function essentially builds a basemap using cartopy and calls `plt.scatter()` to plot the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a fictional observational dataset from scratch\n", + "names = [\"station_\" + str(i) for i in np.arange(10)]\n", + "lat = 45 + np.random.rand(10) * 3\n", + "lon = np.linspace(-76, -70, 10)\n", + "tas = 20 + np.random.rand(10) * 7\n", + "tas[9] = np.nan\n", + "yrs = 10 + 30 * np.random.rand(10)\n", + "yrs[0] = np.nan\n", + "\n", + "attrs = {\n", + " \"units\": \"degC\",\n", + " \"standard_name\": \"air_temperature\",\n", + " \"long_name\": \"Near-Surface Daily Maximum Air Temperature\",\n", + "}\n", + "\n", + "tas = xr.DataArray(\n", + " data=tas,\n", + " coords={\n", + " \"station\": names,\n", + " \"lat\": (\"station\", lat),\n", + " \"lon\": (\"station\", lon),\n", + " \"years\": (\"station\", yrs),\n", + " },\n", + " dims=[\"station\"],\n", + " attrs=attrs,\n", + ")\n", + "tas.name = \"tas\"\n", + "tas = tas.to_dataset()\n", + "tas.attrs[\"description\"] = \"Observations\"\n", + "\n", + "# Set nice features\n", + "features = {\n", + " \"land\": {\"color\": \"#f0f0f0\"},\n", + " \"rivers\": {\"edgecolor\": \"#cfd3d4\"},\n", + " \"lakes\": {\"facecolor\": \"#cfd3d4\"},\n", + " \"coastline\": {\"edgecolor\": \"black\"},\n", + "}\n", + "\n", + "# Plot\n", + "ax = fg.scattermap(\n", + " tas,\n", + " sizes=\"years\",\n", + " size_range=(15, 100),\n", + " divergent=23.5,\n", + " features=features,\n", + " plot_kw={\n", + " \"edgecolor\": \"black\",\n", + " },\n", + " fig_kw={\"figsize\": (9, 6)},\n", + " legend_kw={\"loc\": \"lower left\", \"title\": \"Number of years of data\"},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/station_map.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "It is possible to plot observations on top of gridded data by calling both `gridmap()` and `scattermap()` and fixing the colormap limits (`vmin` and `vmax`), like demonstrated below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "# defining our limits\n", + "vmin = 20\n", + "vmax = 35\n", + "\n", + "# plotting the gridded data\n", + "ax = fg.gridmap(\n", + " ds_space - 273.15,\n", + " plot_kw={\"vmin\": vmin, \"vmax\": vmax, \"add_colorbar\": False},\n", + " features=[\"coastline\", \"ocean\"],\n", + " show_time=\"lower right\",\n", + ")\n", + "ax.set_extent([-76.5, -69, 44.5, 52], crs=ccrs.PlateCarree()) # equivalent to set_xlim and set_ylim for projections\n", + "\n", + "# plotting the observations\n", + "fg.scattermap(\n", + " tas,\n", + " ax=ax,\n", + " transform=ccrs.PlateCarree(),\n", + " plot_kw={\"vmin\": vmin, \"vmax\": vmax, \"edgecolor\": \"grey\"},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/station+grid_map.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## Hatching on Maps\n", + "The hatchmap function plots hatches on top of a map. It is a thin wrap around the [plt.contourf()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.contourf.html) function, with very similar functionality to `gridmap()` and similar data arguments to `timeseries()`. It can be overlaid on top of a map created with `gridmap()` as shown below. `hatchmap` can also be used with [plt.contourf()](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.contourf.html) levels in plot_kw." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# from xclim import ensembles\n", + "# import xscen as xs\n", + "# urls = ['https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.1/NorESM1-M_rcp85_prcptot_monthly.nc',\n", + "# 'https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.1/MPI-ESM-LR_rcp85_prcptot_monthly.nc',\n", + "# 'https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.1/IPSL-CM5B-LR_rcp85_prcptot_monthly.nc',\n", + "# ]\n", + "# ens = ensembles.create_ensemble(urls)\n", + "# fut = ens.sel(time=slice(\"2020\", \"2050\")).prcptot\n", + "# ref = ens.sel(time=slice(\"1990\", \"2020\")).prcptot\n", + "# chng_f= ensembles.robustness_fractions(\n", + "# fut, ref, test=\"threshold\", abs_thresh=2\n", + "# ).changed\n", + "# sup_8 = chng_f.where(chng_f>0.8).to_dataset()\n", + "# inf_5 = chng_f.where(chng_f<0.5).to_dataset()\n", + "\n", + "# ens_stats = ensembles.ensemble_mean_std_max_min(ens)\n", + "\n", + "# out=ens_stats.prcptot_mean.mean(dim='time', keep_attrs='True').to_dataset()\n", + "# xs.save_to_netcdf(out, '../../src/figanos/data/test_data/hatchmap-ens_stats.nc')\n", + "# xs.save_to_netcdf(sup_8, '../../src/figanos/data/test_data/hatchmap-sup_8.nc')\n", + "# xs.save_to_netcdf(inf_5, '../../src/figanos/data/test_data/hatchmap-inf_5.nc')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "from figanos import pitou\n", + "\n", + "# Helper function for loading testing data\n", + "p = pitou()\n", + "\n", + "ens_stats = xr.open_dataset(p.fetch(\"hatchmap-ens_stats.nc\")).prcptot_mean\n", + "sup_8 = xr.open_dataset(p.fetch(\"hatchmap-sup_8.nc\")).changed\n", + "inf_5 = xr.open_dataset(p.fetch(\"hatchmap-inf_5.nc\")).changed\n", + "\n", + "ax = fg.gridmap(ens_stats, features=[\"coastline\", \"ocean\"], frame=True)\n", + "\n", + "fg.hatchmap(\n", + " {\"Over 0.8\": sup_8, \"Under 0.5\": inf_5},\n", + " ax=ax,\n", + " plot_kw={\"Over 0.8\": {\"hatches\": \"*\"}},\n", + " features=[\"coastline\", \"ocean\"],\n", + " frame=True,\n", + " legend_kw={\"title\": \"Ensemble change\"},\n", + ")\n", + "ax.set_title(\"Ensemble plot - hatchmap and gridmap\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/hatch_map.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "## GeoDataFrame on Maps\n", + "\n", + "The gdfmap function plots geometries contained in a GeoPandas [GeoDataFrame](https://geopandas.org/en/stable/docs/user_guide/data_structures.html#geodataframe) on maps. It is a thin wrapper around the [GeoDataFrame.plot()](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.plot.html#geopandas.GeoDataFrame.plot) method, with very similar functionality to `gridmap()` and most of the same features.\n", + "\n", + "To use this function, the data to be linked to the colormap has to be included in the GeoDataFrame. Its name (as a string) must be passed to the `df_col` argument. Like described above, if the `cmap` argument is `None`, the function will look for common variable names in the name of this column, and use an appropriate colormap if a match is found." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "import geopandas as gpd\n", + "\n", + "qc_bound = gpd.read_file(\n", + " \"https://pavics.ouranos.ca/geoserver/public/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=public%3Aquebec_admin_boundaries&maxFeatures=50&outputFormat=application%2Fjson\"\n", + ")\n", + "qc_bound[\"pr\"] = qc_bound[\"RES_CO_REG\"].astype(float) # create fake precipitation data\n", + "\n", + "ax = fg.gdfmap(\n", + " qc_bound,\n", + " \"pr\",\n", + " levels=16,\n", + " plot_kw={\"legend_kwds\": {\"label\": \"Fake precipitation (fake units)\"}},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/gdf_map.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "Projections can be used like in `gridmap()`, although some of the Cartopy projections might lead to unexpected results due to the interaction between Cartopy and GeoPandas, especially when the whole globe is plotted.\n", + "\n", + "Also note that the colorbar parameters have to be accessed through the `legend_kwds` argument of [GeoDataFrame.plot()](https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.plot.html#geopandas.GeoDataFrame.plot)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "r = gpd.read_file(\n", + " \"https://www.donneesquebec.ca/recherche/dataset/11a317d0-97a2-4896-85b5-4cb26ccf5dc6/resource/4c6fe152-8c82-4d36-a8e0-9b584b9cde18/download/cours-eau-v3r.json\"\n", + ")\n", + "ax = fg.gdfmap(\n", + " r,\n", + " \"OBJECTID\",\n", + " cmap=\"cool\",\n", + " projection=ccrs.Mercator(),\n", + " features={\"ocean\": {\"color\": \"#a2bdeb\"}},\n", + " plot_kw={\"legend_kwds\": {\"orientation\": \"vertical\"}},\n", + " frame=True,\n", + ")\n", + "ax.set_title(\"Waterways of Trois-Rivières\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/gdf2_map.png\", bbox_inches='tight')" + ] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/notebooks/figanos_misc.ipynb b/docs/notebooks/figanos_misc.ipynb new file mode 100644 index 00000000..3e177589 --- /dev/null +++ b/docs/notebooks/figanos_misc.ipynb @@ -0,0 +1,736 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Miscellanous\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "# setup notebook\n", + "%config InlineBackend.print_figure_kwargs = {'bbox_inches':'tight'}\n", + "from __future__ import annotations\n", + "\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import xarray as xr\n", + "import figanos.matplotlib as fg\n", + "from matplotlib import pyplot as plt\n", + "import numpy as np\n", + "from xclim import sdba\n", + "import xclim as xc\n", + "\n", + "fg.utils.set_mpl_style(\"ouranos\")\n", + "\n", + "# load dataset\n", + "url = \"https://pavics.ouranos.ca//twitcher/ows/proxy/thredds/dodsC/birdhouse/disk2/cccs_portal/indices/Final/BCCAQv2_CMIP6/tx_max/YS/ssp585/ensemble_percentiles/tx_max_ann_BCCAQ2v2+ANUSPLIN300_historical+ssp585_1950-2100_30ymean_percentiles.nc\"\n", + "opened = xr.open_dataset(url, decode_timedelta=False)\n", + "ds_time = opened.isel(lon=500, lat=250)[[\"tx_max_p50\", \"tx_max_p10\", \"tx_max_p90\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Climate Stripes\n", + "\n", + "Climate stripe diagrams are a way to present the relative change of climate variables or indicators over time, in a simple and aesthetically-pleasing manner. Figanos creates such plots through the stripes function.\n", + "\n", + "While the vast majority of these diagrams will show the yearly change of a variable relative to a reference point, `stripes()` will adjust the size of the stripes to fill the figure to accommodate datasets with time intervals greater than a year.\n", + "\n", + "The function accepts DataArrays, one-variable Datasets, and a dictionary containing scenarios (DataArrays or Datasets) to be stacked. The plot will be divided in as many sub-axes as there are entries in the dictionary. Normally, these scenarios would contain identical data up to a certain year, where the scenarios diverge; the `divide` argument should be used to create an axis separation at this point of divergence." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# Create two datasets of mean annual temperature relative to the 1981-2010 period\n", + "url1 = \"https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.3/MPI-ESM-LR_rcp85_tx_mean_annual.nc\"\n", + "rcp85 = xr.open_dataset(url1, decode_timedelta=False)\n", + "rcp85 = rcp85.sel(lon=-73, lat=46, method=\"nearest\")\n", + "rcp85_deltas = rcp85 - rcp85.sel(time=slice(\"1981\", \"2010\")).mean(dim=\"time\")\n", + "rcp85_deltas.tx_mean_annual.attrs[\"long_name\"] = \"Mean annual daily max temp relative to 1981-2010\"\n", + "rcp85_deltas.tx_mean_annual.attrs[\"units\"] = \"K\"\n", + "\n", + "url2 = \"https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.3/MPI-ESM-LR_rcp45_tx_mean_annual.nc\"\n", + "rcp45 = xr.open_dataset(url2, decode_timedelta=False)\n", + "rcp45 = rcp45.sel(lon=-73, lat=46, method=\"nearest\")\n", + "rcp45_deltas = rcp45 - rcp45.sel(time=slice(\"1981\", \"2010\")).mean(dim=\"time\")\n", + "rcp45_deltas.tx_mean_annual.attrs[\"long_name\"] = \"Annual mean of daily max temp relative to 1981-2010\"\n", + "rcp45_deltas.tx_mean_annual.attrs[\"units\"] = \"K\"\n", + "\n", + "# Plot\n", + "fg.stripes({\"rcp45\": rcp45_deltas, \"rcp85\": rcp85_deltas}, divide=2006)\n", + "\n", + "# plt.savefig(\"images/stripes.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "Like most of the other functions, `stripes()` will attempt to find a colormap that is appropriate for the data variables." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a similar dataset with precipitation data\n", + "url3 = (\n", + " \"https://pavics.ouranos.ca/twitcher/ows/proxy/thredds/dodsC/birdhouse/ouranos/portraits-clim-1.3/MPI-ESM-LR_rcp85_precip_accumulation_annual.nc\"\n", + ")\n", + "prec = xr.open_dataset(url3, decode_timedelta=False)\n", + "prec = prec.sel(lon=-73, lat=46, method=\"nearest\")\n", + "prec_deltas = prec - prec.sel(time=slice(\"1981\", \"2010\")).mean(dim=\"time\")\n", + "prec_deltas.precip_accumulation_annual.attrs[\"long_name\"] = \"Total annual precipitation change relative to 1981-2010\"\n", + "prec_deltas.precip_accumulation_annual.attrs[\"units\"] = \"mm\"\n", + "\n", + "ax = fg.stripes(prec_deltas)\n", + "ax.set_title(\"Precipitation\")" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## Violin Plots\n", + "\n", + "Violin plots are a practical tool for visualizing the statistical distribution of data in an ensemble, combining a box plot with a kernel density plot. The violin function wraps Seaborn's [violinplot](https://seaborn.pydata.org/generated/seaborn.violinplot.html#seaborn.violinplot) function to directly accept xarray objects, and incorporates other figanos features. The `data` argument can be a DataArray (one \"violin\"), a Dataset (as many \"violins\" as there are variables in the Dataset), or a dictionary of either types. In the case of a dictionary, its keys will become the \"violin\" labels.\n", + "\n", + "As with other functions, when `use_attrs` is passed and `data` is a dictionary, attributes from the first dictionary entry will be put on the plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "fg.violin(ds_time, use_attrs={\"title\": \"description\"})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/violin.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "The optional `color` argument combines the Seaborn function's `color` and `palette` arguments. A single color or a list of colors can be passed. Integers can be passed instead of strings to refer to colors of the currently used stylesheet. If the list of colors is shorter than the number of variables on the plot, the colors are repeated." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "my_data = {\n", + " \"p10\": ds_time.tx_max_p10,\n", + " \"p50\": ds_time.tx_max_p50,\n", + " \"p90\": ds_time.tx_max_p90,\n", + "}\n", + "\n", + "ax = fg.violin(my_data, plot_kw={\"orient\": \"h\"}, color=[3, \"purple\", \"#78bf84\"])" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Heatmaps\n", + "\n", + "Similarly to violin plots, the heatmap function wraps Seaborn's [heatmap](https://seaborn.pydata.org/generated/seaborn.heatmap.html) function to directly accept xarray objects, and incorporates other figanos features. The `data` argument can be a DataArray, a Dataset, or a dictionary of either types and of `length=1`. There is no real benefit to using a dictionary, but it is accepted in order to be coherent with other functions in the package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a diagnostics Dataset from scratch\n", + "improvement = np.random.rand(7, 7)\n", + "diagnostics = xr.DataArray(\n", + " data=improvement,\n", + " coords=dict(\n", + " realization=[\n", + " \"model1\",\n", + " \"model2\",\n", + " \"model3\",\n", + " \"model4\",\n", + " \"model5\",\n", + " \"model6\",\n", + " \"model7\",\n", + " ],\n", + " properties=[\n", + " \"aca_pr\",\n", + " \"aca_tasmax\",\n", + " \"aca_tasmin\",\n", + " \"corr_tasmax_pr\",\n", + " \"corr_tasmax_tasmin\",\n", + " \"mean_tasmax\",\n", + " \"mean_pr\",\n", + " ],\n", + " ),\n", + ")\n", + "\n", + "diagnostics.attrs[\"long_name\"] = \"% of improved grid cells\"\n", + "\n", + "# Plot a heatmap\n", + "fg.heatmap(\n", + " diagnostics,\n", + " divergent=0.5,\n", + " plot_kw={\"vmin\": 0, \"linecolor\": \"w\", \"linewidth\": 1.5},\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "In order to produce reliable results, the xarray object passed to `heatmap()` has to have only two dimensions. Under the hood, the function converts the DataArray containing the data to a pandas DataFrame before plotting it. Using `transpose=True` swaps the `x` and `y` axes.\n", + "\n", + "The colorbar kwargs are accessible through the nesting of `cbar_kws` in `plot_kw`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "ax = fg.heatmap(\n", + " diagnostics,\n", + " transpose=True,\n", + " cmap=\"bwr_r\",\n", + " divergent=0.5,\n", + " plot_kw={\n", + " \"cbar_kws\": {\"label\": \"Proportion of cells improved\"},\n", + " \"annot\": True,\n", + " },\n", + ")\n", + "\n", + "# Remove the grid labels\n", + "ax.set_xlabel(\"\")\n", + "ax.set_ylabel(\"\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/heatmap.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "## Triangle Heatmaps\n", + "\n", + "The `triheatmap` function is based on the matplotlib function [tripcolor](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.tripcolor.html). It can create a heatmap with 2 or 4 triangles in each square of the heatmap.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a fake data\n", + "da = xr.DataArray(\n", + " data=np.random.rand(2, 3, 4),\n", + " coords=dict(\n", + " realization=[\"A\", \"B\"],\n", + " method=[\"a\", \"b\", \"c\"],\n", + " experiment=[\"ssp126\", \"ssp245\", \"ssp370\", \"ssp585\"],\n", + " ),\n", + ")\n", + "da.name = \"pr\" # to guess the cmap\n", + "# will be automatically detected for the cbar label\n", + "da.attrs[\"long_name\"] = \"precipitation\"\n", + "da.attrs[\"units\"] = \"mm\"\n", + "\n", + "# Plot a heatmap\n", + "fg.triheatmap(\n", + " da,\n", + " z=\"experiment\", # which dimension should be represented by triangles\n", + " divergent=True, # for the cmap\n", + " cbar=\"unique\", # only show one cbar\n", + " plot_kw={\n", + " \"vmin\": -1,\n", + " \"vmax\": 1,\n", + " }, # we are only showing the 1st cbar, so make sure the cbar of each triangle is the same\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/triangle1.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a fake data\n", + "da = xr.DataArray(\n", + " data=np.random.rand(4, 3, 2),\n", + " coords=dict(\n", + " realization=[\"A\", \"B\", \"C\", \"D\"],\n", + " method=[\"a\", \"b\", \"c\"],\n", + " season=[\"DJF\", \"JJA\"],\n", + " ),\n", + ")\n", + "da.attrs[\"description\"] = \"La plus belle saison de ma vie\"\n", + "\n", + "# Plot a heatmap\n", + "fg.triheatmap(\n", + " da,\n", + " z=\"season\",\n", + " cbar=\"each\", # show a cbar per triangle\n", + " use_attrs={\"title\": \"description\"},\n", + " cbar_kw=[\n", + " {\"label\": \"winter\"},\n", + " {\"label\": \"summer\"},\n", + " ], # Use a list to change the cbar associated with each triangle type (upper or lower)\n", + " plot_kw=[{\"cmap\": \"winter\"}, {\"cmap\": \"summer\"}],\n", + ") # Use a list to change each triangle type (upper or lower)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/triangle2.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## Taylor Diagrams\n", + "\n", + "Taylor diagrams are a useful way to compare simulation datasets to a reference dataset. They allow for graphical representation of the standard deviation of both the simulation and reference datasets, the correlation between both, and the root mean squared error (a function of the two previous statistical properties).\n", + "\n", + "The `taylordiagram()` function creates each point on the Taylor diagram from an object created using `xclim.sdba.measures.taylordiagram`, as illustrated below.\n", + "\n", + "### Important Notes\n", + "* The structure of the matplotlib axes being different from the other figanos functions, this function does not have an `ax` argument, and creates its own figure.\n", + "* To change the axis labels, use the `std_label` and `corr_label` arguments, rather than the `ax.set_xlabel()` method.\n", + "* Dataset with negative correlations with the reference dataset will not be plotted.\n", + "* To modify the appearance of the reference point (on the `x`-axis), use the keyword 'reference' in `plot_kw`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "da_ref = ds_time[\"tx_max_p50\"]\n", + "\n", + "# Toy data with same mean as `da_ref` & modify deviations with trigonometric functions\n", + "homogenous_ref_mean = xr.full_like(da_ref, da_ref.mean(dim=\"time\"))\n", + "simd = {}\n", + "for i, f_trig in enumerate([np.cos, lambda x: np.cos(x) ** 2, np.tan]):\n", + " da = homogenous_ref_mean + f_trig(da_ref.values)\n", + " da.attrs[\"units\"] = da_ref.attrs[\"units\"]\n", + " simd[f\"model{i}\"] = sdba.measures.taylordiagram(sim=da, ref=da_ref)\n", + "\n", + "fg.taylordiagram(\n", + " simd,\n", + " std_range=(0, 1.3),\n", + " contours=5,\n", + " contours_kw={\"colors\": \"green\"},\n", + " plot_kw={\"reference\": {\"marker\": \"*\"}},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": {}, + "outputs": [], + "source": [ + "# plt.savefig(\"images/taylor.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "### Normalized Taylor Diagram\n", + "\n", + "If we normalize the standard deviation of our measures, many Taylor diagrams with difference references can be combined in a single plot. In the following example, we have datasets with two variables (`tasmax, pr`) and three location coordinates. For each location (3) and variable (2), a `taylordiagram` measure is computed. Each set of correlation and standard deviation is then plotted." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": {}, + "outputs": [], + "source": [ + "from xclim.testing.utils import nimbus\n", + "import pooch # FIXME: remove downloader when xclim fixes the issue with nimbus\n", + "from figanos import __version__ as __figanos_version__\n", + "\n", + "downloader = pooch.HTTPDownloader(headers={\"User-Agent\": f\"figanos {__figanos_version__}\"})\n", + "n = nimbus()\n", + "\n", + "ref = n.fetch(\"sdba/ahccd_1950-2013.nc\", downloader=downloader)\n", + "ds_ref = xr.open_dataset(ref)\n", + "sim = n.fetch(\"sdba/nrcan_1950-2013.nc\", downloader=downloader)\n", + "ds_sim = xr.open_dataset(sim)\n", + "\n", + "for v in ds_ref.data_vars:\n", + " ds_sim[v] = xc.core.units.convert_units_to(ds_sim[v], ds_ref[v], context=\"hydro\")\n", + "\n", + "# Here, we have three locations, two variables. We stack variables to convert from\n", + "# a Dataset to a DataArray.\n", + "da_ref = sdba.stack_variables(ds_ref)\n", + "da_sim = sdba.stack_variables(ds_sim)\n", + "\n", + "# Each location/variable will have its own set of taylor parameters\n", + "out = sdba.measures.taylordiagram(ref=da_ref, sim=da_sim, dim=\"time\")\n", + "\n", + "# If we normalize the taylor diagrams, they can be compared on the same plot\n", + "out[{\"taylor_param\": [0, 1]}] = out[{\"taylor_param\": [0, 1]}] / out[{\"taylor_param\": 0}]\n", + "\n", + "# in xclim >= 0.50.0 : Normalization can be done when computing taylordiagram measure\n", + "# out = sdba.measures.taylordiagram(ref=da_ref, sim=da_sim, dim=\"time\", normalize=True)\n", + "\n", + "# The `markers_key` and `colors_key` are used to separate between two different features.\n", + "# Here, the type of marker is used to distinguish between locations, and the color\n", + "# distinguishes between variables. If those parameters are not specified, then each\n", + "# pair (location, multivar) has simply its own color.\n", + "fg.taylordiagram(out, markers_key=\"location\", colors_key=\"multivar\", ref_std_line=True)" + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "## Partition Plots\n", + "\n", + "Partition plots show the fraction of uncertainty associated with different components.\n", + "Xclim has a few different [partition functions](https://xclim.readthedocs.io/en/stable/api.html#uncertainty-partitioning).\n", + "\n", + "This tutorial is a reproduction of [xclim's documentation](https://xclim.readthedocs.io/en/stable/notebooks/partitioning.html).\n", + "\n", + "
\n", + "\n", + "Note that you could also use the [xscen library](https://xscen.readthedocs.io/en/latest/index.html) to build and ensemble from a catalog with `xscen.ensembles.build_partition_data`.\n", + "\n", + "
" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "# Fetch data\n", + "import pandas as pd\n", + "\n", + "import xclim.ensembles\n", + "\n", + "# The directory in the Atlas repo where the data is stored\n", + "# host = \"https://github.com/IPCC-WG1/Atlas/raw/main/datasets-aggregated-regionally/data/CMIP6/CMIP6_tas_land/\"\n", + "host = \"https://raw.githubusercontent.com/IPCC-WG1/Atlas/main/datasets-aggregated-regionally/data/CMIP6/CMIP6_tas_land/\"\n", + "\n", + "# The file pattern, e.g. CMIP6_ACCESS-CM2_ssp245_r1i1p1f1.csv\n", + "pat = \"CMIP6_{model}_{scenario}_{member}.csv\"\n", + "\n", + "# Here we'll download data only for a very small demo sample of models and scenarios.\n", + "\n", + "# Download data for a few models and scenarios.\n", + "models = [\"ACCESS-CM2\", \"CMCC-CM2-SR5\", \"CanESM5\"]\n", + "members = [\"r1i1p1f1\", \"r1i1p1f1\", \"r1i1p1f1\"]\n", + "scenarios = [\"ssp245\", \"ssp370\", \"ssp585\"]\n", + "\n", + "# Create the input ensemble.\n", + "data = []\n", + "for model, member in zip(models, members):\n", + " for scenario in scenarios:\n", + " url = host + pat.format(model=model, scenario=scenario, member=member)\n", + "\n", + " # Fetch data using pandas\n", + " df = pd.read_csv(url, index_col=0, comment=\"#\", parse_dates=True)[\"world\"]\n", + " # Convert to a DataArray, complete with coordinates.\n", + " da = xr.DataArray(df).expand_dims(model=[model], scenario=[scenario]).rename(date=\"time\")\n", + " data.append(da)\n", + "\n", + "# Combine DataArrays from the different models and scenarios into one.\n", + "ens_mon = xr.combine_by_coords(data)[\"world\"]\n", + "\n", + "# Then resample the monthly time series at the annual frequency\n", + "ens = ens_mon.resample(time=\"Y\").mean()" + ] + }, + { + "cell_type": "markdown", + "id": "29", + "metadata": {}, + "source": [ + "Compute uncertainties with xclim and use `fractional_uncertainty` to have the right format to plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "30", + "metadata": {}, + "outputs": [], + "source": [ + "# compute uncertainty\n", + "mean, uncertainties = xclim.ensembles.hawkins_sutton(ens, baseline=(\"2016\", \"2030\"))\n", + "\n", + "\n", + "# frac= xc.ensembles.fractional_uncertainty(uncertainties)\n", + "\n", + "\n", + "# FIXME: xc.ensembles.fractional_uncertainty has not been released yet. Until until it is released, here it is.\n", + "def fractional_uncertainty(u: xr.DataArray):\n", + " \"\"\"Return the fractional uncertainty.\n", + "\n", + " Parameters\n", + " ----------\n", + " u : xr.DataArray\n", + " Array with uncertainty components along the `uncertainty` dimension.\n", + "\n", + " Returns\n", + " -------\n", + " xr.DataArray\n", + " Fractional, or relative uncertainty with respect to the total uncertainty.\n", + " \"\"\"\n", + " uncertainty = u / u.sel(uncertainty=\"total\") * 100\n", + " uncertainty.attrs.update(u.attrs)\n", + " uncertainty.attrs[\"long_name\"] = \"Fraction of total variance\"\n", + " uncertainty.attrs[\"units\"] = \"%\"\n", + " return uncertainty\n", + "\n", + "\n", + "frac = fractional_uncertainty(uncertainties)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "# plot\n", + "fg.partition(\n", + " frac,\n", + " start_year=\"2016\", # change the x-axis\n", + " show_num=True, # put the number of element of each uncertainty source in the legend FIXME: will only appear after xclim releases 0.48\n", + " fill_kw={\n", + " \"variability\": {\"color\": \"#DC551A\"},\n", + " \"model\": {\"color\": \"#2B2B8B\"},\n", + " \"scenario\": {\"color\": \"#275620\"},\n", + " },\n", + " line_kw={\"lw\": 2},\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/partition.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "33", + "metadata": {}, + "source": [ + "## Logos\n", + "\n", + "Logos can also be added to plots if desired using the `figanos.utils.plot_logo()` function. This function requires that logos are passed as `pathlib.Path()` objects or installed and called by their name (as `str`).\n", + "\n", + "Figanos offers the `Logos()` convenience class for setup and management of logos so that they can be reused as needed. Logos can be used to set default logos as well as install custom logos, if desired. Logo files are saved to the user's config folder so that they can be reused.\n", + "\n", + "By default, the `figanos_logo.png` is installed on initialization, while the Ouranos set of logos can be installed if desired.\n", + "\n", + "For more information on logos, see the Logos documentation.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34", + "metadata": {}, + "outputs": [], + "source": [ + "from figanos import Logos\n", + "\n", + "# Installing the default logos\n", + "logos = Logos()\n", + "print(f\"Default logo is found at: {logos.default}.\")\n", + "\n", + "# Installing the Ouranos logos\n", + "logos.install_ouranos_logos(permitted=True)\n", + "\n", + "# Show all installed logos\n", + "logos.installed()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "# To set a new default logo we can simply use an existing entry\n", + "logos.set_logo(logos.logo_ouranos_horizontal_couleur, \"default\")\n", + "print(f\"Default logo is found at: {logos.default}\")\n", + "logos.set_logo(logos.logo_ouranos_vertical_couleur, \"my_custom_logo\")\n", + "print(f\"my_custom_logo installed at: {logos.my_custom_logo}.\")\n", + "\n", + "# Show all installed logos\n", + "logos.installed()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": {}, + "outputs": [], + "source": [ + "ax = fg.timeseries(my_data, show_lat_lon=\"upper left\", legend=\"edge\")\n", + "\n", + "# Plotting with the default logo\n", + "# fg.utils.plot_logo(ax, loc='lower right', alpha=0.8, width=120)\n", + "\n", + "# Plotting with a custom logo, resized with pixels\n", + "fg.utils.plot_logo(\n", + " ax,\n", + " logo=\"my_custom_logo\",\n", + " loc=\"lower right\",\n", + " width=100,\n", + " alpha=0.8,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/logo.png\", bbox_inches='tight')" + ] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/notebooks/figanos_multiplots.ipynb b/docs/notebooks/figanos_multiplots.ipynb index ff6024e9..a5288290 100644 --- a/docs/notebooks/figanos_multiplots.ipynb +++ b/docs/notebooks/figanos_multiplots.ipynb @@ -29,9 +29,10 @@ "import cartopy.crs as ccrs\n", "import figanos.matplotlib as fg\n", "import numpy as np\n", + "from matplotlib import pyplot as plt\n", "\n", "# use ouranos style\n", - "fg.utils.set_mpl_style('ouranos')" + "fg.utils.set_mpl_style(\"ouranos\")" ] }, { @@ -48,7 +49,7 @@ "outputs": [], "source": [ "# Create a xarray object from a NetCDF\n", - "url = 'https://pavics.ouranos.ca//twitcher/ows/proxy/thredds/dodsC/birdhouse/disk2/cccs_portal/indices/Final/BCCAQv2_CMIP6/tx_max/YS/ssp585/ensemble_percentiles/tx_max_ann_BCCAQ2v2+ANUSPLIN300_historical+ssp585_1950-2100_30ymean_percentiles.nc'\n", + "url = \"https://pavics.ouranos.ca//twitcher/ows/proxy/thredds/dodsC/birdhouse/disk2/cccs_portal/indices/Final/BCCAQv2_CMIP6/tx_max/YS/ssp585/ensemble_percentiles/tx_max_ann_BCCAQ2v2+ANUSPLIN300_historical+ssp585_1950-2100_30ymean_percentiles.nc\"\n", "opened = xr.open_dataset(url, decode_timedelta=False)" ] }, @@ -59,13 +60,13 @@ "outputs": [], "source": [ "ds_time = opened.isel(lon=[500], lat=[150, 250])\n", - "im = fg.timeseries({'p50': ds_time.tx_max_p50, 'p90': ds_time.tx_max_p90},\n", - " plot_kw={'p50': {\"col\": \"lat\"}, 'p90': {\"col\": \"lat\"}},\n", - " fig_kw={'figsize':(10,4)},\n", - " legend=\"edge\",\n", - " show_lat_lon=True,\n", - " )\n", - "\n" + "im = fg.timeseries(\n", + " {\"p50\": ds_time.tx_max_p50, \"p90\": ds_time.tx_max_p90},\n", + " plot_kw={\"p50\": {\"col\": \"lat\"}, \"p90\": {\"col\": \"lat\"}},\n", + " fig_kw={\"figsize\": (10, 4)},\n", + " legend=\"edge\",\n", + " show_lat_lon=True,\n", + ")" ] }, { @@ -75,18 +76,25 @@ "outputs": [], "source": [ "# Create fake scenarios\n", - "ds_time = ds_time[['tx_max_p10', 'tx_max_p50', 'tx_max_p90']]\n", - "data = {'tasmax_ssp434': ds_time,\n", - " 'tasmax_ssp245': ds_time.copy()-10,\n", - " 'tasmax_ssp585': ds_time.copy()+10}\n", + "ds_time = ds_time[[\"tx_max_p10\", \"tx_max_p50\", \"tx_max_p90\"]]\n", + "data = {\n", + " \"tasmax_ssp434\": ds_time,\n", + " \"tasmax_ssp245\": ds_time.copy() - 10,\n", + " \"tasmax_ssp585\": ds_time.copy() + 10,\n", + "}\n", "\n", - "fg.timeseries(data=data,\n", - " legend='facetgrid',\n", - " show_lat_lon=False,\n", - " fig_kw = {'figsize':(9,4)},\n", - " plot_kw={'tasmax_ssp434': {\"col\": \"lat\"}, 'tasmax_ssp245': {\"col\": \"lat\"}, \"tasmax_ssp585\": {\"col\": \"lat\"}},\n", - " enumerate_subplots=True \n", - " )" + "fg.timeseries(\n", + " data=data,\n", + " legend=\"facetgrid\",\n", + " show_lat_lon=False,\n", + " fig_kw={\"figsize\": (9, 4)},\n", + " plot_kw={\n", + " \"tasmax_ssp434\": {\"col\": \"lat\"},\n", + " \"tasmax_ssp245\": {\"col\": \"lat\"},\n", + " \"tasmax_ssp585\": {\"col\": \"lat\"},\n", + " },\n", + " enumerate_subplots=True,\n", + ")" ] }, { @@ -104,19 +112,31 @@ "outputs": [], "source": [ "# Select a time and slicing our starting Dataset\n", - "ds_space = opened[['tx_max_p50']].isel(time=[0, 1, 2]).sel(lat=slice(40,65), lon=slice(-90,-55))\n", + "ds_space = opened[[\"tx_max_p50\"]].isel(time=[0, 1, 2]).sel(lat=slice(40, 65), lon=slice(-90, -55))\n", "\n", "# Defining a spatial projection\n", "projection = ccrs.LambertConformal()\n", "\n", - "im = fg.gridmap(ds_space,\n", - " projection = projection,\n", - " plot_kw = {\"col\": \"time\"},\n", - " features = ['coastline','ocean'],\n", - " frame = False,\n", - " use_attrs={\"suptitle\": \"description\"},\n", - " enumerate_subplots=True\n", - " )\n" + "im = fg.gridmap(\n", + " ds_space,\n", + " projection=projection,\n", + " plot_kw={\"col\": \"time\"},\n", + " features=[\"coastline\", \"ocean\"],\n", + " frame=False,\n", + " use_attrs={\"suptitle\": \"description\"},\n", + " enumerate_subplots=True,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/multiple.png\", bbox_inches='tight')" ] }, { @@ -125,48 +145,52 @@ "metadata": {}, "outputs": [], "source": [ - "names = ['station_' + str(i) for i in np.arange(5)]\n", - "lat = 45 + np.random.rand(5)*3\n", - "lon = np.linspace(-76,-70, 5)\n", + "names = [\"station_\" + str(i) for i in np.arange(5)]\n", + "lat = 45 + np.random.rand(5) * 3\n", + "lon = np.linspace(-76, -70, 5)\n", "tas = np.array([[20, 25, 30, 15, 5], [5, 0, 10, 2, 3]])\n", - "yrs = np.array([[35, 65, 45, 25, 95],\n", - " [15, 75, 10, 15, 50]])\n", + "yrs = np.array([[35, 65, 45, 25, 95], [15, 75, 10, 15, 50]])\n", "\n", - "attrs = {'units': 'degC', 'standard_name': 'air_temperature', 'long_name': 'Near-Surface Daily Maximum Air Temperature'}\n", + "attrs = {\n", + " \"units\": \"degC\",\n", + " \"standard_name\": \"air_temperature\",\n", + " \"long_name\": \"Near-Surface Daily Maximum Air Temperature\",\n", + "}\n", "\n", - "tas = xr.DataArray(data=tas,\n", - " coords={'season': ['DFJ', 'MAM'],\n", - " 'station': names,\n", - " 'lat':('station', lat),\n", - " 'lon': ('station', lon),\n", - " 'years': (('season', 'station'), yrs),\n", - " },\n", - " dims=['season', 'station'],\n", - " attrs=attrs)\n", - "obs = xr.Dataset({'tas': tas})\n", + "tas = xr.DataArray(\n", + " data=tas,\n", + " coords={\n", + " \"season\": [\"DFJ\", \"MAM\"],\n", + " \"station\": names,\n", + " \"lat\": (\"station\", lat),\n", + " \"lon\": (\"station\", lon),\n", + " \"years\": ((\"season\", \"station\"), yrs),\n", + " },\n", + " dims=[\"season\", \"station\"],\n", + " attrs=attrs,\n", + ")\n", + "obs = xr.Dataset({\"tas\": tas})\n", "\n", "# plot\n", - "fg.scattermap(obs,\n", - " transform=ccrs.PlateCarree(),\n", - " sizes='years',\n", - " size_range=(25, 100),\n", - " plot_kw={\n", - " \"xlim\": (-77,-69),\n", - " \"ylim\":(43,50),\n", - " \"col\": \"season\",\n", - " },\n", - " features={\n", - " \"land\": {\"color\": \"#f0f0f0\"},\n", - " \"rivers\": {\"edgecolor\": \"#cfd3d4\"},\n", - " \"lakes\": {\"facecolor\": \"#cfd3d4\"},\n", - " \"coastline\": {\"edgecolor\": \"black\"},\n", - " },\n", - " fig_kw={\"figsize\": (7, 4)},\n", - " legend_kw={\n", - " 'ncol':4,\n", - " 'bbox_to_anchor':(0.15, 0.05)\n", - " },\n", - " )\n" + "fg.scattermap(\n", + " obs,\n", + " transform=ccrs.PlateCarree(),\n", + " sizes=\"years\",\n", + " size_range=(25, 100),\n", + " plot_kw={\n", + " \"xlim\": (-77, -69),\n", + " \"ylim\": (43, 50),\n", + " \"col\": \"season\",\n", + " },\n", + " features={\n", + " \"land\": {\"color\": \"#f0f0f0\"},\n", + " \"rivers\": {\"edgecolor\": \"#cfd3d4\"},\n", + " \"lakes\": {\"facecolor\": \"#cfd3d4\"},\n", + " \"coastline\": {\"edgecolor\": \"black\"},\n", + " },\n", + " fig_kw={\"figsize\": (7, 4)},\n", + " legend_kw={\"ncol\": 4, \"bbox_to_anchor\": (0.15, 0.05)},\n", + ")" ] }, { @@ -175,31 +199,22 @@ "metadata": {}, "outputs": [], "source": [ - "sup_305k = ds_space.where(ds_space.tx_max_p50>305)\n", - "inf_300k = ds_space.where(ds_space.tx_max_p50<300)\n", + "sup_305k = ds_space.where(ds_space.tx_max_p50 > 305)\n", + "inf_300k = ds_space.where(ds_space.tx_max_p50 < 300)\n", "\n", - "im = fg.hatchmap({'sup_305k': sup_305k, 'inf_300k': inf_300k},\n", - " plot_kw={\n", - " 'sup_305k': {\n", - " 'hatches': '*',\n", - " 'col': 'time',\n", - " \"x\": \"lon\",\n", - " \"y\": \"lat\"\n", - " },\n", - " 'inf_300k': {\n", - " 'hatches': 'x',\n", - " 'col': 'time',\n", - " \"x\": \"lon\",\n", - " \"y\": \"lat\"\n", - " },\n", - " },\n", - " features = ['coastline','ocean'],\n", - " frame = True,\n", - " legend_kw = {'title': 'Ensemble change'},\n", - " enumerate_subplots=True, \n", - " )\n", + "im = fg.hatchmap(\n", + " {\"sup_305k\": sup_305k, \"inf_300k\": inf_300k},\n", + " plot_kw={\n", + " \"sup_305k\": {\"hatches\": \"*\", \"col\": \"time\", \"x\": \"lon\", \"y\": \"lat\"},\n", + " \"inf_300k\": {\"hatches\": \"x\", \"col\": \"time\", \"x\": \"lon\", \"y\": \"lat\"},\n", + " },\n", + " features=[\"coastline\", \"ocean\"],\n", + " frame=True,\n", + " legend_kw={\"title\": \"Ensemble change\"},\n", + " enumerate_subplots=True,\n", + ")\n", "\n", - "im.fig.suptitle(\"Multiple hatchmaps\", y=1.08)\n" + "im.fig.suptitle(\"Multiple hatchmaps\", y=1.08)" ] }, { @@ -217,14 +232,14 @@ "metadata": {}, "outputs": [], "source": [ - "ds_space = opened[['tx_max_p50']].isel(time=[0, 1, 2]).sel(lat=slice(40,65), lon=slice(-90,-55))\n", + "ds_space = opened[[\"tx_max_p50\"]].isel(time=[0, 1, 2]).sel(lat=slice(40, 65), lon=slice(-90, -55))\n", "\n", "# Select a spatial subdomain\n", - "sl = slice(100,100+5)\n", + "sl = slice(100, 100 + 5)\n", "da = ds_space.isel(lat=sl, lon=sl).drop(\"horizon\").tx_max_p50\n", - "da[\"lon\"] = np.round(da.lon,2)\n", - "da[\"lat\"] = np.round(da.lat,2)\n", - "fg.heatmap(da, plot_kw = {\"col\": \"time\"})" + "da[\"lon\"] = np.round(da.lon, 2)\n", + "da[\"lat\"] = np.round(da.lat, 2)\n", + "fg.heatmap(da, plot_kw={\"col\": \"time\"})" ] }, { @@ -238,7 +253,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "To overlay two facetgrids plots, you can create the first facetgrid with `col` or `row` and then loop through the `ax` of the first facetgrid and the `xr.object` to plot the second facetgrid." + "To overlay two `facetgrid` plots, you can create the first `facetgrid` with `col` or `row` and then loop through the `ax` of the first `facetgrid` and the `xr.object` to plot the second `facetgrid`." ] }, { @@ -247,24 +262,35 @@ "metadata": {}, "outputs": [], "source": [ - "names = ['station_' + str(i) for i in np.arange(5)]\n", - "lat = 45 + np.random.rand(5)*3\n", - "lon = np.linspace(-76,-70, 5)\n", - "tas = np.array([[290, 300, 295, 305, 301],\n", - " [275, 285, 277, 301, 345],\n", - " [302, 293, 295, 292, 280]])\n", + "names = [\"station_\" + str(i) for i in np.arange(5)]\n", + "lat = 45 + np.random.rand(5) * 3\n", + "lon = np.linspace(-76, -70, 5)\n", + "tas = np.array(\n", + " [\n", + " [290, 300, 295, 305, 301],\n", + " [275, 285, 277, 301, 345],\n", + " [302, 293, 295, 292, 280],\n", + " ]\n", + ")\n", "\n", - "attrs = {'units': 'degK', 'standard_name': 'air_temperature', 'long_name': ds_space.tx_max_p50.attrs['description']}\n", + "attrs = {\n", + " \"units\": \"degK\",\n", + " \"standard_name\": \"air_temperature\",\n", + " \"long_name\": ds_space.tx_max_p50.attrs[\"description\"],\n", + "}\n", "\n", - "tas = xr.DataArray(data=tas,\n", - " coords={'time': ds_space.time.values,\n", - " 'station': names,\n", - " 'lat':('station', lat),\n", - " 'lon': ('station', lon),\n", - " },\n", - " dims=['time', 'station'],\n", - " attrs=attrs)\n", - "obs2 = xr.Dataset({'tas': tas})" + "tas = xr.DataArray(\n", + " data=tas,\n", + " coords={\n", + " \"time\": ds_space.time.values,\n", + " \"station\": names,\n", + " \"lat\": (\"station\", lat),\n", + " \"lon\": (\"station\", lon),\n", + " },\n", + " dims=[\"time\", \"station\"],\n", + " attrs=attrs,\n", + ")\n", + "obs2 = xr.Dataset({\"tas\": tas})" ] }, { @@ -273,33 +299,39 @@ "metadata": {}, "outputs": [], "source": [ - "V_MIN=280\n", - "V_MAX=310\n", - "ds_space = opened[['tx_max_p50']].isel(time=[0, 1, 2]).sel(lat=slice(40,65), lon=slice(-90,-55))\n", + "V_MIN = 280\n", + "V_MAX = 310\n", + "ds_space = opened[[\"tx_max_p50\"]].isel(time=[0, 1, 2]).sel(lat=slice(40, 65), lon=slice(-90, -55))\n", "\n", - "im = fg.gridmap(ds_space,\n", - " projection = projection,\n", - " plot_kw = {\"col\": \"time\",\n", - " \"xlim\": (-77,-69),\n", - " \"ylim\": (43,50),\n", - " \"vmin\": V_MIN, \"vmax\": V_MAX,\n", - " },\n", - " features = ['coastline','ocean'],\n", - " frame = False,\n", - " )\n", + "im = fg.gridmap(\n", + " ds_space,\n", + " projection=projection,\n", + " plot_kw={\n", + " \"col\": \"time\",\n", + " \"xlim\": (-77, -69),\n", + " \"ylim\": (43, 50),\n", + " \"vmin\": V_MIN,\n", + " \"vmax\": V_MAX,\n", + " },\n", + " features=[\"coastline\", \"ocean\"],\n", + " frame=False,\n", + ")\n", "for i, fax in enumerate(im.axs.flat):\n", - " fg.scattermap(obs2.isel(time=i),\n", - " ax=fax,\n", - " transform=ccrs.PlateCarree(),\n", - " plot_kw={'x':'lon',\n", - " 'y':'lat',\n", - " 'vmin': V_MIN,\n", - " 'vmax': V_MAX,\n", - " 'edgecolor':'grey',\n", - " 'add_colorbar': False},\n", - " show_time=False,\n", - " )\n", - "im.fig.suptitle('Scattermaps over gridmaps', x=0.45, y=0.95)\n" + " fg.scattermap(\n", + " obs2.isel(time=i),\n", + " ax=fax,\n", + " transform=ccrs.PlateCarree(),\n", + " plot_kw={\n", + " \"x\": \"lon\",\n", + " \"y\": \"lat\",\n", + " \"vmin\": V_MIN,\n", + " \"vmax\": V_MAX,\n", + " \"edgecolor\": \"grey\",\n", + " \"add_colorbar\": False,\n", + " },\n", + " show_time=False,\n", + " )\n", + "im.fig.suptitle(\"Scattermaps over gridmaps\", x=0.45, y=0.95)" ] }, { @@ -307,7 +339,7 @@ "metadata": {}, "source": [ "### Limitations\n", - "When the argument `col_wrap` is used for a facetgrid whose number of plots is not a multiple of `col_wrap`, no plot will be shown (see [issue](https://github.com/pydata/xarray/discussions/8563)). `set_extend` needs to be passed to every axis in the facetgrid to avoid this issue.\n" + "When the argument `col_wrap` is used for a facetgrid whose number of plots is not a multiple of `col_wrap`, no plot will be shown (see [issue](https://github.com/pydata/xarray/discussions/8563)). `set_extend` needs to be passed to every axis in the `facetgrid` to avoid this issue.\n" ] }, { @@ -317,32 +349,33 @@ "outputs": [], "source": [ "# Select a time and slicing for our starting Dataset\n", - "ds_space = opened[['tx_max_p50']].isel(time=[0, 1, 2]).sel(lat=slice(40,65), lon=slice(-90,-55))\n", + "ds_space = opened[[\"tx_max_p50\"]].isel(time=[0, 1, 2]).sel(lat=slice(40, 65), lon=slice(-90, -55))\n", "\n", - "im = fg.gridmap(ds_space,\n", - " projection = ccrs.LambertConformal(),\n", - " plot_kw = {\"col\": \"time\",\n", - " \"col_wrap\": 2},\n", - " features = ['coastline','ocean'],\n", - " frame = False,\n", - " use_attrs={\"suptitle\": \"long_name\"},\n", - " fig_kw = {\"figsize\": (6, 6)}\n", - " )\n", + "im = fg.gridmap(\n", + " ds_space,\n", + " projection=ccrs.LambertConformal(),\n", + " plot_kw={\"col\": \"time\", \"col_wrap\": 2},\n", + " features=[\"coastline\", \"ocean\"],\n", + " frame=False,\n", + " use_attrs={\"suptitle\": \"long_name\"},\n", + " fig_kw={\"figsize\": (6, 6)},\n", + ")\n", "for i, fax in enumerate(im.axs.flat):\n", - " fax.set_extent([\n", - " ds_space.lon.min().item(),\n", - " ds_space.lon.max().item(),\n", - " ds_space.lat.min().item(),\n", - " ds_space.lat.max().item(),\n", + " fax.set_extent(\n", + " [\n", + " ds_space.lon.min().item(),\n", + " ds_space.lon.max().item(),\n", + " ds_space.lat.min().item(),\n", + " ds_space.lat.max().item(),\n", " ]\n", - " )\n" + " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Xarray plots by default facetgrid ylabels to the right (next to the colorbar). The example below shows how to move the xlabels to the left." + "Xarray plots by default `facetgrid` `ylabels` to the right (next to the colorbar). The example below shows how to move the `xlabels` to the left." ] }, { @@ -355,36 +388,50 @@ "\n", "op = opened.isel(time=[0, 1])\n", "data = xr.DataArray(\n", - " data=np.array([op.tx_max_p10.values, op.tx_max_p50.values, op.tx_max_p90.values]),\n", - " dims=['percentile', 'time', 'lat', 'lon'],\n", - " coords={'percentile': [10, 50, 90], 'time': op.time.values, 'lat': op.lat.values, 'lon': op.lon.values},\n", - " attrs = {'units': 'degC', 'standard_name': 'air_temperature', 'long_name': 'Near-Surface Daily Maximum Air Temperature'}\n", - " )\n", + " data=np.array([op.tx_max_p10.values, op.tx_max_p50.values, op.tx_max_p90.values]),\n", + " dims=[\"percentile\", \"time\", \"lat\", \"lon\"],\n", + " coords={\n", + " \"percentile\": [10, 50, 90],\n", + " \"time\": op.time.values,\n", + " \"lat\": op.lat.values,\n", + " \"lon\": op.lon.values,\n", + " },\n", + " attrs={\n", + " \"units\": \"degC\",\n", + " \"standard_name\": \"air_temperature\",\n", + " \"long_name\": \"Near-Surface Daily Maximum Air Temperature\",\n", + " },\n", + ")\n", "\n", - "im = fg.gridmap(data,\n", - " projection = ccrs.LambertConformal(),\n", - " plot_kw = {\"col\": \"time\",\n", - " \"row\": \"percentile\",\n", - " },\n", - " features = ['coastline','ocean'],\n", - " frame = False,\n", - " use_attrs = {\"suptitle\": \"long_name\"},\n", - " fig_kw = {\"figsize\": (8, 7)},\n", - " )\n", + "im = fg.gridmap(\n", + " data,\n", + " projection=ccrs.LambertConformal(),\n", + " plot_kw={\n", + " \"col\": \"time\",\n", + " \"row\": \"percentile\",\n", + " },\n", + " features=[\"coastline\", \"ocean\"],\n", + " frame=False,\n", + " use_attrs={\"suptitle\": \"long_name\"},\n", + " fig_kw={\"figsize\": (8, 7)},\n", + ")\n", "\n", "# Modify x-label positions (hardcoded in xarray.plot)\n", "for i, fax in enumerate(im.axs.flat):\n", " for txt in fax.texts:\n", " if len(txt.get_text()) > 0:\n", " txt.set_x(-1.2)\n", - " txt.set_text('percentile ' + txt.get_text())\n", - " txt.set_rotation('vertical')\n", - " # txt.set_visible(False)\n", - "\n" + " txt.set_text(\"percentile \" + txt.get_text())\n", + " txt.set_rotation(\"vertical\")" ] } ], "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, "language_info": { "codemirror_mode": { "name": "ipython", @@ -395,7 +442,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.5" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/docs/notebooks/figanos_timeseries.ipynb b/docs/notebooks/figanos_timeseries.ipynb new file mode 100644 index 00000000..e5a61c15 --- /dev/null +++ b/docs/notebooks/figanos_timeseries.ipynb @@ -0,0 +1,377 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Timeseries\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "# setup notebook\n", + "%config InlineBackend.print_figure_kwargs = {'bbox_inches':'tight'}\n", + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2", + "metadata": {}, + "outputs": [], + "source": [ + "import xarray as xr\n", + "import figanos.matplotlib as fg\n", + "import numpy as np\n", + "from matplotlib import pyplot as plt\n", + "import xclim as xc\n", + "\n", + "# load Ouranos style and colors\n", + "fg.utils.set_mpl_style(\"ouranos\")\n", + "from matplotlib.patches import Rectangle\n", + "\n", + "# load dataset\n", + "url = \"https://pavics.ouranos.ca//twitcher/ows/proxy/thredds/dodsC/birdhouse/disk2/cccs_portal/indices/Final/BCCAQv2_CMIP6/tx_max/YS/ssp585/ensemble_percentiles/tx_max_ann_BCCAQ2v2+ANUSPLIN300_historical+ssp585_1950-2100_30ymean_percentiles.nc\"\n", + "opened = xr.open_dataset(url, decode_timedelta=False)\n", + "ds_time = opened.isel(lon=500, lat=250)[[\"tx_max_p50\", \"tx_max_p10\", \"tx_max_p90\"]]" + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## Using the Ouranos stylesheet\n", + "\n", + "Most parameters affecting the style of plots can be set through matplotlib stylesheets. Figanos includes custom stylesheets that can be accessed through the `set_mpl_style()` function. Paths to your own stylesheets (`'.mplstyle'` extension) can also be passed to this function. To use the built-in matplotlib styles, use `mpl.style.use()`.\n", + "\n", + "The currently available stylesheets are as follows:\n", + "\n", + "* `\"ouranos\"`: General stylesheet, including default colors.\n", + "* `\"transparent\"`: Adds transparency to the styles (fully transparent figure background and 30% opacity on the axes).\n", + "\n", + "One of the features of the stylesheet is to redefine the default colors to match Ouranos palette." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": {}, + "outputs": [], + "source": [ + "# display the cycler colors\n", + "from matplotlib.patches import Rectangle\n", + "import matplotlib\n", + "\n", + "style_colors = matplotlib.rcParams[\"axes.prop_cycle\"].by_key()[\"color\"]\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 3))\n", + "for color, x in zip(style_colors, np.arange(0, len(style_colors) * 2, 2)):\n", + " ax.add_patch(Rectangle(xy=(x, 1), width=0.8, height=0.5, facecolor=color))\n", + " ax.text(x, 0.5, str(color), color=color)\n", + "\n", + "ax.set_ylim(0, 2)\n", + "ax.set_xlim(0, 14)\n", + "ax.set_aspect(\"equal\")\n", + "ax.set_axis_off()" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "## Basic timeseries\n", + "The [**timeseries()**](#timeseries) function accepts DataArrays or Datasets." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "fg.timeseries(ds_time.tx_max_p50);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/basic_timeseries.png\", bbox_inches='tight')" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Using the dictionary interface\n", + "\n", + "To plot many lines, users need to input dictionaries instead of a simple dataset.\n", + "\n", + "The main elements of a plot are dependent on four arguments, each accepting dictionaries:\n", + "\n", + "1. `data` : a dictionary containing the Xarray objects and their respective keys, used as labels on the plot.\n", + "2. `use_attrs`: a dictionary linking attributes from the Xarray object to plot text elements.\n", + "3. `fig_kw`: a dictionary to pass arguments to the `plt.figure()` instance.\n", + "4. `plot_kw` : a dictionary using the same keys as `data` to pass arguments to the underlying plotting function, in this case [matplotlib.axes.Axes.plot](https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.plot.html).\n", + "\n", + "When labels are passed in `data`, any 'label' argument passed in `plot_kw` will be ignored." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "my_data = {\n", + " \"50th percentile\": ds_time.tx_max_p50,\n", + " \"90th percentile\": ds_time.tx_max_p90,\n", + "}\n", + "plot_kws = {\"90th percentile\": {\"linestyle\": \"--\"}}\n", + "\n", + "fg.timeseries(my_data, plot_kw=plot_kws, show_lat_lon=False)" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Customizing plots\n", + "\n", + "Plots created with Figanos can be customized in two different ways:\n", + "\n", + "1. By using the built-in options through arguments (e.g. changing the type of the legend with the `legend` argument).\n", + "2. By creating a Matplotlib `Axes` class instance and using its methods (e.g. setting a new title with `ax.set_title()`).\n", + "\n", + "Both of these types of customization are demonstrated below. In some cases, both methods can achieve the same result." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "ax = fg.timeseries(\n", + " my_data,\n", + " show_lat_lon=\"upper left\", # fun legend option, moved latitude and longitude tag\n", + " legend=\"edge\",\n", + " use_attrs={\"ylabel\": \"standard_name\"}, # will look for an attribute 'standard name' in the first entry of my_data\n", + ")\n", + "ax.set_title(\"Custom Title\", loc=\"left\") # when the title is left aligned, the \"loc=left\" argument must be used.\n", + "# to remove a title, use ax.set_title('', loc='left')\n", + "ax.set_xlabel(\"Custom xlabel\")\n", + "ax.set_ylabel(\"Custom ylabel\")\n", + "ax.grid(False) # removing the gridlines\n", + "ax.set_yticks([300, 310]) # Custom yticks" + ] + }, + { + "cell_type": "markdown", + "id": "12", + "metadata": {}, + "source": [ + "## Ensembles\n", + "\n", + "When Datasets are passed to the timeseries function, certain names and data configurations will be recognized and will result in certain kinds of plots.\n", + "\n", + "| Dataset configuration | Resulting plot | Notes |\n", + "|:----------:|:--------------:|:----------------:|\n", + "|Variables contain a substring of the format \"\\_pNN\", where N are numbers|Shaded line graph with the central line being the middle percentile|\n", + "|Contains a dimension named \"percentiles\"|Shaded line graph with the central line being the middle percentile| Behaviour is shared with DataArrays containing the same dimension.|\n", + "|Variables contain \"min\" and \"max\" and \"mean\" (can be capitalized) |Shaded line graph with the central line being the mean|\n", + "|Contains a dimension named \"realization\"|Line graph with one line per realization | When plot_kw is specified, all realizations within the Dataset will share one style. Behaviour is shared with DataArrays containing the same dimension.|\n", + "|Any other Dataset| Line graph with one line per variable||\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": {}, + "outputs": [], + "source": [ + "# Use 'median' as a key to make it the line label in the legend.\n", + "# legend='full' will create a legend entry for the shaded area\n", + "fg.plot.timeseries({\"median\": ds_time}, legend=\"full\", show_lat_lon=False)" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Whenever multiple lines are plotted from a single Dataset, their legend label will be the concatenation of the Dataset name (its key in the `data` argument) and the name of the variables or coordinates from which the data is taken, unless the Dataset is passed to the function without a dictionary. When all lines from a Dataset have the same appearance, only the Dataset label will be shown." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a Dataset with different names as to not trigger the shaded line plot\n", + "ds_mod = ds_time.copy()\n", + "ds_mod = ds_mod.rename({\"tx_max_p50\": \"var1\", \"tx_max_p10\": \"var2\", \"tx_max_p90\": \"var3\"})\n", + "\n", + "fg.timeseries({\"ds\": ds_mod}, show_lat_lon=True)" + ] + }, + { + "cell_type": "markdown", + "id": "16", + "metadata": {}, + "source": [ + "## Translation\n", + "Figanos can automatically use translated version of the attributes to populate the plot. It also knows a few translations of usual terms, for the moment only in French." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "# Populate the example data with french attributes\n", + "ds_time.tx_max_p50.attrs.update(\n", + " description_fr=\"Moyenne 30 ans du Maximum annuel de la température maximale quotidienne, 50e centile de l'ensemble.\",\n", + " long_name_fr=\"Moyenne 30 ans du Maximum de la température maximale quotidienne\",\n", + ")\n", + "with xc.set_options(metadata_locales=[\"fr\"]):\n", + " fg.timeseries(ds_time.tx_max_p50)" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Keyword - colour association\n", + "\n", + "Following the IPCC visual style guidelines and the practices of many other climate organizations, some scenarios (RCPs, SSPs), models and projects (CMIPs) are associated with specific colors. These colours can be implemented in timeseries() through the keys of the `data` argument. If a formulation of such scenarios or model names is found in a key, the corresponding line will be given the appropriate colour. For scenarios, alternative formats such as _ssp585_ or _rcp45_ are also accepted instead of the more formal _SSP5-8.5_ on _RCP4.5_. Model names do not currently have this flexibility. If multiple matching substrings exist, the following order of priority will dictate which colour is used:\n", + "\n", + "1. SSP scenarios\n", + "2. RCP scenarios\n", + "3. Model names\n", + "4. CMIP5 or CMIP6\n", + "\n", + "Here is the list of the accepted substrings and colors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "color_dict = fg.utils.categorical_colors()\n", + "\n", + "keys = np.array(list(color_dict.keys()))\n", + "keys = np.array_split(keys, 3)\n", + "\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 10))\n", + "ax.set_ylim(-25, 3)\n", + "ax.set_xlim(0, 12)\n", + "ax.set_axis_off()\n", + "for colorlist, x in zip(keys, [1, 5.5, 10]):\n", + " for y in np.arange(len(colorlist)):\n", + " ax.text(\n", + " x,\n", + " -y,\n", + " colorlist[y],\n", + " va=\"bottom\",\n", + " ha=\"left\",\n", + " backgroundcolor=\"white\",\n", + " weight=\"normal\",\n", + " color=\"k\",\n", + " )\n", + " ax.add_patch(\n", + " Rectangle(\n", + " xy=(x - 1, -y),\n", + " width=0.5,\n", + " height=0.5,\n", + " facecolor=color_dict[colorlist[y]],\n", + " edgecolor=\"0.8\",\n", + " )\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "data = {\n", + " \"tasmax_ssp434\": ds_time,\n", + " \"tasmax_ssp245\": ds_time.copy() - 10,\n", + " \"tasmax_ssp585\": ds_time.copy() + 10,\n", + "}\n", + "\n", + "fg.timeseries(data=data, legend=\"edge\", show_lat_lon=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "nbsphinx": "hidden" + }, + "outputs": [], + "source": [ + "# plt.savefig(\"images/ensemble_timeseries.png\", bbox_inches='tight')" + ] + } + ], + "metadata": { + "celltoolbar": "Edit Metadata", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.8" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/notebooks/index.rst b/docs/notebooks/index.rst new file mode 100644 index 00000000..b03ca915 --- /dev/null +++ b/docs/notebooks/index.rst @@ -0,0 +1,11 @@ +======== +Examples +======== + +.. toctree:: + :maxdepth: 1 + + figanos_timeseries + figanos_maps + figanos_misc + figanos_multiplots diff --git a/pyproject.toml b/pyproject.toml index d6f2ea77..8cad5ddf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,7 +80,7 @@ docs = [ "h5py", "holoviews", "netcdf4", - "pooch >=1.8.0", # for xclim-testdata + "pooch >=1.8.0", # for local and xclim-testdata "zarr >=2.13.0", # Documentation "ipykernel", @@ -198,6 +198,7 @@ include = [ "README.rst", "docs/*.rst", "docs/Makefile", + "docs/_static/_gallery/*.png", "docs/_static/_images/*.gif", "docs/_static/_images/*.jpg", "docs/_static/_images/*.png", @@ -223,7 +224,8 @@ exclude = [ "docs/_*", "docs/apidoc/modules.rst", "docs/apidoc/figanos*.rst", - "docs/locales" + "docs/locales", + "src/figanos/data/test_data/*.nc" ] [tool.isort] @@ -312,7 +314,7 @@ exclude = [ ".eggs", ".git", "build", - "docs" + "docs/conf.py" ] [tool.ruff.format] diff --git a/src/figanos/__init__.py b/src/figanos/__init__.py index 611f88b3..e92355a1 100644 --- a/src/figanos/__init__.py +++ b/src/figanos/__init__.py @@ -25,3 +25,4 @@ from . import matplotlib from ._data import data from ._logo import Logos +from ._testing import pitou diff --git a/src/figanos/_testing.py b/src/figanos/_testing.py new file mode 100644 index 00000000..210e2a09 --- /dev/null +++ b/src/figanos/_testing.py @@ -0,0 +1,55 @@ +from functools import wraps +from typing import IO, Callable, Optional, Union + +__all__ = ["pitou"] + + +def pitou(): + """Return a Pooch instance for the figanos testing data.""" + from figanos import __version__ as __figanos_version__ + + try: + import pooch + except ImportError: + raise ImportError( + "The 'pooch' package is required to fetch the figanos testing data. " + "You can install it using 'pip install pooch' or 'pip install \"figanos[docs]\"'." + ) + + _pitou = pooch.Pooch( + path=pooch.os_cache("figanos"), + # base_url="https://raw.githubusercontent.com/Ouranosinc/figanos/main/src/figanos/data/test_data/", + base_url="https://raw.githubusercontent.com/Ouranosinc/figanos/split-doc/src/figanos/data/test_data/", + registry={ + "hatchmap-ens_stats.nc": "fc52d0551747fa0a7153f1ecfebf3e697993590c6c7c4c6a6f9f32700df9d32d", + "hatchmap-inf_5.nc": "8f22522dc153d8d347bdf97bf85e49d08a5ecbc61c64372e713a0d25638e48ac", + "hatchmap-sup_8.nc": "a409ebbd6ce3c0ca319f676cc21677ba500983b80ff65a64ef3d467008db824a", + }, + ) + + # Add a custom fetch method to the Pooch instance + # Needed to address: https://github.com/readthedocs/readthedocs.org/issues/11763 + _pitou.fetch_diversion = _pitou.fetch + + # Overload the fetch method to add user-agent headers + @wraps(_pitou.fetch_diversion) + def _fetch(*args: str, **kwargs: Union[bool, Callable]) -> str: + def _downloader( + url: str, + output_file: Union[str, IO], + poocher: pooch.Pooch, + check_only: Optional[bool] = False, + ) -> None: + """Download the file from the URL and save it to the save_path.""" + headers = {"User-Agent": f"figanos ({__figanos_version__})"} + downloader = pooch.HTTPDownloader(headers=headers) + return downloader(url, output_file, poocher, check_only=check_only) + + # default to our http/s downloader with user-agent headers + kwargs.setdefault("downloader", _downloader) + return _pitou.fetch_diversion(*args, **kwargs) + + # Replace the fetch method with the custom fetch method + _pitou.fetch = _fetch + + return _pitou diff --git a/src/figanos/data/test_data/hatchmap-ens_stats.nc b/src/figanos/data/test_data/hatchmap-ens_stats.nc new file mode 100644 index 00000000..0549077c Binary files /dev/null and b/src/figanos/data/test_data/hatchmap-ens_stats.nc differ diff --git a/src/figanos/data/test_data/hatchmap-inf_5.nc b/src/figanos/data/test_data/hatchmap-inf_5.nc new file mode 100644 index 00000000..586be6e4 Binary files /dev/null and b/src/figanos/data/test_data/hatchmap-inf_5.nc differ diff --git a/src/figanos/data/test_data/hatchmap-sup_8.nc b/src/figanos/data/test_data/hatchmap-sup_8.nc new file mode 100644 index 00000000..5c6e37b8 Binary files /dev/null and b/src/figanos/data/test_data/hatchmap-sup_8.nc differ diff --git a/src/figanos/matplotlib/plot.py b/src/figanos/matplotlib/plot.py index 01e9592e..43aedb4d 100644 --- a/src/figanos/matplotlib/plot.py +++ b/src/figanos/matplotlib/plot.py @@ -480,7 +480,6 @@ def timeseries( return ax else: - if legend is not None: if not im.axs[-1, -1].get_legend_handles_labels()[ 0 @@ -534,7 +533,7 @@ def gridmap( use_attrs: dict[str, Any] | None = None, fig_kw: dict[str, Any] | None = None, plot_kw: dict[str, Any] | None = None, - projection: ccrs.Projection = ccrs.PlateCarree(), + projection: ccrs.Projection = ccrs.LambertConformal(), transform: ccrs.Projection | None = None, features: list[str] | dict[str, dict[str, Any]] | None = None, geometries_kw: dict[str, Any] | None = None, @@ -787,11 +786,19 @@ def gridmap( if show_time: if isinstance(show_time, bool): plot_coords( - ax, plot_data, param="time", loc="lower right", backgroundalpha=1 + ax, + plot_data, + param="time", + loc="lower right", + backgroundalpha=1, ) elif isinstance(show_time, (str, tuple, int)): plot_coords( - ax, plot_data, param="time", loc=show_time, backgroundalpha=1 + ax, + plot_data, + param="time", + loc=show_time, + backgroundalpha=1, ) # when im is an ax, it has a colorbar attribute. If it is a facetgrid, it has a cbar attribute. @@ -826,17 +833,24 @@ def gridmap( if show_time: if isinstance(show_time, bool): plot_coords( - None, plot_data, param="time", loc="lower right", backgroundalpha=1 + None, + plot_data, + param="time", + loc="lower right", + backgroundalpha=1, ) elif isinstance(show_time, (str, tuple, int)): plot_coords( - None, plot_data, param="time", loc=show_time, backgroundalpha=1 + None, + plot_data, + param="time", + loc=show_time, + backgroundalpha=1, ) use_attrs.setdefault("suptitle", "long_name") im = set_plot_attrs(use_attrs, data, facetgrid=im) if enumerate_subplots and isinstance(im, xr.plot.facetgrid.FacetGrid): - print("here") for idx, ax in enumerate(im.axs.flat): ax.set_title(f"{string.ascii_lowercase[idx]}) {ax.get_title()}") @@ -849,7 +863,7 @@ def gdfmap( ax: cartopy.mpl.geoaxes.GeoAxes | cartopy.mpl.geoaxes.GeoAxesSubplot | None = None, fig_kw: dict[str, Any] | None = None, plot_kw: dict[str, Any] | None = None, - projection: ccrs.Projection = ccrs.PlateCarree(), + projection: ccrs.Projection = ccrs.LambertConformal(), features: list[str] | dict[str, dict[str, Any]] | None = None, cmap: str | matplotlib.colors.Colormap | None = None, levels: int | list[int | float] | None = None, @@ -945,7 +959,11 @@ def gdfmap( if (levels is not None) or (divergent is not False): norm = custom_cmap_norm( - cmap, plot_kw["vmin"], plot_kw["vmax"], levels=levels, divergent=divergent + cmap, + plot_kw["vmin"], + plot_kw["vmax"], + levels=levels, + divergent=divergent, ) plot_kw.setdefault("norm", norm) @@ -1258,7 +1276,8 @@ def stripes( elif cmap is None: cdata = Path(__file__).parents[1] / "data/ipcc_colors/variable_groups.json" cmap = create_cmap( - get_var_group(path_to_json=cdata, da=list(data.values())[0]), divergent=True + get_var_group(path_to_json=cdata, da=list(data.values())[0]), + divergent=True, ) # create cmap norm @@ -1470,7 +1489,10 @@ def draw_heatmap(*args, **kwargs): ) ax = sns.heatmap(d, **kwargs) ax.set_xticklabels( - ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor" + ax.get_xticklabels(), + rotation=45, + ha="right", + rotation_mode="anchor", ) ax.tick_params(axis="both", direction="out") set_plot_attrs( @@ -1506,7 +1528,12 @@ def draw_heatmap(*args, **kwargs): g = sns.FacetGrid(df, **plot_kw_fg) cax = g.fig.add_axes([0.95, 0.05, 0.02, 0.9]) g.map_dataframe( - draw_heatmap, *heatmap_dims, da_name, **plot_kw_hm, cbar=True, cbar_ax=cax + draw_heatmap, + *heatmap_dims, + da_name, + **plot_kw_hm, + cbar=True, + cbar_ax=cax, ) g.fig.subplots_adjust(right=0.9) if "figsize" in fig_kw.keys(): @@ -1520,8 +1547,8 @@ def scattermap( use_attrs: dict[str, Any] | None = None, fig_kw: dict[str, Any] | None = None, plot_kw: dict[str, Any] | None = None, - projection: ccrs.Projection = ccrs.PlateCarree(), - transform: ccrs.Projection | None = ccrs.PlateCarree(), + projection: ccrs.Projection = ccrs.LambertConformal(), + transform: ccrs.Projection | None = None, features: list[str] | dict[str, dict[str, Any]] | None = None, geometries_kw: dict[str, Any] | None = None, sizes: str | bool | None = None, @@ -1656,11 +1683,13 @@ def scattermap( # setup transform if transform is None: - if "lat" in data.dims and "lon" in data.dims: - transform = ccrs.PlateCarree() - elif "rlat" in data.dims and "rlon" in data.dims: + if "rlat" in data.dims and "rlon" in data.dims: if hasattr(data, "rotated_pole"): transform = get_rotpole(data) + elif ( + "lat" in data.coords and "lon" in data.coords + ): # need to work with station dims + transform = ccrs.PlateCarree() # setup fig, ax if ax is None and ("row" not in plot_kw.keys() and "col" not in plot_kw.keys()): @@ -1829,11 +1858,19 @@ def scattermap( if show_time: if isinstance(show_time, bool): plot_coords( - ax, plot_data, param="time", loc="lower right", backgroundalpha=1 + ax, + plot_data, + param="time", + loc="lower right", + backgroundalpha=1, ) elif isinstance(show_time, (str, tuple, int)): plot_coords( - ax, plot_data, param="time", loc=show_time, backgroundalpha=1 + ax, + plot_data, + param="time", + loc=show_time, + backgroundalpha=1, ) if (frame is False) and (im.colorbar is not None): @@ -1862,11 +1899,19 @@ def scattermap( if show_time: if isinstance(show_time, bool): plot_coords( - None, plot_data, param="time", loc="lower right", backgroundalpha=1 + None, + plot_data, + param="time", + loc="lower right", + backgroundalpha=1, ) elif isinstance(show_time, (str, tuple, int)): plot_coords( - None, plot_data, param="time", loc=show_time, backgroundalpha=1 + None, + plot_data, + param="time", + loc=show_time, + backgroundalpha=1, ) # size legend @@ -2277,7 +2322,7 @@ def hatchmap( use_attrs: dict[str, Any] | None = None, fig_kw: dict[str, Any] | None = None, plot_kw: dict[str, Any] | None = None, - projection: ccrs.Projection = ccrs.PlateCarree(), + projection: ccrs.Projection = ccrs.LambertConformal(), transform: ccrs.Projection | None = None, features: list[str] | dict[str, dict[str, Any]] | None = None, geometries_kw: dict[str, Any] | None = None, @@ -2594,11 +2639,19 @@ def hatchmap( if show_time: if isinstance(show_time, bool): plot_coords( - ax, plot_data, param="time", loc="lower right", backgroundalpha=1 + ax, + plot_data, + param="time", + loc="lower right", + backgroundalpha=1, ) elif isinstance(show_time, (str, tuple, int)): plot_coords( - ax, plot_data, param="time", loc=show_time, backgroundalpha=1 + ax, + plot_data, + param="time", + loc=show_time, + backgroundalpha=1, ) # when im is an ax, it has a colorbar attribute. If it is a facetgrid, it has a cbar attribute. @@ -2622,7 +2675,11 @@ def hatchmap( if show_time: if show_time is True: plot_coords( - None, dattrs, param="time", loc="lower right", backgroundalpha=1 + None, + dattrs, + param="time", + loc="lower right", + backgroundalpha=1, ) elif isinstance(show_time, (str, tuple, int)): plot_coords( @@ -2744,12 +2801,20 @@ def partition( num = len(data.attrs.get(u, [])) # compatible with pre PR PR #1529 label = f"{u} ({num})" if show_num and num else u ax.fill_between( - time, past_y, present_y, label=label, **fill_kw.get(u, fk_direct) + time, + past_y, + present_y, + label=label, + **fill_kw.get(u, fk_direct), ) black_lines.append(present_y) past_y = present_y ax.fill_between( - time, past_y, 100, label="variability", **fill_kw.get("variability", fk_direct) + time, + past_y, + 100, + label="variability", + **fill_kw.get("variability", fk_direct), ) # Draw black lines @@ -2894,7 +2959,6 @@ def triheatmap( # plot if len(d) == 2: - x = np.arange(m + 1) y = np.arange(n + 1) xss, ys = np.meshgrid(x, y) @@ -2905,7 +2969,11 @@ def triheatmap( for i in range(m) ] triangles2 = [ - (i + 1 + j * (m + 1), i + 1 + (j + 1) * (m + 1), i + (j + 1) * (m + 1)) + ( + i + 1 + j * (m + 1), + i + 1 + (j + 1) * (m + 1), + i + (j + 1) * (m + 1), + ) for j in range(n) for i in range(m) ] @@ -2922,7 +2990,6 @@ def triheatmap( ax.set_yticks(np.array(range(n)) + 0.5, labels=labels_y, rotation=90) elif len(d) == 4: - xv, yv = np.meshgrid( np.arange(-0.5, m), np.arange(-0.5, n) ) # vertices of the little squares @@ -2944,7 +3011,11 @@ def triheatmap( for i in range(m) ] triangles_s = [ - (i + 1 + (j + 1) * (m + 1), i + (j + 1) * (m + 1), cstart + i + j * m) + ( + i + 1 + (j + 1) * (m + 1), + i + (j + 1) * (m + 1), + cstart + i + j * m, + ) for j in range(n) for i in range(m) ] @@ -2955,7 +3026,12 @@ def triheatmap( ] triangul = [ Triangulation(x, y, triangles) - for triangles in [triangles_n, triangles_e, triangles_s, triangles_w] + for triangles in [ + triangles_n, + triangles_e, + triangles_s, + triangles_w, + ] ] imgs = [ diff --git a/src/figanos/matplotlib/utils.py b/src/figanos/matplotlib/utils.py index de7c8eab..f6f43a65 100644 --- a/src/figanos/matplotlib/utils.py +++ b/src/figanos/matplotlib/utils.py @@ -86,7 +86,7 @@ def empty_dict(param) -> dict: def check_timeindex( - xr_objs: xr.DataArray | xr.Dataset | dict[str, Any] + xr_objs: xr.DataArray | xr.Dataset | dict[str, Any], ) -> xr.DataArray | xr.Dataset | dict[str, Any]: """Check if the time index of Xarray objects in a dict is CFtime and convert to pd.DatetimeIndex if True. @@ -104,7 +104,9 @@ def check_timeindex( for name, obj in xr_objs.items(): if "time" in obj.dims: if isinstance(obj.get_index("time"), xr.CFTimeIndex): - conv_obj = obj.convert_calendar("standard", use_cftime=None) + conv_obj = obj.convert_calendar( + "standard", use_cftime=None, align_on="year" + ) xr_objs[name] = conv_obj warnings.warn( "CFTimeIndex converted to pandas DatetimeIndex with a 'standard' calendar." @@ -113,7 +115,9 @@ def check_timeindex( else: if "time" in xr_objs.dims: if isinstance(xr_objs.get_index("time"), xr.CFTimeIndex): - conv_obj = xr_objs.convert_calendar("standard", use_cftime=None) + conv_obj = xr_objs.convert_calendar( + "standard", use_cftime=None, align_on="year" + ) xr_objs = conv_obj warnings.warn( "CFTimeIndex converted to pandas DatetimeIndex with a 'standard' calendar." @@ -766,7 +770,12 @@ def split_legend( if in_plot is True: ax.text( - last_x + label_bump, last_y, label, ha="left", va="center", color=color + last_x + label_bump, + last_y, + label, + ha="left", + va="center", + color=color, ) else: trans = mpl.transforms.blended_transform_factory(ax.transAxes, ax.transData) @@ -784,7 +793,10 @@ def split_legend( def fill_between_label( - sorted_lines: dict[str, Any], name: str, array_categ: dict[str, Any], legend: str + sorted_lines: dict[str, Any], + name: str, + array_categ: dict[str, Any], + legend: str, ) -> str: """Create a label for the shading around a line in line plots. @@ -806,7 +818,11 @@ def fill_between_label( """ if legend != "full": label = None - elif array_categ[name] in ["ENS_PCT_VAR_DS", "ENS_PCT_DIM_DS", "ENS_PCT_DIM_DA"]: + elif array_categ[name] in [ + "ENS_PCT_VAR_DS", + "ENS_PCT_DIM_DS", + "ENS_PCT_DIM_DA", + ]: label = get_localized_term("{}th-{}th percentiles").format( get_suffix(sorted_lines["lower"]), get_suffix(sorted_lines["upper"]) ) @@ -1142,7 +1158,8 @@ def add_cartopy_features( else: scale = features[feat].pop("scale") ax.add_feature( - getattr(cfeature, feat.upper()).with_scale(scale), **features[feat] + getattr(cfeature, feat.upper()).with_scale(scale), + **features[feat], ) features[feat]["scale"] = scale # put back return ax diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index f160dd5a..00000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Unit test package for figanos.""" diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..8f5dc7f5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +"""Configurations for unit tests."""