Skip to content

Commit

Permalink
Merge pull request #136 from Ouranosinc/facetgrids
Browse files Browse the repository at this point in the history
Facetgrid (multiplots)
  • Loading branch information
sarahclaude authored Feb 15, 2024
2 parents e852d2f + 6bd521d commit 3084e8e
Show file tree
Hide file tree
Showing 6 changed files with 1,317 additions and 402 deletions.
6 changes: 4 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ New features and enhancements
* Logo plotting now supports both PNG and SVG file types (via `cairosvg`). (:pull:`119`).
* Use small geojson in the notebook. (:pull:`124`).
* Add the Colours of Figanos page (:issue:`126`, :pull:`127`).
* `figanos` now adheres to PEPs 517/518/621 using the `flit` backend for building and packaging. (:pull:`135`).
* New function ``fg.matplotlib.partition``. (:pull:`134`).
* Figanos now adheres to PEPs 517/518/621 using the `flit` backend for building and packaging. (:pull:`135`).
* New function ``fg.partition`` (:pull:`134`).
* Add wrapper around ``xarray.plot.facetgrid`` for map functions (``fg.gridmap``, ``fg.scattermap``, ``fg.hatchmap``). (:issue:`51`, :pull:`136`).
* `figanos` now uses `Semantic Versioning v2.0 <https://semver.org/spec/v2.0.0.html>`_. (:pull:`143`).
* Add wrapper around ``xarray.plot.facetgrid`` for multiple functions (``fg.gridmap``, ``fg.scattermap``, ``fg.hatchmap``, ``fg.timeseries``). (:issue:`51`, :pull:`136`).

Bug fixes
^^^^^^^^^
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Need help?
usage
notebooks/figanos_docs
notebooks/figanos_colours
notebooks/figanos_multiplots
api
contributing
authors
Expand Down
97 changes: 67 additions & 30 deletions docs/notebooks/figanos_docs.ipynb

Large diffs are not rendered by default.

375 changes: 375 additions & 0 deletions docs/notebooks/figanos_multiplots.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,375 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Multiple plots"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Figanos also creates [xr.plot.facetgrid.FacetGrid](https://docs.xarray.dev/en/latest/generated/xarray.plot.FacetGrid.html) due to being wrapped around xarray plotting functions. This allows for multiple plots to be created at once. The following example shows how to create multiple timeseries and maps plots."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"pycharm": {
"is_executing": true
}
},
"outputs": [],
"source": [
"# import necessary libraries\n",
"import xarray as xr\n",
"import cartopy.crs as ccrs\n",
"import figanos.matplotlib as fg\n",
"import numpy as np\n",
"\n",
"# use ouranos style\n",
"fg.utils.set_mpl_style('ouranos')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Timeseries"
]
},
{
"cell_type": "code",
"execution_count": null,
"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)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"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"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#creating 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",
"\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",
" )"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Maps\n",
"Create multiple maps plot with figanos wrapped around [xr.plot.facetgrid.FacetGrid](https://docs.xarray.dev/en/latest/generated/xarray.plot.FacetGrid.html) by passing the key row `row` and `col` in the argument `plot_kw`."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#Selecting 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",
"\n",
"# defining our 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",
"\n",
" )\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"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([[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",
"\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={'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",
"\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"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from xclim import ensembles\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",
"\n",
"im.fig.suptitle(\"Multiple hatchmaps\", y=1.08)\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Plot over each other"
]
},
{
"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."
]
},
{
"cell_type": "code",
"execution_count": null,
"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",
"\n",
"attrs = {'units': 'degK', 'standard_name': 'air_temperature', 'long_name': ds_space.tx_max_p50.attrs['description']}\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})"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"vmin=280\n",
"vmax=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\": vmin, \"vmax\": vmax,\n",
" },\n",
" features = ['coastline','ocean'],\n",
" frame = False,\n",
" use_attrs={\"suptitle\": \"description\"}\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': vmin,\n",
" 'vmax': vmax,\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"
]
},
{
"cell_type": "markdown",
"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"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"#Selecting 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",
"\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",
"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",
" ]\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."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\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",
"\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",
"\n",
"# modify xlabels 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"
]
}
],
"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.11.0"
}
},
"nbformat": 4,
"nbformat_minor": 1
}
Loading

0 comments on commit 3084e8e

Please sign in to comment.