From 0e233516439501e8a1eff6630ec5cc8d3b9b4d71 Mon Sep 17 00:00:00 2001 From: bikegeek Date: Wed, 21 Jan 2026 13:53:04 -0700 Subject: [PATCH 01/28] Issue #556 this is the original BasePlot class, containing Plotly and Matplotlib support --- metplotpy/plots/base_plot_plotly.py | 495 ++++++++++++++++++++++++++++ 1 file changed, 495 insertions(+) create mode 100644 metplotpy/plots/base_plot_plotly.py diff --git a/metplotpy/plots/base_plot_plotly.py b/metplotpy/plots/base_plot_plotly.py new file mode 100644 index 00000000..03a84ef1 --- /dev/null +++ b/metplotpy/plots/base_plot_plotly.py @@ -0,0 +1,495 @@ +# ============================* + # ** Copyright UCAR (c) 2020 + # ** University Corporation for Atmospheric Research (UCAR) + # ** National Center for Atmospheric Research (NCAR) + # ** Research Applications Lab (RAL) + # ** P.O.Box 3000, Boulder, Colorado, 80307-3000, USA + # ============================* + + + +# !/usr/bin/env conda run -n blenny_363 python +""" +Class Name: base_plot.py + """ +__author__ = 'Tatiana Burek' + +import os +import logging +import warnings +import numpy as np +import yaml +from typing import Union +import kaleido +import metplotpy.plots.util +from metplotpy.plots.util import strtobool +from .config import Config +from metplotpy.plots.context_filter import ContextFilter + +# kaleido 0.x will be deprecated after September 2025 and Chrome will no longer +# be included with kaleido from version 1.0.0. Explicitly get Chrome via call to kaleido. + +# In some instances, we do NOT want Chrome to be installed at run-time. If the +# PRE_LOAD_CHROME environment variable exists, or set to TRUE, +# then Chrome will be assumed to have been pre-loaded. Otherwise, +# invoke get_chrome_sync() to install Chrome in the +# /path-to-python-libs/pythonx.yz/site-packages/... directory + +# Check if the PRE_LOAD_CHROME env variable exists +aquire_chrome = False + +turn_on_logging = strtobool('LOG_BASE_PLOT') +# Log when Chrome is downloaded at runtime +if turn_on_logging is True: + log = logging.getLogger("base_plot") + log.setLevel(logging.INFO) + + formatter = logging.Formatter("%(asctime)s [%(levelname)s] | %(name)s | %(message)s") + + # set the WRITE_LOG env var to True to save the log message to a + # separate log file + write_log = strtobool('WRITE_LOG') + if write_log is True: + file_handler = logging.FileHandler("./base_plot.log") + file_handler.setFormatter(formatter) + log.addHandler(file_handler) + +# Only load Chrome at run-time if PRE_LOAD_CHROME is False or not defined. +# Some applications may not want to load Chrome at runtime and +# will set the PRE_LOAD_CHROME to True to indicate that it is already +# loaded/downloaded prior to runtime. +chrome_env =strtobool ('PRE_LOAD_CHROME') +if chrome_env is False: + aquire_chrome=True + kaleido.get_chrome_sync() + + +# Log when kaleido is downloading Chrome +if aquire_chrome is True and turn_on_logging is True: + log.info("Plotly kaleido is loading Chrome at run time") + +class BasePlot: + """A class that provides methods for building Plotly plot's common features + like title, axis, legend. + + To use: + use as an abstract class for the common plot types + """ + + # image formats supported by plotly + IMAGE_FORMATS = ("png", "jpeg", "webp", "svg", "pdf", "eps") + DEFAULT_IMAGE_FORMAT = 'png' + + def __init__(self, parameters, default_conf_filename): + """Inits BasePlot with user defined and default dictionaries. + Removes the old image if it exists + + Args: + @param parameters - dictionary containing user defined parameters + @param default_conf_filename - the name of the default config file + for the plot type that is a subclass. + + + """ + + # Determine location of the default YAML config files and then + # read defaults stored in YAML formatted file into the dictionary + if 'METPLOTPY_BASE' in os.environ: + location = os.path.join(os.environ['METPLOTPY_BASE'], 'metplotpy/plots/config') + else: + location = os.path.realpath(os.path.join(os.path.dirname(__file__), 'config')) + + with open(os.path.join(location, default_conf_filename), 'r') as stream: + try: + defaults = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as exc: + print(exc) + + # merge user defined parameters into defaults if they exist + if parameters: + self.parameters = {**defaults, **parameters} + else: + self.parameters = defaults + + self.figure = None + self.remove_file() + self.config_obj = Config(self.parameters) + + def get_image_format(self): + """Reads the image format type from user provided image name. + Uses file extension as a type. If the file extension is not valid - + returns 'png' as a default + + Args: + + Returns: + - image format + """ + + # get image name from properties + image_name = self.get_config_value('image_name') + if image_name: + + # extract and validate the file extension + strings = image_name.split('.') + if strings and strings[-1] in self.IMAGE_FORMATS: + return strings[-1] + + # print the message if invalid and return default + print('Unrecognised image format. png will be used') + return self.DEFAULT_IMAGE_FORMAT + + def get_legend(self): + """Creates a Plotly legend dictionary with values from users and default parameters + If users parameters dictionary doesn't have needed values - use defaults + + Args: + + Returns: + - dictionary used by Plotly to build the legend + """ + + current_legend = dict( + x=self.get_config_value('legend', 'x'), # x-position + y=self.get_config_value('legend', 'y'), # y-position + font=dict( + family=self.get_config_value('legend', 'font', 'family'), # font family + size=self.get_config_value('legend', 'font', 'size'), # font size + color=self.get_config_value('legend', 'font', 'color'), # font color + ), + bgcolor=self.get_config_value('legend', 'bgcolor'), # background color + bordercolor=self.get_config_value('legend', 'bordercolor'), # border color + borderwidth=self.get_config_value('legend', 'borderwidth'), # border width + xanchor=self.get_config_value('legend', 'xanchor'), # horizontal position anchor + yanchor=self.get_config_value('legend', 'yanchor') # vertical position anchor + ) + return current_legend + + def get_legend_style(self): + """ + Retrieve the legend style settings that are set + in the METviewer tool + + Args: + + Returns: + - a dictionary that holds the legend settings that + are set in METviewer + """ + legend_box = self.get_config_value('legend_box').lower() + if legend_box == 'o': + # Draws a box around the legend + borderwidth = 1 + elif legend_box == 'n': + # Do not draw border around the legend labels. + borderwidth = 0 + + legend_ncol = self.get_config_value('legend_ncol') + if legend_ncol > 1: + legend_orientation = "h" + else: + legend_orientation = "v" + legend_inset = self.get_config_value('legend_inset') + legend_size = self.get_config_value('legend_size') + legend_settings = dict(border_width=borderwidth, + orientation=legend_orientation, + legend_inset=dict(x=legend_inset['x'], + y=legend_inset['y']), + legend_size=legend_size) + + return legend_settings + + def get_title(self): + """Creates a Plotly title dictionary with values from users and default parameters + If users parameters dictionary doesn't have needed values - use defaults + + Args: + + Returns: + - dictionary used by Plotly to build the title + """ + current_title = dict( + text=self.get_config_value('title'), # plot's title + # Sets the container `x` refers to. "container" spans the entire `width` of the plot. + # "paper" refers to the width of the plotting area only. + xref="paper", + x=0.5 # x position with respect to `xref` + ) + return current_title + + def get_xaxis(self): + """Creates a Plotly x-axis dictionary with values from users and default parameters + If users parameters dictionary doesn't have needed values - use defaults + + Args: + + Returns: + - dictionary used by Plotly to build the x-axis + """ + current_xaxis = dict( + linecolor=self.get_config_value('xaxis', 'linecolor'), # x-axis line color + # whether or not a line bounding x-axis is drawn + showline=self.get_config_value('xaxis', 'showline'), + linewidth=self.get_config_value('xaxis', 'linewidth') # width (in px) of x-axis line + ) + return current_xaxis + + def get_yaxis(self): + """Creates a Plotly y-axis dictionary with values from users and default parameters + If users parameters dictionary doesn't have needed values - use defaults + + Args: + + Returns: + - dictionary used by Plotly to build the y-axis + """ + current_yaxis = dict( + linecolor=self.get_config_value('yaxis', 'linecolor'), # y-axis line color + linewidth=self.get_config_value('yaxis', 'linewidth'), # width (in px) of y-axis line + # whether or not a line bounding y-axis is drawn + showline=self.get_config_value('yaxis', 'showline'), + # whether or not grid lines are drawn + showgrid=self.get_config_value('yaxis', 'showgrid'), + ticks=self.get_config_value('yaxis', 'ticks'), # whether ticks are drawn or not. + tickwidth=self.get_config_value('yaxis', 'tickwidth'), # Sets the tick width (in px). + tickcolor=self.get_config_value('yaxis', 'tickcolor'), # Sets the tick color. + # the width (in px) of the grid lines + gridwidth=self.get_config_value('yaxis', 'gridwidth'), + gridcolor=self.get_config_value('yaxis', 'gridcolor') # the color of the grid lines + ) + + # Sets the range of the range slider. defaults to the full y-axis range + y_range = self.get_config_value('yaxis', 'range') + if y_range is not None: + current_yaxis['range'] = y_range + return current_yaxis + + def get_xaxis_title(self): + """Creates a Plotly x-axis label title dictionary with values + from users and default parameters. + If users parameters dictionary doesn't have needed values - use defaults + + Args: + + Returns: + - dictionary used by Plotly to build the x-axis label title as annotation + """ + x_axis_label = dict( + x=self.get_config_value('xaxis', 'x'), # x-position of label + y=self.get_config_value('xaxis', 'y'), # y-position of label + showarrow=False, + text=self.get_config_value('xaxis', 'title', 'text'), + xref="paper", # the annotation's x coordinate axis + yref="paper", # the annotation's y coordinate axis + font=dict( + family=self.get_config_value('xaxis', 'title', 'font', 'family'), + size=self.get_config_value('xaxis', 'title', 'font', 'size'), + color=self.get_config_value('xaxis', 'title', 'font', 'color'), + ) + ) + return x_axis_label + + def get_yaxis_title(self): + """Creates a Plotly y-axis label title dictionary with values + from users and default parameters + If users parameters dictionary doesn't have needed values - use defaults + + Args: + + Returns: + - dictionary used by Plotly to build the y-axis label title as annotation + """ + y_axis_label = dict( + x=self.get_config_value('yaxis', 'x'), # x-position of label + y=self.get_config_value('yaxis', 'y'), # y-position of label + showarrow=False, + text=self.get_config_value('yaxis', 'title', 'text'), + textangle=-90, # the angle at which the `text` is drawn with respect to the horizontal + xref="paper", # the annotation's x coordinate axis + yref="paper", # the annotation's y coordinate axis + font=dict( + family=self.get_config_value('xaxis', 'title', 'font', 'family'), + size=self.get_config_value('xaxis', 'title', 'font', 'size'), + color=self.get_config_value('xaxis', 'title', 'font', 'color'), + ) + ) + return y_axis_label + + def get_config_value(self, *args): + """Gets the value of a configuration parameter. + Looks for parameter in the user parameter dictionary + + Args: + @ param args - chain of keys that defines a key to the parameter + + Returns: + - a value for the parameter of None + """ + + return self._get_nested(self.parameters, args) + + def _get_nested(self, data, args): + """Recursive function that uses the tuple with keys to find a value + in multidimensional dictionary. + + Args: + @data - dictionary for the lookup + @args - a tuple with keys + + Returns: + - a value for the parameter of None + """ + + if args and data: + + # get value for the first key + element = args[0] + if element: + value = data.get(element) + + # if the size of key tuple is 1 - the search is over + if len(args) == 1: + return value + + # if the size of key tuple is > 1 - search using other keys + return self._get_nested(value, args[1:]) + return None + + def get_img_bytes(self): + """Returns an image as a bytes object in a format specified in the config file + + Args: + + Returns: + - an image as a bytes object + """ + if self.figure: + return self.figure.to_image(format=self.get_config_value('image_format'), + width=self.get_config_value('width'), + height=self.get_config_value('height'), + scale=self.get_config_value('scale')) + + return None + + def save_to_file(self): + """Saves the image to a file specified in the config file. + Prints a message if fails + + Args: + + Returns: + + """ + image_name = self.get_config_value('plot_filename') + + # Suppress deprecation warnings from third-party packages that are not in our control. + warnings.filterwarnings("ignore", category=DeprecationWarning) + + # Create the directory for the output plot if it doesn't already exist + dirname = os.path.dirname(os.path.abspath(image_name)) + if not os.path.exists(dirname): + os.mkdir(dirname) + if self.figure: + try: + self.figure.write_image(image_name) + except FileNotFoundError: + self.logger.error(f"FileNotFoundError: Cannot save to file" + f" {image_name}") + # print("Can't save to file " + image_name) + except ResourceWarning: + self.logger.warning(f"ResourceWarning: in _kaleido" + f" {image_name}") + + except ValueError as ex: + self.logger.error(f"ValueError: Could not save output file.") + else: + self.logger.error(f"The figure {dirname} cannot be saved.") + print("Oops! The figure was not created. Can't save.") + + def remove_file(self): + """Removes previously made image file . + """ + image_name = self.get_config_value('plot_filename') + + # remove the old file if it exist + if image_name is not None and os.path.exists(image_name): + os.remove(image_name) + + def show_in_browser(self): + """Creates a plot and opens it in the browser. + + Args: + + Returns: + + """ + if self.figure: + self.figure.show() + else: + self.logger.error(" Figure not created. Nothing to show in the " + "browser. ") + print("Oops! The figure was not created. Can't show") + + def _add_lines(self, config_obj: Config, x_points_index: Union[list, None] = None) -> None: + """ Adds custom horizontal and/or vertical line to the plot. + All line's metadata is in the config_obj.lines + Args: + @config_obj - plot's configurations + @x_points_index - list of x-values that are used to create a plot + Returns: + """ + if hasattr(config_obj, 'lines') and config_obj.lines is not None: + shapes = [] + for line in config_obj.lines: + # draw horizontal line + if line['type'] == 'horiz_line': + shapes.append(dict( + type='line', + yref='y', y0=line['position'], y1=line['position'], + xref='paper', x0=0, x1=0.95, + line={'color': line['color'], + 'dash': line['line_style'], + 'width': line['line_width']}, + )) + elif line['type'] == 'vert_line': + # draw vertical line + try: + if x_points_index is None: + val = line['position'] + else: + ordered_indy_label = config_obj.create_list_by_plot_val_ordering(config_obj.indy_label) + index = ordered_indy_label.index(line['position']) + val = x_points_index[index] + shapes.append(dict( + type='line', + yref='paper', y0=0, y1=1, + xref='x', x0=val, x1=val, + line={'color': line['color'], + 'dash': line['line_style'], + 'width': line['line_width']}, + )) + except ValueError: + line_position = line["position"] + self.logger.warning(f" Vertical line with position " + f"{line_position} cannot be created.") + print(f'WARNING: vertical line with position ' + f'{line_position} can\'t be created') + # ignore everything else + + # draw lines + self.figure.update_layout(shapes=shapes) + + @staticmethod + def get_array_dimensions(data): + """Returns the dimension of the array + + Args: + @param data - input array + Returns: + - an integer representing the array's dimension or None + """ + if data is None: + return None + + np_array = np.array(data) + return len(np_array.shape) From df0dc9e375f6db94111e8af913d0f7d5f9303350 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:25:06 -0700 Subject: [PATCH 02/28] Per #556, create _plotly versions of util.py, constants.py, and config.py. Updated plots that use plotly to import from those versions so it is clear which plots still rely on plotly and need to be updated. This will also allow optional support of plotly for certain plots if we are not able to fully get rid of the plotly dependency in this development cycle. Also removed some unused imports. Replaced util.py function apply_weight_style with get_font_params since the existing version will not be able to be used with matplotlib --- metplotpy/plots/bar/bar.py | 4 +- metplotpy/plots/bar/bar_config.py | 6 +- metplotpy/plots/bar/bar_series.py | 2 +- metplotpy/plots/base_plot_plotly.py | 4 +- metplotpy/plots/box/box.py | 6 +- metplotpy/plots/box/box_config.py | 6 +- metplotpy/plots/box/box_series.py | 4 +- metplotpy/plots/config.py | 7 - metplotpy/plots/config_plotly.py | 897 ++++++++++++++++++ metplotpy/plots/constants.py | 4 +- metplotpy/plots/constants_plotly.py | 100 ++ metplotpy/plots/contour/contour.py | 6 +- metplotpy/plots/contour/contour_config.py | 6 +- metplotpy/plots/contour/contour_series.py | 5 +- metplotpy/plots/eclv/eclv.py | 6 +- metplotpy/plots/eclv/eclv_series.py | 5 +- metplotpy/plots/ens_ss/ens_ss.py | 4 +- metplotpy/plots/ens_ss/ens_ss_config.py | 6 +- metplotpy/plots/ens_ss/ens_ss_series.py | 5 +- .../equivalence_testing_bounds.py | 7 +- .../equivalence_testing_bounds_series.py | 8 +- metplotpy/plots/histogram/hist.py | 6 +- metplotpy/plots/histogram/hist_config.py | 6 +- metplotpy/plots/histogram/hist_series.py | 5 +- metplotpy/plots/histogram/histogram.py | 3 +- metplotpy/plots/histogram/prob_hist.py | 2 +- metplotpy/plots/histogram/rank_hist.py | 3 +- metplotpy/plots/histogram/rel_hist.py | 3 +- metplotpy/plots/histogram_2d/histogram_2d.py | 4 +- metplotpy/plots/line/line.py | 7 +- metplotpy/plots/line/line_config.py | 6 +- metplotpy/plots/line/line_series.py | 18 +- metplotpy/plots/mpr_plot/mpr_plot.py | 4 +- metplotpy/plots/polar_plot/polar_plot.py | 4 +- .../plots/reliability_diagram/reliability.py | 6 +- .../reliability_diagram/reliability_config.py | 6 +- metplotpy/plots/revision_box/revision_box.py | 6 +- .../plots/revision_box/revision_box_config.py | 6 +- .../plots/revision_series/revision_series.py | 7 +- .../revision_series/revision_series_config.py | 6 +- metplotpy/plots/roc_diagram/roc_diagram.py | 16 +- .../plots/roc_diagram/roc_diagram_config.py | 8 +- .../plots/roc_diagram/roc_diagram_series.py | 2 +- metplotpy/plots/scatter/scatter.py | 6 +- metplotpy/plots/scatter/scatter_config.py | 1 - metplotpy/plots/tcmpr_plots/box/tcmpr_box.py | 2 +- .../plots/tcmpr_plots/box/tcmpr_box_point.py | 2 +- .../plots/tcmpr_plots/box/tcmpr_point.py | 2 +- .../tcmpr_plots/line/mean/tcmpr_line_mean.py | 2 +- .../line/mean/tcmpr_series_line_mean.py | 2 +- .../line/median/tcmpr_line_median.py | 2 +- .../plots/tcmpr_plots/line/tcmpr_line.py | 2 +- .../plots/tcmpr_plots/rank/tcmpr_rank.py | 3 +- .../tcmpr_plots/relperf/tcmpr_relperf.py | 5 +- .../skill/mean/tcmpr_series_skill_mean.py | 2 +- .../skill/mean/tcmpr_skill_mean.py | 2 +- .../skill/median/tcmpr_skill_median.py | 2 +- .../plots/tcmpr_plots/skill/tcmpr_skill.py | 2 +- metplotpy/plots/tcmpr_plots/tcmpr.py | 10 +- metplotpy/plots/tcmpr_plots/tcmpr_config.py | 7 +- metplotpy/plots/tcmpr_plots/tcmpr_series.py | 2 +- metplotpy/plots/util.py | 82 +- metplotpy/plots/util_plotly.py | 698 ++++++++++++++ metplotpy/plots/wind_rose/wind_rose.py | 4 +- 64 files changed, 1858 insertions(+), 214 deletions(-) create mode 100644 metplotpy/plots/config_plotly.py create mode 100644 metplotpy/plots/constants_plotly.py create mode 100644 metplotpy/plots/util_plotly.py diff --git a/metplotpy/plots/bar/bar.py b/metplotpy/plots/bar/bar.py index cb881886..f62dd456 100644 --- a/metplotpy/plots/bar/bar.py +++ b/metplotpy/plots/bar/bar.py @@ -23,10 +23,10 @@ from plotly.subplots import make_subplots import metcalcpy.util.utils as calc_util -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util from metplotpy.plots.bar.bar_config import BarConfig from metplotpy.plots.bar.bar_series import BarSeries -from metplotpy.plots.base_plot import BasePlot +from metplotpy.plots.base_plot_plotly import BasePlot from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ PLOTLY_PAPER_BGCOOR diff --git a/metplotpy/plots/bar/bar_config.py b/metplotpy/plots/bar/bar_config.py index 5a6436ac..091d1214 100644 --- a/metplotpy/plots/bar/bar_config.py +++ b/metplotpy/plots/bar/bar_config.py @@ -17,9 +17,9 @@ import itertools -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/bar/bar_series.py b/metplotpy/plots/bar/bar_series.py index f455bd15..aa73bd23 100644 --- a/metplotpy/plots/bar/bar_series.py +++ b/metplotpy/plots/bar/bar_series.py @@ -20,7 +20,7 @@ from pandas import DataFrame import metcalcpy.util.utils as utils -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util from .. import GROUP_SEPARATOR from ..series import Series diff --git a/metplotpy/plots/base_plot_plotly.py b/metplotpy/plots/base_plot_plotly.py index 03a84ef1..ccb915ed 100644 --- a/metplotpy/plots/base_plot_plotly.py +++ b/metplotpy/plots/base_plot_plotly.py @@ -21,8 +21,8 @@ import yaml from typing import Union import kaleido -import metplotpy.plots.util -from metplotpy.plots.util import strtobool + +from metplotpy.plots.util_plotly import strtobool from .config import Config from metplotpy.plots.context_filter import ContextFilter diff --git a/metplotpy/plots/box/box.py b/metplotpy/plots/box/box.py index 6c41762a..e798fc39 100644 --- a/metplotpy/plots/box/box.py +++ b/metplotpy/plots/box/box.py @@ -28,11 +28,11 @@ import metcalcpy.util.utils as calc_util -from metplotpy.plots.base_plot import BasePlot +from metplotpy.plots.base_plot_plotly import BasePlot from metplotpy.plots.box.box_config import BoxConfig from metplotpy.plots.box.box_series import BoxSeries -from metplotpy.plots import util -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR +from metplotpy.plots import util_plotly as util +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR class Box(BasePlot): diff --git a/metplotpy/plots/box/box_config.py b/metplotpy/plots/box/box_config.py index 410fc8c3..a576ef65 100644 --- a/metplotpy/plots/box/box_config.py +++ b/metplotpy/plots/box/box_config.py @@ -17,9 +17,9 @@ import itertools -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/box/box_series.py b/metplotpy/plots/box/box_series.py index 9546be9a..a0434f3a 100644 --- a/metplotpy/plots/box/box_series.py +++ b/metplotpy/plots/box/box_series.py @@ -22,7 +22,7 @@ from pandas import DataFrame import metcalcpy.util.utils as utils -import metplotpy.plots.util +import metplotpy.plots.util_plotly as util from ..series import Series @@ -215,7 +215,7 @@ def _calculate_derived_values(self, log_level = self.config.log_level log_filename = self.config.log_filename - logger = metplotpy.plots.util.get_common_logger(log_level, log_filename) + logger = util.get_common_logger(log_level, log_filename) logger.info(f"Start calculating derived values: " diff --git a/metplotpy/plots/config.py b/metplotpy/plots/config.py index 9f1f9f7f..d35dc70e 100644 --- a/metplotpy/plots/config.py +++ b/metplotpy/plots/config.py @@ -881,13 +881,6 @@ def _get_lines(self) -> Union[list, None]: # convert position to string if line_type=vert_line line['position'] = str(line['position']) - # convert line_style - line_style = line['line_style'] - if line_style in constants.LINE_STYLE_TO_PLOTLY_DASH.keys(): - line['line_style'] = constants.LINE_STYLE_TO_PLOTLY_DASH[line_style] - else: - line['line_style'] = None - # convert line_width to float try: line['line_width'] = float(line['line_width']) diff --git a/metplotpy/plots/config_plotly.py b/metplotpy/plots/config_plotly.py new file mode 100644 index 00000000..874d20be --- /dev/null +++ b/metplotpy/plots/config_plotly.py @@ -0,0 +1,897 @@ +# ============================* + # ** Copyright UCAR (c) 2020 + # ** University Corporation for Atmospheric Research (UCAR) + # ** National Center for Atmospheric Research (NCAR) + # ** Research Applications Lab (RAL) + # ** P.O.Box 3000, Boulder, Colorado, 80307-3000, USA + # ============================* + + + +""" +Class Name: config.py + +Holds values set in the config file(s) +""" +__author__ = 'Minna Win' + +import itertools +from typing import Union + +import metcalcpy.util.utils as utils +import metplotpy.plots.util_plotly as util +from . import constants_plotly as constants + + +class Config: + """ + Handles reading in and organizing configuration settings in the yaml configuration file. + """ + + def __init__(self, parameters): + + self.parameters = parameters + + # Logging + self.log_filename = self.get_config_value('log_filename') + self.log_level = self.get_config_value('log_level') + self.logger = util.get_common_logger(self.log_level, self.log_filename) + + # + # Configuration settings that apply to the plot + # + self.output_image = self.get_config_value('plot_filename') + self.title_font = constants.DEFAULT_TITLE_FONT + self.title_color = constants.DEFAULT_TITLE_COLOR + self.xaxis = self.get_config_value('xaxis') + self.yaxis_1 = self.get_config_value('yaxis_1') + self.yaxis_2 = self.get_config_value('yaxis_2') + self.title = self.get_config_value('title') + self.use_ee = self._get_bool('event_equal') + self.indy_vals = self.get_config_value('indy_vals') + self.indy_label = self._get_indy_label() + self.indy_var = self.get_config_value('indy_var') + self.show_plot_in_browser = self.get_config_value('show_plot_in_browser') + + # Plot figure dimensions can be in either inches or pixels + pixels = self.get_config_value('plot_units') + plot_width = self.get_config_value('plot_width') + self.plot_width = self.calculate_plot_dimension(plot_width, pixels) + plot_height = self.get_config_value('plot_height') + self.plot_height = self.calculate_plot_dimension(plot_height, pixels) + self.plot_caption = self.get_config_value('plot_caption') + # plain text, bold, italic, bold italic are choices in METviewer UI + self.caption_weight = self.get_config_value('caption_weight') + self.caption_color = self.get_config_value('caption_col') + # relative magnification + self.caption_size = self.get_config_value('caption_size') + + # up-down location relative to the x-axis line + self.caption_offset = self.get_config_value('caption_offset') + # left-right position + self.caption_align = self.get_config_value('caption_align') + + # legend style settings as defined in METviewer + user_settings = self._get_legend_style() + + # list of the x, y + # bbox_to_anchor() setting used in determining + # the location of the bounding box which defines + # the legend. + + bbox_x = user_settings.get('bbox_x') + if bbox_x is not None: + self.bbox_x = float(user_settings['bbox_x']) + + bbox_y = user_settings.get('bbox_y') + if bbox_y is not None: + self.bbox_y = float(user_settings['bbox_y']) + + legend_magnification = user_settings.get('legend_size') + if legend_magnification is not None: + self.legend_size = int(constants.DEFAULT_LEGEND_FONTSIZE * legend_magnification) + + self.legend_ncol = self.get_config_value('legend_ncol') + legend_box = self.get_config_value('legend_box') + self.draw_box = False + if legend_box is not None: + legend_box = legend_box.lower() + if legend_box == 'o': + # Don't draw a box around legend labels + self.draw_box = True + + + # some settings used by some but not all plot types + + # Plotly plots often require offsets to the margins + self.plot_margins = self.get_config_value('mar') + self.grid_on = self._get_bool('grid_on') + if self.get_config_value('mar_offset'): + self.plot_margins = dict(l=0, + r=self.parameters['mar'][3] + 20, + t=self.parameters['mar'][2] + 80, + b=self.parameters['mar'][0] + 80, + pad=5 + ) + + self.grid_col = self.get_config_value('grid_col') + if self.grid_col: + self.blended_grid_col = util.alpha_blending(self.grid_col, 0.5) + self.show_nstats = self._get_bool('show_nstats') + self.indy_stagger = self._get_bool('indy_stagger') + + # Some of the plot types use Matplotlib, these settings are only relevant + # for plots implemented with Matplotlib. + + # left-right location of x-axis label/title relative to the y-axis line + # make adjustments between METviewer default and Matplotlib's center + # METviewer default value of 2 corresponds to Matplotlib value of .5 + # + mv_x_title_offset = self.get_config_value('xlab_offset') + if mv_x_title_offset: + self.x_title_offset = float(mv_x_title_offset) - 1.5 + + + # up-down of x-axis label/title position + # make adjustments between METviewer default and Matplotlib's center + # METviewer default is .5, Matplotlib center is 0.05, so subtract 0.55 from the + # METviewer setting to get the correct Matplotlib y-value (up/down) + # for the x-title position + mv_x_title_align = self.get_config_value('xlab_align') + if mv_x_title_align: + self.x_title_align = float(mv_x_title_align) - .55 + + # Need to use a combination of Matplotlib's font weight and font style to + + # re-create the METviewer xlab_weight. Use the + # MV_TO_MPL_CAPTION_STYLE dictionary to map these caption styles to + # what was requested in METviewer + mv_xlab_weight = self.get_config_value('xlab_weight') + self.xlab_weight = constants.MV_TO_MPL_CAPTION_STYLE[mv_xlab_weight] + + self.x_tickangle = self.parameters['xtlab_orient'] + if self.x_tickangle in constants.XAXIS_ORIENTATION.keys(): + self.x_tickangle = constants.XAXIS_ORIENTATION[self.x_tickangle] + self.x_tickfont_size = self.parameters['xtlab_size'] * constants.MPL_FONT_SIZE_DEFAULT + + # y-axis labels and y-axis ticks + self.y_title_font_size = self.parameters['ylab_size'] * constants.DEFAULT_CAPTION_FONTSIZE + self.y_tickangle = self.parameters['ytlab_orient'] + if self.y_tickangle in constants.YAXIS_ORIENTATION.keys(): + self.y_tickangle = constants.YAXIS_ORIENTATION[self.y_tickangle] + self.y_tickfont_size = self.parameters['ytlab_size'] * constants.MPL_FONT_SIZE_DEFAULT + + # left-right position of y-axis label/title position + # make adjustments between METviewer default and Matplotlib's center + # METviewer default is .5, Matplotlib center is -0.05 + mv_y_title_align = self.get_config_value('ylab_align') + self.y_title_align = float(mv_y_title_align) - 0.55 + + # up-down location of y-axis label/title relative to the x-axis line + # make adjustments between METviewer default and Matplotlib's center + # METviewer default value of -2 corresponds to Matplotlib value of 0.4 + # + mv_y_title_offset = self.get_config_value('ylab_offset') + self.y_title_offset = float(mv_y_title_offset) + 2.4 + + # Need to use a combination of Matplotlib's font weight and font style to + # re-create the METviewer ylab_weight. Use the + # MV_TO_MPL_CAPTION_STYLE dictionary to map these caption styles to + # what was requested in METviewer + mv_ylab_weight = self.get_config_value('ylab_weight') + self.ylab_weight = constants.MV_TO_MPL_CAPTION_STYLE[mv_ylab_weight] + + # Adjust the caption left/right relative to the y-axis + # METviewer default is set to 0, corresponds to y=0.05 in Matplotlib + mv_caption_align = self.get_config_value('caption_align') + self.caption_align = float(mv_caption_align) + 0.13 + + # The plot's title size, title weight, and positioning in left-right and up-down directions + mv_title_size = self.get_config_value('title_size') + self.title_size = mv_title_size * constants.MPL_FONT_SIZE_DEFAULT + + mv_title_weight = self.get_config_value('title_weight') + # use the same constants dictionary as used for captions + self.title_weight = constants.MV_TO_MPL_CAPTION_STYLE[mv_title_weight] + + # These values can't be used as-is, the only choice for aligning in Matplotlib + # are center (default), left, and right + mv_title_align = self.get_config_value('title_align') + self.title_align = float(mv_title_align) + + # does nothing because the vertical position in Matplotlib is + # automatically chosen to avoid labels and ticks on the topmost + # x-axis + mv_title_offset = self.get_config_value('title_offset') + self.title_offset = float(mv_title_offset) + + # legend style settings as defined in METviewer + user_settings = self._get_legend_style() + + # list of the x, y, and loc values for the + # bbox_to_anchor() setting used in determining + + # the location of the bounding box which defines + # the legend. + # adjust METviewer values to be consistent with the Matplotlib scale + # The METviewer x default is set to 0, which corresponds to a Matplotlib + # x-value of 0.5 (roughly centered with respect to the x-axis) + mv_bbox_x = float(user_settings['bbox_x']) + self.bbox_x = mv_bbox_x + 0.5 + + # METviewer legend box y-value is set to -.25 by default, which corresponds + # to a Matplotlib y-value of -.1 + mv_bbox_y = float(user_settings['bbox_y']) + self.bbox_y = mv_bbox_y + .15 + legend_magnification = user_settings['legend_size'] + self.legend_size = int(constants.DEFAULT_LEGEND_FONTSIZE * legend_magnification) + self.legend_ncol = self.get_config_value('legend_ncol') + + # Don't draw a box around legend labels unless an 'o' is set + self.draw_box = False + legend_box = self.get_config_value('legend_box').lower() + + if legend_box == 'o': + self.draw_box = True + + # These are the inner keys to the series_val setting, and + # they represent the series variables of + # interest. The keys correspond to the column names + # in the input dataframe. + self.series_vals_1 = self._get_series_vals(1) + self.series_vals_2 = self._get_series_vals(2) + self.all_series_vals = self.series_vals_1.copy() + if self.series_vals_2: + self.all_series_vals.extend(self.series_vals_2) + + # Represent the names of the forecast variables (inner keys) to the fcst_var_val setting. + # These are the names of the columns in the input dataframe. + self.fcst_var_val_1 = self._get_fcst_vars(1) + self.fcst_var_val_2 = self._get_fcst_vars(2) + + # Get the list of the statistics of interest + self.list_stat_1 = self.get_config_value('list_stat_1') + self.list_stat_2 = self.get_config_value('list_stat_2') + + # These are the inner values to the series_val setting (these correspond to the + # keys returned in self.series_vals above). These are the specific variable values to + # be used in subsetting the input dataframe (e.g. for key='model', and value='SH_CMORPH', + # we want to subset data where column name is 'model', with coincident rows of 'SH_CMORPH'. + self.series_val_names = self._get_series_val_names() + self.series_ordering = None + self.indy_plot_val = self.get_config_value('indy_plot_val') + self.lines = self._get_lines() + + + def get_config_value(self, *args:Union[str,int,float]): + """Gets the value of a configuration parameter. + Looks for parameter in the user parameter dictionary + + Args: + @ param args - chain of keys that defines a key to the parameter + + Returns: + - a value for the parameter of None + """ + + return self._get_nested(self.parameters, args) + + def _get_nested(self, data:dict, args:tuple): + """Recursive function that uses the tuple with keys to find a value + in multidimensional dictionary. + + Args: + @data - dictionary for the lookup + @args - a tuple with keys + + Returns: + - a value for the parameter of None + """ + + if args and data: + + # get value for the first key + element = args[0] + if element: + value = data.get(element) + + # if the size of key tuple is 1 - the search is over + if len(args) == 1: + return value + + # if the size of key tuple is > 1 - search using other keys + return self._get_nested(value, args[1:]) + return None + + def _get_legend_style(self) -> dict: + """ + Retrieve the legend style settings that are set + in the METviewer tool + + Args: + + Returns: + - a dictionary that holds the legend settings that + are set in METviewer + """ + legend_box = self.get_config_value('legend_box') + if legend_box: + legend_box = legend_box.lower() + + legend_ncol = self.get_config_value('legend_ncol') + legend_inset = self.get_config_value('legend_inset') + if legend_inset: + legend_bbox_x = legend_inset['x'] + legend_bbox_y = legend_inset['y'] + legend_size = self.get_config_value('legend_size') + legend_settings = dict(bbox_x=legend_bbox_x, + bbox_y=legend_bbox_y, + legend_size=legend_size, + legend_ncol=legend_ncol, + legend_box=legend_box) + else: + legend_settings = dict() + + return legend_settings + + def _get_series_vals(self, index:int) -> list: + """ + Get a tuple of lists of all the variable values that correspond to the inner + key of the series_val dictionaries (series_val_1 and series_val_2). + These values will be used with lists of other config values to + create filtering criteria. This is useful to subset the input data + to assist in identifying the data points for this series. + + Args: + index: The number defining which of series_vals_1 or series_vals_2 to consider + + Returns: + lists of *all* the values of the inner dictionary + of the series_vals dictionaries + + """ + + if index == 1: + # evaluate series_val_1 setting + series_val_dict = self.get_config_value('series_val_1') + elif index == 2: + # evaluate series_val_2 setting + series_val_dict = self.get_config_value('series_val_2') + else: + raise ValueError('Index value must be either 1 or 2.') + + # check for empty setting. If so, return an empty list + if series_val_dict: + val_dict_list = [*series_val_dict.values()] + else: + val_dict_list = [] + + # Unpack and access the values corresponding to the inner keys + # (series_var1, series_var2, ..., series_varn). + return val_dict_list + + def _get_series_columns(self, index): + ''' Retrieve the column name that corresponds to this ''' + + def _get_fcst_vars(self, index: int) -> list: + """ + Retrieve a list of the inner keys (fcst_vars) to the fcst_var_val dictionary. + + Args: + index: identifier used to differentiate between fcst_var_val_1 and + fcst_var_val_2 config settings + Returns: + a list containing all the fcst variables requested in the + fcst_var_val setting in the config file. This will be + used to subset the input data that corresponds to a particular series. + + """ + if index == 1: + fcst_var_val_dict = self.get_config_value('fcst_var_val_1') + if fcst_var_val_dict: + all_fcst_vars = [*fcst_var_val_dict.keys()] + else: + all_fcst_vars = [] + elif index == 2: + fcst_var_val_dict = self.get_config_value('fcst_var_val_2') + if fcst_var_val_dict: + all_fcst_vars = [*fcst_var_val_dict.keys()] + else: + all_fcst_vars = [] + else: + all_fcst_vars = [] + + return all_fcst_vars + + def _get_series_val_names(self) -> list: + """ + Get a list of all the variable value names (i.e. inner key of the + series_val dictionary). These values will be used with lists of + other config values to create filtering criteria. This is useful + to subset the input data to assist in identifying the data points + for this series. + + Args: + + Returns: + a "list of lists" of *all* the keys to the inner dictionary of + the series_val dictionary + + """ + + series_val_dict = self.get_config_value('series_val_1') + + # Unpack and access the values corresponding to the inner keys + # (series_var1, series_var2, ..., series_varn). + if series_val_dict: + return [*series_val_dict.keys()] + return [] + + def calculate_number_of_series(self) -> int: + """ + From the number of items in the permutation list, + determine how many series "objects" are to be plotted. + + Args: + + Returns: + the number of series + + """ + + # Retrieve the lists from the series_val_1 dictionary + series_vals_list = self.series_vals_1 + + # Utilize itertools' product() to create the cartesian product of all elements + # in the lists to produce all permutations of the series_val values and the + # fcst_var_val values. + permutations = [p for p in itertools.product(*series_vals_list)] + + return len(permutations) + + def _get_colors(self) -> list: + """ + Retrieves the colors used for lines and markers, from the + config file (default or custom). + Args: + + Returns: + colors_list or colors_from_config: a list of the colors to be used for the lines + (and their corresponding marker symbols) + """ + + colors_settings = self.get_config_value('colors') + return self.create_list_by_series_ordering(list(colors_settings)) + + def _get_con_series(self) -> list: + """ + Retrieves the 'connect across NA' values used for lines and markers, from the + config file (default or custom). + Args: + + Returns: + con_series_list or con_series_from_config: a list of 1 and/or 0 to + be used for the lines + """ + con_series_settings = self.get_config_value('con_series') + return self.create_list_by_series_ordering(list(con_series_settings)) + + def _get_show_legend(self) -> list: + """ + Retrieves the 'show_legend' values used for displaying or + not the legend of a trace in the legend box, from the + config file. If 'show_legend' is not provided - throws an error + Args: + + Returns: + show_legend_list or show_legend_from_config: a list of 1 and/or 0 to + be used for the traces + """ + show_legend_settings = self.get_config_value('show_legend') + + # Support all variations of setting the show_legend: '1', 1, "true" (any combination of cases), True (boolean) + updated_show_legend_settings = [] + for legend_setting in show_legend_settings: + legend_setting = str(legend_setting).lower() + if legend_setting == '1' or legend_setting == 'true' or legend_setting == 1 or legend_setting is True: + updated_show_legend_settings.append(int(1)) + else: + updated_show_legend_settings.append(int(0)) + + + if show_legend_settings is None: + raise ValueError("ERROR: show_legend parameter is not provided.") + + return self.create_list_by_series_ordering(list(updated_show_legend_settings)) + + def _get_markers(self): + """ + Retrieve all the markers. + + Args: + + Returns: + markers: a list of the markers + """ + markers = self.get_config_value('series_symbols') + markers_list = [] + for marker in markers: + if marker in constants.AVAILABLE_MARKERS_LIST: + # markers is the matplotlib symbol: .,o, ^, d, H, or s + markers_list.append(marker) + else: + # markers are indicated by name: small circle, circle, triangle, + # diamond, hexagon, square + markers_list.append(constants.PCH_TO_MATPLOTLIB_MARKER[marker.lower()]) + markers_list_ordered = self.create_list_by_series_ordering(list(markers_list)) + return markers_list_ordered + + def _get_linewidths(self) -> Union[list, None]: + """ Retrieve all the linewidths from the configuration file, if not + specified in any config file, use the default values of 2 + + Args: + + Returns: + linewidth_list: a list of linewidths corresponding to each line (model) + """ + linewidths = self.get_config_value('series_line_width') + if linewidths is not None: + return self.create_list_by_series_ordering(list(linewidths)) + else: + return None + + def _get_linestyles(self) -> list: + """ + Retrieve all the linestyles from the config file. + + Args: + + Returns: + list of line styles, each line style corresponds to a particular series + """ + linestyles = self.get_config_value('series_line_style') + linestyle_list_ordered = self.create_list_by_series_ordering(list(linestyles)) + return linestyle_list_ordered + + + def _get_user_legends(self, legend_label_type: str ) -> list: + """ + Retrieve the text that is to be displayed in the legend at the bottom of the plot. + Each entry corresponds to a series. + + Args: + @parm legend_label_type: The legend label, such as 'Performance', + used when the user hasn't indicated a legend in the + configuration file. + + Returns: + a list consisting of the series label to be displayed in the plot legend. + + """ + all_legends = self.get_config_value('user_legend') + + # for legend labels that aren't set (ie in conf file they are set to '') + # create a legend label based on the permutation of the series names + # appended by 'user_legend label'. For example, for: + # series_val_1: + # model: + # - NoahMPv3.5.1_d01 + # vx_mask: + # - CONUS + # The constructed legend label will be "NoahMPv3.5.1_d01 CONUS Performance" + + + # Check for empty list as setting in the config file + legends_list = [] + + # set a flag indicating when a legend label is specified + legend_label_unspecified = True + + # Check if a stat curve was requested, if so, then the number + # of series_val_1 values will be inconsistent with the number of + # legend labels 'specified' (either with actual labels or whitespace) + + num_series = self.calculate_number_of_series() + if len(all_legends) == 0: + for i in range(num_series): + legends_list.append(' ') + else: + for legend in all_legends: + if len(legend) == 0: + legend = ' ' + legends_list.append(legend) + else: + legend_label_unspecified = False + legends_list.append(legend) + + ll_list = [] + series_list = self.all_series_vals + + # Some diagrams don't require a series_val1 value, hence + # resulting in a zero-sized series_list. In this case, + # the legend label will just be the legend_label_type. + if len(series_list) == 0 and legend_label_unspecified: + # check if summary_curve is present + if 'summary_curve' in self.parameters.keys() and self.parameters['summary_curve'] != 'none': + return [legend_label_type, self.parameters['summary_curve'] + ' ' + legend_label_type] + else: + return [legend_label_type] + + perms = utils.create_permutations(series_list) + for idx,ll in enumerate(legends_list): + if ll == ' ': + if len(series_list) > 1: + label_parts = [perms[idx][0], ' ', perms[idx][1], ' ', legend_label_type] + else: + label_parts = [perms[idx][0], ' ', legend_label_type] + legend_label = ''.join(label_parts) + ll_list.append(legend_label) + else: + ll_list.append(ll) + if 'summary_curve' in self.parameters.keys() and self.parameters['summary_curve'] != 'none': + ll_list.append(self.parameters['summary_curve'] + ' ' + legend_label_type) + + legends_list_ordered = self.create_list_by_series_ordering(ll_list) + return legends_list_ordered + + def _get_plot_resolution(self) -> int: + """ + Retrieve the plot_res and plot_unit to determine the dpi + setting in matplotlib. + + Args: + + Returns: + plot resolution in units of dpi (dots per inch) + + """ + # Initialize to the default resolution + # set by matplotlib + dpi = 100 + + # first check if plot_res is set in config file + if self.get_config_value('plot_res'): + resolution = self.get_config_value('plot_res') + + # check if the units value has been set in the config file + if self.get_config_value('plot_units'): + units = self.get_config_value('plot_units').lower() + if units == 'in': + return resolution + + if units == 'mm': + # convert mm to inches so we can + # set dpi value + return resolution * constants.MM_TO_INCHES + + # units not supported, assume inches + return resolution + + # units not indicated, assume + # we are dealing with inches + return resolution + + # no plot_res value is set, return the default + # dpi used by matplotlib + return dpi + + def create_list_by_series_ordering(self, setting_to_order) -> list: + """ + Generate a list of series plotting settings based on what is set + in series_order in the config file. + If the series_order is specified: + series_order: + -3 + -1 + -2 + + and color is set: + color: + -red + -blue + -green + + + Then the following is expected: + the first series' color is 'blue' + the second series' color is 'green' + the third series' color is 'red' + + This allows the user the flexibility to change marker symbols, colors, and + other line qualities between the series (lines) without having to re-order + *all* the values. + + Args: + + setting_to_order: the name of the setting (eg axis_line_width) to be + ordered based on the order indicated + in the config file under the series_order setting. + + Returns: + a list reflecting the order that is consistent with what was set in series_order + + """ + + # create a natural order if series_ordering is missing + if self.series_ordering is None: + self.series_ordering = list(range(1, len(setting_to_order) + 1)) + + # Make the series ordering list zero-based to sync with Python's zero-based counting + series_ordered_zb = [sorder - 1 for sorder in self.series_ordering] + + if len(setting_to_order) == len(series_ordered_zb): + # Reorder the settings according to the zero based series order. + settings_reordered = [setting_to_order[i] for i in series_ordered_zb] + return settings_reordered + + return setting_to_order + + + def create_list_by_plot_val_ordering(self, setting_to_order: str) -> list: + """ + Generate a list of indy parameters settings based on what is set + in indy_plot_val in the config file. + If the is specified: + -3 + -1 + -2 + + and indy_vals is set: + indy_vals: + -120000 + -150000 + -180000 + + Then the following is expected: + the first indy_val is 1850000 + the second indy_val is 120000 + the third indy_val is 150000 + + + Args: + + setting_to_order: the name of the setting (eg indy_vals) to be + ordered based on the order indicated + in the config file under the indy_plot_val setting. + + Returns: + a list reflecting the order that is consistent with what was set in indy_plot_val + """ + + # order the input list according to the series_order setting + ordered_settings_list = [] + # create a natural order if series_ordering is missing + if self.indy_plot_val is None or len(self.indy_plot_val) == 0: + self.indy_plot_val = list(range(1, len(setting_to_order) + 1)) + + # Make the series ordering list zero-based to sync with Python's zero-based counting + indy_ordered_zb = [sorder - 1 for sorder in self.indy_plot_val] + for idx, indy in enumerate(indy_ordered_zb): + ordered_settings_list.insert(indy, setting_to_order[idx]) + + return ordered_settings_list + + + def calculate_plot_dimension(self, config_value: str , output_units: str) -> int: + ''' + To calculate the width or height that defines the size of the plot. + Matplotlib defines these values in inches, Python plotly defines these + in terms of pixels. METviewer accepts units of inches or mm for width and + height, so conversion from mm to inches or mm to pixels is necessary, depending + on the requested output units, output_units. + + Args: + @param config_value: The plot dimension to convert, either a width or height, + in inches or mm + @param output_units: pixels or in (inches) to indicate which + units to use to define plot size. Python plotly uses pixels and + Matplotlib uses inches. + Returns: + converted_value : converted value from in/mm to pixels or mm to inches based + on input values + ''' + + value2convert = self.get_config_value(config_value) + resolution = self.get_config_value('plot_res') + units = self.get_config_value('plot_units') + + # initialize converted_value to some small value + converted_value = 0 + + # convert to pixels + # plotly uses pixels for setting plot size (width and height) + if output_units.lower() == 'pixels': + if units.lower() == 'in': + # value in pixels + converted_value = int(resolution * value2convert) + elif units.lower() == 'mm': + # Convert mm to pixels + converted_value = int(resolution * value2convert * constants.MM_TO_INCHES) + + # Matplotlib uses inches (in) for setting plot size (width and height) + elif output_units.lower() == 'in': + if units.lower() == 'mm': + # Convert mm to inches + converted_value = value2convert * constants.MM_TO_INCHES + else: + converted_value = value2convert + + # plotly does not allow any value smaller than 10 pixels + if output_units.lower() == 'pixels' and converted_value < 10: + converted_value = 10 + + return converted_value + + def _get_bool(self, param: str) -> Union[bool, None]: + """ + Validates the value of the parameter and returns a boolean + Args: + :param param: name of the parameter + Returns: + :return: boolean value or None + """ + + param_val = self.get_config_value(param) + if isinstance(param_val, bool): + return param_val + + if isinstance(param_val, str): + return param_val.upper() == 'TRUE' + + return None + + def _get_indy_label(self): + if 'indy_label' in self.parameters.keys(): + return self.get_config_value('indy_label') + return self.indy_vals + + def _get_lines(self) -> Union[list, None]: + """ + Initialises the custom lines properties and returns a validated list + Args: + + Returns: + :return: list of lines properties or None + """ + + # get property value from the parameters + lines = self.get_config_value('lines') + + # if the property exists - proceed + if lines is not None: + # validate data and replace the values + for line in lines: + + # validate line_type + line_type = line['type'] + if line_type not in ('horiz_line', 'vert_line') : + print(f'WARNING: custom line type {line["type"]} is not supported') + line['type'] = None + else: + # convert position to float if line_type=horiz_line + if line['type'] == 'horiz_line': + try: + line['position'] = float(line['position']) + except ValueError: + print(f'WARNING: custom line position {line["position"]} is invalid') + line['type'] = None + else: + # convert position to string if line_type=vert_line + line['position'] = str(line['position']) + + # convert line_style + line_style = line['line_style'] + if line_style in constants.LINE_STYLE_TO_PLOTLY_DASH.keys(): + line['line_style'] = constants.LINE_STYLE_TO_PLOTLY_DASH[line_style] + else: + line['line_style'] = None + + # convert line_width to float + try: + line['line_width'] = float(line['line_width']) + except ValueError: + print(f'WARNING: custom line width {line["line_width"]} is invalid') + line['type'] = None + + return lines diff --git a/metplotpy/plots/constants.py b/metplotpy/plots/constants.py index 2c8fb35c..0717fe4d 100644 --- a/metplotpy/plots/constants.py +++ b/metplotpy/plots/constants.py @@ -79,10 +79,10 @@ 'o': 'circle', '^': 'triangle-up', 'd': 'diamond', 'H': 'circle-open', 'h': 'hexagon2', 's': 'square'} -PCH_TO_PLOTLY_MARKER_SIZE = {'.': 5, 'o': 8, 's': 6, '^': 8, 'd': 6, 'H': 7} +# approximated from plotly marker size to matplotlib marker size +PCH_TO_MATPLOTLIB_MARKER_SIZE = {'.': 14, 'o': 36, 's': 20, '^': 36, 'd': 20, 'H': 28} TYPE_TO_PLOTLY_MODE = {'b': 'lines+markers', 'p': 'markers', 'l': 'lines'} -LINE_STYLE_TO_PLOTLY_DASH = {'-': None, '--': 'dash', ':': 'dot', '-:': 'dashdot'} XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} diff --git a/metplotpy/plots/constants_plotly.py b/metplotpy/plots/constants_plotly.py new file mode 100644 index 00000000..2c8fb35c --- /dev/null +++ b/metplotpy/plots/constants_plotly.py @@ -0,0 +1,100 @@ +# ============================* + # ** Copyright UCAR (c) 2020 + # ** University Corporation for Atmospheric Research (UCAR) + # ** National Center for Atmospheric Research (NCAR) + # ** Research Applications Lab (RAL) + # ** P.O.Box 3000, Boulder, Colorado, 80307-3000, USA + # ============================* + + + +""" +Module Name: constants.py + +Mapping of constants used in plotting, as dictionaries. +METviewer values are keys, Matplotlib representations are the values. + +""" +__author__ = 'Minna Win' + +# CONVERSION FACTORS + +# used to convert plot units in mm to +# inches, so we can pass in dpi to matplotlib +MM_TO_INCHES = 0.03937008 + +# Available Matplotlib Line styles +# ':' ... +# '-.' _._. +# '--' ----- +# '-' ______ (solid line) +# ' ' no line + +# METviewer drop-down choices: +# p points (...) +# l lines (---, dashed line) +# o overplotted (_._ mix of dash and dots) +# b joined lines (____ solid line) +# s stairsteps (same as overplotted) +# h histogram like (no line style, this is unsupported) +# n none (no line style) + +# linestyles can be indicated by "long" name (points, lines, etc.) or +# by single letter designation ('p', 'n', etc) +LINESTYLE_BY_NAMES = {'solid': '-', 'points': ':', 'lines': '--', 'overplotted': '-.', + 'joined lines': '-', 'stairstep': '-.', + 'histogram': ' ', 'none': ' ', 'p': ':', + 'l': '--', 'o': '-.', 'b': '-', + 's': '-.', 'h': ' ', 'n': ' '} + +ACCEPTABLE_CI_VALS = ['NONE', 'BOOT', "STD", 'MET_PRM', 'MET_BOOT'] + +DEFAULT_TITLE_FONT = 'sans-serif' +DEFAULT_TITLE_COLOR = 'black' +DEFAULT_TITLE_FONTSIZE = 10 + +# Default size used in plotly legend text +DEFAULT_LEGEND_FONTSIZE = 12 +DEFAULT_CAPTION_FONTSIZE = 14 +DEFAULT_CAPTION_Y_OFFSET = -3.1 +DEFAULT_TITLE_FONT_SIZE = 11 +DEFAULT_TITLE_OFFSET = (-0.48) + + +AVAILABLE_MARKERS_LIST = ["o", "^", "s", "d", "H", ".", "h"] +AVAILABLE_PLOTLY_MARKERS_LIST = ["circle-open", "circle", + "square", "diamond", + "hexagon", "triangle-up", "asterisk-open"] + +PCH_TO_MATPLOTLIB_MARKER = {'20': '.', '19': 'o', '17': '^', '1': 'H', + '18': 'd', '15': 's', 'small circle': '.', + 'circle': 'o', 'square': 's', + 'triangle': '^', 'rhombus': 'd', 'ring': 'h'} + +PCH_TO_PLOTLY_MARKER = {'0': 'circle-open', '19': 'circle', '20': 'circle', + '17': 'triangle-up', '15': 'square', '18': 'diamond', + '1': 'hexagon2', 'small circle': 'circle-open', + 'circle': 'circle', 'square': 'square', 'triangle': 'triangle-up', + 'rhombus': 'diamond', 'ring': 'hexagon2', '.': 'circle', + 'o': 'circle', '^': 'triangle-up', 'd': 'diamond', 'H': 'circle-open', + 'h': 'hexagon2', 's': 'square'} + +PCH_TO_PLOTLY_MARKER_SIZE = {'.': 5, 'o': 8, 's': 6, '^': 8, 'd': 6, 'H': 7} + +TYPE_TO_PLOTLY_MODE = {'b': 'lines+markers', 'p': 'markers', 'l': 'lines'} +LINE_STYLE_TO_PLOTLY_DASH = {'-': None, '--': 'dash', ':': 'dot', '-:': 'dashdot'} +XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} +YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} + +PLOTLY_PAPER_BGCOOR = "white" +PLOTLY_AXIS_LINE_COLOR = "#c2c2c2" +PLOTLY_AXIS_LINE_WIDTH = 2 + +# Caption weights supported in Matplotlib are normal, italic and oblique. +# Map these onto the MetViewer requested values of 1 (normal), 2 (bold), +# 3 (italic), 4 (bold italic), and 5 (symbol) using a dictionary +MV_TO_MPL_CAPTION_STYLE = {1:('normal', 'normal'), 2:('normal','bold'), 3:('italic', 'normal') + , 4:('italic', 'bold'),5:('oblique','normal')} + +# Matplotlib constants +MPL_FONT_SIZE_DEFAULT = 11 diff --git a/metplotpy/plots/contour/contour.py b/metplotpy/plots/contour/contour.py index 353a1d72..b12cdb66 100644 --- a/metplotpy/plots/contour/contour.py +++ b/metplotpy/plots/contour/contour.py @@ -24,9 +24,9 @@ from plotly.subplots import make_subplots from plotly.graph_objects import Figure -from metplotpy.plots.constants import PLOTLY_PAPER_BGCOOR -from metplotpy.plots.base_plot import BasePlot -from metplotpy.plots import util +from metplotpy.plots.constants_plotly import PLOTLY_PAPER_BGCOOR +from metplotpy.plots.base_plot_plotly import BasePlot +from metplotpy.plots import util_plotly as util from metplotpy.plots.contour.contour_config import ContourConfig from metplotpy.plots.contour.contour_series import ContourSeries from metplotpy.plots.series import Series diff --git a/metplotpy/plots/contour/contour_config.py b/metplotpy/plots/contour/contour_config.py index 6d28d4c7..4336c1e3 100644 --- a/metplotpy/plots/contour/contour_config.py +++ b/metplotpy/plots/contour/contour_config.py @@ -15,9 +15,9 @@ """ __author__ = 'Tatiana Burek' -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/contour/contour_series.py b/metplotpy/plots/contour/contour_series.py index 024bd3de..5f55acc6 100644 --- a/metplotpy/plots/contour/contour_series.py +++ b/metplotpy/plots/contour/contour_series.py @@ -18,7 +18,7 @@ import numpy as np import warnings -import metplotpy.plots.util +import metplotpy.plots.util_plotly as util from ..series import Series @@ -34,8 +34,7 @@ def __init__(self, config, idx: int, input_data, series_list: list, series_name: Union[list, tuple], y_axis: int = 1): self.series_list = series_list self.series_name = series_name - self.logger = metplotpy.plots.util.get_common_logger(config.log_level, - config.log_filename) + self.logger = util.get_common_logger(config.log_level, config.log_filename) super().__init__(config, idx, input_data, y_axis) diff --git a/metplotpy/plots/eclv/eclv.py b/metplotpy/plots/eclv/eclv.py index 5860b572..446e4e2a 100644 --- a/metplotpy/plots/eclv/eclv.py +++ b/metplotpy/plots/eclv/eclv.py @@ -24,12 +24,12 @@ from datetime import datetime from metcalcpy.event_equalize import event_equalize -from metplotpy.plots.base_plot import BasePlot -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH +from metplotpy.plots.base_plot_plotly import BasePlot +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH from metplotpy.plots.eclv.eclv_config import EclvConfig from metplotpy.plots.eclv.eclv_series import EclvSeries from metplotpy.plots.line.line import Line -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util from metplotpy.plots.series import Series diff --git a/metplotpy/plots/eclv/eclv_series.py b/metplotpy/plots/eclv/eclv_series.py index 94433a3a..0c34becf 100644 --- a/metplotpy/plots/eclv/eclv_series.py +++ b/metplotpy/plots/eclv/eclv_series.py @@ -18,7 +18,7 @@ from scipy.stats import norm import metcalcpy.util.utils as utils -import metplotpy.plots.util +import metplotpy.plots.util_plotly as util from ..line.line_series import LineSeries @@ -40,8 +40,7 @@ def _create_series_points(self) -> list: Returns: dictionary with CI ,point values and number of stats as keys """ - logger = metplotpy.plots.util.get_common_logger(self.log_level, - self.log_filename) + logger = util.get_common_logger(self.log_level, self.log_filename) logger.info(f"Creating series points: {datetime.now()}") # different ways to subset data for normal and derived series diff --git a/metplotpy/plots/ens_ss/ens_ss.py b/metplotpy/plots/ens_ss/ens_ss.py index d6d153ee..2cbf9ad9 100644 --- a/metplotpy/plots/ens_ss/ens_ss.py +++ b/metplotpy/plots/ens_ss/ens_ss.py @@ -29,8 +29,8 @@ from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR from metplotpy.plots.ens_ss.ens_ss_config import EnsSsConfig from metplotpy.plots.ens_ss.ens_ss_series import EnsSsSeries -from metplotpy.plots.base_plot import BasePlot -import metplotpy.plots.util as util +from metplotpy.plots.base_plot_plotly import BasePlot +import metplotpy.plots.util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/ens_ss/ens_ss_config.py b/metplotpy/plots/ens_ss/ens_ss_config.py index a1939721..298428c5 100644 --- a/metplotpy/plots/ens_ss/ens_ss_config.py +++ b/metplotpy/plots/ens_ss/ens_ss_config.py @@ -17,9 +17,9 @@ import itertools -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/ens_ss/ens_ss_series.py b/metplotpy/plots/ens_ss/ens_ss_series.py index 508a6e51..28d599c6 100644 --- a/metplotpy/plots/ens_ss/ens_ss_series.py +++ b/metplotpy/plots/ens_ss/ens_ss_series.py @@ -19,7 +19,7 @@ import numpy as np import metcalcpy.util.utils as utils -import metplotpy.plots.util +import metplotpy.plots.util_plotly as util from .. import GROUP_SEPARATOR from ..series import Series @@ -71,8 +71,7 @@ def _create_series_points(self) -> dict: Returns: dictionary with CI ,point values and number of stats as keys """ - ens_logger = metplotpy.plots.util.get_common_logger(self.log_level, - self.log_filename) + ens_logger = util.get_common_logger(self.log_level, self.log_filename) ens_logger.info(f"Begin creating the series points: {datetime.now()}") # different ways to subset data for normal and derived series # this is a normal series diff --git a/metplotpy/plots/equivalence_testing_bounds/equivalence_testing_bounds.py b/metplotpy/plots/equivalence_testing_bounds/equivalence_testing_bounds.py index 5d3798c0..957db07f 100644 --- a/metplotpy/plots/equivalence_testing_bounds/equivalence_testing_bounds.py +++ b/metplotpy/plots/equivalence_testing_bounds/equivalence_testing_bounds.py @@ -17,21 +17,20 @@ import re import csv -import yaml import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots from plotly.graph_objects import Figure -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ PLOTLY_PAPER_BGCOOR from metplotpy.plots.equivalence_testing_bounds.equivalence_testing_bounds_series \ import EquivalenceTestingBoundsSeries from metplotpy.plots.line.line_config import LineConfig from metplotpy.plots.line.line_series import LineSeries -from metplotpy.plots.base_plot import BasePlot -from metplotpy.plots import util +from metplotpy.plots.base_plot_plotly import BasePlot +from metplotpy.plots import util_plotly as util import metcalcpy.util.utils as calc_util diff --git a/metplotpy/plots/equivalence_testing_bounds/equivalence_testing_bounds_series.py b/metplotpy/plots/equivalence_testing_bounds/equivalence_testing_bounds_series.py index 1548dc9c..f91aa8be 100644 --- a/metplotpy/plots/equivalence_testing_bounds/equivalence_testing_bounds_series.py +++ b/metplotpy/plots/equivalence_testing_bounds/equivalence_testing_bounds_series.py @@ -24,7 +24,7 @@ import metcalcpy.util.correlation as pg import metcalcpy.util.utils as utils -import metplotpy.plots.util +import metplotpy.plots.util_plotly as util from metcalcpy.sum_stat import calculate_statistic from .. import GROUP_SEPARATOR from ..line.line_series import LineSeries @@ -54,8 +54,7 @@ def _create_series_points(self) -> dict: dictionary with CI ,point values and number of stats as keys """ - logger = metplotpy.plots.util.get_common_logger(self.log_level, - self.log_filename) + logger = util.get_common_logger(self.log_level, self.log_filename) logger.info(f"Creating series points (calculating the values for " f"each point: {datetime.now()}") @@ -147,8 +146,7 @@ def _calculate_tost_paired(self, series_data_1: DataFrame, series_data_2: DataFr :param series_data_2: 2nd data frame sorted by fcst_init_beg """ - logger = metplotpy.plots.util.get_common_logger(self.log_level, - self.log_filename) + logger = util.get_common_logger(self.log_level, self.log_filename) logger.info(f"Validating dataframe fcst_valid_beg: " f"{datetime.now()}") all_zero_1 = all(elem is None or math.isnan(elem) diff --git a/metplotpy/plots/histogram/hist.py b/metplotpy/plots/histogram/hist.py index a3012a5c..b504e3aa 100644 --- a/metplotpy/plots/histogram/hist.py +++ b/metplotpy/plots/histogram/hist.py @@ -27,11 +27,11 @@ from plotly.graph_objects import Figure from metplotpy.plots.histogram import hist_config -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ PLOTLY_PAPER_BGCOOR from metplotpy.plots.histogram.hist_series import HistSeries -from metplotpy.plots.base_plot import BasePlot -from metplotpy.plots import util +from metplotpy.plots.base_plot_plotly import BasePlot +from metplotpy.plots import util_plotly as util import metcalcpy.util.utils as utils from metcalcpy.event_equalize import event_equalize diff --git a/metplotpy/plots/histogram/hist_config.py b/metplotpy/plots/histogram/hist_config.py index 16945ab6..5016e347 100644 --- a/metplotpy/plots/histogram/hist_config.py +++ b/metplotpy/plots/histogram/hist_config.py @@ -16,9 +16,9 @@ import itertools -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/histogram/hist_series.py b/metplotpy/plots/histogram/hist_series.py index 38335ba2..1e30222d 100644 --- a/metplotpy/plots/histogram/hist_series.py +++ b/metplotpy/plots/histogram/hist_series.py @@ -17,7 +17,7 @@ import numpy as np import metcalcpy.util.utils as utils -import metplotpy.plots.util +import metplotpy.plots.util_plotly as util from ..series import Series @@ -68,8 +68,7 @@ def _create_series_points(self) -> list: Returns: """ - logger = metplotpy.plots.util.get_common_logger(self.log_level, - self.log_filename) + logger = util.get_common_logger(self.log_level, self.log_filename) logger.info(f"Begin creating the series points: {datetime.now()}") all_filters = [] diff --git a/metplotpy/plots/histogram/histogram.py b/metplotpy/plots/histogram/histogram.py index 231d333f..4ef52dea 100644 --- a/metplotpy/plots/histogram/histogram.py +++ b/metplotpy/plots/histogram/histogram.py @@ -13,13 +13,12 @@ """ __author__ = 'Tatiana Burek' -import os import plotly.graph_objects as go import yaml import pandas as pd import numpy as np -from metplotpy.plots.base_plot import BasePlot +from metplotpy.plots.base_plot_plotly import BasePlot class Histogram(BasePlot): diff --git a/metplotpy/plots/histogram/prob_hist.py b/metplotpy/plots/histogram/prob_hist.py index d4cd7aae..c9e2ba57 100644 --- a/metplotpy/plots/histogram/prob_hist.py +++ b/metplotpy/plots/histogram/prob_hist.py @@ -18,7 +18,7 @@ from metplotpy.plots.histogram.hist import Hist from metplotpy.plots.histogram.hist_series import HistSeries -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util class ProbHist(Hist): diff --git a/metplotpy/plots/histogram/rank_hist.py b/metplotpy/plots/histogram/rank_hist.py index a9a054b9..0a54ee6f 100644 --- a/metplotpy/plots/histogram/rank_hist.py +++ b/metplotpy/plots/histogram/rank_hist.py @@ -13,10 +13,9 @@ """ __author__ = 'Tatiana Burek' -import yaml from datetime import datetime from metplotpy.plots.histogram.hist import Hist -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util from metplotpy.plots.histogram.hist_series import HistSeries diff --git a/metplotpy/plots/histogram/rel_hist.py b/metplotpy/plots/histogram/rel_hist.py index 3040451e..5035fcd1 100644 --- a/metplotpy/plots/histogram/rel_hist.py +++ b/metplotpy/plots/histogram/rel_hist.py @@ -13,10 +13,9 @@ """ __author__ = 'Tatiana Burek' -import yaml from datetime import datetime -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util from metplotpy.plots.histogram.hist import Hist from metplotpy.plots.histogram.hist_series import HistSeries diff --git a/metplotpy/plots/histogram_2d/histogram_2d.py b/metplotpy/plots/histogram_2d/histogram_2d.py index 3f22aa06..4dff7b3d 100644 --- a/metplotpy/plots/histogram_2d/histogram_2d.py +++ b/metplotpy/plots/histogram_2d/histogram_2d.py @@ -29,12 +29,12 @@ import xarray as xr import plotly.graph_objects as go -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util """ Import BasePlot class """ -from metplotpy.plots.base_plot import BasePlot +from metplotpy.plots.base_plot_plotly import BasePlot class Histogram_2d(BasePlot): diff --git a/metplotpy/plots/line/line.py b/metplotpy/plots/line/line.py index c49c3614..dd71a617 100644 --- a/metplotpy/plots/line/line.py +++ b/metplotpy/plots/line/line.py @@ -20,7 +20,6 @@ from typing import Union from itertools import chain -import yaml import numpy as np import pandas as pd @@ -28,12 +27,12 @@ from plotly.subplots import make_subplots from plotly.graph_objects import Figure -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ PLOTLY_PAPER_BGCOOR from metplotpy.plots.line.line_config import LineConfig from metplotpy.plots.line.line_series import LineSeries -from metplotpy.plots.base_plot import BasePlot -from metplotpy.plots import util +from metplotpy.plots.base_plot_plotly import BasePlot +from metplotpy.plots import util_plotly as util from metplotpy.plots.series import Series import metcalcpy.util.utils as calc_util diff --git a/metplotpy/plots/line/line_config.py b/metplotpy/plots/line/line_config.py index d4acbe88..d1031846 100644 --- a/metplotpy/plots/line/line_config.py +++ b/metplotpy/plots/line/line_config.py @@ -17,9 +17,9 @@ import itertools -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/line/line_series.py b/metplotpy/plots/line/line_series.py index 04c644e4..cea0dd7a 100644 --- a/metplotpy/plots/line/line_series.py +++ b/metplotpy/plots/line/line_series.py @@ -26,7 +26,7 @@ from scipy.stats import norm import metcalcpy.util.utils as utils -import metplotpy.plots.util +import metplotpy.plots.util_plotly as util from ..series import Series from .. import GROUP_SEPARATOR @@ -50,8 +50,7 @@ def __init__(self, config, idx: int, input_data, series_list: list, # Retrieve any fixed variables - self.logger = metplotpy.plots.util.get_common_logger(config.log_level, - config.log_filename) + self.logger = util.get_common_logger(config.log_level, config.log_filename) def _create_all_fields_values_no_indy(self) -> dict: """ @@ -91,8 +90,7 @@ def _calc_point_stat(self, data: list) -> Union[float, None]: :return: mean, median or sum of the values from the input list or None if the statistic parameter is invalid """ - logger = metplotpy.plots.util.get_common_logger(self.log_level, - self.log_filename) + logger = util.get_common_logger(self.log_level, self.log_filename) logger.info(f"Begin calculating plot_stat parameter: " f"{datetime.now()}") # calculate point stat @@ -111,8 +109,7 @@ def _calc_point_stat(self, data: list) -> Union[float, None]: else: point_stat = None - logger = metplotpy.plots.util.get_common_logger(self.log_level, - self.log_filename) + logger = util.get_common_logger(self.log_level, self.log_filename) logger.info(f"Begin calculating plot_stat parameter: " f"{datetime.now()}") return point_stat @@ -127,8 +124,7 @@ def _create_series_points(self) -> dict: Returns: dictionary with CI ,point values and number of stats as keys """ - logger = metplotpy.plots.util.get_common_logger(self.log_level, - self.log_filename) + logger = util.get_common_logger(self.log_level, self.log_filename) logger.info(f"Begin calculating values for each series point: " f"{datetime.now()}") series_data_1 = None @@ -141,8 +137,8 @@ def _create_series_points(self) -> dict: # @nan_val is substituted for the 'NA' in the list of values # that correspond to a column. - filtered_df = metplotpy.plots.util.filter_by_fixed_vars(self.input_data, - self.config.fixed_vars_vals) + filtered_df = util.filter_by_fixed_vars(self.input_data, + self.config.fixed_vars_vals) else: # Nothing specified in the fixed_vars_vals_input setting, # use the original input data diff --git a/metplotpy/plots/mpr_plot/mpr_plot.py b/metplotpy/plots/mpr_plot/mpr_plot.py index 329d5b27..391169ed 100644 --- a/metplotpy/plots/mpr_plot/mpr_plot.py +++ b/metplotpy/plots/mpr_plot/mpr_plot.py @@ -23,12 +23,12 @@ from plotly.subplots import make_subplots import plotly.io as pio -from metplotpy.plots.base_plot import BasePlot +from metplotpy.plots.base_plot_plotly import BasePlot from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR from metplotpy.plots.mpr_plot.mpr_plot_config import MprPlotConfig from metplotpy.plots.wind_rose.wind_rose import WindRosePlot -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util class MprPlotInfo(): diff --git a/metplotpy/plots/polar_plot/polar_plot.py b/metplotpy/plots/polar_plot/polar_plot.py index 4b5a9f99..0703d1e0 100644 --- a/metplotpy/plots/polar_plot/polar_plot.py +++ b/metplotpy/plots/polar_plot/polar_plot.py @@ -41,9 +41,7 @@ """ Import BasePlot class """ -from plots.base_plot import BasePlot -#from ..base_plot import BasePlot - +from metplotpy.plots.base_plot import BasePlot class PolarPlot(BasePlot): diff --git a/metplotpy/plots/reliability_diagram/reliability.py b/metplotpy/plots/reliability_diagram/reliability.py index b83f9572..bb40daba 100644 --- a/metplotpy/plots/reliability_diagram/reliability.py +++ b/metplotpy/plots/reliability_diagram/reliability.py @@ -26,9 +26,9 @@ from plotly.subplots import make_subplots from plotly.graph_objects import Figure -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR -from metplotpy.plots.base_plot import BasePlot -from metplotpy.plots import util +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR +from metplotpy.plots.base_plot_plotly import BasePlot +from metplotpy.plots import util_plotly as util from metplotpy.plots.reliability_diagram.reliability_config import ReliabilityConfig from metplotpy.plots.reliability_diagram.reliability_series import ReliabilitySeries diff --git a/metplotpy/plots/reliability_diagram/reliability_config.py b/metplotpy/plots/reliability_diagram/reliability_config.py index d011c054..3f1175d1 100644 --- a/metplotpy/plots/reliability_diagram/reliability_config.py +++ b/metplotpy/plots/reliability_diagram/reliability_config.py @@ -18,9 +18,9 @@ import itertools from datetime import datetime -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util class ReliabilityConfig(Config): diff --git a/metplotpy/plots/revision_box/revision_box.py b/metplotpy/plots/revision_box/revision_box.py index d9336712..8dd029ff 100644 --- a/metplotpy/plots/revision_box/revision_box.py +++ b/metplotpy/plots/revision_box/revision_box.py @@ -15,13 +15,13 @@ from datetime import datetime import plotly.graph_objects as go -from metplotpy.plots.base_plot import BasePlot +from metplotpy.plots.base_plot_plotly import BasePlot from metplotpy.plots.box.box import Box -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util import metcalcpy.util.utils as calc_util -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH from metplotpy.plots.revision_box.revision_box_config import RevisionBoxConfig from metplotpy.plots.revision_box.revision_box_series import RevisionBoxSeries diff --git a/metplotpy/plots/revision_box/revision_box_config.py b/metplotpy/plots/revision_box/revision_box_config.py index 8f74d50f..873100a1 100644 --- a/metplotpy/plots/revision_box/revision_box_config.py +++ b/metplotpy/plots/revision_box/revision_box_config.py @@ -14,9 +14,9 @@ """ import itertools -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/revision_series/revision_series.py b/metplotpy/plots/revision_series/revision_series.py index acf7621e..ea015463 100644 --- a/metplotpy/plots/revision_series/revision_series.py +++ b/metplotpy/plots/revision_series/revision_series.py @@ -17,16 +17,15 @@ from typing import Union -import yaml import numpy as np import plotly.graph_objects as go -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH -from metplotpy.plots.base_plot import BasePlot +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH +from metplotpy.plots.base_plot_plotly import BasePlot from metplotpy.plots.line.line import Line -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util from metplotpy.plots.series import Series import metcalcpy.util.utils as calc_util diff --git a/metplotpy/plots/revision_series/revision_series_config.py b/metplotpy/plots/revision_series/revision_series_config.py index d031be4f..f7e5a0d8 100644 --- a/metplotpy/plots/revision_series/revision_series_config.py +++ b/metplotpy/plots/revision_series/revision_series_config.py @@ -14,9 +14,9 @@ """ import itertools from datetime import datetime -from ..config import Config -from .. import constants -from .. import util +from ..config_plotly import Config +from .. import constants_plotly as constants +from .. import util_plotly as util import metcalcpy.util.utils as utils diff --git a/metplotpy/plots/roc_diagram/roc_diagram.py b/metplotpy/plots/roc_diagram/roc_diagram.py index c728e0c3..1832faa7 100644 --- a/metplotpy/plots/roc_diagram/roc_diagram.py +++ b/metplotpy/plots/roc_diagram/roc_diagram.py @@ -17,20 +17,18 @@ from datetime import datetime import re import warnings -# with warnings.catch_warnings(): -# warnings.simplefilter("ignore", category="DeprecationWarning") -# warnings.simplefilter("ignore", category="ResourceWarning") import pandas as pd import plotly.graph_objects as go from plotly.subplots import make_subplots -from metplotpy.plots import util -from metplotpy.plots import constants -from metplotpy.plots.base_plot import BasePlot + +from metplotpy.plots import util_plotly as util +from metplotpy.plots import constants_plotly as constants +from metplotpy.plots.base_plot_plotly import BasePlot from metplotpy.plots.roc_diagram.roc_diagram_config import ROCDiagramConfig from metplotpy.plots.roc_diagram.roc_diagram_series import ROCDiagramSeries + import metcalcpy.util.utils as calc_util -from metplotpy.plots.util import prepare_pct_roc, prepare_ctc_roc class ROCDiagram(BasePlot): @@ -236,7 +234,7 @@ def _create_series(self, input_data): 'fn_on': group_stats_fn_on, } df_summary_curve.reset_index() - pody, pofd, thresh = prepare_ctc_roc(df_summary_curve,self.config_obj.ctc_ascending) + pody, pofd, thresh = util.prepare_ctc_roc(df_summary_curve,self.config_obj.ctc_ascending) else: df_summary_curve = pd.DataFrame(columns=['thresh_i', 'on_i', 'oy_i']) thresh_i_list = df_sum_main['thresh_i'].unique() @@ -250,7 +248,7 @@ def _create_series(self, input_data): df_summary_curve.loc[len(df_summary_curve)] = {'thresh_i': thresh, 'on_i': on_i_sum, 'oy_i': oy_i_sum, } df_summary_curve.reset_index() - pody, pofd, thresh = prepare_pct_roc(df_summary_curve) + pody, pofd, thresh = util.prepare_pct_roc(df_summary_curve) series_obj = ROCDiagramSeries(self.config_obj, num_series -1, None) series_obj.series_points = (pofd, pody, thresh, None) diff --git a/metplotpy/plots/roc_diagram/roc_diagram_config.py b/metplotpy/plots/roc_diagram/roc_diagram_config.py index 3f11e0b8..a2ca8a01 100644 --- a/metplotpy/plots/roc_diagram/roc_diagram_config.py +++ b/metplotpy/plots/roc_diagram/roc_diagram_config.py @@ -15,11 +15,9 @@ """ __author__ = 'Minna Win' - -import sys -from ..config import Config -from .. import util -from .. import constants +from ..config_plotly import Config +from .. import util_plotly as util +from .. import constants_plotly as constants class ROCDiagramConfig(Config): def __init__(self, parameters): diff --git a/metplotpy/plots/roc_diagram/roc_diagram_series.py b/metplotpy/plots/roc_diagram/roc_diagram_series.py index 9d3f59c2..81be29fe 100644 --- a/metplotpy/plots/roc_diagram/roc_diagram_series.py +++ b/metplotpy/plots/roc_diagram/roc_diagram_series.py @@ -17,7 +17,7 @@ import pandas as pd import metcalcpy.util.utils as utils from ..series import Series -from ..util import prepare_pct_roc, prepare_ctc_roc +from ..util_plotly import prepare_pct_roc, prepare_ctc_roc class ROCDiagramSeries(Series): diff --git a/metplotpy/plots/scatter/scatter.py b/metplotpy/plots/scatter/scatter.py index 2f13b588..3abaead7 100644 --- a/metplotpy/plots/scatter/scatter.py +++ b/metplotpy/plots/scatter/scatter.py @@ -16,13 +16,11 @@ import matplotlib.pyplot as plt import numpy as np from matplotlib.font_manager import FontProperties -import yaml + import pandas as pd from metplotpy.plots.base_plot import BasePlot from metplotpy.plots.scatter.scatter_config import ScatterConfig from metplotpy.plots import util -from metplotpy.plots.util import get_params -from metcalcpy.util.read_env_vars_in_config import parse_config class Scatter(BasePlot): """ @@ -201,7 +199,7 @@ def main(config_filename=None): Returns: None """ - docs = get_params(config_filename) + docs = util.get_params(config_filename) try: plot = Scatter(docs) diff --git a/metplotpy/plots/scatter/scatter_config.py b/metplotpy/plots/scatter/scatter_config.py index 0e5c5665..817c7c95 100644 --- a/metplotpy/plots/scatter/scatter_config.py +++ b/metplotpy/plots/scatter/scatter_config.py @@ -15,7 +15,6 @@ from .. import constants from .. import util -import metcalcpy.util.utils as utils class ScatterConfig(Config): """ Configuration object for the scatter plot. diff --git a/metplotpy/plots/tcmpr_plots/box/tcmpr_box.py b/metplotpy/plots/tcmpr_plots/box/tcmpr_box.py index ec086111..f4367821 100755 --- a/metplotpy/plots/tcmpr_plots/box/tcmpr_box.py +++ b/metplotpy/plots/tcmpr_plots/box/tcmpr_box.py @@ -5,7 +5,7 @@ from metplotpy.plots.tcmpr_plots.box.tcmpr_box_point import TcmprBoxPoint from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprBox(TcmprBoxPoint): diff --git a/metplotpy/plots/tcmpr_plots/box/tcmpr_box_point.py b/metplotpy/plots/tcmpr_plots/box/tcmpr_box_point.py index bff7b448..f39de279 100755 --- a/metplotpy/plots/tcmpr_plots/box/tcmpr_box_point.py +++ b/metplotpy/plots/tcmpr_plots/box/tcmpr_box_point.py @@ -2,7 +2,7 @@ from metplotpy.plots.tcmpr_plots.tcmpr import Tcmpr from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprBoxPoint(Tcmpr): def __init__(self, config_obj, column_info, col, case_data, input_df, baseline_data, stat_name): diff --git a/metplotpy/plots/tcmpr_plots/box/tcmpr_point.py b/metplotpy/plots/tcmpr_plots/box/tcmpr_point.py index 44cab41b..c4488999 100755 --- a/metplotpy/plots/tcmpr_plots/box/tcmpr_point.py +++ b/metplotpy/plots/tcmpr_plots/box/tcmpr_point.py @@ -3,7 +3,7 @@ import plotly.graph_objects as go -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util from metplotpy.plots.tcmpr_plots.box.tcmpr_box_point import TcmprBoxPoint from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries diff --git a/metplotpy/plots/tcmpr_plots/line/mean/tcmpr_line_mean.py b/metplotpy/plots/tcmpr_plots/line/mean/tcmpr_line_mean.py index e73a9608..20c6fea5 100755 --- a/metplotpy/plots/tcmpr_plots/line/mean/tcmpr_line_mean.py +++ b/metplotpy/plots/tcmpr_plots/line/mean/tcmpr_line_mean.py @@ -3,7 +3,7 @@ from metplotpy.plots.tcmpr_plots.line.mean.tcmpr_series_line_mean import TcmprSeriesLineMean from metplotpy.plots.tcmpr_plots.line.tcmpr_line import TcmprLine -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprLineMean(TcmprLine): diff --git a/metplotpy/plots/tcmpr_plots/line/mean/tcmpr_series_line_mean.py b/metplotpy/plots/tcmpr_plots/line/mean/tcmpr_series_line_mean.py index cc013ca6..fc533388 100755 --- a/metplotpy/plots/tcmpr_plots/line/mean/tcmpr_series_line_mean.py +++ b/metplotpy/plots/tcmpr_plots/line/mean/tcmpr_series_line_mean.py @@ -19,7 +19,7 @@ import metcalcpy.util.utils as utils from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries from metplotpy.plots.tcmpr_plots.tcmpr_util import get_mean_ci -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprSeriesLineMean(TcmprSeries): diff --git a/metplotpy/plots/tcmpr_plots/line/median/tcmpr_line_median.py b/metplotpy/plots/tcmpr_plots/line/median/tcmpr_line_median.py index a5cced49..4352c990 100755 --- a/metplotpy/plots/tcmpr_plots/line/median/tcmpr_line_median.py +++ b/metplotpy/plots/tcmpr_plots/line/median/tcmpr_line_median.py @@ -3,7 +3,7 @@ from metplotpy.plots.tcmpr_plots.line.median.tcmpr_series_line_median import TcmprSeriesLineMedian from metplotpy.plots.tcmpr_plots.line.tcmpr_line import TcmprLine -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprLineMedian(TcmprLine): diff --git a/metplotpy/plots/tcmpr_plots/line/tcmpr_line.py b/metplotpy/plots/tcmpr_plots/line/tcmpr_line.py index f759e839..b43593cd 100755 --- a/metplotpy/plots/tcmpr_plots/line/tcmpr_line.py +++ b/metplotpy/plots/tcmpr_plots/line/tcmpr_line.py @@ -3,7 +3,7 @@ from metplotpy.plots.tcmpr_plots.tcmpr import Tcmpr from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprLine(Tcmpr): def __init__(self, config_obj, column_info, col, case_data, input_df, baseline_data, stat_name): diff --git a/metplotpy/plots/tcmpr_plots/rank/tcmpr_rank.py b/metplotpy/plots/tcmpr_plots/rank/tcmpr_rank.py index 8d348dcc..49dcb265 100755 --- a/metplotpy/plots/tcmpr_plots/rank/tcmpr_rank.py +++ b/metplotpy/plots/tcmpr_plots/rank/tcmpr_rank.py @@ -17,10 +17,9 @@ import plotly.graph_objects as go from metplotpy.plots.tcmpr_plots.tcmpr import Tcmpr -from metplotpy.plots.tcmpr_plots.tcmpr_config import TcmprConfig from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries from metplotpy.plots.tcmpr_plots.tcmpr_util import get_case_data -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprRank(Tcmpr): diff --git a/metplotpy/plots/tcmpr_plots/relperf/tcmpr_relperf.py b/metplotpy/plots/tcmpr_plots/relperf/tcmpr_relperf.py index f6c35089..377121db 100755 --- a/metplotpy/plots/tcmpr_plots/relperf/tcmpr_relperf.py +++ b/metplotpy/plots/tcmpr_plots/relperf/tcmpr_relperf.py @@ -1,17 +1,14 @@ import os -from typing import Union from datetime import datetime import numpy as np -from pandas import DataFrame import plotly.graph_objects as go from metcalcpy.util import utils -from metplotpy.plots.series import Series from metplotpy.plots.tcmpr_plots.tcmpr import Tcmpr from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries from metplotpy.plots.tcmpr_plots.tcmpr_util import get_case_data -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util diff --git a/metplotpy/plots/tcmpr_plots/skill/mean/tcmpr_series_skill_mean.py b/metplotpy/plots/tcmpr_plots/skill/mean/tcmpr_series_skill_mean.py index d7632c17..06de9ce4 100755 --- a/metplotpy/plots/tcmpr_plots/skill/mean/tcmpr_series_skill_mean.py +++ b/metplotpy/plots/tcmpr_plots/skill/mean/tcmpr_series_skill_mean.py @@ -16,7 +16,7 @@ import numpy as np from pandas import DataFrame from datetime import datetime -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util import metcalcpy.util.utils as utils from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries diff --git a/metplotpy/plots/tcmpr_plots/skill/mean/tcmpr_skill_mean.py b/metplotpy/plots/tcmpr_plots/skill/mean/tcmpr_skill_mean.py index 1f11bffc..500c7f50 100755 --- a/metplotpy/plots/tcmpr_plots/skill/mean/tcmpr_skill_mean.py +++ b/metplotpy/plots/tcmpr_plots/skill/mean/tcmpr_skill_mean.py @@ -7,7 +7,7 @@ from metcalcpy.util import utils from metplotpy.plots.tcmpr_plots.skill.mean.tcmpr_series_skill_mean import TcmprSeriesSkillMean from metplotpy.plots.tcmpr_plots.skill.tcmpr_skill import TcmprSkill -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprSkillMean(TcmprSkill): diff --git a/metplotpy/plots/tcmpr_plots/skill/median/tcmpr_skill_median.py b/metplotpy/plots/tcmpr_plots/skill/median/tcmpr_skill_median.py index cc0d9406..bdd2872d 100755 --- a/metplotpy/plots/tcmpr_plots/skill/median/tcmpr_skill_median.py +++ b/metplotpy/plots/tcmpr_plots/skill/median/tcmpr_skill_median.py @@ -4,7 +4,7 @@ from metcalcpy.util import utils from metplotpy.plots.tcmpr_plots.skill.median.tcmpr_series_skill_median import TcmprSeriesSkillMedian from metplotpy.plots.tcmpr_plots.skill.tcmpr_skill import TcmprSkill -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprSkillMedian(TcmprSkill): diff --git a/metplotpy/plots/tcmpr_plots/skill/tcmpr_skill.py b/metplotpy/plots/tcmpr_plots/skill/tcmpr_skill.py index 795dbe7e..ef6d6797 100755 --- a/metplotpy/plots/tcmpr_plots/skill/tcmpr_skill.py +++ b/metplotpy/plots/tcmpr_plots/skill/tcmpr_skill.py @@ -5,7 +5,7 @@ from metplotpy.plots.tcmpr_plots.tcmpr import Tcmpr from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries from metcalcpy.util import utils -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprSkill(Tcmpr): diff --git a/metplotpy/plots/tcmpr_plots/tcmpr.py b/metplotpy/plots/tcmpr_plots/tcmpr.py index 9988e064..32d92684 100755 --- a/metplotpy/plots/tcmpr_plots/tcmpr.py +++ b/metplotpy/plots/tcmpr_plots/tcmpr.py @@ -22,15 +22,15 @@ import numpy as np import pandas as pd import plotly.graph_objects as go -import yaml + from plotly.graph_objects import Figure from plotly.subplots import make_subplots import metcalcpy.util.utils as calc_util from metcalcpy.event_equalize import event_equalize -from metplotpy.plots import util -from metplotpy.plots.base_plot import BasePlot -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR +from metplotpy.plots import util_plotly as util +from metplotpy.plots.base_plot_plotly import BasePlot +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR from metplotpy.plots.tcmpr_plots.tcmpr_config import TcmprConfig from metplotpy.plots.tcmpr_plots.tcmpr_series import TcmprSeries from metplotpy.plots.tcmpr_plots.tcmpr_util import init_hfip_baseline, common_member, get_dep_column @@ -585,7 +585,7 @@ def create_plot(config_obj: dict) -> None: quotechar='"', skipinitialspace=True, encoding='utf-8') logger = util.get_common_logger(config_obj.log_level, config_obj.log_filename) -\ + for plot_type in config_obj.plot_type_list: # Apply event equalization, if requested diff --git a/metplotpy/plots/tcmpr_plots/tcmpr_config.py b/metplotpy/plots/tcmpr_plots/tcmpr_config.py index 0d75e358..7fc6de59 100755 --- a/metplotpy/plots/tcmpr_plots/tcmpr_config.py +++ b/metplotpy/plots/tcmpr_plots/tcmpr_config.py @@ -17,10 +17,9 @@ import itertools import metcalcpy.util.utils as utils -from .. import constants -from .. import util -from ..config import Config -import metplotpy.plots.util as util +from .. import constants_plotly as constants +from ..config_plotly import Config +import metplotpy.plots.util_plotly as util class TcmprConfig(Config): diff --git a/metplotpy/plots/tcmpr_plots/tcmpr_series.py b/metplotpy/plots/tcmpr_plots/tcmpr_series.py index 3d88bfef..f3dcab37 100755 --- a/metplotpy/plots/tcmpr_plots/tcmpr_series.py +++ b/metplotpy/plots/tcmpr_plots/tcmpr_series.py @@ -22,7 +22,7 @@ import metcalcpy.util.utils as utils from .tcmpr_util import get_prop_ci from ..series import Series -import metplotpy.plots.util as util +import metplotpy.plots.util_plotly as util class TcmprSeries(Series): diff --git a/metplotpy/plots/util.py b/metplotpy/plots/util.py index ad7c9954..c219edc4 100644 --- a/metplotpy/plots/util.py +++ b/metplotpy/plots/util.py @@ -23,7 +23,8 @@ import numpy as np from typing import Union import pandas as pd -from plotly.graph_objects import Figure +import matplotlib.pyplot as plt + from metplotpy.plots.context_filter import ContextFilter as cf import metcalcpy.util.pstd_statistics as pstats import metcalcpy.util.ctc_statistics as cstats @@ -98,8 +99,6 @@ def make_plot(config_filename, plot_class): try: plot = plot_class(params) plot.save_to_file() - #if plot.config_obj.show_in_browser: - # plot.show_in_browser() plot.write_html() plot.write_output_file() name = plot_class.__name__ if not hasattr(plot_class, 'LONG_NAME') else plot_class.LONG_NAME @@ -126,26 +125,25 @@ def alpha_blending(hex_color: str, alpha: float) -> str: return matplotlib.colors.rgb2hex(final) -def apply_weight_style(text: str, weight: int) -> str: - """ - Applied HTML style weight to text: - 1 - none - 2 - bold - 3 - italic - 4 - bold italic - - :param text: text to style - :param weight: - int representation of the style - :return: styled text +def get_font_params(weight: int) -> dict: + """Convert integer font style/weight value to a dictionary of + font properties, fontweight for bold and fontstyle for italic. + 1=plain text, 2=bold, 3=italic, 4=bold italic + REMOVE: Replaces apply_weight_style function used for plotly. + + @param weight integer representation of the style/weight + @returns dictionary containing font properties like fontweight and fontstyle """ - if len(text) > 0: - if weight == 2: - return '' + text + '' - if weight == 3: - return '' + text + '' - if weight == 4: - return '' + text + '' - return text + font_params = { + 'fontweight': 'normal', + 'fontstyle': 'normal', + } + if weight in (2, 4): + font_params['fontweight'] = 'bold' + if weight in (3, 4): + font_params['fontstyle'] = 'italic' + + return font_params def nicenumber(x, to_round): @@ -200,36 +198,24 @@ def pretty(low, high, number_of_intervals) -> Union[np.ndarray, list]: return np.arange(miny, maxy + 0.5 * d, d) -def add_horizontal_line(figure: Figure, y: float, line_properties: dict) -> None: - """ - Adds a horizontal line to the Plotly Figure - :param figure: Plotly plot to add a line to - :param y: y value for the line - :param line_properties: dictionary with line properties like color, width, dash - :return: +def add_horizontal_line(y: float, line_properties: dict) -> None: + """Adds a horizontal line to the matplotlib plot + + @param y y value for the line + @param line_properties dictionary with line properties like color, width, dash + @returns None """ - figure.add_shape( - type='line', - yref='y', y0=y, y1=y, - xref='paper', x0=0, x1=1, - line=line_properties, - ) + plt.axhline(y=y, xmin=0, xmax=1, **line_properties) -def add_vertical_line(figure: Figure, x: float, line_properties: dict) -> None: - """ - Adds a vertical line to the Plotly Figure - :param figure: Plotly plot to add a line to - :param x: x value for the line - :param line_properties: dictionary with line properties like color, width, dash - :return: +def add_vertical_line(x: float, line_properties: dict) -> None: + """Adds a vertical line to the matplotlib plot + + @param x x value for the line + @param line_properties dictionary with line properties like color, width, dash + @returns None """ - figure.add_shape( - type='line', - yref='paper', y0=0, y1=1, - xref='x', x0=x, x1=x, - line=line_properties, - ) + plt.axvline(x=x, ymin=0, ymax=1, **line_properties) def abline(x_value: float, intercept: float, slope: float) -> float: diff --git a/metplotpy/plots/util_plotly.py b/metplotpy/plots/util_plotly.py new file mode 100644 index 00000000..ad7c9954 --- /dev/null +++ b/metplotpy/plots/util_plotly.py @@ -0,0 +1,698 @@ +# ============================* +# ** Copyright UCAR (c) 2020 +# ** University Corporation for Atmospheric Research (UCAR) +# ** National Center for Atmospheric Research (NCAR) +# ** Research Applications Lab (RAL) +# ** P.O.Box 3000, Boulder, Colorado, 80307-3000, USA +# ============================* + + +""" + Collection of utility functions used by multiple plotting classes +""" +__author__ = 'Minna Win' + +import argparse +import sys +import os +import logging +import gc +import re +from datetime import datetime +import matplotlib +import numpy as np +from typing import Union +import pandas as pd +from plotly.graph_objects import Figure +from metplotpy.plots.context_filter import ContextFilter as cf +import metcalcpy.util.pstd_statistics as pstats +import metcalcpy.util.ctc_statistics as cstats +from metcalcpy.util.read_env_vars_in_config import parse_config + +COLORSCALES = { + 'green_red': ['#E6FFE2', '#B3FAAD', '#74F578', '#30D244', '#00A01E', '#F6A1A2', + '#E26667', '#C93F41', '#A42526'], + 'blue_white_brown': ['#1962CF', '#3E94F2', '#B4F0F9', '#00A01E', '#4AF058', + '#C7FFC0', '#FFFFFF', '#FFE97F', + '#FF3A20', '#A50C0F', '#E1BFB5', '#A0786F', '#643D34'], + 'cm_colors': ["#80FFFF", "#95FFFF", "#AAFFFF", "#BFFFFF", "#D4FFFF", "#EAFFFF", + "#FFFFFF", "#FFEAFF", "#FFD5FF", + "#FFBFFF", "#FFAAFF", "#FF95FF", "#FF80FF"], + 'topo_colors': ["#4C00FF", "#0000FF", "#004CFF", "#0099FF", "#00E5FF", "#00FF4D", + "#1AFF00", "#80FF00", "#E6FF00", + "#FFFF00", "#FFE53B", "#FFDB77", "#FFE0B3"], + 'terrain_colors': ["#00A600", "#24B300", "#4CBF00", "#7ACC00", "#ADD900", "#E6E600", + "#E7CB21", "#E9BA43", + "#EBB165", "#EDB387", "#EFBEAA", "#F0D3CE", "#F2F2F2"], + 'heat_colors': ["#FF0000", "#FF1C00", "#FF3900", "#FF5500", "#FF7100", "#FF8E00", + "#FFAA00", "#FFC600", "#FFE300", + "#FFFF00", "#FFFF2A", "#FFFF80", "#FFFFD5"], + 'rainbow': ["#FF0000", "#FF7600", "#FFEB00", "#9DFF00", "#27FF00", "#00FF4E", + "#00FFC4", "#00C4FF", "#004EFF", + "#2700FF", "#9D00FF", "#FF00EB", "#FF0076"] +} + + +def read_config_from_command_line(): + """ + Read the "custom" config file from the command line + + Args: + + Returns: + The full path to the config file + """ + # Create Parser + parser = argparse.ArgumentParser(description='Read in config file') + + # Add arguments + parser.add_argument('Path', metavar='path', type=str, + help='the full path to config file') + + # Execute the parse_args() method + args = parser.parse_args() + return args.Path + + +def get_params(config_filename): + """!Read config_filename or get config file from command line, then parse + config file and return it as a dictionary. + + @param config_filename The full path to the config file or None + @returns dictionary containing parameters for plot + """ + config_file = config_filename if config_filename else read_config_from_command_line() + return parse_config(config_file) + + +def make_plot(config_filename, plot_class): + """!Get plot parameters and create the plot. + + @param config_filename The full path to the config or None + @param plot_class class of plot to produce, e.g. Bar or Box + @returns plot class object or None if something went wrong + """ + # Retrieve the contents of the custom config file to over-ride + # or augment settings defined by the default config file. + params = get_params(config_filename) + try: + plot = plot_class(params) + plot.save_to_file() + #if plot.config_obj.show_in_browser: + # plot.show_in_browser() + plot.write_html() + plot.write_output_file() + name = plot_class.__name__ if not hasattr(plot_class, 'LONG_NAME') else plot_class.LONG_NAME + plot.logger.info(f"Finished {name} plot at {datetime.now()}") + return plot + except ValueError as val_er: + print(val_er) + + return None + + +def alpha_blending(hex_color: str, alpha: float) -> str: + """ Alpha color blending as if on the white background. + Useful for gridlines + + Args: + @param hex_color - the color in hex + @param alpha - Alpha value between 0 and 1 + Returns: blended hex color + """ + foreground_tuple = matplotlib.colors.hex2color(hex_color) + foreground_arr = np.array(foreground_tuple) + final = tuple((1. - alpha) + foreground_arr * alpha) + return matplotlib.colors.rgb2hex(final) + + +def apply_weight_style(text: str, weight: int) -> str: + """ + Applied HTML style weight to text: + 1 - none + 2 - bold + 3 - italic + 4 - bold italic + + :param text: text to style + :param weight: - int representation of the style + :return: styled text + """ + if len(text) > 0: + if weight == 2: + return '' + text + '' + if weight == 3: + return '' + text + '' + if weight == 4: + return '' + text + '' + return text + + +def nicenumber(x, to_round): + """ + Calculates a close nice number, i. e. a number with simple decimals. + :param x: A number + :param to_round: Should the number be rounded? + :return: A number with simple decimals + """ + exp = np.floor(np.log10(x)) + f = x / 10 ** exp + + if to_round: + if f < 1.5: + nf = 1. + elif f < 3.: + nf = 2. + elif f < 7.: + nf = 5. + else: + nf = 10. + else: + if f <= 1.: + nf = 1. + elif f <= 2.: + nf = 2. + elif f <= 5.: + nf = 5. + else: + nf = 10. + + return nf * 10. ** exp + + +def pretty(low, high, number_of_intervals) -> Union[np.ndarray, list]: + """ + Compute a sequence of about n+1 equally spaced ‘round’ values which cover the + range of the values in x + Can be used to create axis labels or bins + :param low: min value + :param high: max value + :param number_of_intervals: number of intervals + :return: + """ + if number_of_intervals == 1: + return [-1, 0] + + num_range = nicenumber(high - low, False) + d = nicenumber(num_range / (number_of_intervals - 1), True) + miny = np.floor(low / d) * d + maxy = np.ceil(high / d) * d + return np.arange(miny, maxy + 0.5 * d, d) + + +def add_horizontal_line(figure: Figure, y: float, line_properties: dict) -> None: + """ + Adds a horizontal line to the Plotly Figure + :param figure: Plotly plot to add a line to + :param y: y value for the line + :param line_properties: dictionary with line properties like color, width, dash + :return: + """ + figure.add_shape( + type='line', + yref='y', y0=y, y1=y, + xref='paper', x0=0, x1=1, + line=line_properties, + ) + + +def add_vertical_line(figure: Figure, x: float, line_properties: dict) -> None: + """ + Adds a vertical line to the Plotly Figure + :param figure: Plotly plot to add a line to + :param x: x value for the line + :param line_properties: dictionary with line properties like color, width, dash + :return: + """ + figure.add_shape( + type='line', + yref='paper', y0=0, y1=1, + xref='x', x0=x, x1=x, + line=line_properties, + ) + + +def abline(x_value: float, intercept: float, slope: float) -> float: + """ + Calculates y coordinate based on x-value, intercept and slope + :param x_value: x coordinate + :param intercept: intercept + :param slope: slope + :return: y value + """ + return slope * x_value + intercept + + +def is_threshold_value(values: Union[pd.core.series.Series, list]): + """ + Determines if a pandas Series of values are threshold values (e.g. '>=1', '<5.0', + '>21') + + Args: + @param values: pandas Series of independent variables comprising the x-axis + + Returns: + A tuple of boolean values: + True if any of these values is a threshold (ie. operator and number) and True if + these are mixed threshold + (==SFP50,==FBIAS1, etc. ). False otherwise. + + """ + + thresh_ctr = 0 + percent_thresh_ctr = 0 + is_percent_thresh = False + is_thresh = False + # Check all the threshold values, there may be some threshold values that do not + # have an equality operator when equality is implied. + for cur_value in values: + match_pct = re.match( + r'(\<|\<=|\==|\>=|\>)(\s)*(SFP|SOP|SCP|USP|CDP|FBIAS)(\s)*([+-]?([0-9]*[' + r'.])?[0-9]+)', + str(cur_value)) + match_thresh = re.match(r'(\<|\<=|\==|\>=|\>)(\s)*([+-]?([0-9]*[.])?[0-9]+)', + str(cur_value)) + if match_pct: + # This is a percent threshold, with values like '==FBIAS1'. + percent_thresh_ctr += 1 + elif match_thresh: + thresh_ctr += 1 + + if thresh_ctr >= 1: + is_thresh = True + if percent_thresh_ctr >= 1: + is_percent_thresh = True + + return is_thresh, is_percent_thresh + + +def sort_threshold_values(thresh_values: pd.core.series.Series) -> list: + """ + Sort the threshold values based on operator and numerical value + + Args: + @param thresh_values: a pandas Series of threshold values (operator + number) + + Return: + sorted_thresholds: A list of threshold values as strings (operator+numerical + value) + """ + + operators = [] + values = [] + for cur_val in thresh_values: + # treat the thresh value as comprised of two groups, one + # for the operator and the other for the value (which can be a + # negative value) + match = re.match(r'(\<|\<=|\==|\>=|\>)(\s)*([+-]?([0-9]*[.])?[0-9]+)', + str(cur_val)) + if match: + operators.append(match.group(1)) + value = float(match.group(3)) + values.append(value) + else: + # This is a bare number (float or int) + operators.append(None) + values.append(float(cur_val)) + + # Apply weights to the operators + wt_maps = {'<': 1, '<=': 2, '==': 3, '>=': 4, '>': 5} + wts = [] + + for operator in operators: + # assign weight for == if no + # operator is indicated, assuming + # that a fcst_thresh of 5 is the same as + # ==5 + # otherwise, assign the appropriate weight to + # the operator + if operator is None: + wts.append(3) + else: + wts.append(wt_maps[operator]) + + # Create a pandas dataframe to use the ability to sort by multiple columns + thresh_dict = {'thresh': thresh_values, 'thresh_values': values, 'op_wts': wts} + df = pd.DataFrame(thresh_dict) + + # cols is the list of columns upon which we should sort + twocols = ['thresh_values', 'op_wts'] + sorted_val_wt = df.sort_values(by=twocols, inplace=False, ascending=True, + ignore_index=True) + + # now the dataframe has the xyz_thresh values sorted appropriately + return sorted_val_wt['thresh'] + + +def get_common_logger(log_level, log_filename): + ''' + Args: + @param log_level: The log level + @param log_filename: The full path to the log file + filename + Returns: + common_logger: the logger common to all the METplotpy modules that are + currently in use by a plot type. + ''' + + # If directory for logfile doesn't exist, create it + log_dir = os.path.dirname(log_filename) + try: + os.makedirs(log_dir, exist_ok=True) + except OSError: + pass + + # Supported log levels. + log_level = log_level.upper() + log_levels = {'DEBUG': logging.DEBUG, 'INFO': logging.INFO, + 'WARNING': logging.WARNING, 'ERROR': logging.ERROR, + 'CRITICAL': logging.CRITICAL} + + if log_filename.lower() == 'stdout': + logging.basicConfig(level=log_levels[log_level], + format='%(asctime)s||User:%(' + 'user)s||%(funcName)s|| [%(levelname)s]: %(' + 'message)s', + datefmt='%Y-%m-%d %H:%M:%S', + stream=sys.stdout) + else: + + logging.basicConfig(level=log_levels[log_level], + format='%(asctime)s||User:%(' + 'user)s||%(funcName)s|| [%(levelname)s]: %(' + 'message)s', + datefmt='%Y-%m-%d %H:%M:%S', + filename=log_filename, + filemode='w') + logging.getLogger(name='matplotlib').setLevel(logging.CRITICAL) + common_logger = logging.getLogger(__name__) + f = cf() + common_logger.addFilter(f) + + return common_logger + + +def is_thresh_column(column_name: str) -> bool: + ''' + Determines if a column is a threshold column, i.e. cov_thresh, fcst_thresh, + or obs_thresh. + + Args: + + @param column_name: A string representation of the column name + + Returns: True if this column is a threshold column, False otherwise + ''' + + match = re.match(r'.*_thresh.*', column_name) + if match: + return True + else: + return False + + +def filter_by_fixed_vars(input_df: pd.DataFrame, settings_dict: dict) -> pd.DataFrame: + """ + Filter the input data based on values in the settings_dict dictionary. + For each key (corresponding to a column in the input_df dataframe), + create a query string. Use that query string to filter the input dataframe. + Repeat for all the keys and their corresponding values in the settings_dict. + + Use the pandas query() to perform database-like syntax for filtering the data: + col in ('a', 'b', '3', ...,'z') + + where col is the name of the key and the values in the parens represent + the values corresponding to that key. + + Since Python handles nan values in an unexpected way, if 'NA' is a value in the + list of values corresponding to a key, then different syntax will be required: + + col.isnull() | col in ('a', 'b', ..., 'z') + + Args: + input_df: The input dataframe to be subset. This is needed to check for + valid columns. + settings_dict: The dictionary representation of the settings in the YAML + configuration file + + Returns: + filtered_df: The filtered dataframe + """ + + # check if columns (keys) in the settings_dict exist in the dataframe before + # attempting to subset. If the settings_dict has keys that do not have + # corresponding column values in the dataframe, return the input dataframe. + valid_columns = [col for col in settings_dict if col in input_df.columns] + + if len(valid_columns) == 0: + print( + "No columns in data match what is requested for filtering by fixed variable. Input dataframe will be " + "returned") + return input_df + + # The pandas query method does not work as expected if + # one of the values in the list is 'NA'. When 'NA' is an element in the list + # use the col.isnull() syntax with the col in ('a', 'b', ..., 'z') syntax + # for the remaining values. + + # Create a query string for each column and save in a list + query_string_list = [] + + # Use an intermediate dataframe for filtering iteratively by column + filtered_df = input_df.copy(deep=True) + + for idx, col in enumerate(valid_columns): + # Variables for creating the query string + prev_val_string = "" + single_quote = "'" + list_sep = ", " + list_start = "(" + list_terminator = ")" + or_token = "| " + in_token = " in " + isnull_token = ".isnull()" + is_last_val = False + na_found = False + updated_vals = [] + + # Remove NA from the list of values and create a new + # list of values containing the remaining non-NA values. + values = settings_dict[col] + + # Check for incorrectly formatted fixed_vars_vals_input that is generated + # by the MVBatch.java: + # fixed_vars_vals_input: + # vx_mask: regionA + # + # the correct format: + # fixed_vars_vals_input: + # vx_mask: [regionA] + # + # OR + # + # fixed_vars_vals_input: + # vx_mask: + # - regionA + # + # + # Check if the value to the key (i.e. vx_mask, etc) is a string and convert it to a list + # i.e.: + # correct_value = [value] + # + # where value corresponds to regionA in example above + # + if type(values) is str: + values = [values] + + for val in values: + if val == 'NA': + na_found = True + else: + updated_vals.append(val) + + # Create the query string based on whether NA values exist. + if na_found: + if len(updated_vals) == 0: + # NA was the only value for this column, create the query + # then move onto the next column + prev_val_string = col + isnull_token + query_string_list.append(prev_val_string) + + else: + # At least one non-NA value in the list of values + prev_val_string = col + isnull_token + or_token + is_last_val = False + # Build remaining portion of the query (ie the col in ('a', 'b', + # 'c')) + for val_idx, val in enumerate(updated_vals): + # Identify when the last value in the list + # has been reached to avoid adding a ',' after + # the last value. + last_val = val_idx + 1 + + if last_val == len(updated_vals): + is_last_val = True + + # Create the 'col in' portion of the query + if val_idx == 0 and is_last_val: + # Both the first and last element in the list (i.e. list of one + # element) + prev_val_string = prev_val_string + col + in_token + \ + list_start + single_quote + val + \ + single_quote + list_terminator + + elif val_idx == 0 and (not is_last_val): + # First value of a list of values + prev_val_string = prev_val_string + col + in_token + \ + list_start + single_quote + val + \ + single_quote + list_sep + + + + elif val_idx > 0 and not is_last_val: + # One of the middle values in the list + query_string = prev_val_string + single_quote + val + \ + single_quote + list_sep + + else: + # The last value in the list + prev_val_string = prev_val_string + single_quote + val + \ + single_quote + list_terminator + + query_string_list.append(prev_val_string) + + + else: + + # No NA's found in values. Create the query: col in ('a', 'b', 'c') + prev_val_string = "" + is_last_val = False + + for val_idx, val in enumerate(updated_vals): + + # Identify when the last value in the list + # has been reached to avoid adding a ',' after + # the last value. + last_val = val_idx + 1 + + if last_val == len(updated_vals): + is_last_val = True + + # Only one value in the values list (both first and last element) + if val_idx == 0 and is_last_val: + prev_val_string = prev_val_string + col + in_token + list_start \ + + single_quote + val + single_quote + \ + list_terminator + + elif val_idx == 0 and (not is_last_val): + # First value of a list of values + prev_val_string = prev_val_string + col + in_token + \ + list_start + single_quote + val + \ + single_quote + list_sep + + elif val_idx > 0 and not is_last_val: + # One of the middle values in the list + prev_val_string = prev_val_string + single_quote + val + single_quote + list_sep + else: + # Last value in the list + prev_val_string = prev_val_string + single_quote + val + single_quote + list_terminator + + query_string_list.append(prev_val_string) + + # Perform query for each column (key) + for cur_query in query_string_list: + working_df = filtered_df.query(cur_query) + filtered_df = working_df.copy(deep=True) + + # clean up + del working_df + gc.collect() + + return filtered_df + + +def prepare_pct_roc(subset_df): + """ + Initialize the PCT ROC plot data, appends a beginning and end point + :param subset_df: PCT data + :return: PCT ROC plot data + """ + roc_df = pstats._calc_pct_roc(subset_df) + pody = roc_df['pody'] + pody = pd.concat([pd.Series([1]), pody], ignore_index=True) + pody = pd.concat([pody, pd.Series([0])]) + pofd = roc_df['pofd'] + pofd = pd.concat([pd.Series([1]), pofd], ignore_index=True) + pofd = pd.concat([pofd, pd.Series([0])], ignore_index=True) + thresh = roc_df['thresh'] + thresh = pd.concat([pd.Series(['']), thresh], ignore_index=True) + thresh = pd.concat([thresh, pd.Series([''])], ignore_index=True) + + return pody, pofd, thresh + + +def prepare_ctc_roc(subset_df, is_ascending): + """ + Initialize the CTC ROC plot data, appends a beginning and end point + :param subset_df: CTC data + :param is_ascending: thresh order + :return: CTC ROC plot data + """ + df_roc = cstats.calculate_ctc_roc(subset_df, ascending=is_ascending) + pody = df_roc['pody'] + pody = pd.concat([pd.Series([1]), pody], ignore_index=True) + pody = pd.concat([pody, pd.Series([0])], ignore_index=True) + pofd = df_roc['pofd'] + pofd = pd.concat([pd.Series([1]), pofd], ignore_index=True) + pofd = pd.concat([pofd, pd.Series([0])], ignore_index=True) + thresh = df_roc['thresh'] + thresh = pd.concat([pd.Series(['']), thresh], ignore_index=True) + thresh = pd.concat([thresh, pd.Series([''])], ignore_index=True) + + return pody, pofd, thresh + + +def strtobool(env_var:str)->bool: + """ + Since distutils.util.strtobool was deprecated in Python 3.12, implement + our own version. + + In the distutils.util.strtobool, a simple one line command was used to determine + whether an environment variable was set to True or False. In this + example, the default value is set to False in the event that the environment + variable is not defined: + + turn_on_logging = strtobool(os.getenv('LOG_BASE_PLOT', 'False') ) + + Environment variables can be set as string or bool. Evaluate whether a string + value for true or false (support case-insensitive text) is True/False and + set the default value. + + Args: + @parm env_vars: string name of the environment variable to evaluate + + turn_on_logging = strtobool(os.getenv('LOG_BASE_PLOT') ) + """ + + true_list = ['true', 't', '1',] + false_list = ['false', 'f', '0' ] + # if the environment variable does not exist, then return False + try: + val = os.environ[env_var] + except KeyError: + return False + + # If the environment variable is None, return false + if val is None: + return False + else: + # Check for variations of truth values + lower = val.lower() + if lower in true_list: + return True + elif lower in false_list: + return False + else: + msg = "Value does not represent a truth value (i.e. true or false)" + raise ValueError(msg) + + diff --git a/metplotpy/plots/wind_rose/wind_rose.py b/metplotpy/plots/wind_rose/wind_rose.py index fea5d568..ca6a7cff 100644 --- a/metplotpy/plots/wind_rose/wind_rose.py +++ b/metplotpy/plots/wind_rose/wind_rose.py @@ -26,10 +26,10 @@ import plotly.graph_objects as go from plotly.subplots import make_subplots -from metplotpy.plots.base_plot import BasePlot +from metplotpy.plots.base_plot_plotly import BasePlot from metplotpy.plots.wind_rose.wind_rose_config import WindRoseConfig from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR -from metplotpy.plots import util +from metplotpy.plots import util_plotly as util class WindRosePlot(BasePlot): From 1a38b17f5abc997f4462e5c2beebd81f4fffd6db Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:40:53 -0700 Subject: [PATCH 03/28] Do not use util.apply_weight_style -- it adds html which isn't used by matplotlib. Instead set xaxis label weight similar to taylor_diagram logic --- metplotpy/plots/scatter/scatter_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metplotpy/plots/scatter/scatter_config.py b/metplotpy/plots/scatter/scatter_config.py index 817c7c95..1d471ef3 100644 --- a/metplotpy/plots/scatter/scatter_config.py +++ b/metplotpy/plots/scatter/scatter_config.py @@ -98,8 +98,8 @@ def __init__(self, parameters): if self.x_tickangle in constants.XAXIS_ORIENTATION.keys(): self.x_tickangle = constants.XAXIS_ORIENTATION[self.x_tickangle] self.x_tickfont_size = self.parameters['xtlab_size'] + constants.DEFAULT_TITLE_FONTSIZE - self.xaxis = util.apply_weight_style(self.xaxis, self.parameters['xlab_weight']) - + xlab_weight = self.parameters['xlab_weight'] + self.xlab_weight = constants.MV_TO_MPL_CAPTION_STYLE[xlab_weight] ############################################## self.marker_symbol = self._get_marker() From ceb58021d3a251d942d6e0ed2b786d6a030b7ac0 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:46:16 -0700 Subject: [PATCH 04/28] add fix for creating parent directories to plotly version of base plot --- metplotpy/plots/base_plot_plotly.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metplotpy/plots/base_plot_plotly.py b/metplotpy/plots/base_plot_plotly.py index ccb915ed..197720e9 100644 --- a/metplotpy/plots/base_plot_plotly.py +++ b/metplotpy/plots/base_plot_plotly.py @@ -387,8 +387,7 @@ def save_to_file(self): # Create the directory for the output plot if it doesn't already exist dirname = os.path.dirname(os.path.abspath(image_name)) - if not os.path.exists(dirname): - os.mkdir(dirname) + os.makedirs(dirname, exist_ok=True) if self.figure: try: self.figure.write_image(image_name) From 69d5b386a33472f059adadb1534d525f12e8c24b Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:33:08 -0700 Subject: [PATCH 05/28] hotfix: fix handling of missing data by changing replace value from string 9999 to integer 9999 and use np.nan instead of string 'NA' --- metplotpy/plots/skew_t/skew_t.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metplotpy/plots/skew_t/skew_t.py b/metplotpy/plots/skew_t/skew_t.py index 1c171f57..e6713117 100644 --- a/metplotpy/plots/skew_t/skew_t.py +++ b/metplotpy/plots/skew_t/skew_t.py @@ -67,7 +67,7 @@ def extract_sounding_data(input_file, output_directory): # Read in the current sounding data file, replacing any 9999 values with NaN. df_raw: pandas.DataFrame = pd.read_csv(sounding_data_file, sep=r'\s+', skiprows=1, engine='python') - df_raw.replace('9999', 'NA', inplace=True) + df_raw.replace(9999, np.nan, inplace=True) # Rename some columns so they are more descriptive df: pandas.DataFrame = df_raw.rename(columns={'TIME': 'FIELD', '(HR)': 'UNITS'}) From 150a3c77b2e0acf4260eefd9c7d6ca31bd49c0de Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:33:51 -0700 Subject: [PATCH 06/28] hotfix: remove string representing color that causes UserWarning and causes yaml configurations for lines to be ignored --- metplotpy/plots/skew_t/skew_t.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/metplotpy/plots/skew_t/skew_t.py b/metplotpy/plots/skew_t/skew_t.py index e6713117..2402ffc4 100644 --- a/metplotpy/plots/skew_t/skew_t.py +++ b/metplotpy/plots/skew_t/skew_t.py @@ -590,14 +590,14 @@ def create_skew_t(input_file: str, config: dict, logger: logging) -> None: temp_linewidth = config['temp_line_thickness'] temp_linestyle = config['temp_line_style'] temp_linecolor = config['temp_line_color'] - skew.plot(pressure, temperature, 'r', linewidth=temp_linewidth, + skew.plot(pressure, temperature, linewidth=temp_linewidth, linestyle=temp_linestyle, color=temp_linecolor) dewpt_linewidth = config['dewpt_line_thickness'] dewpt_linestyle = config['dewpt_line_style'] dewpt_linecolor = config['dewpt_line_color'] logger.info(f"Generate the dew point line for {cur_time} hour") - skew.plot(pressure, dew_pt, 'g', linewidth=dewpt_linewidth, + skew.plot(pressure, dew_pt, linewidth=dewpt_linewidth, linestyle=dewpt_linestyle, color=dewpt_linecolor) # Adiabat and mixing lines. From 0cbf05426fe5281ac5b11fa152aa1a62de437da6 Mon Sep 17 00:00:00 2001 From: MWin <3753118+bikegeek@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:01:52 -0700 Subject: [PATCH 07/28] Feature 561 v4.0.0 beta1 (#562) * Update version and date for beta1 release * Updates for beta1 release * Fixed error with "not enough underlines" * Update release-notes.rst remove one underscore * Update release-notes.rst removed one too many underlines * Update release-notes.rst one too many underlines in the upgrade instructions --- docs/Users_Guide/release-notes.rst | 11 ++++++----- docs/conf.py | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/Users_Guide/release-notes.rst b/docs/Users_Guide/release-notes.rst index 9f4a6ad8..787730f0 100644 --- a/docs/Users_Guide/release-notes.rst +++ b/docs/Users_Guide/release-notes.rst @@ -7,8 +7,9 @@ describes the bugfix, enhancement, or new feature: `METplotpy GitHub issues. `_ -METplotpy Version 3.2.0-RC1 release notes (20250930) -==================================================== +METplotpy Version 4.0.0-beta1 release notes (20260205) +====================================================== + .. dropdown:: New Plots @@ -16,12 +17,12 @@ METplotpy Version 3.2.0-RC1 release notes (20250930) .. dropdown:: Enhancements - * Wind Rose: support specifying range radialaxes range (`#534 `_) - * Support YAML config files generated by METviewer MVBatch (`#540 `_) + * None .. dropdown:: Bugfixes - * Bugfix Vertical Plot fix incorrect x-values/x-axis labels (`#537 `_) + * Use the METviewer data output directory when the points path is not defined in the yaml config file created by METviewer (`#552 `_) + * WindRose plot shows incorrect legend when max wind speed is less than requested (`#547 `_) .. dropdown:: Documentation diff --git a/docs/conf.py b/docs/conf.py index 1bb37e21..ffff38ac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,14 +22,14 @@ # -- Project information ----------------------------------------------------- project = 'METplotpy' -copyright = '2025, NSF NCAR' +copyright = '2026, NSF NCAR' author = 'UCAR/NSF NCAR, NOAA, CSU/CIRA, and CU/CIRES' author_list = 'Adriaansen, D., C. Kalb, D. Fillmore, T. Jensen, L. Goodrich, M. Win-Gildenmeister, T. Burek, and H. Fisher' verinfo = version release = f'{version}' -release_year = '2025' +release_year = '2026' -release_date = f'{release_year}-09-30' +release_date = f'{release_year}-02-05' copyright = f'{release_year}, {author}' From 2b9219f0b688c30fd8649f26154158310ca6fae1 Mon Sep 17 00:00:00 2001 From: MWin <3753118+bikegeek@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:51:25 -0700 Subject: [PATCH 08/28] Revert "Feature 561 v4.0.0 beta1 (#562)" (#563) This reverts commit 0cbf05426fe5281ac5b11fa152aa1a62de437da6. --- docs/Users_Guide/release-notes.rst | 11 +++++------ docs/conf.py | 6 +++--- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/Users_Guide/release-notes.rst b/docs/Users_Guide/release-notes.rst index 787730f0..9f4a6ad8 100644 --- a/docs/Users_Guide/release-notes.rst +++ b/docs/Users_Guide/release-notes.rst @@ -7,9 +7,8 @@ describes the bugfix, enhancement, or new feature: `METplotpy GitHub issues. `_ -METplotpy Version 4.0.0-beta1 release notes (20260205) -====================================================== - +METplotpy Version 3.2.0-RC1 release notes (20250930) +==================================================== .. dropdown:: New Plots @@ -17,12 +16,12 @@ METplotpy Version 4.0.0-beta1 release notes (20260205) .. dropdown:: Enhancements - * None + * Wind Rose: support specifying range radialaxes range (`#534 `_) + * Support YAML config files generated by METviewer MVBatch (`#540 `_) .. dropdown:: Bugfixes - * Use the METviewer data output directory when the points path is not defined in the yaml config file created by METviewer (`#552 `_) - * WindRose plot shows incorrect legend when max wind speed is less than requested (`#547 `_) + * Bugfix Vertical Plot fix incorrect x-values/x-axis labels (`#537 `_) .. dropdown:: Documentation diff --git a/docs/conf.py b/docs/conf.py index ffff38ac..1bb37e21 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,14 +22,14 @@ # -- Project information ----------------------------------------------------- project = 'METplotpy' -copyright = '2026, NSF NCAR' +copyright = '2025, NSF NCAR' author = 'UCAR/NSF NCAR, NOAA, CSU/CIRA, and CU/CIRES' author_list = 'Adriaansen, D., C. Kalb, D. Fillmore, T. Jensen, L. Goodrich, M. Win-Gildenmeister, T. Burek, and H. Fisher' verinfo = version release = f'{version}' -release_year = '2026' +release_year = '2025' -release_date = f'{release_year}-02-05' +release_date = f'{release_year}-09-30' copyright = f'{release_year}, {author}' From af1c931c6af4ef384fe1fa7c0249bb59850f0cb5 Mon Sep 17 00:00:00 2001 From: MWin <3753118+bikegeek@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:55:29 -0700 Subject: [PATCH 09/28] Revert "Revert "Feature 561 v4.0.0 beta1 (#562)" (#563)" (#564) This reverts commit 2b9219f0b688c30fd8649f26154158310ca6fae1. --- docs/Users_Guide/release-notes.rst | 11 ++++++----- docs/conf.py | 6 +++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/Users_Guide/release-notes.rst b/docs/Users_Guide/release-notes.rst index 9f4a6ad8..787730f0 100644 --- a/docs/Users_Guide/release-notes.rst +++ b/docs/Users_Guide/release-notes.rst @@ -7,8 +7,9 @@ describes the bugfix, enhancement, or new feature: `METplotpy GitHub issues. `_ -METplotpy Version 3.2.0-RC1 release notes (20250930) -==================================================== +METplotpy Version 4.0.0-beta1 release notes (20260205) +====================================================== + .. dropdown:: New Plots @@ -16,12 +17,12 @@ METplotpy Version 3.2.0-RC1 release notes (20250930) .. dropdown:: Enhancements - * Wind Rose: support specifying range radialaxes range (`#534 `_) - * Support YAML config files generated by METviewer MVBatch (`#540 `_) + * None .. dropdown:: Bugfixes - * Bugfix Vertical Plot fix incorrect x-values/x-axis labels (`#537 `_) + * Use the METviewer data output directory when the points path is not defined in the yaml config file created by METviewer (`#552 `_) + * WindRose plot shows incorrect legend when max wind speed is less than requested (`#547 `_) .. dropdown:: Documentation diff --git a/docs/conf.py b/docs/conf.py index 1bb37e21..ffff38ac 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,14 +22,14 @@ # -- Project information ----------------------------------------------------- project = 'METplotpy' -copyright = '2025, NSF NCAR' +copyright = '2026, NSF NCAR' author = 'UCAR/NSF NCAR, NOAA, CSU/CIRA, and CU/CIRES' author_list = 'Adriaansen, D., C. Kalb, D. Fillmore, T. Jensen, L. Goodrich, M. Win-Gildenmeister, T. Burek, and H. Fisher' verinfo = version release = f'{version}' -release_year = '2025' +release_year = '2026' -release_date = f'{release_year}-09-30' +release_date = f'{release_year}-02-05' copyright = f'{release_year}, {author}' From b6647403df78c9158dc6ab878d9bc8045c5d7ab0 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Thu, 5 Feb 2026 11:36:06 -0700 Subject: [PATCH 10/28] remove more plotly-specific stuff from matplotlib version --- metplotpy/plots/base_plot.py | 46 +++--------------------------------- 1 file changed, 3 insertions(+), 43 deletions(-) diff --git a/metplotpy/plots/base_plot.py b/metplotpy/plots/base_plot.py index 6925ce64..5048ff62 100644 --- a/metplotpy/plots/base_plot.py +++ b/metplotpy/plots/base_plot.py @@ -20,27 +20,15 @@ import numpy as np import yaml from typing import Union -import kaleido + import metplotpy.plots.util from metplotpy.plots.util import strtobool from .config import Config from metplotpy.plots.context_filter import ContextFilter -# kaleido 0.x will be deprecated after September 2025 and Chrome will no longer -# be included with kaleido from version 1.0.0. Explicitly get Chrome via call to kaleido. - -# In some instances, we do NOT want Chrome to be installed at run-time. If the -# PRE_LOAD_CHROME environment variable exists, or set to TRUE, -# then Chrome will be assumed to have been pre-loaded. Otherwise, -# invoke get_chrome_sync() to install Chrome in the -# /path-to-python-libs/pythonx.yz/site-packages/... directory - -# Check if the PRE_LOAD_CHROME env variable exists -aquire_chrome = False - turn_on_logging = strtobool('LOG_BASE_PLOT') # Log when Chrome is downloaded at runtime -if turn_on_logging is True: +if turn_on_logging: log = logging.getLogger("base_plot") log.setLevel(logging.INFO) @@ -49,24 +37,11 @@ # set the WRITE_LOG env var to True to save the log message to a # separate log file write_log = strtobool('WRITE_LOG') - if write_log is True: + if write_log: file_handler = logging.FileHandler("./base_plot.log") file_handler.setFormatter(formatter) log.addHandler(file_handler) -# Only load Chrome at run-time if PRE_LOAD_CHROME is False or not defined. -# Some applications may not want to load Chrome at runtime and -# will set the PRE_LOAD_CHROME to True to indicate that it is already -# loaded/downloaded prior to runtime. -chrome_env =strtobool ('PRE_LOAD_CHROME') -if chrome_env is False: - aquire_chrome=True - kaleido.get_chrome_sync() - - -# Log when kaleido is downloading Chrome -if aquire_chrome is True and turn_on_logging is True: - log.info("Plotly kaleido is loading Chrome at run time") class BasePlot: """A class that provides methods for building Plotly plot's common features @@ -414,21 +389,6 @@ def remove_file(self): if image_name is not None and os.path.exists(image_name): os.remove(image_name) - def show_in_browser(self): - """Creates a plot and opens it in the browser. - - Args: - - Returns: - - """ - if self.figure: - self.figure.show() - else: - self.logger.error(" Figure not created. Nothing to show in the " - "browser. ") - print("Oops! The figure was not created. Can't show") - def _add_lines(self, config_obj: Config, x_points_index: Union[list, None] = None) -> None: """ Adds custom horizontal and/or vertical line to the plot. All line's metadata is in the config_obj.lines From 07cf5bd48ecbb2af3cd4ebdad11411fb62dae87e Mon Sep 17 00:00:00 2001 From: bikegeek Date: Thu, 5 Feb 2026 13:29:29 -0700 Subject: [PATCH 11/28] Issue #556 Remove any Plotly-specific code and imports. Update copyright date and information. --- metplotpy/plots/base_plot.py | 147 +---------------------------------- 1 file changed, 2 insertions(+), 145 deletions(-) diff --git a/metplotpy/plots/base_plot.py b/metplotpy/plots/base_plot.py index 5048ff62..8a088f38 100644 --- a/metplotpy/plots/base_plot.py +++ b/metplotpy/plots/base_plot.py @@ -1,14 +1,13 @@ # ============================* - # ** Copyright UCAR (c) 2020 + # ** Copyright UCAR (c) 2026 # ** University Corporation for Atmospheric Research (UCAR) - # ** National Center for Atmospheric Research (NCAR) + # ** National Science Foundation National Center for Atmospheric Research (NSF NCAR) # ** Research Applications Lab (RAL) # ** P.O.Box 3000, Boulder, Colorado, 80307-3000, USA # ============================* -# !/usr/bin/env conda run -n blenny_363 python """ Class Name: base_plot.py """ @@ -20,11 +19,8 @@ import numpy as np import yaml from typing import Union - -import metplotpy.plots.util from metplotpy.plots.util import strtobool from .config import Config -from metplotpy.plots.context_filter import ContextFilter turn_on_logging = strtobool('LOG_BASE_PLOT') # Log when Chrome is downloaded at runtime @@ -114,31 +110,7 @@ def get_image_format(self): print('Unrecognised image format. png will be used') return self.DEFAULT_IMAGE_FORMAT - def get_legend(self): - """Creates a Plotly legend dictionary with values from users and default parameters - If users parameters dictionary doesn't have needed values - use defaults - - Args: - - Returns: - - dictionary used by Plotly to build the legend - """ - current_legend = dict( - x=self.get_config_value('legend', 'x'), # x-position - y=self.get_config_value('legend', 'y'), # y-position - font=dict( - family=self.get_config_value('legend', 'font', 'family'), # font family - size=self.get_config_value('legend', 'font', 'size'), # font size - color=self.get_config_value('legend', 'font', 'color'), # font color - ), - bgcolor=self.get_config_value('legend', 'bgcolor'), # background color - bordercolor=self.get_config_value('legend', 'bordercolor'), # border color - borderwidth=self.get_config_value('legend', 'borderwidth'), # border width - xanchor=self.get_config_value('legend', 'xanchor'), # horizontal position anchor - yanchor=self.get_config_value('legend', 'yanchor') # vertical position anchor - ) - return current_legend def get_legend_style(self): """ @@ -174,121 +146,6 @@ def get_legend_style(self): return legend_settings - def get_title(self): - """Creates a Plotly title dictionary with values from users and default parameters - If users parameters dictionary doesn't have needed values - use defaults - - Args: - - Returns: - - dictionary used by Plotly to build the title - """ - current_title = dict( - text=self.get_config_value('title'), # plot's title - # Sets the container `x` refers to. "container" spans the entire `width` of the plot. - # "paper" refers to the width of the plotting area only. - xref="paper", - x=0.5 # x position with respect to `xref` - ) - return current_title - - def get_xaxis(self): - """Creates a Plotly x-axis dictionary with values from users and default parameters - If users parameters dictionary doesn't have needed values - use defaults - - Args: - - Returns: - - dictionary used by Plotly to build the x-axis - """ - current_xaxis = dict( - linecolor=self.get_config_value('xaxis', 'linecolor'), # x-axis line color - # whether or not a line bounding x-axis is drawn - showline=self.get_config_value('xaxis', 'showline'), - linewidth=self.get_config_value('xaxis', 'linewidth') # width (in px) of x-axis line - ) - return current_xaxis - - def get_yaxis(self): - """Creates a Plotly y-axis dictionary with values from users and default parameters - If users parameters dictionary doesn't have needed values - use defaults - - Args: - - Returns: - - dictionary used by Plotly to build the y-axis - """ - current_yaxis = dict( - linecolor=self.get_config_value('yaxis', 'linecolor'), # y-axis line color - linewidth=self.get_config_value('yaxis', 'linewidth'), # width (in px) of y-axis line - # whether or not a line bounding y-axis is drawn - showline=self.get_config_value('yaxis', 'showline'), - # whether or not grid lines are drawn - showgrid=self.get_config_value('yaxis', 'showgrid'), - ticks=self.get_config_value('yaxis', 'ticks'), # whether ticks are drawn or not. - tickwidth=self.get_config_value('yaxis', 'tickwidth'), # Sets the tick width (in px). - tickcolor=self.get_config_value('yaxis', 'tickcolor'), # Sets the tick color. - # the width (in px) of the grid lines - gridwidth=self.get_config_value('yaxis', 'gridwidth'), - gridcolor=self.get_config_value('yaxis', 'gridcolor') # the color of the grid lines - ) - - # Sets the range of the range slider. defaults to the full y-axis range - y_range = self.get_config_value('yaxis', 'range') - if y_range is not None: - current_yaxis['range'] = y_range - return current_yaxis - - def get_xaxis_title(self): - """Creates a Plotly x-axis label title dictionary with values - from users and default parameters. - If users parameters dictionary doesn't have needed values - use defaults - - Args: - - Returns: - - dictionary used by Plotly to build the x-axis label title as annotation - """ - x_axis_label = dict( - x=self.get_config_value('xaxis', 'x'), # x-position of label - y=self.get_config_value('xaxis', 'y'), # y-position of label - showarrow=False, - text=self.get_config_value('xaxis', 'title', 'text'), - xref="paper", # the annotation's x coordinate axis - yref="paper", # the annotation's y coordinate axis - font=dict( - family=self.get_config_value('xaxis', 'title', 'font', 'family'), - size=self.get_config_value('xaxis', 'title', 'font', 'size'), - color=self.get_config_value('xaxis', 'title', 'font', 'color'), - ) - ) - return x_axis_label - - def get_yaxis_title(self): - """Creates a Plotly y-axis label title dictionary with values - from users and default parameters - If users parameters dictionary doesn't have needed values - use defaults - - Args: - - Returns: - - dictionary used by Plotly to build the y-axis label title as annotation - """ - y_axis_label = dict( - x=self.get_config_value('yaxis', 'x'), # x-position of label - y=self.get_config_value('yaxis', 'y'), # y-position of label - showarrow=False, - text=self.get_config_value('yaxis', 'title', 'text'), - textangle=-90, # the angle at which the `text` is drawn with respect to the horizontal - xref="paper", # the annotation's x coordinate axis - yref="paper", # the annotation's y coordinate axis - font=dict( - family=self.get_config_value('xaxis', 'title', 'font', 'family'), - size=self.get_config_value('xaxis', 'title', 'font', 'size'), - color=self.get_config_value('xaxis', 'title', 'font', 'color'), - ) - ) - return y_axis_label def get_config_value(self, *args): """Gets the value of a configuration parameter. From b616fcd6d5e04259f5aab8f4e00a4f259d2736dd Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:11:06 -0700 Subject: [PATCH 12/28] clean up logic to calculate plot dimensions to always use matplotlib units. added helper function to reduce duplication for logic to convert units --- metplotpy/plots/config.py | 68 ++++++------------- .../performance_diagram_config.py | 4 +- .../taylor_diagram/taylor_diagram_config.py | 4 +- 3 files changed, 24 insertions(+), 52 deletions(-) diff --git a/metplotpy/plots/config.py b/metplotpy/plots/config.py index 7f21dbbf..95a98a9e 100644 --- a/metplotpy/plots/config.py +++ b/metplotpy/plots/config.py @@ -54,12 +54,9 @@ def __init__(self, parameters): self.indy_var = self.get_config_value('indy_var') self.show_plot_in_browser = self.get_config_value('show_plot_in_browser') - # Plot figure dimensions can be in either inches or pixels - pixels = self.get_config_value('plot_units') - plot_width = self.get_config_value('plot_width') - self.plot_width = self.calculate_plot_dimension(plot_width, pixels) - plot_height = self.get_config_value('plot_height') - self.plot_height = self.calculate_plot_dimension(plot_height, pixels) + # Plot figure dimensions should be in inches + self.plot_width = self.calculate_plot_dimension('plot_width') + self.plot_height = self.calculate_plot_dimension('plot_height') self.plot_caption = self.get_config_value('plot_caption') # plain text, bold, italic, bold italic are choices in METviewer UI self.caption_weight = self.get_config_value('caption_weight') @@ -656,17 +653,7 @@ def _get_plot_resolution(self) -> int: # check if the units value has been set in the config file if self.get_config_value('plot_units'): - units = self.get_config_value('plot_units').lower() - if units == 'in': - return resolution - - if units == 'mm': - # convert mm to inches so we can - # set dpi value - return resolution * constants.MM_TO_INCHES - - # units not supported, assume inches - return resolution + return self._convert_units_to_inches(resolution, self.get_config_value('plot_units')) # units not indicated, assume # we are dealing with inches @@ -676,6 +663,19 @@ def _get_plot_resolution(self) -> int: # dpi used by matplotlib return dpi + def _convert_units_to_inches(self, value, units): + units_lower = units.lower() + if units_lower == 'mm': + return value * constants.MM_TO_INCHES + if units_lower == 'cm': + return value * 0.1 * constants.MM_TO_INCHES + + # if unsupported units are specified, log a warning but assume inches + if units_lower != 'in': + self.logger.warning(f"Invalid units specified: {units}. Expected in, mm, or cm. Assuming inches.") + + return value + def create_list_by_series_ordering(self, setting_to_order) -> list: """ Generate a list of series plotting settings based on what is set @@ -773,55 +773,27 @@ def create_list_by_plot_val_ordering(self, setting_to_order: str) -> list: return ordered_settings_list - def calculate_plot_dimension(self, config_value: str , output_units: str) -> int: + def calculate_plot_dimension(self, config_value: str) -> int: ''' To calculate the width or height that defines the size of the plot. - Matplotlib defines these values in inches, Python plotly defines these - in terms of pixels. METviewer accepts units of inches or mm for width and + Matplotlib defines these values in inches. METviewer accepts units of inches or mm for width and height, so conversion from mm to inches or mm to pixels is necessary, depending on the requested output units, output_units. Args: @param config_value: The plot dimension to convert, either a width or height, in inches or mm - @param output_units: pixels or in (inches) to indicate which - units to use to define plot size. Python plotly uses pixels and - Matplotlib uses inches. Returns: converted_value : converted value from in/mm to pixels or mm to inches based on input values ''' value2convert = self.get_config_value(config_value) - resolution = self.get_config_value('plot_res') units = self.get_config_value('plot_units') - # initialize converted_value to some small value - converted_value = 0 - - # convert to pixels - # plotly uses pixels for setting plot size (width and height) - if output_units.lower() == 'pixels': - if units.lower() == 'in': - # value in pixels - converted_value = int(resolution * value2convert) - elif units.lower() == 'mm': - # Convert mm to pixels - converted_value = int(resolution * value2convert * constants.MM_TO_INCHES) - # Matplotlib uses inches (in) for setting plot size (width and height) - elif output_units.lower() == 'in': - if units.lower() == 'mm': - # Convert mm to inches - converted_value = value2convert * constants.MM_TO_INCHES - else: - converted_value = value2convert - - # plotly does not allow any value smaller than 10 pixels - if output_units.lower() == 'pixels' and converted_value < 10: - converted_value = 10 + return self._convert_units_to_inches(value2convert, units) - return converted_value def _get_bool(self, param: str) -> Union[bool, None]: """ diff --git a/metplotpy/plots/performance_diagram/performance_diagram_config.py b/metplotpy/plots/performance_diagram/performance_diagram_config.py index 263227f8..1863b985 100644 --- a/metplotpy/plots/performance_diagram/performance_diagram_config.py +++ b/metplotpy/plots/performance_diagram/performance_diagram_config.py @@ -67,8 +67,8 @@ def __init__(self, parameters): self.linewidth_list = self._get_linewidths() self.linestyles_list = self._get_linestyles() self.user_legends = self._get_user_legends("Performance") - self.plot_width = self.calculate_plot_dimension('plot_width', 'in') - self.plot_height = self.calculate_plot_dimension('plot_height', 'in') + self.plot_width = self.calculate_plot_dimension('plot_width') + self.plot_height = self.calculate_plot_dimension('plot_height') # x-axis labels and x-axis ticks self.x_title_font_size = self.parameters['xlab_size'] * constants.DEFAULT_CAPTION_FONTSIZE diff --git a/metplotpy/plots/taylor_diagram/taylor_diagram_config.py b/metplotpy/plots/taylor_diagram/taylor_diagram_config.py index fd556cb4..c7696cfa 100644 --- a/metplotpy/plots/taylor_diagram/taylor_diagram_config.py +++ b/metplotpy/plots/taylor_diagram/taylor_diagram_config.py @@ -64,8 +64,8 @@ def __init__(self, parameters: dict) -> None: # Convert the plot height and width to inches if units aren't in # inches. if self.plot_units.lower() != 'in': - self.plot_width = self.calculate_plot_dimension('plot_width', 'in') - self.plot_height = self.calculate_plot_dimension('plot_height', 'in') + self.plot_width = self.calculate_plot_dimension('plot_width') + self.plot_height = self.calculate_plot_dimension('plot_height') else: self.plot_width = self.get_config_value('plot_width') self.plot_height = self.get_config_value('plot_height') From 3bc22fd0e71a2191768ee2337d1c382562333f6e Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:46:08 -0700 Subject: [PATCH 13/28] remove plotly-specific variables and update incorrect imports to use plotly version for now --- metplotpy/plots/bar/bar.py | 2 +- metplotpy/plots/constants.py | 16 ---------------- metplotpy/plots/ens_ss/ens_ss.py | 2 +- metplotpy/plots/mpr_plot/mpr_plot.py | 2 +- metplotpy/plots/wind_rose/wind_rose.py | 2 +- 5 files changed, 4 insertions(+), 20 deletions(-) diff --git a/metplotpy/plots/bar/bar.py b/metplotpy/plots/bar/bar.py index f7e8507a..771ffbf4 100644 --- a/metplotpy/plots/bar/bar.py +++ b/metplotpy/plots/bar/bar.py @@ -27,7 +27,7 @@ from metplotpy.plots.bar.bar_config import BarConfig from metplotpy.plots.bar.bar_series import BarSeries from metplotpy.plots.base_plot_plotly import BasePlot -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, \ PLOTLY_PAPER_BGCOOR diff --git a/metplotpy/plots/constants.py b/metplotpy/plots/constants.py index 0717fe4d..4e2f3755 100644 --- a/metplotpy/plots/constants.py +++ b/metplotpy/plots/constants.py @@ -62,34 +62,18 @@ AVAILABLE_MARKERS_LIST = ["o", "^", "s", "d", "H", ".", "h"] -AVAILABLE_PLOTLY_MARKERS_LIST = ["circle-open", "circle", - "square", "diamond", - "hexagon", "triangle-up", "asterisk-open"] PCH_TO_MATPLOTLIB_MARKER = {'20': '.', '19': 'o', '17': '^', '1': 'H', '18': 'd', '15': 's', 'small circle': '.', 'circle': 'o', 'square': 's', 'triangle': '^', 'rhombus': 'd', 'ring': 'h'} -PCH_TO_PLOTLY_MARKER = {'0': 'circle-open', '19': 'circle', '20': 'circle', - '17': 'triangle-up', '15': 'square', '18': 'diamond', - '1': 'hexagon2', 'small circle': 'circle-open', - 'circle': 'circle', 'square': 'square', 'triangle': 'triangle-up', - 'rhombus': 'diamond', 'ring': 'hexagon2', '.': 'circle', - 'o': 'circle', '^': 'triangle-up', 'd': 'diamond', 'H': 'circle-open', - 'h': 'hexagon2', 's': 'square'} - # approximated from plotly marker size to matplotlib marker size PCH_TO_MATPLOTLIB_MARKER_SIZE = {'.': 14, 'o': 36, 's': 20, '^': 36, 'd': 20, 'H': 28} -TYPE_TO_PLOTLY_MODE = {'b': 'lines+markers', 'p': 'markers', 'l': 'lines'} XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} -PLOTLY_PAPER_BGCOOR = "white" -PLOTLY_AXIS_LINE_COLOR = "#c2c2c2" -PLOTLY_AXIS_LINE_WIDTH = 2 - # Caption weights supported in Matplotlib are normal, italic and oblique. # Map these onto the MetViewer requested values of 1 (normal), 2 (bold), # 3 (italic), 4 (bold italic), and 5 (symbol) using a dictionary diff --git a/metplotpy/plots/ens_ss/ens_ss.py b/metplotpy/plots/ens_ss/ens_ss.py index 06b0dc8c..a037ab29 100644 --- a/metplotpy/plots/ens_ss/ens_ss.py +++ b/metplotpy/plots/ens_ss/ens_ss.py @@ -26,7 +26,7 @@ from plotly.graph_objects import Figure from metcalcpy.event_equalize import event_equalize -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR from metplotpy.plots.ens_ss.ens_ss_config import EnsSsConfig from metplotpy.plots.ens_ss.ens_ss_series import EnsSsSeries from metplotpy.plots.base_plot_plotly import BasePlot diff --git a/metplotpy/plots/mpr_plot/mpr_plot.py b/metplotpy/plots/mpr_plot/mpr_plot.py index 0398876e..5cfbeb0a 100644 --- a/metplotpy/plots/mpr_plot/mpr_plot.py +++ b/metplotpy/plots/mpr_plot/mpr_plot.py @@ -24,7 +24,7 @@ import plotly.io as pio from metplotpy.plots.base_plot_plotly import BasePlot -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR from metplotpy.plots.mpr_plot.mpr_plot_config import MprPlotConfig from metplotpy.plots.wind_rose.wind_rose import WindRosePlot diff --git a/metplotpy/plots/wind_rose/wind_rose.py b/metplotpy/plots/wind_rose/wind_rose.py index 88ae596b..cd386c42 100644 --- a/metplotpy/plots/wind_rose/wind_rose.py +++ b/metplotpy/plots/wind_rose/wind_rose.py @@ -28,7 +28,7 @@ from metplotpy.plots.base_plot_plotly import BasePlot from metplotpy.plots.wind_rose.wind_rose_config import WindRoseConfig -from metplotpy.plots.constants import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR +from metplotpy.plots.constants_plotly import PLOTLY_AXIS_LINE_COLOR, PLOTLY_AXIS_LINE_WIDTH, PLOTLY_PAPER_BGCOOR from metplotpy.plots import util_plotly as util From fda41c53205061fc4ed7f871734de1e6d901b856 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 6 Feb 2026 12:46:26 -0700 Subject: [PATCH 14/28] resolve SonarQube complaints and clean up logic --- metplotpy/plots/base_plot.py | 117 ++++++++++++++++++----------------- metplotpy/plots/config.py | 2 +- metplotpy/plots/constants.py | 1 + 3 files changed, 61 insertions(+), 59 deletions(-) diff --git a/metplotpy/plots/base_plot.py b/metplotpy/plots/base_plot.py index 8a088f38..e9a18539 100644 --- a/metplotpy/plots/base_plot.py +++ b/metplotpy/plots/base_plot.py @@ -107,7 +107,7 @@ def get_image_format(self): return strings[-1] # print the message if invalid and return default - print('Unrecognised image format. png will be used') + print(f'Unrecognised image format. {self.DEFAULT_IMAGE_FORMAT} will be used') return self.DEFAULT_IMAGE_FORMAT @@ -124,12 +124,10 @@ def get_legend_style(self): are set in METviewer """ legend_box = self.get_config_value('legend_box').lower() + borderwidth = 0 if legend_box == 'o': # Draws a box around the legend borderwidth = 1 - elif legend_box == 'n': - # Do not draw border around the legend labels. - borderwidth = 0 legend_ncol = self.get_config_value('legend_ncol') if legend_ncol > 1: @@ -138,11 +136,15 @@ def get_legend_style(self): legend_orientation = "v" legend_inset = self.get_config_value('legend_inset') legend_size = self.get_config_value('legend_size') - legend_settings = dict(border_width=borderwidth, - orientation=legend_orientation, - legend_inset=dict(x=legend_inset['x'], - y=legend_inset['y']), - legend_size=legend_size) + legend_settings = { + "border_width": borderwidth, + "orientation": legend_orientation, + "legend_inset": { + 'x': legend_inset['x'], + 'y': legend_inset['y'], + }, + 'legend_size': legend_size, + } return legend_settings @@ -224,17 +226,11 @@ def save_to_file(self): try: self.figure.write_image(image_name) except FileNotFoundError: - self.logger.error(f"FileNotFoundError: Cannot save to file" - f" {image_name}") - # print("Can't save to file " + image_name) - except ResourceWarning: - self.logger.warning(f"ResourceWarning: in _kaleido" - f" {image_name}") - + self.logger.error(f"FileNotFoundError: Cannot save to file {image_name}") except ValueError as ex: - self.logger.error(f"ValueError: Could not save output file.") + self.logger.error(f"ValueError: Could not save output file. {ex}") else: - self.logger.error(f"The figure {dirname} cannot be saved.") + self.logger.error(f"The figure {image_name} cannot be saved.") print("Oops! The figure was not created. Can't save.") def remove_file(self): @@ -254,46 +250,51 @@ def _add_lines(self, config_obj: Config, x_points_index: Union[list, None] = Non @x_points_index - list of x-values that are used to create a plot Returns: """ - if hasattr(config_obj, 'lines') and config_obj.lines is not None: - shapes = [] - for line in config_obj.lines: - # draw horizontal line - if line['type'] == 'horiz_line': - shapes.append(dict( - type='line', - yref='y', y0=line['position'], y1=line['position'], - xref='paper', x0=0, x1=0.95, - line={'color': line['color'], - 'dash': line['line_style'], - 'width': line['line_width']}, - )) - elif line['type'] == 'vert_line': - # draw vertical line - try: - if x_points_index is None: - val = line['position'] - else: - ordered_indy_label = config_obj.create_list_by_plot_val_ordering(config_obj.indy_label) - index = ordered_indy_label.index(line['position']) - val = x_points_index[index] - shapes.append(dict( - type='line', - yref='paper', y0=0, y1=1, - xref='x', x0=val, x1=val, - line={'color': line['color'], - 'dash': line['line_style'], - 'width': line['line_width']}, - )) - except ValueError: - line_position = line["position"] - self.logger.warning(f" Vertical line with position " - f"{line_position} cannot be created.") - print(f'WARNING: vertical line with position ' - f'{line_position} can\'t be created') - # ignore everything else - - # draw lines - self.figure.update_layout(shapes=shapes) + if not hasattr(config_obj, 'lines') or config_obj.lines is None: + return + + shapes = [] + for line in config_obj.lines: + # draw horizontal line + if line['type'] == 'horiz_line': + shapes.append({ + 'type': 'line', + 'yref': 'y', 'y0': line['position'], 'y1': line['position'], + 'xref': 'paper', 'x0': 0, 'x1': 0.95, + 'line': { + 'color': line['color'], + 'dash': line['line_style'], + 'width': line['line_width'], + }, + }) + elif line['type'] == 'vert_line': + # draw vertical line + try: + if x_points_index is None: + val = line['position'] + else: + ordered_indy_label = config_obj.create_list_by_plot_val_ordering(config_obj.indy_label) + index = ordered_indy_label.index(line['position']) + val = x_points_index[index] + shapes.append({ + 'type': 'line', + 'yref': 'paper', 'y0': 0, 'y1': 1, + 'xref': 'x', 'x0': val, 'x1': val, + 'line': { + 'color': line['color'], + 'dash': line['line_style'], + 'width': line['line_width'], + } + }) + except ValueError: + line_position = line["position"] + msg = f"Vertical line with position {line_position} cannot be created." + self.logger.warning(msg) + print(msg) + # ignore everything else + + # draw lines + self.figure.update_layout(shapes=shapes) @staticmethod def get_array_dimensions(data): diff --git a/metplotpy/plots/config.py b/metplotpy/plots/config.py index 95a98a9e..821ae9a3 100644 --- a/metplotpy/plots/config.py +++ b/metplotpy/plots/config.py @@ -668,7 +668,7 @@ def _convert_units_to_inches(self, value, units): if units_lower == 'mm': return value * constants.MM_TO_INCHES if units_lower == 'cm': - return value * 0.1 * constants.MM_TO_INCHES + return value * constants.CM_TO_INCHES # if unsupported units are specified, log a warning but assume inches if units_lower != 'in': diff --git a/metplotpy/plots/constants.py b/metplotpy/plots/constants.py index 4e2f3755..68e992ab 100644 --- a/metplotpy/plots/constants.py +++ b/metplotpy/plots/constants.py @@ -22,6 +22,7 @@ # used to convert plot units in mm to # inches, so we can pass in dpi to matplotlib MM_TO_INCHES = 0.03937008 +CM_TO_INCHES = MM_TO_INCHES * 0.1 # Available Matplotlib Line styles # ':' ... From 392ee95e5a2f4a9c7e18b98f6dc3bf498aca9bfc Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Fri, 6 Feb 2026 13:08:33 -0700 Subject: [PATCH 15/28] more SonarQube issues resolved --- metplotpy/plots/config.py | 162 ++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 77 deletions(-) diff --git a/metplotpy/plots/config.py b/metplotpy/plots/config.py index 821ae9a3..f06df04a 100644 --- a/metplotpy/plots/config.py +++ b/metplotpy/plots/config.py @@ -105,16 +105,17 @@ def __init__(self, parameters): self.plot_margins = self.get_config_value('mar') self.grid_on = self._get_bool('grid_on') if self.get_config_value('mar_offset'): - self.plot_margins = dict(l=0, - r=self.parameters['mar'][3] + 20, - t=self.parameters['mar'][2] + 80, - b=self.parameters['mar'][0] + 80, - pad=5 - ) + self.plot_margins = { + 'l': 0, + 'r': self.parameters['mar'][3] + 20, + 't': self.parameters['mar'][2] + 80, + 'b': self.parameters['mar'][0] + 80, + 'pad': 5, + } self.grid_col = self.get_config_value('grid_col') if self.grid_col: - self.blended_grid_col = metplotpy.plots.util.alpha_blending(self.grid_col, 0.5) + self.blended_grid_col = metplotpy.plots.util.alpha_blending(self.grid_col, 0.5) self.show_nstats = self._get_bool('show_nstats') self.indy_stagger = self._get_bool('indy_stagger') @@ -322,13 +323,15 @@ def _get_legend_style(self) -> dict: legend_bbox_x = legend_inset['x'] legend_bbox_y = legend_inset['y'] legend_size = self.get_config_value('legend_size') - legend_settings = dict(bbox_x=legend_bbox_x, - bbox_y=legend_bbox_y, - legend_size=legend_size, - legend_ncol=legend_ncol, - legend_box=legend_box) + legend_settings = { + 'bbox_x': legend_bbox_x, + 'bbox_y': legend_bbox_y, + 'legend_size': legend_size, + 'legend_ncol': legend_ncol, + 'legend_box': legend_box, + } else: - legend_settings = dict() + legend_settings = {} return legend_settings @@ -443,7 +446,7 @@ def calculate_number_of_series(self) -> int: # Utilize itertools' product() to create the cartesian product of all elements # in the lists to produce all permutations of the series_val values and the # fcst_var_val values. - permutations = [p for p in itertools.product(*series_vals_list)] + permutations = list(itertools.product(*series_vals_list)) return len(permutations) @@ -557,6 +560,16 @@ def _get_user_legends(self, legend_label_type: str ) -> list: Retrieve the text that is to be displayed in the legend at the bottom of the plot. Each entry corresponds to a series. + For legend labels that aren't set (ie in conf file they are set to '') + create a legend label based on the permutation of the series names + appended by 'user_legend label'. For example, for: + series_val_1: + model: + - NoahMPv3.5.1_d01 + vx_mask: + - CONUS + The constructed legend label will be "NoahMPv3.5.1_d01 CONUS Performance" + Args: @parm legend_label_type: The legend label, such as 'Performance', used when the user hasn't indicated a legend in the @@ -566,41 +579,7 @@ def _get_user_legends(self, legend_label_type: str ) -> list: a list consisting of the series label to be displayed in the plot legend. """ - all_legends = self.get_config_value('user_legend') - - # for legend labels that aren't set (ie in conf file they are set to '') - # create a legend label based on the permutation of the series names - # appended by 'user_legend label'. For example, for: - # series_val_1: - # model: - # - NoahMPv3.5.1_d01 - # vx_mask: - # - CONUS - # The constructed legend label will be "NoahMPv3.5.1_d01 CONUS Performance" - - - # Check for empty list as setting in the config file - legends_list = [] - - # set a flag indicating when a legend label is specified - legend_label_unspecified = True - - # Check if a stat curve was requested, if so, then the number - # of series_val_1 values will be inconsistent with the number of - # legend labels 'specified' (either with actual labels or whitespace) - - num_series = self.calculate_number_of_series() - if len(all_legends) == 0: - for i in range(num_series): - legends_list.append(' ') - else: - for legend in all_legends: - if len(legend) == 0: - legend = ' ' - legends_list.append(legend) - else: - legend_label_unspecified = False - legends_list.append(legend) + legends_list, legend_label_unspecified = self._get_legends_list() ll_list = [] series_list = self.all_series_vals @@ -612,8 +591,7 @@ def _get_user_legends(self, legend_label_type: str ) -> list: # check if summary_curve is present if 'summary_curve' in self.parameters.keys() and self.parameters['summary_curve'] != 'none': return [legend_label_type, self.parameters['summary_curve'] + ' ' + legend_label_type] - else: - return [legend_label_type] + return [legend_label_type] perms = utils.create_permutations(series_list) for idx,ll in enumerate(legends_list): @@ -632,6 +610,35 @@ def _get_user_legends(self, legend_label_type: str ) -> list: legends_list_ordered = self.create_list_by_series_ordering(ll_list) return legends_list_ordered + def _get_legends_list(self): + all_legends = self.get_config_value('user_legend') + + # Check for empty list as setting in the config file + legends_list = [] + + # set a flag indicating when a legend label is specified + legend_label_unspecified = True + + # Check if a stat curve was requested, if so, then the number + # of series_val_1 values will be inconsistent with the number of + # legend labels 'specified' (either with actual labels or whitespace) + + num_series = self.calculate_number_of_series() + if len(all_legends) == 0: + for _ in range(num_series): + legends_list.append(' ') + else: + for legend in all_legends: + if len(legend) == 0: + legend = ' ' + legends_list.append(legend) + else: + legend_label_unspecified = False + legends_list.append(legend) + + return legends_list, legend_label_unspecified + + def _get_plot_resolution(self) -> int: """ Retrieve the plot_res and plot_unit to determine the dpi @@ -829,34 +836,35 @@ def _get_lines(self) -> Union[list, None]: # get property value from the parameters lines = self.get_config_value('lines') + if lines is None: + return None # if the property exists - proceed - if lines is not None: - # validate data and replace the values - for line in lines: - - # validate line_type - line_type = line['type'] - if line_type not in ('horiz_line', 'vert_line') : - print(f'WARNING: custom line type {line["type"]} is not supported') + # validate data and replace the values + for line in lines: + + # validate line_type + if line['type'] not in ('horiz_line', 'vert_line') : + print(f'WARNING: custom line type {line["type"]} is not supported') + line['type'] = None + continue + + # convert position to float if line_type=horiz_line + if line['type'] == 'horiz_line': + try: + line['position'] = float(line['position']) + except ValueError: + print(f'WARNING: custom line position {line["position"]} is invalid') line['type'] = None - else: - # convert position to float if line_type=horiz_line - if line['type'] == 'horiz_line': - try: - line['position'] = float(line['position']) - except ValueError: - print(f'WARNING: custom line position {line["position"]} is invalid') - line['type'] = None - else: - # convert position to string if line_type=vert_line - line['position'] = str(line['position']) - - # convert line_width to float - try: - line['line_width'] = float(line['line_width']) - except ValueError: - print(f'WARNING: custom line width {line["line_width"]} is invalid') - line['type'] = None + else: + # convert position to string if line_type=vert_line + line['position'] = str(line['position']) + + # convert line_width to float + try: + line['line_width'] = float(line['line_width']) + except ValueError: + print(f'WARNING: custom line width {line["line_width"]} is invalid') + line['type'] = None return lines From 0ab752812fd269c7642f2c935debd494d5ae7eff Mon Sep 17 00:00:00 2001 From: bikegeek Date: Fri, 6 Feb 2026 16:58:36 -0700 Subject: [PATCH 16/28] Issue #556 Updates to base_plot to support title,caption, x-axis label and y-axis label style, weight, and font size. Moved the add_horizontal_line() and add_vertical_line() code from the util.py module to this module as this will be needed for all plot types. TODO comments are used to denote code that will need to be removed when all plot types have migratee to Matplotlib. --- metplotpy/plots/base_plot.py | 87 ++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/metplotpy/plots/base_plot.py b/metplotpy/plots/base_plot.py index 8a088f38..08586724 100644 --- a/metplotpy/plots/base_plot.py +++ b/metplotpy/plots/base_plot.py @@ -17,11 +17,13 @@ import logging import warnings import numpy as np +from matplotlib.font_manager import FontProperties import yaml from typing import Union from metplotpy.plots.util import strtobool from .config import Config + turn_on_logging = strtobool('LOG_BASE_PLOT') # Log when Chrome is downloaded at runtime if turn_on_logging: @@ -146,6 +148,57 @@ def get_legend_style(self): return legend_settings + def get_weights_size_styles(self): + """ + Set up the font properties for the plot title: style (regular, italic), size, and + weight (normal, bold) for the title, captions, x-axis label, and y-axis label. + + Returns: + weights_size_styles: A dictionary containing the font property information + for the title, captions, x-axis label, and y-axis label + """ + weights_size_styles = {} + + # For title + title_property= FontProperties() + title_property.set_size(self.config_obj.title_size) + style = self.config_obj.title_weight[0] + wt = self.config_obj.title_weight[1] + title_property.set_style(style) + title_property.set_weight(wt) + weights_size_styles['title'] = title_property + + # For caption + caption_property = FontProperties() + caption_property.set_size(self.config_obj.caption_size) + cap_style = self.config_obj.caption_weight[0] + cap_wt = self.config_obj.caption_weight[1] + caption_property.set_style(cap_style) + caption_property.set_weight(cap_wt) + weights_size_styles['caption'] = caption_property + + # For xaxis label + xlab_property= FontProperties() + + xlab_property.set_size(self.config_obj.x_title_font_size) + xlab_style = self.config_obj.xlab_weight[0] + xlab_wt = self.config_obj.xlab_weight[1] + xlab_property.set_style(xlab_style) + xlab_property.set_weight(xlab_wt) + weights_size_styles['xlab'] = xlab_property + + # For yaxis label + ylab_property = FontProperties() + ylab_property.set_size(self.config_obj.y_title_font_size) + ylab_style = self.config_obj.ylab_weight[0] + ylab_wt = self.config_obj.ylab_weight[1] + ylab_property.set_style(ylab_style) + ylab_property.set_weight(ylab_wt) + weights_size_styles['ylab'] = ylab_property + + return weights_size_styles + + def get_config_value(self, *args): """Gets the value of a configuration parameter. @@ -203,6 +256,7 @@ def get_img_bytes(self): return None + # TODO Plotly-specific method, NOT needed for Matplotlib def save_to_file(self): """Saves the image to a file specified in the config file. Prints a message if fails @@ -214,8 +268,9 @@ def save_to_file(self): """ image_name = self.get_config_value('plot_filename') - # Suppress deprecation warnings from third-party packages that are not in our control. - warnings.filterwarnings("ignore", category=DeprecationWarning) + # Catch deprecation warnings from third-party packages as + # errors and log the message. + warnings.filterwarnings("error", category=DeprecationWarning) # Create the directory for the output plot if it doesn't already exist dirname = os.path.dirname(os.path.abspath(image_name)) @@ -227,9 +282,11 @@ def save_to_file(self): self.logger.error(f"FileNotFoundError: Cannot save to file" f" {image_name}") # print("Can't save to file " + image_name) - except ResourceWarning: - self.logger.warning(f"ResourceWarning: in _kaleido" + except ResourceWarning as rw: + self.logger.warning(f"ResourceWarning {rw}: in " f" {image_name}") + except DeprecationWarning as dw: + self.logger.warning(f"DeprecationWarning {dw} in: {image_name}") except ValueError as ex: self.logger.error(f"ValueError: Could not save output file.") @@ -246,6 +303,8 @@ def remove_file(self): if image_name is not None and os.path.exists(image_name): os.remove(image_name) +# TODO Remove Plotly specific, use add_horizontal_line() and add_vertical_line() below +# Plotly-specific, def _add_lines(self, config_obj: Config, x_points_index: Union[list, None] = None) -> None: """ Adds custom horizontal and/or vertical line to the plot. All line's metadata is in the config_obj.lines @@ -295,6 +354,26 @@ def _add_lines(self, config_obj: Config, x_points_index: Union[list, None] = Non # draw lines self.figure.update_layout(shapes=shapes) + def add_horizontal_line(plt,y: float, line_properties: dict) -> None: + """Adds a horizontal line to the matplotlib plot + + @param plt: Matplotlib pyplot object + @param y y value for the line + @param line_properties dictionary with line properties like color, width, dash + @returns None + """ + plt.axhline(y=y, xmin=0, xmax=1, **line_properties) + + def add_vertical_line(plt, x: float, line_properties: dict) -> None: + """Adds a vertical line to the matplotlib plot + + @param plt: Matplotlib pyplot object + @param x x value for the line + @param line_properties dictionary with line properties like color, width, dash + @returns None + """ + plt.axvline(x=x, ymin=0, ymax=1, **line_properties) + @staticmethod def get_array_dimensions(data): """Returns the dimension of the array From 14469fb6707ae39349586175b2727697ef3a8e77 Mon Sep 17 00:00:00 2001 From: bikegeek Date: Fri, 6 Feb 2026 17:22:09 -0700 Subject: [PATCH 17/28] Issue #556 use the get_weights_size_style() from base_plot.py to plot title and caption --- .../plots/taylor_diagram/taylor_diagram.py | 49 ++++++++----------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/metplotpy/plots/taylor_diagram/taylor_diagram.py b/metplotpy/plots/taylor_diagram/taylor_diagram.py index b6b7bc54..77882284 100644 --- a/metplotpy/plots/taylor_diagram/taylor_diagram.py +++ b/metplotpy/plots/taylor_diagram/taylor_diagram.py @@ -279,34 +279,26 @@ def _create_figure(self) -> None: self.ax.plot(np.arccos(correlation), stdev, marker=marker, ms=10, ls='', color=marker_colors, label=legend) - # use FontProperties to re-create the weights used in METviewer - fontobj = FontProperties() - font_title = fontobj.copy() - font_title.set_size(self.config_obj.title_size) - style = self.config_obj.title_weight[0] - wt = self.config_obj.title_weight[1] - font_title.set_style(style) - font_title.set_weight(wt) - - plt.title(self.config_obj.title, - fontproperties=font_title, - color=constants.DEFAULT_TITLE_COLOR, - pad=28) - - # Plot the caption, leverage FontProperties to re-create the 'weights' menu in - # METviewer (i.e. use a combination of style and weight to create the bold - # italic - # caption weight in METviewer) - fontobj = FontProperties() - font = fontobj.copy() - font.set_size(self.config_obj.caption_size) - style = self.config_obj.caption_weight[0] - wt = self.config_obj.caption_weight[1] - font.set_style(style) - font.set_weight(wt) - plt.figtext(self.config_obj.caption_align, self.config_obj.caption_offset, - self.config_obj.plot_caption, - fontproperties=font, color=self.config_obj.caption_color) + # get the weights, sizes, and style for the title, caption, x-axis label, and + # y-axis label + wts_size_styles = self.get_weights_size_styles() + + # Plot the title + plt.title( + self.config_obj.title, + fontproperties=wts_size_styles['title'], + color=constants.DEFAULT_TITLE_COLOR, + pad=28 + ) + + # Plot the caption + caption = wts_size_styles['caption'] + + plt.figtext( + self.config_obj.caption_align, self.config_obj.caption_offset, + self.config_obj.plot_caption, + fontproperties=caption, color=self.config_obj.caption_color + ) # Add a figure legend @@ -331,6 +323,7 @@ def _create_figure(self) -> None: plt.tight_layout() plt.plot() + # Save the figure, based on whether we are displaying only positive # correlations or all # correlations. From ad3c90f46238c67b190d4b9867511d126f5e17c2 Mon Sep 17 00:00:00 2001 From: bikegeek Date: Fri, 6 Feb 2026 17:32:21 -0700 Subject: [PATCH 18/28] Fixed comment to remove Plotly reference in --- metplotpy/plots/config.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/metplotpy/plots/config.py b/metplotpy/plots/config.py index f06df04a..8863c279 100644 --- a/metplotpy/plots/config.py +++ b/metplotpy/plots/config.py @@ -780,7 +780,7 @@ def create_list_by_plot_val_ordering(self, setting_to_order: str) -> list: return ordered_settings_list - def calculate_plot_dimension(self, config_value: str) -> int: + def calculate_plot_dimension(self, config_value: str, output_units:str) -> int: ''' To calculate the width or height that defines the size of the plot. Matplotlib defines these values in inches. METviewer accepts units of inches or mm for width and @@ -790,14 +790,29 @@ def calculate_plot_dimension(self, config_value: str) -> int: Args: @param config_value: The plot dimension to convert, either a width or height, in inches or mm + @param output_units: pixels or in (inches) to indicate which + units to use to define plot size. Matplotlib uses inches. Returns: converted_value : converted value from in/mm to pixels or mm to inches based on input values ''' - + value2convert = self.get_config_value(config_value) + resolution = self.get_config_value('plot_res') units = self.get_config_value('plot_units') + # initialize converted_value to some small value + converted_value = 0 + + # convert to pixels + if output_units.lower() == 'pixels': + if units.lower() == 'in': + # value in pixels + converted_value = int(resolution * value2convert) + elif units.lower() == 'mm': + # Convert mm to pixels + converted_value = int(resolution * value2convert * constants.MM_TO_INCHES) + # Matplotlib uses inches (in) for setting plot size (width and height) return self._convert_units_to_inches(value2convert, units) From 1c0d319e440ddfa6a459850037f538dcfaff1409 Mon Sep 17 00:00:00 2001 From: bikegeek Date: Fri, 6 Feb 2026 17:48:40 -0700 Subject: [PATCH 19/28] Revert to previous version, which already removed Plotly-specific code in calculate_plot_dimension --- metplotpy/plots/config.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/metplotpy/plots/config.py b/metplotpy/plots/config.py index 8863c279..a1dec79f 100644 --- a/metplotpy/plots/config.py +++ b/metplotpy/plots/config.py @@ -56,7 +56,7 @@ def __init__(self, parameters): # Plot figure dimensions should be in inches self.plot_width = self.calculate_plot_dimension('plot_width') - self.plot_height = self.calculate_plot_dimension('plot_height') + self.plot_height = self.calculate_plot_dimension('plot_height' ) self.plot_caption = self.get_config_value('plot_caption') # plain text, bold, italic, bold italic are choices in METviewer UI self.caption_weight = self.get_config_value('caption_weight') @@ -780,7 +780,7 @@ def create_list_by_plot_val_ordering(self, setting_to_order: str) -> list: return ordered_settings_list - def calculate_plot_dimension(self, config_value: str, output_units:str) -> int: + def calculate_plot_dimension(self, config_value: str) -> int: ''' To calculate the width or height that defines the size of the plot. Matplotlib defines these values in inches. METviewer accepts units of inches or mm for width and @@ -798,21 +798,8 @@ def calculate_plot_dimension(self, config_value: str, output_units:str) -> int: ''' value2convert = self.get_config_value(config_value) - resolution = self.get_config_value('plot_res') units = self.get_config_value('plot_units') - # initialize converted_value to some small value - converted_value = 0 - - # convert to pixels - if output_units.lower() == 'pixels': - if units.lower() == 'in': - # value in pixels - converted_value = int(resolution * value2convert) - elif units.lower() == 'mm': - # Convert mm to pixels - converted_value = int(resolution * value2convert * constants.MM_TO_INCHES) - # Matplotlib uses inches (in) for setting plot size (width and height) return self._convert_units_to_inches(value2convert, units) From 6f68ae8ecf7d5849338da8bd24ee3f7308f31a60 Mon Sep 17 00:00:00 2001 From: bikegeek Date: Fri, 6 Feb 2026 17:50:02 -0700 Subject: [PATCH 20/28] Added TODO for code that should be removed when all plots have been migrated to Matplotlib --- metplotpy/plots/util.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/metplotpy/plots/util.py b/metplotpy/plots/util.py index c219edc4..7ec21b3e 100644 --- a/metplotpy/plots/util.py +++ b/metplotpy/plots/util.py @@ -24,6 +24,7 @@ from typing import Union import pandas as pd import matplotlib.pyplot as plt +from jinja2.lexer import TOKEN_DOT from metplotpy.plots.context_filter import ContextFilter as cf import metcalcpy.util.pstd_statistics as pstats @@ -86,6 +87,10 @@ def get_params(config_filename): return parse_config(config_file) + +# TODO Remove, Plotly specific +# Matplotlib only needs to do a plt.savefig() +# command def make_plot(config_filename, plot_class): """!Get plot parameters and create the plot. @@ -99,7 +104,7 @@ def make_plot(config_filename, plot_class): try: plot = plot_class(params) plot.save_to_file() - plot.write_html() + # plot.write_html() plot.write_output_file() name = plot_class.__name__ if not hasattr(plot_class, 'LONG_NAME') else plot_class.LONG_NAME plot.logger.info(f"Finished {name} plot at {datetime.now()}") @@ -198,6 +203,7 @@ def pretty(low, high, number_of_intervals) -> Union[np.ndarray, list]: return np.arange(miny, maxy + 0.5 * d, d) +# TODO remove, moved to base_plot.py def add_horizontal_line(y: float, line_properties: dict) -> None: """Adds a horizontal line to the matplotlib plot @@ -208,6 +214,7 @@ def add_horizontal_line(y: float, line_properties: dict) -> None: plt.axhline(y=y, xmin=0, xmax=1, **line_properties) +# TODO remove, moved to base_plot.py def add_vertical_line(x: float, line_properties: dict) -> None: """Adds a vertical line to the matplotlib plot From f92761a241d1cfdaac84c927c6e46d28414ced10 Mon Sep 17 00:00:00 2001 From: bikegeek Date: Fri, 6 Feb 2026 17:50:52 -0700 Subject: [PATCH 21/28] Added TODO comments to identify code that will need to be removed when all plots have migrated to Matplotlib --- metplotpy/plots/constants.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/metplotpy/plots/constants.py b/metplotpy/plots/constants.py index 68e992ab..91542257 100644 --- a/metplotpy/plots/constants.py +++ b/metplotpy/plots/constants.py @@ -64,6 +64,12 @@ AVAILABLE_MARKERS_LIST = ["o", "^", "s", "d", "H", ".", "h"] +# TODO Remove, Plotly specific +AVAILABLE_PLOTLY_MARKERS_LIST = ["circle-open", "circle", + "square", "diamond", + "hexagon", "triangle-up", "asterisk-open"] + + PCH_TO_MATPLOTLIB_MARKER = {'20': '.', '19': 'o', '17': '^', '1': 'H', '18': 'd', '15': 's', 'small circle': '.', 'circle': 'o', 'square': 's', @@ -75,6 +81,30 @@ XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} +# TODO REMOVE PLOTLY-specific +PCH_TO_PLOTLY_MARKER = {'0': 'circle-open', '19': 'circle', '20': 'circle', + '17': 'triangle-up', '15': 'square', '18': 'diamond', + '1': 'hexagon2', 'small circle': 'circle-open', + 'circle': 'circle', 'square': 'square', 'triangle': 'triangle-up', + 'rhombus': 'diamond', 'ring': 'hexagon2', '.': 'circle', + 'o': 'circle', '^': 'triangle-up', 'd': 'diamond', 'H': 'circle-open', + 'h': 'hexagon2', 's': 'square'} + +# approximated from plotly marker size to matplotlib marker size +PCH_TO_MATPLOTLIB_MARKER_SIZE = {'.': 14, 'o': 36, 's': 20, '^': 36, 'd': 20, 'H': 28} + +# TODO Remove, Plotly specific +TYPE_TO_PLOTLY_MODE = {'b': 'lines+markers', 'p': 'markers', 'l': 'lines'} + +# used for tick angles +XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} +YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} + +# TODO Remove these three lines, Plotly specific +PLOTLY_PAPER_BGCOOR = "white" +PLOTLY_AXIS_LINE_COLOR = "#c2c2c2" +PLOTLY_AXIS_LINE_WIDTH = 2 + # Caption weights supported in Matplotlib are normal, italic and oblique. # Map these onto the MetViewer requested values of 1 (normal), 2 (bold), # 3 (italic), 4 (bold italic), and 5 (symbol) using a dictionary From b7e827d963889bfd60d1cfe0ec2fe0914496826d Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:11:07 -0700 Subject: [PATCH 22/28] remove plotly variables from matplotlib version --- metplotpy/plots/constants.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/metplotpy/plots/constants.py b/metplotpy/plots/constants.py index 91542257..8e822b53 100644 --- a/metplotpy/plots/constants.py +++ b/metplotpy/plots/constants.py @@ -64,12 +64,6 @@ AVAILABLE_MARKERS_LIST = ["o", "^", "s", "d", "H", ".", "h"] -# TODO Remove, Plotly specific -AVAILABLE_PLOTLY_MARKERS_LIST = ["circle-open", "circle", - "square", "diamond", - "hexagon", "triangle-up", "asterisk-open"] - - PCH_TO_MATPLOTLIB_MARKER = {'20': '.', '19': 'o', '17': '^', '1': 'H', '18': 'd', '15': 's', 'small circle': '.', 'circle': 'o', 'square': 's', @@ -81,30 +75,13 @@ XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} -# TODO REMOVE PLOTLY-specific -PCH_TO_PLOTLY_MARKER = {'0': 'circle-open', '19': 'circle', '20': 'circle', - '17': 'triangle-up', '15': 'square', '18': 'diamond', - '1': 'hexagon2', 'small circle': 'circle-open', - 'circle': 'circle', 'square': 'square', 'triangle': 'triangle-up', - 'rhombus': 'diamond', 'ring': 'hexagon2', '.': 'circle', - 'o': 'circle', '^': 'triangle-up', 'd': 'diamond', 'H': 'circle-open', - 'h': 'hexagon2', 's': 'square'} - # approximated from plotly marker size to matplotlib marker size PCH_TO_MATPLOTLIB_MARKER_SIZE = {'.': 14, 'o': 36, 's': 20, '^': 36, 'd': 20, 'H': 28} -# TODO Remove, Plotly specific -TYPE_TO_PLOTLY_MODE = {'b': 'lines+markers', 'p': 'markers', 'l': 'lines'} - # used for tick angles XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} -# TODO Remove these three lines, Plotly specific -PLOTLY_PAPER_BGCOOR = "white" -PLOTLY_AXIS_LINE_COLOR = "#c2c2c2" -PLOTLY_AXIS_LINE_WIDTH = 2 - # Caption weights supported in Matplotlib are normal, italic and oblique. # Map these onto the MetViewer requested values of 1 (normal), 2 (bold), # 3 (italic), 4 (bold italic), and 5 (symbol) using a dictionary From 4467a905a567e8b0450fd164a6dce1edf14b5905 Mon Sep 17 00:00:00 2001 From: George McCabe <23407799+georgemccabe@users.noreply.github.com> Date: Mon, 9 Feb 2026 10:11:16 -0700 Subject: [PATCH 23/28] remove unneeded import --- metplotpy/plots/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/metplotpy/plots/util.py b/metplotpy/plots/util.py index 7ec21b3e..29e3dd2a 100644 --- a/metplotpy/plots/util.py +++ b/metplotpy/plots/util.py @@ -24,7 +24,6 @@ from typing import Union import pandas as pd import matplotlib.pyplot as plt -from jinja2.lexer import TOKEN_DOT from metplotpy.plots.context_filter import ContextFilter as cf import metcalcpy.util.pstd_statistics as pstats From be1adbc857952a76ec73fd8a04c252248a6c1dd9 Mon Sep 17 00:00:00 2001 From: bikegeek Date: Mon, 9 Feb 2026 12:25:03 -0700 Subject: [PATCH 24/28] Issue #556 Removed Plotly-specific code --- metplotpy/plots/base_plot.py | 57 ++---------------------------------- 1 file changed, 2 insertions(+), 55 deletions(-) diff --git a/metplotpy/plots/base_plot.py b/metplotpy/plots/base_plot.py index 60f698ca..7d86bfd3 100644 --- a/metplotpy/plots/base_plot.py +++ b/metplotpy/plots/base_plot.py @@ -303,62 +303,8 @@ def remove_file(self): if image_name is not None and os.path.exists(image_name): os.remove(image_name) -# TODO Remove Plotly specific, use add_horizontal_line() and add_vertical_line() below -# Plotly-specific, - def _add_lines(self, config_obj: Config, x_points_index: Union[list, None] = None) -> None: - """ Adds custom horizontal and/or vertical line to the plot. - All line's metadata is in the config_obj.lines - Args: - @config_obj - plot's configurations - @x_points_index - list of x-values that are used to create a plot - Returns: - """ - if not hasattr(config_obj, 'lines') or config_obj.lines is None: - return - - shapes = [] - for line in config_obj.lines: - # draw horizontal line - if line['type'] == 'horiz_line': - shapes.append({ - 'type': 'line', - 'yref': 'y', 'y0': line['position'], 'y1': line['position'], - 'xref': 'paper', 'x0': 0, 'x1': 0.95, - 'line': { - 'color': line['color'], - 'dash': line['line_style'], - 'width': line['line_width'], - }, - }) - elif line['type'] == 'vert_line': - # draw vertical line - try: - if x_points_index is None: - val = line['position'] - else: - ordered_indy_label = config_obj.create_list_by_plot_val_ordering(config_obj.indy_label) - index = ordered_indy_label.index(line['position']) - val = x_points_index[index] - shapes.append({ - 'type': 'line', - 'yref': 'paper', 'y0': 0, 'y1': 1, - 'xref': 'x', 'x0': val, 'x1': val, - 'line': { - 'color': line['color'], - 'dash': line['line_style'], - 'width': line['line_width'], - } - }) - except ValueError: - line_position = line["position"] - msg = f"Vertical line with position {line_position} cannot be created." - self.logger.warning(msg) - print(msg) - # ignore everything else - - # draw lines - self.figure.update_layout(shapes=shapes) + @staticmethod def add_horizontal_line(plt,y: float, line_properties: dict) -> None: """Adds a horizontal line to the matplotlib plot @@ -369,6 +315,7 @@ def add_horizontal_line(plt,y: float, line_properties: dict) -> None: """ plt.axhline(y=y, xmin=0, xmax=1, **line_properties) + @staticmethod def add_vertical_line(plt, x: float, line_properties: dict) -> None: """Adds a vertical line to the matplotlib plot From 7c33d80b8f9e24418ae55f60c1f81d816d3ef6ce Mon Sep 17 00:00:00 2001 From: bikegeek Date: Mon, 9 Feb 2026 12:25:49 -0700 Subject: [PATCH 25/28] Issue #556 Removed Plotly-specific code --- metplotpy/plots/constants.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/metplotpy/plots/constants.py b/metplotpy/plots/constants.py index 91542257..3d6dddc6 100644 --- a/metplotpy/plots/constants.py +++ b/metplotpy/plots/constants.py @@ -61,15 +61,8 @@ DEFAULT_TITLE_FONT_SIZE = 11 DEFAULT_TITLE_OFFSET = (-0.48) - AVAILABLE_MARKERS_LIST = ["o", "^", "s", "d", "H", ".", "h"] -# TODO Remove, Plotly specific -AVAILABLE_PLOTLY_MARKERS_LIST = ["circle-open", "circle", - "square", "diamond", - "hexagon", "triangle-up", "asterisk-open"] - - PCH_TO_MATPLOTLIB_MARKER = {'20': '.', '19': 'o', '17': '^', '1': 'H', '18': 'd', '15': 's', 'small circle': '.', 'circle': 'o', 'square': 's', @@ -81,29 +74,13 @@ XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} -# TODO REMOVE PLOTLY-specific -PCH_TO_PLOTLY_MARKER = {'0': 'circle-open', '19': 'circle', '20': 'circle', - '17': 'triangle-up', '15': 'square', '18': 'diamond', - '1': 'hexagon2', 'small circle': 'circle-open', - 'circle': 'circle', 'square': 'square', 'triangle': 'triangle-up', - 'rhombus': 'diamond', 'ring': 'hexagon2', '.': 'circle', - 'o': 'circle', '^': 'triangle-up', 'd': 'diamond', 'H': 'circle-open', - 'h': 'hexagon2', 's': 'square'} - # approximated from plotly marker size to matplotlib marker size PCH_TO_MATPLOTLIB_MARKER_SIZE = {'.': 14, 'o': 36, 's': 20, '^': 36, 'd': 20, 'H': 28} -# TODO Remove, Plotly specific -TYPE_TO_PLOTLY_MODE = {'b': 'lines+markers', 'p': 'markers', 'l': 'lines'} - # used for tick angles XAXIS_ORIENTATION = {0: 0, 1: 0, 2: 270, 3: 270} YAXIS_ORIENTATION = {0: -90, 1: 0, 2: 0, 3: -90} -# TODO Remove these three lines, Plotly specific -PLOTLY_PAPER_BGCOOR = "white" -PLOTLY_AXIS_LINE_COLOR = "#c2c2c2" -PLOTLY_AXIS_LINE_WIDTH = 2 # Caption weights supported in Matplotlib are normal, italic and oblique. # Map these onto the MetViewer requested values of 1 (normal), 2 (bold), From e7f564cfc61d145f25c74384c2622d0d2e2e6b46 Mon Sep 17 00:00:00 2001 From: bikegeek Date: Mon, 9 Feb 2026 12:26:28 -0700 Subject: [PATCH 26/28] Some clean up of code to use fewer variables --- metplotpy/plots/taylor_diagram/taylor_diagram.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metplotpy/plots/taylor_diagram/taylor_diagram.py b/metplotpy/plots/taylor_diagram/taylor_diagram.py index 77882284..f029da97 100644 --- a/metplotpy/plots/taylor_diagram/taylor_diagram.py +++ b/metplotpy/plots/taylor_diagram/taylor_diagram.py @@ -292,12 +292,11 @@ def _create_figure(self) -> None: ) # Plot the caption - caption = wts_size_styles['caption'] plt.figtext( self.config_obj.caption_align, self.config_obj.caption_offset, self.config_obj.plot_caption, - fontproperties=caption, color=self.config_obj.caption_color + fontproperties=wts_size_styles['caption'], color=self.config_obj.caption_color ) # Add a figure legend From 191f1f46dda2ae08e9d5032943f6fcb78c4fc28b Mon Sep 17 00:00:00 2001 From: bikegeek Date: Mon, 9 Feb 2026 12:27:04 -0700 Subject: [PATCH 27/28] Issue #556 Removed Plotly-specific code --- metplotpy/plots/util.py | 49 ----------------------------------------- 1 file changed, 49 deletions(-) diff --git a/metplotpy/plots/util.py b/metplotpy/plots/util.py index 7ec21b3e..1978bdfc 100644 --- a/metplotpy/plots/util.py +++ b/metplotpy/plots/util.py @@ -88,33 +88,6 @@ def get_params(config_filename): -# TODO Remove, Plotly specific -# Matplotlib only needs to do a plt.savefig() -# command -def make_plot(config_filename, plot_class): - """!Get plot parameters and create the plot. - - @param config_filename The full path to the config or None - @param plot_class class of plot to produce, e.g. Bar or Box - @returns plot class object or None if something went wrong - """ - # Retrieve the contents of the custom config file to over-ride - # or augment settings defined by the default config file. - params = get_params(config_filename) - try: - plot = plot_class(params) - plot.save_to_file() - # plot.write_html() - plot.write_output_file() - name = plot_class.__name__ if not hasattr(plot_class, 'LONG_NAME') else plot_class.LONG_NAME - plot.logger.info(f"Finished {name} plot at {datetime.now()}") - return plot - except ValueError as val_er: - print(val_er) - - return None - - def alpha_blending(hex_color: str, alpha: float) -> str: """ Alpha color blending as if on the white background. Useful for gridlines @@ -203,28 +176,6 @@ def pretty(low, high, number_of_intervals) -> Union[np.ndarray, list]: return np.arange(miny, maxy + 0.5 * d, d) -# TODO remove, moved to base_plot.py -def add_horizontal_line(y: float, line_properties: dict) -> None: - """Adds a horizontal line to the matplotlib plot - - @param y y value for the line - @param line_properties dictionary with line properties like color, width, dash - @returns None - """ - plt.axhline(y=y, xmin=0, xmax=1, **line_properties) - - -# TODO remove, moved to base_plot.py -def add_vertical_line(x: float, line_properties: dict) -> None: - """Adds a vertical line to the matplotlib plot - - @param x x value for the line - @param line_properties dictionary with line properties like color, width, dash - @returns None - """ - plt.axvline(x=x, ymin=0, ymax=1, **line_properties) - - def abline(x_value: float, intercept: float, slope: float) -> float: """ Calculates y coordinate based on x-value, intercept and slope From 54e94f8f6401f93ad12bad37881696d265c1a31f Mon Sep 17 00:00:00 2001 From: bikegeek Date: Tue, 10 Feb 2026 09:24:20 -0700 Subject: [PATCH 28/28] Point to minimal yaml config file to match section describing using only defaults --- docs/Users_Guide/reliability_diagram.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Users_Guide/reliability_diagram.rst b/docs/Users_Guide/reliability_diagram.rst index 4f15663a..ee0865d8 100644 --- a/docs/Users_Guide/reliability_diagram.rst +++ b/docs/Users_Guide/reliability_diagram.rst @@ -160,7 +160,7 @@ corresponding to the *plot_filename* setting in the default configuration file. Otherwise, this will need to be specified in *plot_filename* in the **minimal_box.yaml** file): -.. literalinclude:: ../../test/reliability_diagram/custom_reliability_use_defaults.yaml +.. literalinclude:: ../../test/reliability_diagram/minimal_reliability.yaml Copy this file to the working directory: