Source code for sensortoolkit.plotting._plot_formatting

# -*- coding: utf-8 -*-
"""
This module contains methods for configuring various plot formatting presets
(e.g., configuring the fontsize, wrapping long figure titles over multiple
lines, specifying figure dimensions for formatting figures with multiple
subplots, each displaying sensor data, etc.).

================================================================================

@Author:
  | Samuel Frederick, NSSC Contractor (ORAU)
  | U.S. EPA / ORD / CEMM / AMCD / SFSB

Created:
  Mon Jan 27 08:49:12 2020
Last Updated:
  Wed Jul 28 14:13:57 2021
"""
from pandas.plotting import register_matplotlib_converters
import numpy as np
from textwrap import wrap
import math
register_matplotlib_converters()


[docs]def set_fontsize(serials): """Selects fontsize for figures based on the number of sensors present in the sensor serial ID dictionary. Args: serials (dict): A dictionary of serial identifiers unique to each sensor in the deployment testing group. Returns: fontsize (float): The fontsize for figures. """ n_sensors = len(serials) if (n_sensors == 1): fontsize = 14 elif (n_sensors < 7 and n_sensors > 1): fontsize = 12.75 else: fontsize = 11.7 return fontsize
[docs]def wrap_text(labels, max_label_len=10): """Formats plotting text with line breaks based on specified text length. Code modified via Stack Overflow user DavidG code (https://stackoverflow.com/questions/47057789/matplotlib-wrap-text-in-legend) Args: labels (list): list plotting labels (strings) such as header/title text max_label_len (int): The maximum number of characters on a single line. Labels longer than this will have a newline character inserted at every max_label_len number of characters. Returns: labels (list): Modified list of labels with the newline character inserted for labels exceeding the max_label_len. """ labels = ['\n'.join(wrap(l, max_label_len)) for l in labels] return labels
[docs]def subplot_dims(n_sensors): """Recommends subplot dimensions based on the nearest perfect square for number of sensors (except when n%10, n_cols in multiples of 5) Args: n_sensors (int): The number of sensors in the deployment group. Returns: (tuple): Two-element tuple containing: - **n_rows** (*int*): The number of subplot rows. - **n_cols** (*int*): The number of subplot columns. """ sqr = np.sqrt(n_sensors) n_rows = math.floor(sqr) if n_sensors % 10 == 0: n_rows = math.floor(n_sensors/5) n_cols = math.ceil(n_sensors / n_rows) return (n_rows, n_cols)
[docs]def sensor_subplot_formatting(number_of_sensors, param_obj, report_fmt, **kwargs): """Configure subplot parameters that control the spacing of subplots, number of subplots and dimensions of the Matplotliub axes object array, color bar formatting, etc. Args: number_of_sensors (int): The number of sensors to display in the figure. param_obj (sensortoolkit.Parameter): The parameter correpsonding to measurement data that will be displayed in the figure. report_fmt (bool): If true, select formatting presets for displaying figures on the reporting template for sensor performance evaluations included alongside US EPA's performance targets documents for air sensors. **Keyword Arguments:** :param bool show_colorbar: If true, a colorbar will be displayed on figures indicating the relative humidity recorded at the same time as the sensor-reference measurement pairs. The RH is superimposed as a colormap on the sensor-reference measurement pair scatter. Defaults to True. :param dict sensor_serials: A dictionary of unique serial identifiers corresponding to each sensor in the evaluation group. Defaults to None. Raises: ValueError: Raise if the number of sensors does not correspond to one of the preset configurations (presets for number_of_sensors <= 9). Returns: (tuple): 17-element tuple containing: - **Nr** (*int*): The number of rows of subplots for the figure instance. - **Nc** (*int*): The number of columns of subplots for the figure instance. - **fig_size** (*tuple*): The size of the figure instance (x_width, y_width). - **suptitle_xpos** (*float*): The relative x-coordinate position of the figure title. - **suptitle_ypos** (*float*): The relative y-coordinate position of the figure title. - **title_text_wrap** (*int*): The number of characters to include on a single line of the title before inserting a new line. - **detail_fontsize** (*int* or *float*): Fontsize for axes tick labels and smaller plotting text elements.. - **wspace** (*float*): The width (x-distance) between each subplot. - **hspace** (*float*): The height (y-distance) between each subplot. - **left** (*float*): The left-most (x-min) limits at which the subplots will be drawn. - **right** (*float*): The right-most (x-max) limits at which the subplots will be drawn. - **top** (*float*): The top-most (y-max) limits at which the subplots will be drawn. - **bottom** (*float*): The bottom-most (y-min) limits at which the subplots will be drawn. - **filename_suffix** (*str*): A string indicating the number of sensors for which subplots are drawn in the figure, added to filename when saving figure to png. - **cbar_padding** (*float*): Padding between the colorbar and the figure subplots. - **cbar_aspect** (*int* or *float*): The aspect ratio (width / height) of the colorbar for relative humidity measurements. - **font_size** (*int* or *float*): The font size for text displayed in the figure. """ RH_colormap = kwargs.get('show_colorbar', True) serials = kwargs.get('sensor_serials', None) param_averaging = param_obj.averaging Nr, Nc = subplot_dims(number_of_sensors) sensor_plural, row_plural, column_plural = '', '', '' if number_of_sensors > 1: sensor_plural = 's' if Nr > 1: row_plural = 's' if Nc > 1: column_plural = 's' print('..creating subplot for', str(number_of_sensors), 'sensor' + sensor_plural, 'with', str(Nr), 'row' + row_plural, 'and', str(Nc), 'column' + column_plural) # Get default fontsize to fall back on if none specified font_size = set_fontsize(serials) if number_of_sensors == 1: # 1x1 subplot if RH_colormap is True: if len(param_averaging) == 1: font_size = 12 fig_size = (4.3, 3.91) wspace = .1 hspace = .1 left = 0.12 right = 0.8 top = 0.85 bottom = 0.15 suptitle_xpos = 0.46 suptitle_ypos = 0.95 title_text_wrap = 30 else: fig_size = (4.9, 5.5) wspace = .01 hspace = .01 left = 0.15 right = 0.85 top = 0.9 bottom = 0.25 else: fig_size = (4.9, 5.5) wspace = .01 hspace = .01 left = 0.15 right = 0.85 top = 0.95 bottom = 0.1 suptitle_xpos = 0.50 suptitle_ypos = 0.98 title_text_wrap = 35 detail_fontsize = .75*font_size filename_suffix = '1_sensor' cbar_padding = .13 cbar_aspect = 20 elif number_of_sensors in (2, 3): # 3x1 subplot if RH_colormap is True: fig_size = (12.3, 5.12) wspace = .38 hspace = .05 left = 0.06 right = 0.94 top = 0.97 bottom = 0.22 else: fig_size = (12, 4) wspace = .4 hspace = .01 left = 0.07 right = 0.93 top = 0.90 bottom = 0.1 suptitle_xpos = 0.50 suptitle_ypos = 0.99 title_text_wrap = 70 detail_fontsize = .9*font_size filename_suffix = '3_sensors' cbar_padding = .16 cbar_aspect = 20 elif number_of_sensors in (4, 5, 6): # 2x3 subplot fig_size = (13, 9) suptitle_ypos = 0.97 title_text_wrap = 70 if RH_colormap is True: wspace = .1 hspace = .34 left = 0.03 right = 0.97 top = 0.90 bottom = 0.17 else: fig_size = (13, 8) wspace = .4 hspace = .33 left = 0.07 right = 0.93 top = 0.89 bottom = 0.1 suptitle_xpos = 0.50 detail_fontsize = .85*font_size filename_suffix = '6_sensors' cbar_padding = .08 cbar_aspect = 20 elif number_of_sensors in (7, 8): # 2x4 subplot Nr = 2 Nc = 4 fig_size = (16, 9) suptitle_ypos = 0.97 title_text_wrap = 70 if RH_colormap is True: wspace = .1 hspace = .34 left = 0.03 right = 0.97 top = 0.90 bottom = 0.17 else: fig_size = (15, 8) wspace = .44 hspace = .35 left = 0.05 right = 0.95 top = 0.89 bottom = 0.1 suptitle_xpos = 0.50 detail_fontsize = .85*font_size filename_suffix = '8_sensors' cbar_padding = .08 cbar_aspect = 20 elif number_of_sensors == 9: # 3x3 subplot Nr = 3 Nc = 3 fig_size = (12, 11) suptitle_ypos = 0.97 title_text_wrap = 70 if RH_colormap is True: wspace = .04 hspace = .4 left = 0.01 right = 0.99 top = 0.92 bottom = 0.15 else: fig_size = (12, 10) wspace = .4 hspace = .35 left = 0.07 right = 0.93 top = 0.89 bottom = 0.1 suptitle_xpos = 0.50 detail_fontsize = .85*font_size filename_suffix = '9_sensors' cbar_padding = .06 cbar_aspect = 20 else: raise ValueError('No formatting presets configured for', str(number_of_sensors)) if report_fmt is True: # Plot 1-hour averaged dataset if len(param_averaging) == 1: font_size = 12 fig_size = (4.3, 3.91) wspace = .1 hspace = .1 left = 0.12 right = 0.8 top = 0.85 bottom = 0.15 suptitle_xpos = 0.5 suptitle_ypos = 1.01 title_text_wrap = 30 suptitle_ypos = 0.95 # Plot both 1-hour and 24-hour averaged datasets else: wspace = .4 hspace = .08 left = 0.1 right = 0.92 top = 0.9 bottom = 0.30 cbar_padding = .0 cbar_aspect = 20 font_size = 9 detail_fontsize = 0.9*font_size filename_suffix = 'report_fmt' return (Nr, Nc, fig_size, suptitle_xpos, suptitle_ypos, title_text_wrap, detail_fontsize, wspace, hspace, left, right, top, bottom, filename_suffix, cbar_padding, cbar_aspect, font_size)
[docs]def get_colormap_range(df_list): """Set default range for colormap based on number of sensors The range is normalized to between zero and one (0.0, 1.0) Args: df_list (list): Sensor datasets. Returns: cmap_range (tuple): Tuple of length 2 containing the lower and upper bounds for the normalized colormap range. """ if len(df_list) < 4: cmap_range = (0, 0.4) else: cmap_range = (0, 1) return cmap_range
[docs]def met_scatter_lims(met_data, param, met_param, xlims, ylims, serials, avg_df): """Set axes limits for plots generated by ``normalized_met_scatter()``. Args: met_data (pandas DataFrame): Reference dataset containing meteorological data for either temperature or relative humidity, logged at 1-hour averages. param (str): The name of the SDFS parameter for which normalized sensor-reference concentration pairs will be displayed along the y-axis. met_param (str): The name of the meteorological parameter displayed along the x-axis. The name is the SDFS parameter name assocaited with the meteorological parameter; for temperature, pass ``'Temp'``, for relative humidity, pass ``'RH'``. xlims (Two-element tuple): The x-limits for the normalized meteorological scatterplot. Data along the x-axis are meteorological measurements (either temperature or relative humidity). ylims (Two-element tuple): The y-limits for the normalized meteorological scatterplot. Data along the y-axis are normalized sensor/reference concentration pairs. serials (dict): A dictionary of serial identifiers unique to each sensor in the deployment testing group. avg_df (pandas DataFrame): A dataset containing the intersensor average for concurrently recorded sensor measurements for each parameter measured by the air sensor. The 1st and 99th percentile of the normalized (sensor/reference) intersensor average values are used to set the y-limits for plots. Returns: (tuple): Four-element tuple containing: - **xmin** (*float*) - **xmax** (*float*) - **ymin** (*float*) - **ymax** (*float*) """ # Automatically generate x-axis limits if none specified if xlims is None: xmax = met_data[met_param + '_Value'].max() xmin = met_data[met_param + '_Value'].min() xmax = round(xmax, -1) xmin = math.floor(xmin) else: xmin, xmax = xlims[0], xlims[1] # Automatically generate y-axis limits if none specified if ylims is None: ymin = avg_df['mean_Normalized_' + param + '_Value'].quantile(0.01) ymax = avg_df['mean_Normalized_' + param + '_Value'].quantile(0.99) if ymax < 5.0: rounding_place = 1 # round to nearest tenths place else: rounding_place = -1 # round to nearest tens place ymin = math.floor(ymin) - 0.1*ymin ymax = round(ymax, rounding_place) + 0.1*ymax if ymin == ymax == 0: ymin, ymax = -0.1, 1.1 if ymin < 0.1 and ymin > -.1: ymin = -0.5 if ymax < 1.0: ymin = -1*ymax ymax = 1.1 else: ymin, ymax = ylims[0], ylims[1] return (xmin, xmax), (ymin, ymax)