diff --git a/py-scripts/lf_graph.py b/py-scripts/lf_graph.py index a6b7f2e5..3e0bae41 100755 --- a/py-scripts/lf_graph.py +++ b/py-scripts/lf_graph.py @@ -24,10 +24,26 @@ import matplotlib.pyplot as plt import numpy as np import pdfkit from matplotlib.colors import ListedColormap +import matplotlib.ticker as mticker import argparse +import traceback +import logging + + +# TODO have scipy be part of the base install +try: + from scipy import interpolate + +except Exception as x: + print("Info: scipy package not installed, Needed for smoothing linear plots 'pip install scipy' ") + traceback.print_exception(Exception, x, x.__traceback__, chain=True) + sys.path.append(os.path.join(os.path.abspath(__file__ + "../../../"))) +logger = logging.getLogger(__name__) +lf_logger_config = importlib.import_module("py-scripts.lf_logger_config") + lf_csv = importlib.import_module("py-scripts.lf_csv") lf_csv = lf_csv.lf_csv @@ -57,6 +73,7 @@ class lf_bar_graph: _xaxis_step=1, _xticks_font=None, _xaxis_value_location=0, + _xticks_rotation=None, _text_font=None, _text_rotation=None, _grp_title="", @@ -66,7 +83,10 @@ class lf_bar_graph: _legend_ncol=1, _legend_fontsize=None, _dpi=96, - _enable_csv=False): + _enable_csv=False, + _remove_border=None, + _alignment=None + ): if _data_set is None: _data_set = [[30.4, 55.3, 69.2, 37.1], [45.1, 67.2, 34.3, 22.4], [22.5, 45.6, 12.7, 34.8]] @@ -107,6 +127,9 @@ class lf_bar_graph: self.legend_box = _legend_box self.legend_ncol = _legend_ncol self.legend_fontsize = _legend_fontsize + self.remove_border = _remove_border + self.alignment = _alignment + self.xticks_rotation = _xticks_rotation def build_bar_graph(self): if self.color is None: @@ -116,8 +139,17 @@ class lf_bar_graph: self.color.append(self.color_name[i]) i = i + 1 - plt.subplots(figsize=self.figsize) + fig_size, ax = plt.subplots(figsize=self.figsize, gridspec_kw=self.alignment) i = 0 + # to remove the borders + if self.remove_border is not None: + for border in self.remove_border: + ax.spines[border].set_color(None) + if 'left' in self.remove_border: # to remove the y-axis labeling + yaxis_visable =False + else: + yaxis_visable=True + ax.yaxis.set_visible(yaxis_visable) def show_value(rectangles): for rect in rectangles: @@ -148,10 +180,10 @@ class lf_bar_graph: plt.xticks(np.arange(0, len(self.xaxis_categories), step=self.xaxis_step), - fontsize=self.xticks_font) + fontsize=self.xticks_font,rotation=self.xticks_rotation) else: plt.xticks([i + self._xaxis_value_location for i in np.arange(0, len(self.data_set[0]), step=self.xaxis_step)], - self.xaxis_categories, fontsize=self.xticks_font) + self.xaxis_categories, fontsize=self.xticks_font,rotation=self.xticks_rotation) plt.legend( handles=self.legend_handles, loc=self.legend_loc, @@ -163,7 +195,7 @@ class lf_bar_graph: plt.gcf() plt.savefig("%s.png" % self.graph_image_name, dpi=96) plt.close() - print("{}.png".format(self.graph_image_name)) + logger.debug("{}.png".format(self.graph_image_name)) if self.enable_csv: if self.data_set is not None and self.xaxis_categories is not None: if len(self.xaxis_categories) == len(self.data_set[0]): @@ -179,11 +211,181 @@ class lf_bar_graph: raise ValueError( "Length and x-axis values and y-axis values should be same.") else: - print("No Dataset Found") - print("{}.csv".format(self.graph_image_name)) + logger.debug("No Dataset Found") + logger.debug("{}.csv".format(self.graph_image_name)) return "%s.png" % self.graph_image_name + +class lf_bar_graph_horizontal: + def __init__(self, _data_set=None, + _xaxis_name="x-axis", + _yaxis_name="y-axis", + _yaxis_categories=None, + _yaxis_label=None, + _graph_title="", + _title_size=16, + _graph_image_name="image_name", + _label=None, + _color=None, + _bar_height=0.25, + _color_edge='grey', + _font_weight='bold', + _color_name=None, + _figsize=(10, 5), + _show_bar_value=False, + _yaxis_step=1, + _yticks_font=None, + _yaxis_value_location=0, + _yticks_rotation=None, + _text_font=None, + _text_rotation=None, + _grp_title="", + _legend_handles=None, + _legend_loc="best", + _legend_box=None, + _legend_ncol=1, + _legend_fontsize=None, + _dpi=96, + _enable_csv=False, + _remove_border=None, + _alignment=None + ): + + if _data_set is None: + _data_set = [[30.4, 55.3, 69.2, 37.1], [45.1, 67.2, 34.3, 22.4], [22.5, 45.6, 12.7, 34.8]] + if _yaxis_categories is None: + _yaxis_categories = [1, 2, 3, 4] + if _yaxis_label is None: + _yaxis_label = ["a", "b", "c", "d"] + if _label is None: + _label = ["bi-downlink", "bi-uplink", 'uplink'] + if _color_name is None: + _color_name = ['lightcoral', 'darkgrey', 'r', 'g', 'b', 'y'] + self.data_set = _data_set + self.xaxis_name = _xaxis_name + self.yaxis_name = _yaxis_name + self.yaxis_categories = _yaxis_categories + self.yaxis_label = _yaxis_label + self.title = _graph_title + self.title_size = _title_size + self.graph_image_name = _graph_image_name + self.label = _label + self.color = _color + self.bar_height = _bar_height + self.color_edge = _color_edge + self.font_weight = _font_weight + self.color_name = _color_name + self.figsize = _figsize + self.show_bar_value = _show_bar_value + self.yaxis_step = _yaxis_step + self.yticks_font = _yticks_font + self._yaxis_value_location = _yaxis_value_location + self.text_font = _text_font + self.text_rotation = _text_rotation + self.grp_title = _grp_title + self.enable_csv = _enable_csv + self.lf_csv = lf_csv() + self.legend_handles = _legend_handles + self.legend_loc = _legend_loc + self.legend_box = _legend_box + self.legend_ncol = _legend_ncol + self.legend_fontsize = _legend_fontsize + self.remove_border = _remove_border + self.alignment = _alignment + self.yticks_rotation = _yticks_rotation + + def build_bar_graph_horizontal(self): + if self.color is None: + i = 0 + self.color = [] + for _ in self.data_set: + self.color.append(self.color_name[i]) + i = i + 1 + + fig_size, ax = plt.subplots(figsize=self.figsize, gridspec_kw=self.alignment) + i = 0 + # to remove the borders + if self.remove_border is not None: + for border in self.remove_border: + ax.spines[border].set_color(None) + if 'left' in self.remove_border: # to remove the y-axis labeling + yaxis_visable =False + else: + yaxis_visable=True + ax.yaxis.set_visible(yaxis_visable) + + def show_value(rectangles): + for rect in rectangles: + w = rect.get_width() + y = rect.get_y() + h = rect.get_height() + x = rect.get_x() + # adding 1 may not always work based on the x axis scale may need to be configurable + plt.text(w + 1 , rect.get_y() + rect.get_height() / 4., w, + ha='center', va='bottom', rotation=self.text_rotation, fontsize=self.text_font) + + for _ in self.data_set: + if i > 0: + br = br1 + br2 = [y + self.bar_height for y in br] + rects = plt.barh(br2, self.data_set[i], color=self.color[i], height=self.bar_height, + edgecolor=self.color_edge, label=self.label[i]) + if self.show_bar_value: + show_value(rects) + br1 = br2 + i = i + 1 + else: + br1 = np.arange(len(self.data_set[i])) + rects = plt.barh(br1, self.data_set[i], color=self.color[i], height=self.bar_height, + edgecolor=self.color_edge, label=self.label[i]) + if self.show_bar_value: + show_value(rects) + i = i + 1 + plt.xlabel(self.xaxis_name, fontweight='bold', fontsize=15) + plt.ylabel(self.yaxis_name, fontweight='bold', fontsize=15) + if self.yaxis_categories[0] == 0: + plt.yticks(np.arange(0, + len(self.yaxis_categories), + step=self.yaxis_step), + fontsize=self.yticks_font,rotation=self.yticks_rotation) + else: + plt.yticks([i + self._yaxis_value_location for i in np.arange(0, len(self.data_set[0]), step=self.yaxis_step)], + self.yaxis_categories, fontsize=self.yticks_font,rotation=self.yticks_rotation) + plt.legend( + handles=self.legend_handles, + loc=self.legend_loc, + bbox_to_anchor=self.legend_box, + ncol=self.legend_ncol, + fontsize=self.legend_fontsize) + plt.suptitle(self.title, fontsize=self.title_size) + plt.title(self.grp_title) + plt.gcf() + plt.savefig("%s.png" % self.graph_image_name, dpi=96) + plt.close() + logger.debug("{}.png".format(self.graph_image_name)) + if self.enable_csv: + if self.data_set is not None and self.yaxis_categories is not None: + if len(self.yaxis_categories) == len(self.data_set[0]): + self.lf_csv.columns = [] + self.lf_csv.rows = [] + self.lf_csv.columns.append(self.yaxis_name) + self.lf_csv.columns.extend(self.label) + self.lf_csv.rows.append(self.yaxis_categories) + self.lf_csv.rows.extend(self.data_set) + self.lf_csv.filename = f"{self.graph_image_name}.csv" + self.lf_csv.generate_csv() + else: + raise ValueError( + "Length and x-axis values and y-axis values should be same.") + else: + logger.debug("No Dataset Found") + logger.debug("{}.csv".format(self.graph_image_name)) + return "%s.png" % self.graph_image_name + + + + class lf_scatter_graph: def __init__(self, _x_data_set=None, @@ -254,7 +456,7 @@ class lf_scatter_graph: plt.legend(handles=scatter.legend_elements()[0], labels=self.label) plt.savefig("%s.png" % self.graph_image_name, dpi=96) plt.close() - print("{}.png".format(self.graph_image_name)) + logger.debug("{}.png".format(self.graph_image_name)) if self.enable_csv: self.lf_csv.columns = self.label self.lf_csv.rows = self.y_data_set @@ -263,8 +465,267 @@ class lf_scatter_graph: return "%s.png" % self.graph_image_name +# have a second yaxis with line graph +class lf_bar_line_graph: + def __init__(self, + _data_set1=None, + # Note data_set2, data_set2_poly and data_set2_spline needs same size list + _data_set2=None, + _data_set2_poly=[False], # Values are True or False + _data_set2_poly_degree=[3], + _data_set2_interp1d=[False], # Values are True or False + _xaxis_name="x-axis", + _y1axis_name="y1-axis", + _y2axis_name="y2-axis", + _xaxis_categories=None, + _xaxis_label=None, + _graph_title="", + _title_size=16, + _graph_image_name="image_name", + _label1=None, + _label2=None, + _label2_poly=None, + _label2_interp1d=None, + _color1=None, + _color2=None, + _color2_poly=None, + _color2_interp1d=None, + _bar_width=0.25, + _color_edge='grey', + _font_weight='bold', + _color_name1=None, + _color_name2=None, + _marker=None, + _figsize=(10, 5), + _show_bar_value=False, + _xaxis_step=1, + _xticks_font=None, + _xaxis_value_location=0, + _text_font=None, + _text_rotation=None, + _grp_title="", + _legend_handles=None, + _legend_loc1="best", + _legend_loc2="best", + _legend_box1=None, + _legend_box2=None, + _legend_ncol=1, + _legend_fontsize=None, + _dpi=96, + _enable_csv=False): + + if _data_set1 is None: + _data_set1 = [[30.4, 55.3, 69.2, 37.1], [45.1, 67.2, 34.3, 22.4], [22.5, 45.6, 12.7, 34.8]] + if _xaxis_categories is None: + _xaxis_categories = [1, 2, 3, 4] + if _xaxis_label is None: + _xaxis_label = ["a", "b", "c", "d"] + if _label1 is None: + _label1 = ["bi-downlink", "bi-uplink", 'uplink'] + if _label2 is None: + _label2 = ["bi-downlink", "bi-uplink", 'uplink'] + + if _color_name1 is None: + _color_name1 = ['lightcoral', 'darkgrey', 'r', 'g', 'b', 'y'] + if _color_name2 is None: + _color_name2 = ['lightcoral', 'darkgrey', 'r', 'g', 'b', 'y'] + self.data_set1 = _data_set1 + self.data_set2 = _data_set2 + self.data_set2_poly = _data_set2_poly + self.data_set2_poly_degree = _data_set2_poly_degree + self.data_set2_interp1d = _data_set2_interp1d + self.xaxis_name = _xaxis_name + self.y1axis_name = _y1axis_name + self.y2axis_name = _y2axis_name + self.xaxis_categories = _xaxis_categories + self.xaxis_label = _xaxis_label + self.title = _graph_title + self.title_size = _title_size + self.graph_image_name = _graph_image_name + self.label1 = _label1 + self.label2 = _label2 + self.label2_poly = _label2_poly + self.label2_interp1d = _label2_interp1d + self.color1 = _color1 + self.color2 = _color2 + self.color2_poly = _color2_poly + self.color2_interp1d = _color2_interp1d + self.marker = _marker + self.bar_width = _bar_width + self.color_edge = _color_edge + self.font_weight = _font_weight + self.color_name1 = _color_name1 + self.color_name2 = _color_name2 + self.figsize = _figsize + self.show_bar_value = _show_bar_value + self.xaxis_step = _xaxis_step + self.xticks_font = _xticks_font + self._xaxis_value_location = _xaxis_value_location + self.text_font = _text_font + self.text_rotation = _text_rotation + self.grp_title = _grp_title + self.enable_csv = _enable_csv + self.lf_csv = lf_csv() + self.legend_handles = _legend_handles + self.legend_loc1 = _legend_loc1 + self.legend_loc2 = _legend_loc2 + self.legend_box1 = _legend_box1 + self.legend_box2 = _legend_box2 + self.legend_ncol = _legend_ncol + self.legend_fontsize = _legend_fontsize + + def build_bar_line_graph(self): + if self.color1 is None: + i = 0 + self.color1 = [] + for _ in self.data_set1: + self.color1.append(self.color_name[i]) + i = i + 1 + + fig, ax1 = plt.subplots(figsize=self.figsize) + + ax2 = ax1.twinx() + + i = 0 + + def show_value(rectangles): + for rect in rectangles: + h = rect.get_height() + ax1.text(rect.get_x() + rect.get_width() / 2., h, h, + ha='center', va='bottom', rotation=self.text_rotation, fontsize=self.text_font) + + for _ in self.data_set1: + if i > 0: + br = br1 + br2 = [x + self.bar_width for x in br] + rects = ax1.bar(br2, self.data_set1[i], color=self.color1[i], width=self.bar_width, + edgecolor=self.color_edge, label=self.label1[i]) + if self.show_bar_value: + show_value(rects) + br1 = br2 + i = i + 1 + else: + br1 = np.arange(len(self.data_set1[i])) + rects = ax1.bar(br1, self.data_set1[i], color=self.color1[i], width=self.bar_width, + edgecolor=self.color_edge, label=self.label1[i]) + if self.show_bar_value: + show_value(rects) + i = i + 1 + ax1.set_xlabel(self.xaxis_name, fontweight='bold', fontsize=15) + ax1.set_ylabel(self.y1axis_name, fontweight='bold', fontsize=15) + if self.xaxis_categories[0] == 0: + xsteps = plt.xticks(np.arange(0, + len(self.xaxis_categories), + step=self.xaxis_step), + fontsize=self.xticks_font) + else: + xsteps = plt.xticks([i + self._xaxis_value_location for i in np.arange(0, len(self.data_set1[0]), step=self.xaxis_step)], + self.xaxis_categories, fontsize=self.xticks_font) + ax1.legend( + handles=self.legend_handles, + loc=self.legend_loc1, + bbox_to_anchor=self.legend_box1, + ncol=self.legend_ncol, + fontsize=self.legend_fontsize) + + + # overlay line graph + def show_value2(data): + for item, value in enumerate(data): + ax2.text(item, value, "{value}".format(value=value), ha='center',rotation=self.text_rotation, fontsize=self.text_font) + + i = 0 + for _ in self.data_set2: + br1 = np.arange(len(self.data_set2[i])) + ax2.plot( + br1, + self.data_set2[i], + color=self.color2[i], + label=self.label2[i], + marker=self.marker[i]) + show_value2(self.data_set2[i]) + # do polynomial smoothing + if self.data_set2_poly[i]: + poly = np.polyfit(br1,self.data_set2[i],self.data_set2_poly_degree[i]) + poly_y = np.poly1d(poly)(br1) + ax2.plot( + br1, + poly_y, + color=self.color2_poly[i], + label=self.label2_poly[i] + ) + if self.data_set2_interp1d[i]: + cubic_interpolation_model = interpolate.interp1d(br1, self.data_set2[i],kind="cubic") + + x_sm = np.array(br1) + x_smooth = np.linspace(x_sm.min(), x_sm.max(), 500) + y_smooth = cubic_interpolation_model(x_smooth) + ax2.plot( + x_smooth, + y_smooth, + color=self.color2_interp1d[i], + label=self.label2_interp1d[i] + ) + + i += 1 + ax2.set_xlabel(self.xaxis_name, fontweight='bold', fontsize=15) + ax2.set_ylabel(self.y2axis_name, fontweight='bold', fontsize=15) + ax2.tick_params(axis = 'y', labelcolor = 'orange') + + ax2.legend( + handles=self.legend_handles, + loc=self.legend_loc2, + bbox_to_anchor=self.legend_box2, + ncol=self.legend_ncol, + fontsize=self.legend_fontsize) + plt.suptitle(self.title, fontsize=self.title_size) + plt.title(self.grp_title) + plt.gcf() + plt.savefig("%s.png" % self.graph_image_name, dpi=96) + plt.close() + logger.debug("{}.png".format(self.graph_image_name)) + # TODO work though this for two axis + if self.enable_csv: + if self.data_set is not None and self.xaxis_categories is not None: + if len(self.xaxis_categories) == len(self.data_set[0]): + self.lf_csv.columns = [] + self.lf_csv.rows = [] + self.lf_csv.columns.append(self.xaxis_name) + self.lf_csv.columns.extend(self.label) + self.lf_csv.rows.append(self.xaxis_categories) + self.lf_csv.rows.extend(self.data_set) + self.lf_csv.filename = f"{self.graph_image_name}.csv" + self.lf_csv.generate_csv() + else: + raise ValueError( + "Length and x-axis values and y-axis values should be same.") + else: + logger.debug("No Dataset Found") + logger.debug("{}.csv".format(self.graph_image_name)) + return "%s.png" % self.graph_image_name + + class lf_stacked_graph: + """ + usage: This will generate a vertically stacked graph with list _data_set as well as with dictionary _data_set. + + example : + + For a graph with dictionary data_set + + obj = lf_stacked_graph(_data_set={'FCC0':0, 'FCC1':88.4,'FCC2':77.8,'FCC3':57.8,'FCC4':90.0,'FCC95':60.4,'FCC6':33.0}, + _xaxis_name="", _yaxis_name="", _enable_csv=False, _remove_border=True) + obj.build_stacked_graph() + + For a graph with list data_set + + obj = lf_stacked_graph(_data_set=[['FCC0', 'FCC1', 'FCC2', 'FCC3', 'FCC4', 'FCC95', 'FCC6'], + [0, 88.4, 77.8, 57.8, 90.0, 60.4, 33.0], + [100.0, 11.6, 22.2, 42.2, 10.0, 39.6, 67.0]]) + obj.build_stacked_graph() + + """ def __init__(self, _data_set=None, _xaxis_name="Stations", @@ -273,7 +734,17 @@ class lf_stacked_graph: _graph_image_name="image_name2", _color=None, _figsize=(9, 4), - _enable_csv=True): + _enable_csv=True, + _width=0.79, + _bar_text_color='white', + _bar_font_weight='bold', + _bar_font_size=8, + _legend_title="Issues", + _legend_bbox=(1.13, 1.01), + _legend_loc="upper right", + _remove_border=False, + _bar_text_rotation=0, + _x_ticklabels_rotation=0): if _data_set is None: _data_set = [[1, 2, 3, 4], [1, 1, 1, 1], [1, 1, 1, 1]] if _label is None: @@ -287,9 +758,19 @@ class lf_stacked_graph: self.color = _color self.enable_csv = _enable_csv self.lf_csv = lf_csv() + self.width = _width + self.bar_text_color = _bar_text_color + self.bar_font_weight = _bar_font_weight + self.bar_font_size = _bar_font_size + self.legend_title = _legend_title + self.legend_bbox = _legend_bbox + self.legend_loc = _legend_loc + self.remove_border = _remove_border + self.bar_text_rotation = _bar_text_rotation + self.x_ticklabels_rotation = _x_ticklabels_rotation def build_stacked_graph(self): - plt.subplots(figsize=self.figsize) + fig, axes_subplot = plt.subplots(figsize=self.figsize) if self.color is None: self.color = [ "darkred", @@ -298,22 +779,65 @@ class lf_stacked_graph: "skyblue", "indigo", "plum"] - plt.bar(self.data_set[0], self.data_set[1], color=self.color[0]) - plt.bar( - self.data_set[0], - self.data_set[2], - bottom=self.data_set[1], - color=self.color[1]) - if len(self.data_set) > 3: - for i in range(3, len(self.data_set)): - plt.bar(self.data_set[0], self.data_set[i], - bottom=np.array(self.data_set[i - 2]) + np.array(self.data_set[i - 1]), color=self.color[i - 1]) + if type(self.data_set) is list: + plt.bar(self.data_set[0], self.data_set[1], color=self.color[0]) + plt.bar( + self.data_set[0], + self.data_set[2], + bottom=self.data_set[1], + color=self.color[1]) + if len(self.data_set) > 3: + for i in range(3, len(self.data_set)): + plt.bar(self.data_set[0], self.data_set[i], + bottom=np.array(self.data_set[i - 2]) + np.array(self.data_set[i - 1]),color=self.color[i - 1]) + plt.legend(self.label) + elif type(self.data_set) is dict: + lable_values = [] + pass_values = [] + fail_values = [] + for i in self.data_set: + lable_values.append(i) + for j in self.data_set: + pass_values.append(self.data_set[j]) + fail_values.append(round(float(100.0 - self.data_set[j]), 1)) + + width = self.width + figure_size, axes_subplot = plt.subplots(figsize=self.figsize) + + # building vertical bar plot + bar_1 = plt.bar(lable_values, pass_values, width, color='green') + bar_2 = plt.bar(lable_values, fail_values, width, bottom=pass_values, color='red') + + # inserting bar text + if len(list(self.data_set.keys())) > 10: + self.bar_text_rotation = 90 + self.x_ticklabels_rotation = 90 + for i, v in enumerate(pass_values): + if v != 0: + plt.text(i + .005, v * 0.45, "%s%s" % (v, "%"), color=self.bar_text_color, + fontweight=self.bar_font_weight, + fontsize=self.bar_font_size, ha="center", va="center", rotation=self.bar_text_rotation) + for i, v in enumerate(fail_values): + if v != 0: + plt.text(i + .005, v * 0.45 + pass_values[i], "%s%s" % (v, "%"), color=self.bar_text_color, + fontweight=self.bar_font_weight, fontsize=self.bar_font_size, ha="center", va="center" , + rotation=self.bar_text_rotation) + plt.legend([bar_1, bar_2], self.label, title=self.legend_title, bbox_to_anchor=self.legend_bbox, + loc=self.legend_loc) + axes_subplot.set_xticks(list(self.data_set.keys())) + axes_subplot.set_xticklabels(list(self.data_set.keys()), rotation=self.x_ticklabels_rotation) + + # to remove the borders + if self.remove_border: + for border in ['top', 'right', 'left', 'bottom']: + axes_subplot.spines[border].set_visible(False) + axes_subplot.yaxis.set_visible(False) + plt.xlabel(self.xaxis_name) plt.ylabel(self.yaxis_name) - plt.legend(self.label) - plt.savefig("%s.png" % self.graph_image_name, dpi=96) + plt.savefig("%s.png" % self.graph_image_name, bbox_inches="tight", dpi=96) plt.close() - print("{}.png".format(self.graph_image_name)) + logger.debug("{}.png".format(self.graph_image_name)) if self.enable_csv: self.lf_csv.columns = self.label self.lf_csv.rows = self.data_set @@ -413,7 +937,7 @@ class lf_horizontal_stacked_graph: labelbottom=False) # disable x-axis plt.savefig("%s.png" % self.graph_image_name, dpi=96) plt.close() - print("{}.png".format(self.graph_image_name)) + logger.debug("{}.png".format(self.graph_image_name)) if self.enable_csv: self.lf_csv.columns = self.label self.lf_csv.rows = self.data_set @@ -430,7 +954,7 @@ class lf_line_graph: _xaxis_label=None, _graph_title="", _title_size=16, - _graph_image_name="image_name", + _graph_image_name="line_graph", _label=None, _font_weight='bold', _color=None, @@ -445,9 +969,12 @@ class lf_line_graph: _legend_fontsize=None, _marker=None, _dpi=96, - _enable_csv=False): + _grid=True, + _enable_csv=False, + _reverse_x=False, + _reverse_y=False): if _data_set is None: - _data_set = [[30.4, 55.3, 69.2, 37.1], [45.1, 67.2, 34.3, 22.4], [22.5, 45.6, 12.7, 34.8]] + _data_set = [[30.4, 55.3, 69.2, 37.1, 44.0], [45.1, 67.2, 34.3, 22.4, 37.6], [22.5, 45.6, 12.7, 34.8, 22.5]] if _xaxis_categories is None: _xaxis_categories = [1, 2, 3, 4, 5] if _xaxis_label is None: @@ -456,6 +983,9 @@ class lf_line_graph: _label = ["bi-downlink", "bi-uplink", 'uplink'] if _color is None: _color = ['forestgreen', 'c', 'r', 'g', 'b', 'p'] + if _marker is None: + _marker = ['s', 'o', 'v'] # available markers = '.', 'o', 'v', '<', 's', '*', 'p', 'P' + self.grid = _grid self.data_set = _data_set self.xaxis_name = _xaxis_name self.yaxis_name = _yaxis_name @@ -479,6 +1009,8 @@ class lf_line_graph: self.legend_box = _legend_box self.legend_ncol = _legend_ncol self.legend_fontsize = _legend_fontsize + self.reverse_x = _reverse_x + self.reverse_y = _reverse_y def build_line_graph(self): plt.subplots(figsize=self.figsize) @@ -489,11 +1021,13 @@ class lf_line_graph: data, color=self.color[i], label=self.label[i], - marker=self.marker) + marker=self.marker[i]) i += 1 plt.xlabel(self.xaxis_name, fontweight='bold', fontsize=15) plt.ylabel(self.yaxis_name, fontweight='bold', fontsize=15) + if self.grid: + plt.grid(True, linestyle=':') # available line styles = ':', '-', '--', '-.' plt.legend( handles=self.legend_handles, loc=self.legend_loc, @@ -501,10 +1035,14 @@ class lf_line_graph: ncol=self.legend_ncol, fontsize=self.legend_fontsize) plt.suptitle(self.grp_title, fontsize=self.title_size) + if self.reverse_y: + plt.gca().invert_yaxis() + if self.reverse_x: + plt.gca().invert_xaxis() plt.gcf() plt.savefig("%s.png" % self.graph_image_name, dpi=96) plt.close() - print("{}.png".format(self.graph_image_name)) + logger.debug("{}.png".format(self.graph_image_name)) if self.enable_csv: if self.data_set is not None: self.lf_csv.columns = self.label @@ -512,12 +1050,17 @@ class lf_line_graph: self.lf_csv.filename = f"{self.graph_image_name}.csv" self.lf_csv.generate_csv() else: - print("No Dataset Found") - print("{}.csv".format(self.graph_image_name)) + logger.debug("No Dataset Found") + logger.debug("{}.csv".format(self.graph_image_name)) return "%s.png" % self.graph_image_name def main(): + help_summary = '''\ + This script facilitates the generation of comprehensive graphical reports. It offers a variety of graph types, + including bar graphs, horizontal bar graphs, scatter graphs, bar-line graphs, stacked graphs, horizontal stacked + graphs, and line graphs. + ''' # arguments parser = argparse.ArgumentParser( prog='lf_graph.py', @@ -551,10 +1094,44 @@ INCLUDE_IN_README dest='lfmgr', help='sample argument: where LANforge GUI is running', default='localhost') + # logging configuration + parser.add_argument( + '--debug', + help='--debug this will enable debugging in py-json method', + action='store_true') + parser.add_argument('--log_level', + default=None, + help='Set logging level: debug | info | warning | error | critical') + + parser.add_argument( + "--lf_logger_config_json", + help="--lf_logger_config_json , json configuration of logger") + + parser.add_argument('--help_summary', help='Show summary of what this script does', default=None, + action="store_true") + # the args parser is not really used , this is so the report is not generated when testing # the imports with --help args = parser.parse_args() - print("LANforge manager {lfmgr}".format(lfmgr=args.lfmgr)) + + if args.help_summary: + print(help_summary) + exit(0) + + # set up logger + logger_config = lf_logger_config.lf_logger_config() + + # set the logger level to debug + if args.log_level: + logger_config.set_level(level=args.log_level) + + # lf_logger_config_json will take presidence to changing debug levels + if args.lf_logger_config_json: + # logger_config.lf_logger_config_json = "lf_logger_config.json" + logger_config.lf_logger_config_json = args.lf_logger_config_json + logger_config.load_lf_logger_config() + + logger.debug("LANforge manager {lfmgr}".format(lfmgr=args.lfmgr)) output_html_1 = "graph_1.html" output_pdf_1 = "graph_1.pdf" @@ -594,6 +1171,9 @@ INCLUDE_IN_README _label=["bi-downlink", "bi-uplink", 'uplink'], _color=None, _color_edge='red', + _show_bar_value=True, + _text_font=7, + _text_rotation=None, _enable_csv=True) graph_html_obj = """ @@ -604,6 +1184,10 @@ INCLUDE_IN_README test_file.write(graph_html_obj) test_file.close() + graph = lf_line_graph() + + graph.build_line_graph() + # write to pdf # write logic to generate pdf here # wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb @@ -612,6 +1196,49 @@ INCLUDE_IN_README options = {"enable-local-file-access": None} pdfkit.from_file(output_html_2, output_pdf_2, options=options) + # test build_bar_graph_horizontal with defaults + dataset = [[45, 67, 34, 22, 31, 52, 60, 71, 24, 25, 45, 67, 34, 22, 31, 52, 60, 71, 24, 25], [22, 45, 12, 34, 70, 80, 14, 35, 44, 45,22, 45, 12, 34, 70, 80, 14, 35, 44, 45 ], [30, 55, 69, 37, 77, 24, 25, 77, 77, 80, 30, 55, 69, 37, 77, 24, 25, 77, 77, 80]] + y_axis_values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + + # calculate the height of the y-axis .25 * number of values + y_fig_size = len(y_axis_values) * len(dataset) * .35 + x_fig_size = 10 + + output_html_3 = "graph_3.html" + output_pdf_3 = "graph_3.pdf" + + + + graph = lf_bar_graph_horizontal(_data_set=dataset, + _xaxis_name="Throughput 2 (Mbps)", + _yaxis_name="stations", + _yaxis_categories=y_axis_values, + _graph_image_name="Bi-single_radio_2.4GHz", + _label=["bi-downlink", "bi-uplink", 'uplink'], + _color=None, + _color_edge='red', + _figsize=(x_fig_size, y_fig_size), + _show_bar_value= True, + _text_font=6, + _text_rotation=True, + _enable_csv=True) + graph_html_obj = """ + +

+ """ + # + test_file = open(output_html_3, "w") + test_file.write(graph_html_obj) + test_file.close() + + # write to pdf + # write logic to generate pdf here + # wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb + # sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb + # prevent eerror Blocked access to file + options = {"enable-local-file-access": None} + pdfkit.from_file(output_html_3, output_pdf_3, options=options) + # Unit Test if __name__ == "__main__": diff --git a/py-scripts/lf_report.py b/py-scripts/lf_report.py index f46311b2..d60ea2c9 100755 --- a/py-scripts/lf_report.py +++ b/py-scripts/lf_report.py @@ -86,7 +86,10 @@ class lf_report: self.graph_title = _graph_title self.date = _date self.output_html = _output_html + if _output_html.lower().endswith(".pdf"): + raise ValueError("HTML output file cannot end with suffix '.pdf'") self.path_date_time = _path_date_time + self.report_location = "" # used by lf_check.py to know where to write the meta data "Report Location:::/home/lanforge/html-reports/wifi-capacity-2021-08-17-04-02-56" self.write_output_html = "" self.write_output_index_html = "" self.output_pdf = _output_pdf @@ -101,6 +104,8 @@ class lf_report: self.pdf_link_html = "" self.objective = _obj self.obj_title = _obj_title + self.description = "" + self.desc_title = "" # self.systeminfopath = "" self.date_time_directory = "" self.log_directory = "" @@ -132,9 +137,12 @@ class lf_report: shutil.copy(banner_src_file, banner_dst_file) def move_data(self, directory=None, _file_name=None, directory_name=None): - if directory_name is None: - _src_file = str(self.current_path) + '/' + str(_file_name) - _dst_file = str(self.path_date_time) + '/' + str(directory) + '/' + str(_file_name) + if directory_name is None: + _src_file = str(self.current_path) + '/' + str(_file_name) + if directory is None: + _dst_file = str(self.path_date_time) + else: + _dst_file = str(self.path_date_time) + '/' + str(directory) + '/' + str(_file_name) else: _src_file = str(self.current_path) + '/' + str(directory_name) _dst_file = str(self.path_date_time) + '/' + str(directory_name) @@ -251,6 +259,10 @@ class lf_report: fname, ext = os.path.splitext(_graph_title) self.csv_file_name = fname + ".csv" + def write_dataframe_to_csv(self, _index=False): + csv_file = "{path_date_time}/{csv_file_name}".format(path_date_time=self.path_date_time,csv_file_name=self.csv_file_name) + self.dataframe.to_csv(csv_file,index=_index) + # The _date is set when class is enstanciated / created so this set_date should be used with caution, used to synchronize results def set_date(self, _date): self.date = _date @@ -275,6 +287,10 @@ class lf_report: self.objective = _obj self.obj_title = _obj_title + def set_desc_html(self, _desc_title, _desc): + self.description = _desc + self.desc_title = _desc_title + def set_graph_image(self, _graph_image): self.graph_image = _graph_image @@ -305,8 +321,20 @@ class lf_report: output_file = str(self.path_date_time) + '/' + str(file) logger.info("output file {}".format(output_file)) return output_file + # Report Location:::/ as a key in lf_check.py + def write_report_location(self): + self.report_location = self.path_date_time + logger.info("Report Location:::{report_location}".format(report_location=self.report_location)) + def write_html(self): + if not self.output_html: + logger.info("no html file name, skipping report generation") + return + if self.output_html.lower().endswith(".pdf"): + raise ValueError("write_html: HTML filename [%s] should not end with .pdf" % self.output_html) + if self.write_output_html.endswith(".pdf"): + raise ValueError("wrong suffix for an HTML file: %s" % self.write_output_html) self.write_output_html = str(self.path_date_time) + '/' + str(self.output_html) logger.info("write_output_html: {}".format(self.write_output_html)) try: @@ -319,7 +347,13 @@ class lf_report: return self.write_output_html def write_index_html(self): - self.write_output_index_html = str(self.path_date_time) + '/' + str("index.html") + # LAN-1535 scripting: test_l3.py output masks other output when browsing. + # consider renaming index.html to readme.html + # self.write_output_index_html = str(self.path_date_time) + '/' + str("index.html") + if not self.output_html: + logger.info("no html file name, skipping report generation") + return + self.write_output_index_html = str(self.path_date_time) + '/' + str("readme.html") logger.info("write_output_index_html: {}".format(self.write_output_index_html)) try: test_file = open(self.write_output_index_html, "w") @@ -331,6 +365,11 @@ class lf_report: return self.write_output_index_html def write_html_with_timestamp(self): + if not self.output_html: + logger.info("no html file name, skipping report generation") + return + if self.output_html.lower().endswith(".pdf"): + raise ValueError("write_html_with_timestamp: will not save file with PDF suffix [%s]" % self.output_html) self.write_output_html = "{}/{}-{}".format(self.path_date_time, self.date, self.output_html) logger.info("write_output_html: {}".format(self.write_output_html)) try: @@ -349,7 +388,9 @@ class lf_report: # write logic to generate pdf here # wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb # sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb - + if not self.output_pdf: + logger.info("write_pdf: no pdf file name, skipping pdf output") + return options = {"enable-local-file-access": None, 'orientation': _orientation, 'page-size': _page_size} # prevent error Blocked access to file @@ -363,7 +404,9 @@ class lf_report: # write logic to generate pdf here # wget https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6-1/wkhtmltox_0.12.6-1.focal_amd64.deb # sudo apt install ./wkhtmltox_0.12.6-1.focal_amd64.deb - + if not self.output_pdf: + logger.info("write_pdf_with_timestamp: no pdf file name, skipping pdf output") + return options = {"enable-local-file-access": None, 'orientation': _orientation, 'page-size': _page_size} # prevent error Blocked access to file @@ -374,6 +417,14 @@ class lf_report: pdf_link_path = "{}/{}-{}".format(self.path_date_time, self.date, self.output_pdf) return pdf_link_path + # used for relative pathing + def get_pdf_file(self): + if not self.output_pdf: + logger.info("get_pdf_file: no pdf name, returning None") + return None + pdf_file = "{}-{}".format(self.date, self.output_pdf) + return pdf_file + def build_pdf_link(self, _pdf_link_name, _pdf_link_path): self.pdf_link_html = """ @@ -392,7 +443,8 @@ class lf_report: def generate_report(self): self.write_html() - self.write_pdf() + if self.output_pdf: + self.write_pdf() def build_all(self): self.build_banner() @@ -462,6 +514,32 @@ class lf_report: ) self.html += self.banner_html + def build_banner_left_h2_font(self): + # NOTE: {{ }} are the ESCAPED curly braces + # JBR suggests rename method to start_html_doc() + # This method violates DRY, if the ID of the body/div#BannerBack/div element is actually necessary + # to specify, this needs to be made a parameter for build_banner() or start_html_doc() + self.banner_html = """ + + {head_tag} + +
+
+ +
+

{title}

+

{date}

+
+
+
+ """.format( + head_tag=self.get_html_head(title=self.title), + title=self.title, + date=self.date, + ) + self.html += self.banner_html + + def build_table_title(self): self.table_title_html = """ @@ -483,6 +561,14 @@ class lf_report: """.format(text=self.text) self.html += self.text_html + def build_text_simple(self): + # please do not use 'style=' tags unless you cannot override a class + self.text_html = """ +

{text}

+ """.format(text=self.text) + self.html += self.text_html + + def build_date_time(self): self.date_time = str(datetime.datetime.now().strftime("%Y-%m-%d-%H-h-%m-m-%S-s")).replace(':', '-') return self.date_time @@ -530,10 +616,10 @@ class lf_report: plt.savefig(str(self.path_date_time) + '/pie-chart.png') def save_bar_chart(self, xlabel, ylabel, bar_chart_data, name): - plot = bar_chart_data.plot.bar(alpha=0.9, rot=0, width=0.9, linewidth=0.9) + plot = bar_chart_data.plot.bar(alpha=0.9, rot=0, width=0.9, linewidth=0.9, figsize=(10, 6)) plot.legend(bbox_to_anchor=(1.0, 1.0)) - plot.spines['right'].set_visible(False) - plot.spines['top'].set_visible(False) + # plot.spines['right'].set_visible(False) + # plot.spines['top'].set_visible(False) # plot.set_title(name) for p in plot.patches: height = p.get_height() @@ -545,7 +631,7 @@ class lf_report: annotation_clip=False, ha='center', va='bottom') # plt.xlabel(xlabel) - plt.xticks(rotation=45, horizontalalignment='right', fontweight='light', fontsize='small', ) + plt.xticks(rotation=0, horizontalalignment='right', fontweight='light', fontsize='small', ) plt.ylabel(ylabel) plt.tight_layout() plt.savefig(str(self.path_date_time) + '/' + name + '.png') @@ -647,6 +733,16 @@ function copyTextToClipboard(ele) { objective=self.objective) self.html += self.obj_html + def build_description(self): + self.obj_html = """ + +

{title}

+

{description}

+ """.format(title=self.desc_title, + description=self.description) + self.html += self.obj_html + + def build_graph_title(self): self.table_graph_html = """
@@ -660,6 +756,12 @@ function copyTextToClipboard(ele) { """.format(image=self.graph_image) self.html += self.graph_html_obj + def build_graph_without_border(self): + self.graph_html_obj = """ + + """.format(image=self.graph_image) + self.html += self.graph_html_obj + def end_content_div(self): self.html += "\n
\n" @@ -676,9 +778,45 @@ function copyTextToClipboard(ele) { """.format(image=name) self.html += self.chart_html_obj + def build_chart_custom(self, name, align='center',padding='15px',margin='5px 5px 2em 5px',width='500px',height='500px'): + + self.chart_html_obj = """ + """.format(image=name,align=align,padding=padding,margin=margin,width=width, height=height) + self.html += self.chart_html_obj + + def build_banner_cover(self): + # NOTE: {{ }} are the ESCAPED curly braces + # JBR suggests rename method to start_html_doc() + # This method violates DRY, if the ID of the body/div#BannerBack/div element is actually necessary + # to specify, this needs to be made a parameter for build_banner() or start_html_doc() + self.banner_html = """ + + {head_tag} + +
+
+ +
+

{title}

+

{date}

+
+
+
+ """.format( + head_tag=self.get_html_head(title=self.title), + title=self.title, + date=self.date, + ) + self.html += self.banner_html # Unit Test if __name__ == "__main__": + help_summary = '''\ + This script is designed to generate reports in file formats such as PDF and HTML, accommodating various user + preferences. The reports can encompass a range of elements, including graphs, tables, and customizable objectives, + tailored to meet specific user requirements + ''' parser = argparse.ArgumentParser( prog="lf_report.py", formatter_class=argparse.RawTextHelpFormatter, @@ -686,7 +824,15 @@ if __name__ == "__main__": parser.add_argument('--lfmgr', help='sample argument: where LANforge GUI is running', default='localhost') # the args parser is not really used , this is so the report is not generated when testing # the imports with --help + parser.add_argument('--help_summary', help='Show summary of what this script does', default=None, + action="store_true") args = parser.parse_args() + + # help summary + if args.help_summary: + print(help_summary) + exit(0) + logger.info("LANforge manager {lfmgr}".format(lfmgr=args.lfmgr)) # Testing: generate data frame @@ -723,6 +869,12 @@ if __name__ == "__main__": report.set_table_dataframe(dataframe2) report.build_table() + report.build_chart_title('default width') + report.build_chart("banner.png") + + report.build_chart_title('custom width') + report.build_chart_custom(name="banner.png",width="1000") + # report.build_all() # report.build_footer() report.build_footer_no_png()