Repository: ARM-DOE/ACT

Issue:
Histogram Display Examples
* ACT version: 1.4.5
* Python version: 3.10.8
* Operating System: macOS Ventura 13.3.1

### Description

Within the [histogram display](https://github.com/ARM-DOE/ACT/blob/main/act/plotting/histogramdisplay.py) there are two plotting functions that do not have associated examples within our [Example Gallery](https://arm-doe.github.io/ACT/source/auto_examples/index.html)

These functions are:
- plot_size_distribution
- heat map (though [spatial contour plot](https://arm-doe.github.io/ACT/source/auto_examples/plotting/plot_contour.html#sphx-glr-source-auto-examples-plotting-plot-contour-py) uses this function. Maybe we can add tags to examples for functions used?




Discussion:
Using heat map to compare/contrast similar fields as a 2D histogram would be a good way to showcase using data without time. 

Additionally, could be a good first issue for the SULI interns to tackle, as I have one student working on instrument intercomparisons
I agree that it would be a good first issue. And yeah the heatmap i think would still be a good example as i'm not seeing a standalone use of it.

Resolving patch:
diff --git a/act/plotting/distributiondisplay.py b/act/plotting/distributiondisplay.py
--- a/act/plotting/distributiondisplay.py
+++ b/act/plotting/distributiondisplay.py
@@ -3,6 +3,7 @@
 import matplotlib.pyplot as plt
 import numpy as np
 import xarray as xr
+import pandas as pd
 
 from ..utils import datetime_utils as dt_utils
 from .plot import Display
@@ -323,7 +324,9 @@ def plot_size_distribution(
                     dt_utils.numpy_to_arm_date(self._ds[dsname].time.values[0]),
                 ]
             )
-
+            if time is not None:
+                t = pd.Timestamp(time)
+                set_title += ''.join([' at ', ':'.join([str(t.hour), str(t.minute), str(t.second)])])
         self.axes[subplot_index].set_title(set_title)
         self.axes[subplot_index].step(bins.values, xdata.values, **kwargs)
         self.axes[subplot_index].set_xlabel(xtitle)
@@ -488,6 +491,7 @@ def plot_heatmap(
         density=False,
         set_shading='auto',
         hist_kwargs=dict(),
+        threshold=None,
         **kwargs,
     ):
         """
@@ -502,12 +506,14 @@ def plot_heatmap(
         dsname : str or None
             The name of the datastream the field is contained in. Set
             to None to let ACT automatically determine this.
-        x_bins : array-like or None
+        x_bins : array-like, int, or None
             The histogram bin boundaries to use for the variable on the X axis.
             Set to None to use numpy's default boundaries.
-        y_bins : array-like or None
+            If an int, will indicate the number of bins to use
+        y_bins : array-like, int, or None
             The histogram bin boundaries to use for the variable on the Y axis.
             Set to None to use numpy's default boundaries.
+            If an int, will indicate the number of bins to use
         subplot_index : tuple
             The subplot index to place the plot in
         set_title : str
@@ -517,6 +523,10 @@ def plot_heatmap(
         set_shading : string
             Option to to set the matplotlib.pcolormesh shading parameter.
             Default to 'auto'
+        threshold : float
+            Value on which to threshold the histogram results for plotting.
+            Setting to 0 will ensure that all 0 values are removed from the plot
+            making it easier to distringuish between 0 and low values
         hist_kwargs : Additional keyword arguments to pass to numpy histogram.
 
         Other keyword arguments will be passed into :func:`matplotlib.pyplot.pcolormesh`.
@@ -545,6 +555,12 @@ def plot_heatmap(
         else:
             xtitle = x_field
 
+        if x_bins is not None and isinstance(x_bins, int):
+            x_bins = np.linspace(xdata.values.min(), xdata.values.max(), x_bins)
+
+        if y_bins is not None and isinstance(x_bins, int):
+            y_bins = np.linspace(ydata.values.min(), ydata.values.max(), y_bins)
+
         if x_bins is not None and y_bins is None:
             # We will defaut the y direction to have the same # of bins as x
             y_bins = np.linspace(ydata.values.min(), ydata.values.max(), len(x_bins))
@@ -574,6 +590,10 @@ def plot_heatmap(
                 bins=[x_bins, y_bins],
                 **hist_kwargs
             )
+        # Adding in the ability to threshold the heatmaps
+        if threshold is not None:
+            my_hist[my_hist <= threshold] = np.nan
+
         x_inds = (x_bins[:-1] + x_bins[1:]) / 2.0
         y_inds = (y_bins[:-1] + y_bins[1:]) / 2.0
         xi, yi = np.meshgrid(x_inds, y_inds, indexing='ij')
@@ -718,7 +738,7 @@ def scatter(
         if mdata is not None:
             if cbar_label is None:
                 # Define the y-axis label. If units are available, plot
-                if 'units' in ydata.attrs:
+                if 'units' in mdata.attrs:
                     ztitle = m_field + ''.join([' (', mdata.attrs['units'], ')'])
                 else:
                     ztitle = m_field
diff --git a/examples/plotting/plot_heatmap.py b/examples/plotting/plot_heatmap.py
new file mode 100644
--- /dev/null
+++ b/examples/plotting/plot_heatmap.py
@@ -0,0 +1,30 @@
+"""
+Example plot using heat maps
+----------------------------
+
+Compare MET temperature and RH using a heatmap
+
+Author: Adam Theisen
+
+"""
+
+import act
+import matplotlib.pyplot as plt
+
+# Read MET data in from the test data area
+ds = act.io.armfiles.read_netcdf(act.tests.EXAMPLE_MET_WILDCARD)
+
+# Create a DistributionDisplay object to compare fields
+display = act.plotting.DistributionDisplay(ds, subplot_shape=(1, 2), figsize=(12, 5))
+
+# Plot a heatmap and scatter plot up of RH vs Temperature
+# Set the number of bins for the x-axis to 25 and y to 20
+title = 'Heatmap of MET RH vs Temp'
+display.plot_heatmap('temp_mean', 'rh_mean', x_bins=25, y_bins=20,
+                     threshold=0, subplot_index=(0, 0), set_title=title)
+
+# Plot the scatter plot and shade by wind_speed
+title = 'Scatter plot of MET RH vs Temp'
+display.scatter('temp_mean', 'rh_mean', subplot_index=(0, 1), set_title=title, m_field='time')
+
+plt.show()
diff --git a/examples/plotting/plot_size_distribution.py b/examples/plotting/plot_size_distribution.py
new file mode 100644
--- /dev/null
+++ b/examples/plotting/plot_size_distribution.py
@@ -0,0 +1,32 @@
+"""
+Example plot using heat maps
+----------------------------
+
+Compare MET temperature and RH using a heatmap
+
+Author: Adam Theisen
+
+"""
+
+import act
+import matplotlib.pyplot as plt
+import numpy as np
+
+# Read CCN data in from the test data area
+ds = act.io.armfiles.read_netcdf(act.tests.EXAMPLE_CCN)
+
+# Create a DistributionDisplay object
+display = act.plotting.DistributionDisplay(ds, subplot_shape=(2,), figsize=(12, 10))
+
+# Create a size distribution plot while plotting the
+# size distribution in the second plot
+t_ind = np.datetime64('2017-09-03T15:47:31')
+display.plot_size_distribution('N_CCN_dN', 'droplet_size', time=t_ind, subplot_index=(0,))
+
+# This part shows how you can use different display types in a single plot
+# by assigning the new display object to a figure and axes from the first one.
+display2 = act.plotting.TimeSeriesDisplay(ds)
+display2.assign_to_figure_axis(display.fig, display.axes[1])
+display2.plot('N_CCN_dN')
+
+plt.show()


Test patch:
diff --git a/act/tests/__init__.py b/act/tests/__init__.py
--- a/act/tests/__init__.py
+++ b/act/tests/__init__.py
@@ -59,6 +59,7 @@
             'EXAMPLE_ECOR',
             'EXAMPLE_SEBS',
             'EXAMPLE_ENA_MET',
+            'EXAMPLE_CCN',
         ]
     },
 )
diff --git a/act/tests/data/sgpaosccn2colaE13.b1.20170903.000000.nc b/act/tests/data/sgpaosccn2colaE13.b1.20170903.000000.nc
new file mode 100644
Binary files /dev/null and b/act/tests/data/sgpaosccn2colaE13.b1.20170903.000000.nc differ
diff --git a/act/tests/sample_files.py b/act/tests/sample_files.py
--- a/act/tests/sample_files.py
+++ b/act/tests/sample_files.py
@@ -69,3 +69,4 @@
 EXAMPLE_SEBS = os.path.join(DATA_PATH, 'sgpsebsE14.b1.20190601.000000.cdf')
 EXAMPLE_MFAS_SODAR = os.path.join(DATA_PATH, 'sodar.20230404.mnd')
 EXAMPLE_ENA_MET = os.path.join(DATA_PATH, 'enametC1.b1.20221109.000000.cdf')
+EXAMPLE_CCN = os.path.join(DATA_PATH, 'sgpaosccn2colaE13.b1.20170903.000000.nc')