Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Hatchmap and scattermap fixes #195

Open
wants to merge 28 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7f4f706
fixes in hatchmap and scattermap
vindelico Apr 2, 2024
59dc895
Merge branch 'nan_edgecolor_scattermap' into hatchmap_and_scattermap_…
vindelico Apr 2, 2024
16178c6
update docs
vindelico Apr 17, 2024
ab15418
Merge branch 'nan_edgecolor_scattermap' into hatchmap_and_scattermap_…
vindelico Apr 17, 2024
cb91ead
nan_edgecolor_scattermap merged here; before preparation for PR
vindelico Apr 17, 2024
336c1c9
support edgecolors AND edgecolor used as aliases in matplotlib.pyplot…
vindelico Apr 18, 2024
bf22010
Merge branch 'main' into hatchmap_and_scattermap_fixes
vindelico Apr 19, 2024
284441b
scattermap: changed variable name plot_kw_pop back to plot_kw and rem…
vindelico Apr 24, 2024
2bc3a23
hatchmap: removed deepcopy operation which is already happening in ca…
vindelico Apr 25, 2024
5d28a54
Update CHANGES.rst
vindelico Apr 25, 2024
12cb82e
Update CHANGES.rst
vindelico Apr 25, 2024
77acc52
Update CHANGES.rst
vindelico Apr 25, 2024
3bc5bca
Merge branch 'main' into hatchmap_and_scattermap_fixes
vindelico May 6, 2024
8217744
Merge branch 'main' into hatchmap_and_scattermap_fixes
vindelico May 7, 2024
b47c53f
Merge branch 'main' into hatchmap_and_scattermap_fixes
vindelico May 10, 2024
ede8a63
Merge branch 'main' into hatchmap_and_scattermap_fixes
Zeitsperre Sep 17, 2024
0a99562
fix merge
Zeitsperre Sep 17, 2024
be496f4
minor updates
vindelico Feb 20, 2025
15f6a3c
Merge branch 'hatchmap_and_scattermap_fixes' of github.com:Ouranosinc…
vindelico Feb 20, 2025
66ef174
Merge branch 'main' into hatchmap_and_scattermap_fixes
Zeitsperre Feb 20, 2025
bd758be
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 20, 2025
1f2e55a
fixed figanos_multiplots.ipynb
vindelico Feb 20, 2025
7f2c7fe
catching up with GitHub
vindelico Feb 21, 2025
e267a16
fix missing 'marker' in scattermap
vindelico Feb 21, 2025
676bb36
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 21, 2025
a6077bf
remove wrong kernel from figanos_multiplots.ipynb
vindelico Feb 21, 2025
6e6e1c9
remove wrong kernel from figanos_multiplots.ipynb
vindelico Feb 21, 2025
a17fcce
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Contributors to this version: Trevor James Smith (:user:`Zeitsperre`), Marco Bra

New features and enhancements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* No-legend option in ``hatchmap``; use ``edgecolor`` and ``edgecolors`` as aliases (:pull:`195`)
* Use list or ndarray as levels for colorbar in gridmap and small bug fixes (:pull:`176`).
* Added style sheet ``transparent.mplstyle`` (:issue:`183`, :pull:`185`)
* Fix NaN issues, extreme values in sizes legend and added edgecolors in ``fg.matplotlib.scattermap`` (:pull:`184`).
Expand Down
4 changes: 2 additions & 2 deletions docs/notebooks/figanos_multiplots.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@
"im = fg.hatchmap({'sup_305k': sup_305k, 'inf_300k': inf_300k},\n",
" plot_kw={\n",
" 'sup_305k': {\n",
" 'hatches': '*',\n",
" 'hatches': ['////'], # hatches must be passed as a list\n",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use can instead of must

" 'col': 'time',\n",
" \"x\": \"lon\",\n",
" \"y\": \"lat\"\n",
Expand All @@ -196,7 +196,7 @@
" frame = True,\n",
" legend_kw = {'title': 'Ensemble change'})\n",
"\n",
"im.fig.suptitle(\"Multiple hatchmaps\", y=1.08)\n"
"im.fig.suptitle(\"Multiple hatchmaps\", y=1.08)"
]
},
{
Expand Down
128 changes: 68 additions & 60 deletions figanos/matplotlib/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -1526,17 +1526,15 @@ def scattermap(
if "row" not in plot_kw and "col" not in plot_kw:
use_attrs.setdefault("title", "description")

plot_kw_pop = copy.deepcopy(plot_kw) # copy plot_kw to modify and pop info in it

# extract plot_kw from dict if needed
if isinstance(data, dict) and plot_kw and list(data.keys())[0] in plot_kw.keys():
plot_kw_pop = plot_kw_pop[list(data.keys())[0]]
plot_kw = plot_kw[list(data.keys())[0]]

# figanos does not use xr.plot.scatter default markersize
if "markersize" in plot_kw.keys():
if not sizes:
sizes = plot_kw["markersize"]
plot_kw_pop.pop("markersize")
plot_kw.pop("markersize")

# if data is dict, extract
if isinstance(data, dict):
Expand Down Expand Up @@ -1576,13 +1574,13 @@ def scattermap(
elif ax is not None and ("col" in plot_kw or "row" in plot_kw):
raise ValueError("Cannot use 'ax' and 'col'/'row' at the same time.")
elif ax is None:
plot_kw_pop = {"subplot_kws": {"projection": projection}} | plot_kw_pop
plot_kw = {"subplot_kws": {"projection": projection}} | plot_kw
cfig_kw = fig_kw.copy()
if "figsize" in fig_kw: # add figsize to plot_kw for facetgrid
plot_kw_pop.setdefault("figsize", fig_kw["figsize"])
plot_kw.setdefault("figsize", fig_kw["figsize"])
cfig_kw.pop("figsize")
if len(cfig_kw) >= 1:
plot_kw_pop = {"subplot_kws": {"projection": projection}} | plot_kw_pop
plot_kw = {"subplot_kws": {"projection": projection}} | plot_kw
warnings.warn(
"Only figsize and figure.add_subplot() arguments can be passed to fig_kw when using facetgrid."
)
Expand All @@ -1602,9 +1600,9 @@ def scattermap(
cbar_label = get_attributes(use_attrs["cbar_label"], data)

if "add_colorbar" not in plot_kw or plot_kw["add_colorbar"] is not False:
plot_kw_pop.setdefault("cbar_kwargs", {})
plot_kw_pop["cbar_kwargs"].setdefault("label", wrap_text(cbar_label))
plot_kw_pop["cbar_kwargs"].setdefault("pad", 0.015)
plot_kw.setdefault("cbar_kwargs", {})
plot_kw["cbar_kwargs"].setdefault("label", wrap_text(cbar_label))
plot_kw["cbar_kwargs"].setdefault("pad", 0.015)

# colormap
if isinstance(cmap, str):
Expand Down Expand Up @@ -1656,61 +1654,68 @@ def scattermap(
target_range=size_range,
data_range=None,
)
plot_kw_pop.setdefault("add_legend", False)
plot_kw.setdefault("add_legend", False)
if ax:
plot_kw_pop.setdefault("s", pt_sizes)
plot_kw.setdefault("s", pt_sizes)
else:
plot_kw_pop.setdefault("s", pt_sizes[0])
plot_kw.setdefault("s", pt_sizes[0])

# norm
plot_kw_pop.setdefault("vmin", np.nanmin(plot_data.values[mask]))
plot_kw_pop.setdefault("vmax", np.nanmax(plot_data.values[mask]))
plot_kw.setdefault("vmin", np.nanmin(plot_data.values[mask]))
plot_kw.setdefault("vmax", np.nanmax(plot_data.values[mask]))

norm = custom_cmap_norm(
cmap,
vmin=plot_kw_pop["vmin"],
vmax=plot_kw_pop["vmax"],
vmin=plot_kw["vmin"],
vmax=plot_kw["vmax"],
levels=levels,
divergent=divergent,
)

# matplotlib.pyplot.scatter treats "edgecolor" and "edgecolors" as aliases so we accept "edgecolor" and convert it
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't pop edgecolor and replace it to edgecolors since both of them work and if matplotlib ever made change to one we would have to fix it after. I also think edgecolors is more used to pass many colors as a list.

if "edgecolor" in plot_kw and "edgecolors" not in plot_kw:
plot_kw["edgecolors"] = plot_kw["edgecolor"]
plot_kw.pop("edgecolor")

# set defaults and create copy without vmin, vmax (conflicts with norm)
plot_kw_pop = {
plot_kw = {
"cmap": cmap,
"norm": norm,
"transform": transform,
"zorder": 8,
"marker": "o",
} | plot_kw_pop
"edgecolors": "none",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this line based on previous comment

} | plot_kw

# chek if edgecolors in plot_kw and match len of plot_data
if "edgecolors" in plot_kw:
if matplotlib.colors.is_color_like(plot_kw["edgecolors"]):
plot_kw_pop["edgecolors"] = np.repeat(
plot_kw["edgecolors"] = np.repeat(
plot_kw["edgecolors"], len(plot_data.where(mask).values)
)
elif len(plot_kw["edgecolors"]) != len(plot_data.values):
plot_kw_pop["edgecolors"] = np.repeat(
plot_kw["edgecolors"] = np.repeat(
plot_kw["edgecolors"][0], len(plot_data.where(mask).values)
)
warnings.warn(
"Length of edgecolors does not match length of data. Only first edgecolor is used for plotting."
)
else:
if isinstance(plot_kw["edgecolors"], list):
plot_kw_pop["edgecolors"] = np.array(plot_kw["edgecolors"])
plot_kw_pop["edgecolors"] = plot_kw_pop["edgecolors"][mask]
plot_kw["edgecolors"] = np.array(plot_kw["edgecolors"])
plot_kw["edgecolors"] = plot_kw["edgecolors"][mask]
else:
plot_kw_pop.setdefault("edgecolor", "none")
plot_kw.setdefault("edgecolors", "none")

for key in ["vmin", "vmax"]:
plot_kw_pop.pop(key)
plot_kw.pop(key)
# plot
plot_kw_pop = {"x": "lon", "y": "lat", "hue": plot_data.name} | plot_kw_pop
plot_kw = {"x": "lon", "y": "lat", "hue": plot_data.name} | plot_kw
if ax:
plot_kw_pop.setdefault("ax", ax)
v = plot_data.where(mask).to_dataset()
im = v.plot.scatter(**plot_kw_pop)
plot_kw.setdefault("ax", ax)

plot_data_masked = plot_data.where(mask).to_dataset()
im = plot_data_masked.plot.scatter(**plot_kw)

# add features
if ax:
Expand Down Expand Up @@ -1773,7 +1778,7 @@ def scattermap(
np.resize(sdata.values[mask], (sdata.values[mask].size, 1)),
np.resize(pt_sizes[mask], (pt_sizes[mask].size, 1)),
max_entries=6,
marker=plot_kw_pop["marker"],
marker=plot_kw["marker"],
)
# legend spacing
if size_range[1] > 200:
Expand Down Expand Up @@ -2077,7 +2082,7 @@ def hatchmap(
features: list[str] | dict[str, dict[str, Any]] | None = None,
geometries_kw: dict[str, Any] | None = None,
levels: int | None = None,
legend_kw: dict[str, Any] | None = None,
legend_kw: dict[str, Any] | bool = True,
show_time: bool | str | int | tuple[float, float] = False,
frame: bool = False,
) -> matplotlib.axes.Axes:
Expand Down Expand Up @@ -2108,8 +2113,8 @@ def hatchmap(
cartopy.feature: ['coastline', 'borders', 'lakes', 'land', 'ocean', 'rivers', 'states'].
geometries_kw : dict, optional
Arguments passed to cartopy ax.add_geometry() which adds given geometries (GeoDataFrame geometry) to axis.
legend_kw : dict, optional
Arguments to pass to `ax.legend()`.
legend_kw : dict or boolean, optional
Arguments to pass to `ax.legend()`. No legend is added if legend_kw == False.
show_time : bool, tuple, string or int.
If True, show time (as date) at the bottom right of the figure.
Can be a tuple of axis coordinates (0 to 1, as a fraction of the axis length) representing the location
Expand Down Expand Up @@ -2169,24 +2174,23 @@ def hatchmap(

dattrs = None
plot_data = {}
dc = plot_kw.copy()

# convert data to dict (if not one)
if not isinstance(data, dict):
if isinstance(data, xr.DataArray):
plot_data = {data.name: data}
if list(data.keys())[0] not in plot_kw.keys():
plot_kw = {list(plot_data.keys())[0]: dc}
if data.name not in plot_kw.keys():
plot_kw = {data.name: plot_kw}
elif isinstance(data, xr.Dataset):
dattrs = data
plot_data = {var: data[var] for var in data.data_vars}
for v in plot_data.keys():
if v not in plot_kw.keys():
plot_kw[v] = dc
plot_kw[v] = plot_kw
else:
for k, v in data.items():
if k not in plot_kw.keys():
plot_kw[k] = dc
plot_kw[k] = plot_kw
if isinstance(v, xr.Dataset):
dattrs = k
plot_data[k] = v[list(v.data_vars)[0]]
Expand All @@ -2208,28 +2212,25 @@ def hatchmap(
if transform and (
"xlim" in list(plot_kw.values())[0] and "ylim" in list(plot_kw.values())[0]
):
extend = [
extent = [
list(plot_kw.values())[0]["xlim"][0],
list(plot_kw.values())[0]["xlim"][1],
list(plot_kw.values())[0]["ylim"][0],
list(plot_kw.values())[0]["ylim"][1],
]
{v.pop("xlim") for v in plot_kw.values()}
{v.pop("ylim") for v in plot_kw.values()}
[v.pop(lim) for lim in ["xlim", "ylim"] for v in plot_kw.values() if lim in v]

elif transform and (
"xlim" in list(plot_kw.values())[0] or "ylim" in list(plot_kw.values())[0]
):
extend = None
extent = None
warnings.warn(
"Requires both xlim and ylim with 'transform'. Xlim or ylim was dropped"
)
if "xlim" in list(plot_kw.values())[0].keys():
{v.pop("xlim") for v in plot_kw.values()}
if "ylim" in list(plot_kw.values())[0].keys():
{v.pop("ylim") for v in plot_kw.values()}
[v.pop(lim) for lim in ["xlim", "ylim"] for v in plot_kw.values() if lim in v]

else:
extend = None
extent = None

# setup fig, ax
if ax is None and (
Expand All @@ -2243,11 +2244,11 @@ def hatchmap(
):
raise ValueError("Cannot use 'ax' and 'col'/'row' at the same time.")
elif ax is None:
{
[
v.setdefault("subplot_kws", {}).setdefault("projection", projection)
for v in plot_kw.values()
}
cfig_kw = fig_kw.copy()
]
cfig_kw = copy.deepcopy(fig_kw)
if "figsize" in fig_kw: # add figsize to plot_kw for facetgrid
plot_kw[0].setdefault("figsize", fig_kw["figsize"])
cfig_kw.pop("figsize")
Expand Down Expand Up @@ -2295,9 +2296,9 @@ def hatchmap(
im = v.where(mask is not True).plot.contourf(**plot_kw[k])
artists, labels = im.legend_elements(str_format="{:2.1f}".format)

if ax:
if ax and legend_kw:
ax.legend(artists, labels, **legend_kw)
else:
elif legend_kw:
im.figlegend = im.fig.legend(**legend_kw)

elif len(plot_data) > 1 and "levels" in plot_kw[k]:
Expand All @@ -2311,6 +2312,13 @@ def hatchmap(
if "hatches" not in plot_kw[k].keys():
plot_kw[k]["hatches"] = dfh[n]
n += 1
elif isinstance(
plot_kw[k]["hatches"], str
): # make sure the hatches are in a list
warnings.warn(
"Hatches argument must be of type 'list'. Wrapping string argument as list."
)
plot_kw[k]["hatches"] = [plot_kw[k]["hatches"]]

plot_kw[k].setdefault("transform", transform)
if ax:
Expand Down Expand Up @@ -2344,31 +2352,31 @@ def hatchmap(
geometries_kw,
frame,
)
if extend:
fax.set_extent(extend)
if extent:
fax.set_extent(extent)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if isinstance(plot_kw[k]["hatches"], list):
hc = plot_kw[k]["hatches"][0]
else:
hv = plot_kw[k]["hatches"]

pat_leg.append(
matplotlib.patches.Patch(
hatch=plot_kw[k]["hatches"], fill=False, label=k
hatch=plot_kw[k]["hatches"][0], fill=False, label=k
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
hatch=plot_kw[k]["hatches"][0], fill=False, label=k
hatch=hc, fill=False, label=k

)
)

if pat_leg:
if pat_leg and legend_kw:
legend_kw = {
"loc": "lower right",
"handleheight": 2,
"handlelength": 4,
} | legend_kw

if ax:
if ax and legend_kw:
ax.legend(handles=pat_leg, **legend_kw)
else:
elif legend_kw:
im.figlegend = im.fig.legend(handles=pat_leg, **legend_kw)

# add features
if ax:
if extend:
ax.set_extend(extend)
if extent:
ax.set_extent(extent)
if dattrs:
use_attrs.setdefault("title", "description")

Expand Down