# ╔════════════════════════════════════════════════════════════════════╗
# ║   Comport_AI™ is a free open-source HR predictive analytics tool   ║
# ║   that forecasts the likely range of a worker’s future job         ║
# ║   performance. It treats the likely ceiling and likely floor of    ║
# ║   a worker’s future performance as independent entities that are   ║
# ║   modelled by artificial neural networks whose custom loss         ║
# ║   functions enable them to formulate prediction intervals that     ║
# ║   are as small as possible, while being just large enough to       ║
# ║   contain a worker’s actual future performance value in most       ║
# ║   cases.                                                           ║
# ║                                                                    ║
# ║   Developed by Matthew E. Gladden • ©2021-23 NeuraXenetica LLC     ║
# ║   This software is made available for use under                    ║
# ║   GNU General Public License Version 3                             ║
# ║   (please see https://www.gnu.org/licenses/gpl-3.0.html).          ║
# ╚════════════════════════════════════════════════════════════════════╝

"""
This module creates plots that visualize the results generated by the
Base Target Model, individual Ceiling and Floor Models, and Joint
Range Models.
"""

import os
import warnings

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.colors
from matplotlib.offsetbox import OffsetImage, AnchoredOffsetbox

# Import other modules from this package.
import config as cfg


def generate_plot_return_png_and_save_to_file(
    plot_type_u,
    data_x_u,
    data_y_u,
    x_axis_label_u,
    y_axis_label_u,
    x_tick_labels_rotation_u,
    xy_tick_labels_custom_fontsize_u,
    data_color_main_u,
    data_alpha_main_u,
    color_mapping_for_legend_u,
    plot_title_u,
    logo_location_u,
    name_for_file_u
    ):
    """
    This is the most generalized plotting function, which is capable of 
    generating plots of various types. It will be called by higher-order
    functions that use it to generate one particular type of plot.

    PARAMETERS
    ----------
    plot_type_u : str
        The type of plot to be generated ("column", "hist", "hist2d",
        "line", "scatter", "heatmap")
    data_x_u
        The x-series of the data
    data_y_u
        The y-series of the data (optional for some plot types)
    x_axis_label_u
        The x-axis label
    y_axis_label_u
        The y-axis label (optional for some plot types)
    x_tick_labels_rotation_u
        Rotation of the x-tick labels
    xy_tick_labels_custom_fontsize_u
        Any custom xy-tick label fontsize
    data_color_main_u
        The main color for use in displaying the data
    data_alpha_main_u
        An optional alpha value for the data
    color_mapping_for_legend_u
        The color mapping for use in the legend
    plot_title_u : str
        The title for the whole plot
    logo_location_u : str
        The desired location where the WorkforceSim logo should appear
        in the plot (e.g., "upper right")
    name_for_file_u
        The desired name for the PNG file to be saved
    """

    # ------------------------------------------------------------------
    # Create the empty figure where data will be plotted.
    # ------------------------------------------------------------------

    # This uses a non-interactive backend, which helps avoid errors
    # produced by the fact that Matplotlib isn't thread-safe and plots 
    # are being generated outside of the main thread.
    matplotlib.use('agg')

    plt.rcParams['figure.dpi'] = cfg.PLOT_FIGURE_DPI
    plt.rcParams['savefig.dpi'] = cfg.PLOT_SAVEFIG_DPI

    # Seaborn heatmaps are square.
    if plot_type_u == "heatmap":
        fig = plt.figure(figsize=(
            cfg.PLOT_FIGSIZE[0]*1.0, cfg.PLOT_FIGSIZE[0]*0.9
            ))

    # Plots of predicted and actual values are square.
    elif (plot_type_u == "model_cont_val_preds_scatter") \
            or (plot_type_u == "model_cont_val_preds_scatter") \
            or (plot_type_u == "model_ceiling_scatter") \
            or (plot_type_u == "model_floor_scatter") \
            or (plot_type_u == "joint_range_scatter"):
        fig = plt.figure(figsize=(
            cfg.PLOT_FIGSIZE[0]*1.0,cfg.PLOT_FIGSIZE[0]*0.9
            ))

    # All remaining plots have a landscape-orientation rectangular size.
    else:
        fig = plt.figure(figsize=cfg.PLOT_FIGSIZE)

    ax = plt.subplot2grid((1, 1), (0, 0), rowspan=1, colspan=1)
    fig.patch.set_facecolor(cfg.PLOT_COLOR_LIGHT_PINK)
    ax.patch.set_facecolor(cfg.PLOT_COLOR_BLACK)


    # ------------------------------------------------------------------
    # Plot the data using a particular plot type.
    # ------------------------------------------------------------------

    # A specialized scatterplot for displaying predicted versus 
    # actual values.
    if plot_type_u == "model_cont_val_preds_scatter":
        ax.scatter(
            data_x_u,
            data_y_u,
            alpha=data_alpha_main_u,
            color=data_color_main_u,
            s=0.15,
            zorder=3,
            )

        # For this type of plot, override the background color 
        # that has already been set.
        ax.patch.set_facecolor("white")

        ax.set_aspect("equal")

        # Set both x and y max values to the largest x or y value.
        xy_max_value = max(
            max(cfg.y_preds_base_target_curr),
            cfg.y_valid_slctd_df.iloc[:,0].max()
            )
        ax.set_xlim(0.0, xy_max_value)
        ax.set_ylim(0.0, xy_max_value)

        # These values may be overriden elsewhere.
        ax.set_xlabel("Predicted value")
        ax.set_ylabel("Actual target value")

        # A diagonal line between the lower left and upper right 
        # corners.
        ax.plot(
            [0.03,0.97],
            [0.03,0.97],
            transform=ax.transAxes,
            color="#fc8293",
            linewidth=0.1,
            linestyle="dashed",
            zorder=2,
            )

        # A diagonal line raised by 0.1 and the corresponding area 
        # shaded above it.
        ax.plot(
            [0.0, xy_max_value-0.1],
            [0.1, xy_max_value],
            alpha=0.4,
            color="#ff00ff",
            linewidth=0.3,
            zorder=4,
            )
        plt.fill_between(
            [0.0, xy_max_value-0.1],
            [0.1, xy_max_value],
            xy_max_value,
            alpha=0.05,
            color="#34343C",
            zorder=1,
            )

        # A diagonal line lowered by 0.1 and the corresponding area 
        # shaded beneath it.
        ax.plot(
            [0.1, xy_max_value],
            [0.0, xy_max_value-0.1],
            alpha=0.4,
            color="#ff00ff",
            linewidth=0.3,
            zorder=4,
            )
        plt.fill_between(
            [0.1, xy_max_value],
            0.0,
            [0.0, xy_max_value-0.1],
            alpha=0.05,
            color="#34343C",
            zorder=1,
            )


    # A specialized scatterplot for displaying model results
    # in the form of a continuous value with likely floor-ceiling range.
    elif plot_type_u == "model_cont_val_with_range_preds_scatter":
        ax.scatter(
            data_x_u,
            data_y_u,
            alpha=data_alpha_main_u,
            color=data_color_main_u,
            s=0.5,
            zorder=3,
            marker="o",
            )

        # For this type of plot, override the background color 
        # that has already been set.
        ax.patch.set_facecolor("white")

        # Set both x and y max values to the largest x or y value.
        x_max_value = max(data_x_u)
        y_max_value = max( max(data_y_u), abs(min(data_y_u)) )

        ax.set_xlim(0.0, 1.75)
        ax.set_ylim( -1.5, 1.5 )


        # These values may be overriden elsewhere.
        ax.set_xlabel("Predicted value")
        ax.set_ylabel("Actual target value")

        # A horizontal line at y = 0.0.
        ax.plot(
            [0.0, x_max_value],
            [0.0, 0.0],
            color="#fc8293",
            linewidth=0.05,
            linestyle="dashed",
            zorder=2,
            )

        # A diagonal upward line and the corresponding area shaded 
        # above it.
        ax.plot(
            [0.0, y_max_value],
            [0.0, y_max_value],
            alpha=0.4,
            color="#ff00ff",
            linewidth=0.3,
            #linestyle="dashed",
            zorder=4,
            )
        plt.fill_between(
            [0.0, y_max_value],
            [0.0, y_max_value],
            y_max_value,
            alpha=0.05,
            color="#34343C",
            zorder=1,
            )

        # A diagonal downward line and the corresponding area 
        # shaded below it.
        ax.plot(
            [0.0, y_max_value],
            [0.0, -y_max_value],
            alpha=0.4,
            color="#ff00ff",
            linewidth=0.3,
            #linestyle="dashed",
            zorder=4,
            )
        plt.fill_between(
            [0.0, y_max_value],
            [0.0, -y_max_value],
            -y_max_value,
            alpha=0.05,
            color="#34343C",
            zorder=1,
            )


    # A specialized scatterplot for displaying model results
    # in the form of a continuous value with likely ceiling range.
    elif plot_type_u == "model_ceiling_scatter":
        ax.scatter(
            data_x_u,
            data_y_u,
            alpha=data_alpha_main_u,
            color=data_color_main_u,
            s=0.15,
            zorder=3,
            )

        # For this type of plot, override the background color 
        # that has already been set.
        ax.patch.set_facecolor("white")

        ax.set_aspect("equal")

        # Set both x and y max values to the largest x or y value.
        xy_max_value = max(
            max(cfg.y_preds_base_target_curr),
            cfg.y_valid_slctd_df.iloc[:,0].max()
            )
        ax.set_xlim(0.0, xy_max_value)
        ax.set_ylim(0.0, xy_max_value)

        # These values may be overriden elsewhere.
        ax.set_xlabel("Predicted ceiling value")
        ax.set_ylabel("Actual target value")

        # A diagonal line between the lower left and upper right 
        # corners and the corresponding areas shaded above and below it.
        ax.plot(
            [0.0,xy_max_value],
            [0.0,xy_max_value],
            transform=ax.transAxes,
            alpha=0.15,
            color=cfg.PLOT_COLOR_DARKER_GRAY,
            linewidth=0.5,
            #linestyle="dashed",
            zorder=2,
            )
        plt.fill_between(
            [0.0,xy_max_value],
            [0.0,xy_max_value],
            xy_max_value,
            alpha=1.0,
            color=cfg.PLOT_COLOR_MAGENTA_LIGHT,
            zorder=1,
            )
        plt.fill_between(
            [0.0,xy_max_value],
            0.0,
            [0.0,xy_max_value],
            alpha=1.0,
            color=cfg.PLOT_COLOR_GREEN_LIGHT,
            zorder=1,
            )


    # A specialized scatterplot for displaying model results
    # in the form of a continuous value with likely floor range.
    elif plot_type_u == "model_floor_scatter":
        ax.scatter(
            data_x_u,
            data_y_u,
            alpha=data_alpha_main_u,
            color=data_color_main_u,
            s=0.15,
            zorder=3,
            )

        # For this type of plot, override the background color 
        # that has already been set.
        ax.patch.set_facecolor("white")

        ax.set_aspect("equal")

        # Set both x and y max values to the largest x or y value.
        xy_max_value = max(
            max(cfg.y_preds_base_target_curr),
            cfg.y_valid_slctd_df.iloc[:,0].max()
            )
        ax.set_xlim(0.0, xy_max_value)
        ax.set_ylim(0.0, xy_max_value)

        # These values may be overriden elsewhere.
        ax.set_xlabel("Predicted floor value")
        ax.set_ylabel("Actual target value")

        # A diagonal line between the lower left and upper right 
        # corners and the corresponding areas shaded above and below it.
        ax.plot(
            [0.0,xy_max_value],
            [0.0,xy_max_value],
            transform=ax.transAxes,
            alpha=0.15,
            color=cfg.PLOT_COLOR_DARKER_GRAY,
            linewidth=0.5,
            #linestyle="dashed",
            zorder=2,
            )
        plt.fill_between(
            [0.0,xy_max_value],
            [0.0,xy_max_value],
            xy_max_value,
            alpha=1.0,
            color=cfg.PLOT_COLOR_GREEN_LIGHT,
            zorder=1,
            )
        plt.fill_between(
            [0.0,xy_max_value],
            0.0,
            [0.0,xy_max_value],
            alpha=1.0,
            color=cfg.PLOT_COLOR_MAGENTA_LIGHT,
            zorder=1,
            )


    # A specialized scatterplot for displaying model results
    # in the form of a continuous value with likely ceiling range.
    elif plot_type_u == "joint_range_scatter":

        temp_df = data_x_u

        # Sort the DF's observations from the smallest predicted range 
        # to the largest.
        temp_df = temp_df.sort_values(by="range_size", ascending=True)
        temp_df = temp_df.reset_index(drop=True)

        # Plot the actual targets as dots in relation to their predicted 
        # targets.
        ax.scatter(
            temp_df.index,
            temp_df["actual_target"] - temp_df["predicted_target"],
            alpha=data_alpha_main_u,
            color=cfg.PLOT_COLOR_DARKEST_GRAY,
            s=0.25,
            zorder=3,
            )

        # Plot the predicted ceilings as dots in relation to their
        # predicted targets.
        ax.scatter(
            temp_df.index,
            temp_df["predicted_ceiling"] - temp_df["predicted_target"],
            alpha=data_alpha_main_u,
            color=cfg.PLOT_COLOR_LAVENDER,
            s=0.15,
            zorder=2,
            )

        # Plot the predicted floors as dots in relation to their
        # predicted targets.
        ax.scatter(
            temp_df.index,
            temp_df["predicted_floor"] - temp_df["predicted_target"],
            alpha=data_alpha_main_u,
            color=cfg.PLOT_COLOR_LAVENDER,
            s=0.15,
            zorder=2,
            )

        ax.set_xlim(0.0, len(temp_df))
        ax.set_ylim(-cfg.y_max_for_plotting, cfg.y_max_for_plotting)

        # Use vertical lines to fill the area above the ceiling.
        for i in temp_df.index:
            ax.vlines(
                i,
                temp_df["predicted_ceiling"].values[i] - temp_df["predicted_target"].values[i],
                cfg.y_max_for_plotting,
                alpha=1.0,
                color=cfg.PLOT_COLOR_MAGENTA_LIGHT,
                linewidth=1.0,
                zorder=0,
                )

        # Use vertical lines to fill the area below the floor.
        for i in temp_df.index:
            ax.vlines(
                i,
                -cfg.y_max_for_plotting,
                temp_df["predicted_floor"].values[i] - temp_df["predicted_target"].values[i],
                alpha=1.0,
                color=cfg.PLOT_COLOR_MAGENTA_LIGHT,
                linewidth=1.0,
                zorder=0,
                )

        # Plot the area between the floor and ceiling.
        for i in temp_df.index:
            ax.vlines(
                i,
                temp_df["predicted_ceiling"].values[i] - temp_df["predicted_target"].values[i],
                temp_df["predicted_floor"].values[i] - temp_df["predicted_target"].values[i],
                alpha=1.0,
                color=cfg.PLOT_COLOR_GREEN_LIGHT,
                linewidth=1.0,
                zorder=0,
                )

        # Plot a horizontal line at y=0.
        ax.axhline(
            y=0,
            alpha=0.85,
            color=cfg.PLOT_COLOR_GOLD,
            linewidth=1.1,
            linestyle="dashed",
            zorder=7,
            )


        # For this type of plot, override the background color 
        # that has already been set.
        ax.patch.set_facecolor("white")

    # ------------------------------------------------------------------
    # Add supplemental plot elements that are common to (almost) 
    # all plot types.
    # ------------------------------------------------------------------
    if xy_tick_labels_custom_fontsize_u is None:
        plot_xy_tick_label_fontsize_to_use = cfg.PLOT_XY_TICKS_FONTSIZE
    else:
        plot_xy_tick_label_fontsize_to_use = xy_tick_labels_custom_fontsize_u

    plt.xlabel(
        x_axis_label_u,
        fontsize=cfg.PLOT_XY_LABEL_FONTSIZE,
        labelpad=cfg.PLOT_XY_LABEL_PAD,
        color=cfg.PLOT_COLOR_LAVENDER_DARK,
        )

    if plot_type_u == "joint_range_scatter":
        ax.xaxis.set_tick_params(labelbottom=False)
        ax.set_xticks([])
    else:
        plt.xticks(
            fontsize=plot_xy_tick_label_fontsize_to_use,
            color=cfg.PLOT_COLOR_SALMON_DARK,
            rotation = x_tick_labels_rotation_u,
            )

    plt.ylabel(
        y_axis_label_u,
        fontsize=cfg.PLOT_XY_LABEL_FONTSIZE,
        labelpad=cfg.PLOT_XY_LABEL_PAD,
        color=cfg.PLOT_COLOR_LAVENDER_DARK,
        )
    plt.yticks(
        fontsize=plot_xy_tick_label_fontsize_to_use,
        color=cfg.PLOT_COLOR_SALMON_DARK,
        )
    plt.title(
        plot_title_u,
        fontsize=cfg.PLOT_TITLE_FONTSIZE,
        color=cfg.PLOT_COLOR_DARKER_GRAY,
        )

    plt.grid(visible=None)
    ax.grid(False)
    ax.spines[['top', 'bottom', 'left', 'right']].set_visible(False)

    # Add a WorkforceSim logo.
    if logo_location_u is not None:
        logo_path_and_name = os.path.join(
            cfg.GRAPHICS_DIR,
            "Comport_AI_logo_01L.png"
            )
        im = plt.imread(logo_path_and_name)
        imagebox = OffsetImage(im, zoom=0.01)
        ab = AnchoredOffsetbox(
            loc=logo_location_u,
            child=imagebox,
            frameon=False
            )
        logo_ax = fig.add_axes([0,0,1,1])
        logo_ax.grid(False)
        logo_ax.spines[
            ['top', 'bottom', 'left', 'right']
            ].set_visible(False)
        logo_ax.set(xticklabels=[])
        logo_ax.set(yticklabels=[])
        logo_ax.tick_params(left=False, bottom=False)
        plt.gca().set_position([0,0,1,1])
        logo_ax.set_facecolor('none')
        logo_ax.add_artist(ab)

    y_min, y_max = ax.get_ylim()

    if (cfg.mae is not None) and (cfg.mse is not None):
        ax.text(
            0.03,
            y_max * 0.97,
            " MAE: " + str(round(cfg.mae, 5)) + "\n MSE: " + str(round(cfg.mse, 5)),
            horizontalalignment="left",
            verticalalignment="top",
            fontsize=7,
            color=cfg.PLOT_COLOR_LAVENDER_DARK,
            )
    elif (cfg.patgtc is not None) and (cfg.amorpdac is not None) \
            and (cfg.amirpdbc is not None) and (cfg.oce is not None):
        ax.text(
            0.03,
            y_max * 0.97,
            " PATGTC: " + str(round(cfg.patgtc, 5)) + "\n AMORPDAC: " + str(round(cfg.amorpdac, 5)) \
                + "\n AMIRPDBC: " + str(round(cfg.amirpdbc, 5)) + "\n Overall Ceiling Error: " + str(round(cfg.oce, 5)),
            horizontalalignment="left",
            verticalalignment="top",
            fontsize=7,
            color=cfg.PLOT_COLOR_LAVENDER_DARK,
            )
    elif (cfg.patltf is not None) and (cfg.amorpdbf is not None) \
            and (cfg.amirpdaf is not None) and (cfg.ofe is not None):
        ax.text(
            0.03,
            y_max * 0.97,
            " PATLTF: " + str(round(cfg.patltf, 5)) + "\n AMORPDBF: " + str(round(cfg.amorpdbf, 5)) \
                + "\n AMIRPDAF: " + str(round(cfg.amirpdaf, 5)) + "\n Overall Floor Error: " + str(round(cfg.ofe, 5)),
            horizontalalignment="left",
            verticalalignment="top",
            fontsize=7,
            color=cfg.PLOT_COLOR_LAVENDER_DARK,
            )
    elif (cfg.mprs is not None) and (cfg.irp is not None) \
            and (cfg.pator is not None) and (cfg.msadre is not None) \
            and (cfg.orp is not None):
        ax.text(
            0.03,
            y_max * 0.97,
            " MPRS: " + str(round(cfg.mprs, 5)) \
                + "\n IRP: " + str(round(cfg.irp, 5)) \
                + "\n PATOR: " + str(round(cfg.pator, 5)) \
                + "\n MSADRE: " + str(round(cfg.msadre, 5)) \
                + "\n Overall Range Performance: " + str(round(cfg.orp, 5)),
            horizontalalignment="left",
            verticalalignment="top",
            fontsize=7,
            color=cfg.PLOT_COLOR_LAVENDER_DARK,
            )


    warnings.filterwarnings(
        "ignore", 
        message="This figure includes Axes that are not compatible with " \
            + "tight_layout, so results might be incorrect.")

    # This is needed to prevent x- and y-tick labels from sometimes 
    # being cut off.
    fig.tight_layout()

    # ------------------------------------------------------------------
    # Save the plot to a PNG file.
    # ------------------------------------------------------------------

    # Save the plot as a PNG.
    plt.savefig(
        os.path.join(
            cfg.PLOTS_DIR,
            name_for_file_u,
            ),
        bbox_inches='tight',
        facecolor=fig.get_facecolor(),
        edgecolor='none',
        )

    fig.clf()
    plt.close(fig)


# ----------------------------------------------------------------------
# Below are the more particular functions for generating specific
# types of plots, each of which calls the the generalized plotting 
# function generate_plot_return_png_and_save_to_file, while passing to 
# it arguments specific to its particular type of plot.
# ----------------------------------------------------------------------

def plot_model_results_scatter(
    data_x_u,
    data_y_u,
    ):
    """
    Requests generation of a plot of a model's predicted target values
    and actual target values.

    PARAMETERS
    ----------
    data_x_u
        The x-values for plotting
    data_y_u
        The y-values for plotting
    """

    model_title_for_plot_display \
        = cfg.models_info_dict[cfg.model_id_curr]["model_title_for_plot_display"]
    model_name_for_plot_filename \
        = cfg.models_info_dict[cfg.model_id_curr]["model_name_for_plot_filename"]

    try:
        generate_plot_return_png_and_save_to_file(
            "model_cont_val_preds_scatter",
            data_x_u,
            data_y_u,
            'Predicted target value',
            'Actual target value',
            0,
            None,
            cfg.PLOT_COLOR_BLACK,
            0.65,
            None,
            "Actual target values vs. predictions generated by the " + model_title_for_plot_display,
            "lower left",
            "png_plt_Eff_preds_from_" + model_name_for_plot_filename + ".png",
            )
    except:
        pass


def plot_model_results_floor_or_ceiling_scatter(
    data_x_u,
    data_y_u,
    floor_or_ceiling_u
    ):
    """
    Requests generation of a plot of a model's predicted floor or
    ceiling values and the actual target values.

    PARAMETERS
    ----------
    data_x_u
        The x-values for plotting
    data_y_u
        The y-values for plotting
    floor_or_ceiling_u : str
        Whether the plot is floor a predicted "floor" or "ceiling"
    """

    model_title_for_plot_display \
        = cfg.models_info_dict[cfg.model_id_curr]["model_title_for_plot_display"]
    model_name_for_plot_filename \
        = cfg.models_info_dict[cfg.model_id_curr]["model_name_for_plot_filename"]

    try:
        generate_plot_return_png_and_save_to_file(
            "model_" + floor_or_ceiling_u + "_scatter",
            data_x_u,
            data_y_u,
            "Predicted " + floor_or_ceiling_u + " value",
            'Actual target value',
            0,
            None,
            cfg.PLOT_COLOR_BLACK,
            0.65,
            None,
            "Aptness of " + floor_or_ceiling_u + " predictions generated by " + model_title_for_plot_display,
            "lower left",
            "png_plt_Eff_preds_from_" + model_name_for_plot_filename + ".png",
            )
    except:
        pass


def plot_model_results_joint_range_scatter(
    data_df_u,
    model_title_for_plot_display_u,
    model_name_for_plot_filename_u,
    ):
    """
    Requests generation of a plot of a joint range model's predicted 
    floor and ceiling values, along with the actual target values.

    PARAMETERS
    ----------
    data_df_u
        The DataFrame with the data to be plotted
    model_title_for_plot_display_u
        The title to display on the plot
    model_name_for_plot_filename_u
        The name for use in saving the plot as a PNG
    """

    generate_plot_return_png_and_save_to_file(
        "joint_range_scatter", # plot type ("column", "hist", "hist2d", "line", "scatter")
        data_df_u, # x-series of the data
        None, # y-series of the data (optional)
        'Prediction instance', # x-axis label
        'Actual target value (black dot) in relation to predicted target value (y=0),\nwith predicted ceiling and floor (lavender points at top and bottom of green range)', # y-axis label (optional)
        0, # rotation of x-tick labels
        None, # custom xy-tick label fontsize
        cfg.PLOT_COLOR_BLACK, # the main color for the data
        0.65, # optional alpha value for the data
        None, # the color mapping for use in legend
        "Aptness of predicted range yielded by " + model_title_for_plot_display_u, # title for the whole plot
        "lower left", # location for CAI logo (e.g., "upper right")
        "Range " + model_name_for_plot_filename_u + ".png", # name for PNG file
        )


# ██████████████████████████████████████████████████████████████████████