From 6cfef8bdf9f2e40b04edba4a8c57d50ae90a49c2 Mon Sep 17 00:00:00 2001 From: James Frost Date: Thu, 11 Apr 2024 16:14:46 +0100 Subject: [PATCH 1/2] Close figures after saving, and refactor plotting code Refactors the plotting code to use the explicit (object-oriented) matplotlib API. Figures are explicitly closed, preventing memory leaking. Also added a super-title to the postage stamp plots, so the diagnostic title is there too. Fixes #481 --- src/CSET/operators/plot.py | 52 ++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/CSET/operators/plot.py b/src/CSET/operators/plot.py index 07d081d81..111de8ea5 100644 --- a/src/CSET/operators/plot.py +++ b/src/CSET/operators/plot.py @@ -16,6 +16,7 @@ import fcntl import importlib.resources +import itertools import json import logging import math @@ -138,32 +139,33 @@ def _plot_and_save_contour_plot( """ # Setup plot details, size, resolution, etc. fig = plt.figure(figsize=(15, 15), facecolor="w", edgecolor="k") - - cmap = mpl.colormaps["viridis"] + axes = fig.subplots() # Filled contour plot of the field. - iplt.contourf(cube, cmap=cmap) - axes = fig.gca() + cmap = mpl.colormaps["viridis"] + iplt.contourf(cube, cmap=cmap, axes=axes) # Add coastlines. axes.coastlines(resolution="10m") # Add title. - plt.title(title, fontsize=16) + axes.set_title(title, fontsize=16) # Add colour bar. - cbar = plt.colorbar() + cbar = fig.colorbar() cbar.set_label(label=f"{cube.name()} ({cube.units})", size=20) # Save plot. fig.savefig(filename, bbox_inches="tight", dpi=150) logging.info("Saved contour plot to %s", filename) + plt.close(fig) def _plot_and_save_postage_stamp_contour_plot( cube: iris.cube.Cube, filename: str, stamp_coordinate: str, + title: str, **kwargs, ): """Plot postage stamp contour plots from an ensemble. @@ -185,24 +187,29 @@ def _plot_and_save_postage_stamp_contour_plot( # Use the smallest square grid that will fit the members. grid_size = int(math.ceil(math.sqrt(len(cube.coord(stamp_coordinate).points)))) - plt.figure(figsize=(10, 10)) - # Make a subplot for each member. - subplot = 1 - for member in cube.slices_over(stamp_coordinate): - plt.subplot(grid_size, grid_size, subplot) - plot = iplt.contourf(member) - plt.title(f"Member #{member.coord(stamp_coordinate).points[0]}") - plt.axis("off") - plt.gca().coastlines() - subplot += 1 + fig = plt.figure(figsize=(10, 10)) + # Make a subplot for each member. This returns a 2D array, which we turn + # into a 1D iterable. + axes = itertools.chain.from_iterable( + fig.subplots(grid_size, grid_size, squeeze=False) + ) + for ax, member in zip(axes, cube.slices_over(stamp_coordinate)): + plot = iplt.contourf(member, axes=ax) + ax.set_title(f"Member #{member.coord(stamp_coordinate).points[0]}") + ax.set_axis_off() + ax.coastlines() # Put the shared colorbar in its own axes. - colorbar_axes = plt.gcf().add_axes([0.15, 0.07, 0.7, 0.03]) - colorbar = plt.colorbar(plot, colorbar_axes, orientation="horizontal") + colorbar_axes = fig.add_axes([0.15, 0.07, 0.7, 0.03]) + colorbar = fig.colorbar(plot, colorbar_axes, orientation="horizontal") colorbar.set_label(f"{cube.name()} / {cube.units}") - plt.savefig(filename, bbox_inches="tight", dpi=150) + # Overall figure title. + fig.suptitle(title) + + fig.savefig(filename, bbox_inches="tight", dpi=150) logging.info("Saved contour postage stamp plot to %s", filename) + plt.close(fig) def _plot_and_save_line_series( @@ -222,10 +229,10 @@ def _plot_and_save_line_series( Plot title. """ fig = plt.figure(figsize=(8, 8), facecolor="w", edgecolor="k") - iplt.plot(coord, cube, "o-") + ax = fig.subplots() + iplt.plot(coord, cube, "o-", axes=ax) # Add some labels and tweak the style. - ax = fig.gca() ax.set( xlabel=f"{coord.name()} / {coord.units}", ylabel=f"{cube.name()} / {cube.units}", @@ -238,6 +245,7 @@ def _plot_and_save_line_series( # Save plot. fig.savefig(filename, bbox_inches="tight", dpi=150) logging.info("Saved line plot to %s", filename) + plt.close(fig) #################### @@ -387,7 +395,7 @@ def postage_stamp_contour_plot( except iris.exceptions.CoordinateNotFoundError as err: raise ValueError(f"Cube must have a {coordinate} coordinate.") from err - _plot_and_save_postage_stamp_contour_plot(cube, filename, coordinate) + _plot_and_save_postage_stamp_contour_plot(cube, filename, coordinate, title="") _make_plot_html_page([filename]) return cube From 1895ff940b68822ce49dd55b53e629fb4eee4678 Mon Sep 17 00:00:00 2001 From: James Frost Date: Fri, 12 Apr 2024 10:52:45 +0100 Subject: [PATCH 2/2] Use pyplot API where needed for better cartopy compatibility --- src/CSET/operators/plot.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/CSET/operators/plot.py b/src/CSET/operators/plot.py index 111de8ea5..cc1587ae1 100644 --- a/src/CSET/operators/plot.py +++ b/src/CSET/operators/plot.py @@ -16,7 +16,6 @@ import fcntl import importlib.resources -import itertools import json import logging import math @@ -139,11 +138,12 @@ def _plot_and_save_contour_plot( """ # Setup plot details, size, resolution, etc. fig = plt.figure(figsize=(15, 15), facecolor="w", edgecolor="k") - axes = fig.subplots() # Filled contour plot of the field. cmap = mpl.colormaps["viridis"] - iplt.contourf(cube, cmap=cmap, axes=axes) + contours = iplt.contourf(cube, cmap=cmap) + # Using pyplot interface here as we need iris to generate a cartopy GeoAxes. + axes = plt.gca() # Add coastlines. axes.coastlines(resolution="10m") @@ -152,7 +152,7 @@ def _plot_and_save_contour_plot( axes.set_title(title, fontsize=16) # Add colour bar. - cbar = fig.colorbar() + cbar = fig.colorbar(contours) cbar.set_label(label=f"{cube.name()} ({cube.units})", size=20) # Save plot. @@ -188,13 +188,15 @@ def _plot_and_save_postage_stamp_contour_plot( grid_size = int(math.ceil(math.sqrt(len(cube.coord(stamp_coordinate).points)))) fig = plt.figure(figsize=(10, 10)) - # Make a subplot for each member. This returns a 2D array, which we turn - # into a 1D iterable. - axes = itertools.chain.from_iterable( - fig.subplots(grid_size, grid_size, squeeze=False) - ) - for ax, member in zip(axes, cube.slices_over(stamp_coordinate)): - plot = iplt.contourf(member, axes=ax) + # Make a subplot for each member. + for member, subplot in zip( + cube.slices_over(stamp_coordinate), range(1, grid_size**2 + 1) + ): + # Implicit interface is much easier here, due to needing to have the + # cartopy GeoAxes generated. + plt.subplot(grid_size, grid_size, subplot) + plot = iplt.contourf(member) + ax = plt.gca() ax.set_title(f"Member #{member.coord(stamp_coordinate).points[0]}") ax.set_axis_off() ax.coastlines() @@ -229,8 +231,8 @@ def _plot_and_save_line_series( Plot title. """ fig = plt.figure(figsize=(8, 8), facecolor="w", edgecolor="k") - ax = fig.subplots() - iplt.plot(coord, cube, "o-", axes=ax) + iplt.plot(coord, cube, "o-") + ax = plt.gca() # Add some labels and tweak the style. ax.set(