def axes(self, *args, **kwargs): """set axis properties Args: args: tuple of axes, either left out to apply to all axes or specify e.g. "x", "y" kwargs: dict, key-value where key can be any axis attribute set to value """ which_axes = [0, 1, 2] if args: # remove unrequested axes if "x" not in args: which_axes.remove(0) if "y" not in args: which_axes.remove(1) if "z" not in args: which_axes.remove(2) for k, v in kwargs.items(): # Apparently, does not work simply with class, potentially since attributes are only # added in __init__. Therefore, use one object instead if not hasattr(self._default_axes[0], k): get_logger().warning("Unknown attribute %s of AxisSpec", k) continue for i, ax in enumerate(self._default_axes): if i not in which_axes: # only apply to requested continue setattr(ax, k, v) if self._current_plot_spec: self._current_plot_spec.axes(*args, **kwargs)
def legend(self, **kwargs): """set legend properties """ for k, v in kwargs.items(): if not hasattr(self._legend_spec, k): get_logger().warning("Unknown attribute %s of LegendSpec", k) continue setattr(self._legend_spec, k, v)
def legend(self, **kwargs): """set legend properties """ for k, v in kwargs.items(): if not hasattr(self._default_legend, k): get_logger().warning("Unknown attribute %s of LegendSpec", k) continue setattr(self._default_legend, k, v) if self._current_plot_spec: self._current_plot_spec.legend(**kwargs)
def __init__(self): # The parent FigureSpec this PlotSpec is embedded in self._parent_figure_spec = None # relative positioning in FigureSpec # (left, bottom, right, top) self._rel_coordinates = None # Tuple of row margins [0] = bottom, [1] = top self._row_margins = None # Tuple of column margins [0] = left, [1] = right self._column_margins = None # text to be added to a plot self._texts = [] # legend properties self._legend_spec = LegendSpec() # AxisSpecs of the PlotSpec self._axes = [AxisSpec(), AxisSpec(), AxisSpec()] # LineSpecs self._lines = [] # Potential PlotSpecs to share x- or y-axis with self._share_x = None self._share_y = None # title self._title = None # quickly refer to logger self.logger = get_logger()
def generate_styles(style_class, n_styles=1, **kwargs): """Generate a certain number of a given styles Args: style_class: class of object to be constructed n_styles: int number of styles to be created Returns: list of styles """ # Generate number of requested styles styles = [style_class() for _ in range(n_styles)] # Get the defaults of this class defaults = deepcopy(style_class.defaults) for k in defaults: # Overwrite defaults by potential user input defaults[k] = kwargs.pop(k, defaults[k]) for k, v in defaults.items(): # Go through all style options for i, style in enumerate(styles): if not hasattr(style, k): get_logger().warning("Unknown attribute %s of style class %s", k, style_class.__name__) continue try: # Option might come as an iterable iter(v) chosen_style = v[i % len(v)] except TypeError: # if not, this is assumed to be the option chosen_style = v # now apply the option setattr(style, k, chosen_style) return styles
def __init__(self, n_cols, n_rows, height_ratios=None, width_ratios=None, **kwargs): # construct unique name self.name = f"{FigureSpec.FIGURE_NAME_BASE}_{FigureSpec.N_FIGURES}" FigureSpec.N_FIGURES += 1 # Size in pixels (width, height) self.size = kwargs.pop("size", (300, 300)) # number of columns and rows self.n_cols = n_cols self.n_rows = n_rows # construct height and width ratios of rows and columns self.__make_height_ratios(height_ratios) self.__make_width_ratios(width_ratios) # construct margins of rows and columns self.__make_row_margins(kwargs.pop("row_margin", 0.05)) self.__make_column_margins(kwargs.pop("column_margin", 0.05)) # remember which cells are taken already self.cells_taken = [] # store all PlotSpecs self._plot_specs = [] # set current PlotSpec internally to be able to provide some proxy methods self._current_plot_spec = None # default axes settings self._default_axes = [AxisSpec(), AxisSpec(), AxisSpec()] # default legend settings self._default_legend = LegendSpec() # quickly refer to logger self.logger = get_logger() # automatically define a plot if only one cell if self.n_cols == 1 and self.n_rows == 1: self.define_plot(0,0)
def find_boundaries( objects, x_low=None, x_up=None, y_low=None, y_up=None, z_low=None, z_up=None, # pylint: disable=unused-argument, too-many-branches, too-many-statements x_log=False, y_log=False, z_log=False, reserve_ndc_top=None, reserve_ndc_bottom=None, x_force_limits=False, y_force_limits=False, y_account_for_errors=True): """Find boundaries for any ROOT objects Args: histo: TH1 1D histogram to derive boundaries for x_low: float or None (fix to given float if given) otherwise derive from histogram x_up: float or None (fix to given float if given) otherwise derive from histogram y_low: float or None (fix to given float if given) otherwise derive from histogram y_up: float or None (fix to given float if given) otherwise derive from histogram x_log: bool whether or not x-axis is log scale y_log: bool whether or not y-axis is log scale reserve_ndc_top: float or None, specify whether or not some space should be reserved for the legend at the top reserve_ndc_bottom: float or None, specify whether or not some space should be reserved for the legend at the bottom y_force_limits: bool to really force the limits as set by the user regardless of having a potential overlapping legend Returns: float, float, float, float (derived boundaries) """ #TODO This has to be revised (code duplication, ~brute force implementation) if x_up is not None and x_low is not None and x_up < x_low: # At this point there are numbers for sure. # If specified by the user and in case wrong way round, fix it get_logger().warning("Minimum (%f) is larger than maximum (%f) on x-axis. " \ "Fix it by swapping numbers", x_low, x_up) x_low, x_up = (x_up, x_low) if y_up is not None and y_low is not None and y_up < y_low: # At this point there are numbers for sure. # If specified by the user and in case wrong way round, fix it get_logger().warning("Minimum (%f) is larger than maximum (%f) on y-axis. " \ "Fix it by swapping numbers", y_low, y_up) y_low, y_up = (y_up, y_low) if z_up is not None and z_low is not None and z_up < z_low: # At this point there are numbers for sure. # If specified by the user and in case wrong way round, fix it get_logger().warning("Minimum (%f) is larger than maximum (%f) on z-axis. " \ "Fix it by swapping numbers", z_low, z_up) z_low, z_up = (z_up, z_low) # set largest/lowest float values x_low_new = sys.float_info.max x_up_new = sys.float_info.min y_low_new = sys.float_info.max y_up_new = sys.float_info.min z_low_new = sys.float_info.max z_up_new = sys.float_info.min # set largest/lowest float values for a second search to find boundaries # closest to object's contents x_low_new_no_user = sys.float_info.max x_up_new_no_user = sys.float_info.min y_low_new_no_user = sys.float_info.max y_up_new_no_user = sys.float_info.min z_low_new_no_user = sys.float_info.max z_up_new_no_user = sys.float_info.min # whether or not adjust y-limits adjust_y_limits = True for obj in objects: # for each 1D ROOT object find user and non-user-specific boundaries if not (is_1d(obj) or is_2d(obj)): get_logger().warning("Cannot derive limits for object's class %s", obj.__class__.__name__) continue if isinstance(obj, ROOT_OBJECTS_HIST_1D): x_low_est, x_up_est, y_low_est, y_up_est = \ find_boundaries_TH1(obj, x_low, x_up, y_low, y_up, x_log, y_log, y_account_for_errors=y_account_for_errors) x_low_est_no_user, x_up_est_no_user, y_low_est_no_user, y_up_est_no_user = \ find_boundaries_TH1(obj, x_log=x_log, y_log=y_log, y_account_for_errors=y_account_for_errors) elif isinstance(obj, TF1) and not isinstance(obj, ROOT_OBJECTS_NOT_1D): x_low_est, x_up_est, y_low_est, y_up_est = \ find_boundaries_TF1(obj, x_low, x_up, y_low, y_up, x_log, y_log) x_low_est_no_user, x_up_est_no_user, y_low_est_no_user, y_up_est_no_user = \ find_boundaries_TF1(obj, x_log=x_log, y_log=y_log) elif isinstance(obj, TGraph): x_low_est, x_up_est, y_low_est, y_up_est = \ find_boundaries_TGraph(obj, x_low, x_up, y_low, y_up, x_log, y_log) if x_low_est is None: continue x_low_est_no_user, x_up_est_no_user, y_low_est_no_user, y_up_est_no_user = \ find_boundaries_TGraph(obj, x_log=x_log, y_log=y_log) elif isinstance(obj, ROOT_OBJECTS_HIST_2D): x_low_est, x_up_est, y_low_est, y_up_est, z_low_est, z_up_est = \ find_boundaries_TH2(obj, x_low, x_up, y_low, y_up, z_low, z_up, x_log, y_log, z_log) x_low_est_no_user, x_up_est_no_user, y_low_est_no_user, y_up_est_no_user, z_low_est_no_user, z_up_est_no_user = \ find_boundaries_TH2(obj, x_log=x_log, y_log=y_log, z_log=z_log) # pylint: disable=line-too-long # update boundaries for user-specific settings z_up_new = max(z_up_est, z_up_new) z_low_new = min(z_low_est, z_low_new) # update boundaries for user-independent settings z_up_new_no_user = max(z_up_est_no_user, z_up_new_no_user) z_low_new_no_user = min(z_low_est_no_user, z_low_new_no_user) adjust_y_limits = False elif isinstance(obj, TEfficiency): get_logger().warning( "Finding boundaries for TEfficiency not yet implemented") continue else: get_logger().warning("Unknown class %s", obj.__class__.__name__) continue # update boundaries for user-specific settings x_up_new = max(x_up_est, x_up_new) x_low_new = min(x_low_est, x_low_new) y_up_new = max(y_up_est, y_up_new) y_low_new = min(y_low_est, y_low_new) # update boundaries for user-independent settings x_up_new_no_user = max(x_up_est_no_user, x_up_new_no_user) x_low_new_no_user = min(x_low_est_no_user, x_low_new_no_user) y_up_new_no_user = max(y_up_est_no_user, y_up_new_no_user) y_low_new_no_user = min(y_low_est_no_user, y_low_new_no_user) if y_up is not None and y_up_new < y_up_new_no_user and not y_force_limits: # only do it if y-limits are not forced get_logger().warning("The upper y-limit was chosen to be %f which is however too small " \ "to fit the plots. It is adjusted to the least maximum value of %f.", y_up_new, y_up_new_no_user) y_up_new = y_up_new_no_user if y_low is not None and y_low_new > y_low_new_no_user and not y_force_limits: # only do it if y-limits are not forced get_logger().warning("The lower y-limit was chosen to be %f which is however too large " \ "to fit the plots. It is adjusted to the least maximum value of %f.", y_low_new, y_low_new_no_user) y_low_new = y_low_new_no_user if y_log: if y_low_new <= 0: y_low_new = MIN_LOG_SCALE if y_low_new_no_user <= 0: y_low_new_no_user = MIN_LOG_SCALE # Adjust a bit top and bottom otherwise maxima and minima will exactly touch the x-axis if adjust_y_limits: if y_log: y_diff = log10(y_up_new) - log10(y_low_new) else: y_diff = y_up_new - y_low_new if y_low is None and reserve_ndc_bottom is None: if y_log: y_low_new = 10**(log10(y_low_new) - y_diff * 0.1) else: y_low_new -= 0.1 * y_diff if y_up is None and reserve_ndc_top is None: if y_log: y_up_new = 10**(log10(y_up_new) + y_diff * 0.1) else: y_up_new += 0.1 * y_diff # Now force the limits if requested if x_force_limits and x_low is not None and x_up is not None: x_low_new = x_low x_up_new = x_up if y_force_limits and y_low is not None and y_up is not None: y_low_new = y_low y_up_new = y_up # if z_force_limits and z_low is not None and z_up is not None: # z_low_new = z_low # z_up_new = z_up if z_low_new <= 0 and z_log: # If not compatible with log scale, force it to be # Can happen if fixed by user - but incompatible - # and at the same time log scale is requested get_logger().warning("Have to set z-minimum to something larger than 0 since log-scale " \ "is requested. Set value was %f and reset value is now %f", z_low_new, MIN_LOG_SCALE) z_low_new = MIN_LOG_SCALE if y_low_new <= 0 and y_log: # If not compatible with log scale, force it to be # Can happen if fixed by user - but incompatible - # and at the same time log scale is requested get_logger().warning("Have to set y-minimum to something larger than 0 since log-scale " \ "is requested. Set value was %f and reset value is now %f", y_low_new, MIN_LOG_SCALE) y_low_new = MIN_LOG_SCALE if x_low_new <= 0 and x_log: # If not compatible with log scale, force it to be # Can happen if fixed by user - but incompatible - # and at the same time log scale is requested get_logger().warning("Have to set x-minimum to something larger than 0 since log-scale " \ "is requested. Set value was %f and reset value is now %f", x_low_new, MIN_LOG_SCALE) x_low_new = MIN_LOG_SCALE # compute what we need for the legend if reserve_ndc_top and not y_force_limits: if y_log: y_diff = log10(y_up_new) - log10(y_low_new) y_diff_up_user_no_user = log10(y_up_new) - log10(y_up_new_no_user) else: y_diff = y_up_new - y_low_new y_diff_up_user_no_user = y_up_new - y_up_new_no_user if y_diff_up_user_no_user / y_diff < reserve_ndc_top: get_logger().info("Add space to fit legend") y_diff_with_legend = y_diff / (1 - reserve_ndc_top) if y_log: y_up_new = 10**(log10(y_low_new) + y_diff_with_legend + 0.1 * y_diff) else: y_diff_with_legend = y_diff / (1 - reserve_ndc_top) y_up_new = y_low_new + y_diff_with_legend + 0.1 * y_diff elif reserve_ndc_bottom and not y_force_limits: if y_log: y_diff = log10(y_up_new) - log10(y_low_new) y_diff_low_user_no_user = log10(y_low_new_no_user) - log10( y_low_new) else: y_diff = y_up_new - y_low_new y_diff_low_user_no_user = y_low_new_no_user - y_low_new if y_diff_low_user_no_user / y_diff < reserve_ndc_bottom: get_logger().info("Add space to fit legend") y_diff_with_legend = y_diff / (1 - reserve_ndc_bottom) if y_log: y_low_new = 10**(log10(y_up_new) - y_diff_with_legend - 0.1 * y_diff) else: y_diff_with_legend = y_diff / (1 - reserve_ndc_bottom) y_low_new = y_up_new - y_diff_with_legend - 0.1 * y_diff return x_low_new, x_up_new, y_low_new, y_up_new, z_low_new, z_up_new