From ed9b7b1b66db129192df244ceac386580bf27f29 Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sat, 18 Feb 2023 08:59:26 +0100 Subject: [PATCH 01/40] added Ishikawa plot in response to issue #25222 add organizational charts to supported plots --- examples/specialty_plots/ishikawa.py | 236 +++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 examples/specialty_plots/ishikawa.py diff --git a/examples/specialty_plots/ishikawa.py b/examples/specialty_plots/ishikawa.py new file mode 100644 index 000000000000..5e052dd8fe30 --- /dev/null +++ b/examples/specialty_plots/ishikawa.py @@ -0,0 +1,236 @@ +# ============================================================================= +# Importing +# ============================================================================= +# Python Libraries +from typing import List + +import numpy as np + +from pathlib import Path + +import matplotlib.pyplot as plt +import matplotlib + +# ============================================================================= +# AUX FUNCTIONS +# ============================================================================= + +def drawSpines(data:dict,ax:plt.Axes,parentSpine:None,alpha_ps:float=60.0,alpha_ss:float=0,primary_spines_rel_xpos: List[float] = [np.nan], + spine_color:str="black",recLevel:int=1,MAX_RECURSION_LEVEL:int=4,DEBUG:bool=False): + ''' + Draw an Ishikawa spine with recursion + + Parameters + ---------- + data : dict + dictionary structure containing problem, causes... + ax : plt.Axes + Matplotlib current axes + parentSpine : None + Parent spine object + alpha_ps : float, optional + First spine angle using during recursion. The default is 60.0. + alpha_ss : float, optional + Secondary spine angle using during recursion. The default is 0. + primary_spines_rel_xpos : List[float], optional + Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. + spine_color : str, optional + Spine color. The default is "black". + recLevel : int, optional + Current recursion level. The default is 1. + MAX_RECURSION_LEVEL : int, optional + Maximum recursion level set. The default is 4. + DEBUG : bool, optional + Print debug variables. The default is False. + + Raises + ------ + AttributeError + Maximum recursion level reached or passed a bad parentSpine object format. + + Returns + ------- + None. + + ''' + #stop recursion if maximum level reached + if recLevel > MAX_RECURSION_LEVEL: + raise AttributeError('Max Recursion Level Reached') + + + if isinstance(data, dict): + # switch to correct angle depending on recursion level + if recLevel % 2 != 0: + alpha = alpha_ps + else: + alpha = alpha_ss + + if isinstance(parentSpine,matplotlib.lines.Line2D): + #calculate parent data + ([xpb,xpe],[ypb,ype]) = parentSpine.get_data() + elif isinstance(parentSpine, matplotlib.text.Annotation): + xpb,ypb = parentSpine.xy + xpe,ype = parentSpine._x, parentSpine._y#parentSpine._arrow_relpos + else: + raise AttributeError('Wrong Spine Graphical Element') + + plen = np.sqrt((xpe-xpb)**2+(ype-ypb)**2) + palpha = np.round(np.degrees(np.arctan2(ype-ypb,xpe-xpb))) + + + # calculate spacing + pairs = len(list(data.keys())) // 2 + len(list(data.keys())) % 2 +1 #calculate couple pairs, at least 1 pair to start at middle branch + spacing = (plen) / pairs + + # checking of main spines + + + spine_count=0 + s=spacing + #draw spine + for k in data.keys(): + # calculate arrow position in the graph + if recLevel==1: #fix primary spines spacing + if len(primary_spines_rel_xpos)==len(list(data.keys())): #check of list of float is already covered in function def + x_end = primary_spines_rel_xpos[spine_count] + else: + x_end = xpe-(s/1.5)*np.cos(np.radians(palpha)) + y_end = ype-s*np.sin(np.radians(palpha)) + + x_start = x_end -s*np.cos(np.radians(alpha)) + y_start = y_end -s*np.sin(np.radians(alpha)) + else: + x_end = xpe-s*np.cos(np.radians(palpha)) + y_end = ype-s*np.sin(np.radians(palpha)) + + x_start = x_end -s*np.cos(np.radians(alpha)) + y_start = y_end -s*np.sin(np.radians(alpha)) + if DEBUG == True: + print(f'k: {k}, x_start: {x_start}, y_start: {y_start}, x_end: {x_end}, y_end: {y_end}, alpha: {alpha} \n recLevel: {recLevel}, s:{s}, plen:{plen}, palpha:{palpha}') + + + # draw arrow arc + if recLevel==1: + props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) + spine = ax.annotate(str.upper(k), xy=(x_end, y_end), xytext=(x_start, y_start),arrowprops=dict(arrowstyle="->",facecolor=spine_color),bbox=props,weight='bold') + else: + props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) + spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start),arrowprops=dict(arrowstyle="->",facecolor=spine_color),bbox=props) + # Call recursion to draw subspines + drawSpines(data=data[k],ax=ax, + parentSpine=spine,alpha_ps=alpha_ps,alpha_ss=alpha_ss,spine_color=spine_color, + recLevel=recLevel+1,MAX_RECURSION_LEVEL=MAX_RECURSION_LEVEL) #no primary_spines_rel_xpos is needed to be passed on recursion + + # next spine settings - same level + alpha *= -1 + spine_count += 1 + if spine_count %2 ==0: + s= s+spacing + return None + +def ishikawaplot(data,figSize=(20,10),left_margin:float=0.05,right_margin:float=0.05, alpha_ps:float=60.0,alpha_ss= 0.0, primary_spines_rel_xpos: List[float] = [np.nan], + pd_width:int=0.1,spine_color:str="black") -> plt.figure: + ''' + + + Parameters + ---------- + data : TYPE + DESCRIPTION. + figSize : TYPE, optional + Matplotlib figure size. The default is (20,10). + left_margin : float, optional + Left spacing from frame border. The default is 0.05. + right_margin : float, optional + Right spacing from frame border. The default is 0.05. + alpha_ps : float, optional + First spine angle using during recursion. The default is 60.0. + alpha_ss : TYPE, optional + Secondary spine angle using during recursion. The default is 0.0. + primary_spines_rel_xpos : list, optional + Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. + pd_width : int, optional + Problem description box relative width. The default is 0.1. + spine_color : str, optional + Spine color. The default is "black". + + Returns + ------- + fig : matplotlib.pyplot.figure + Figure object containing the Ishikawa plot + + ''' + + fig = plt.figure(figsize=figSize) + ax = fig.gca() + + #format axis + ax.set_xlim(0,1.0) + ax.set_ylim(0,1.0) + ax.axis('off') + + + #draw main spine + mainSpine = ax.axhline(y=0.5,xmin=left_margin,xmax=1-right_margin-pd_width,color=spine_color) + + #draw fish head + props = dict(boxstyle='round', facecolor='wheat', alpha=1.0) + # add problem tag + ax.text((1-right_margin-pd_width), 0.5, str.upper(list(data.keys())[0]), fontsize=12,weight='bold',verticalalignment='center',horizontalalignment='left', bbox=props) + + #draw fish tail + x = (0.0,0.0,left_margin) + y = (0.5-left_margin,0.5+left_margin,0.5) + ax.fill(x,y,color=spine_color) + + #draw spines with recursion + drawSpines(data=data[list(data.keys())[0]],ax=ax,parentSpine=mainSpine,alpha_ps = alpha_ps, alpha_ss = alpha_ss, spine_color=spine_color,primary_spines_rel_xpos=primary_spines_rel_xpos) + + + plt.tight_layout() + return fig + +# ============================================================================= +# Rooting +# ============================================================================= +current_dir = Path(__file__).parent +output_path = current_dir / "Ishikawa.jpeg" + + +# ============================================================================= +# USER DATA +# ============================================================================= +data = {'problem': {'machine': {'cause1':''}, + 'process': {'cause2': {'subcause1':'','subcause2':''} + }, + 'man': {'cause3': {'subcause3':'','subcause4':''} + }, + 'design': {'cause4': {'subcause5':'','subcause6':''} + } + + } + } + + +# ============================================================================= +# MAIN CALL +# ============================================================================= +if __name__ == '__main__': + + #try also without opt primary_spines rel_xpos + fig = ishikawaplot(data,figSize=(20,10),primary_spines_rel_xpos=[0.8,0.7,0.6,0.4]) + + fig.show() + + fig.savefig(output_path, + #dpi=800, + format=None, + #metadata=None, + bbox_inches=None, + pad_inches=0.0, + facecolor='auto', + edgecolor='auto', + orientation='landscape', + transparent=False, + backend=None) + \ No newline at end of file From 963663df3ee4c62c19d588be837821805f29822c Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sun, 19 Feb 2023 11:12:49 +0100 Subject: [PATCH 02/40] fixing code style for flake8 first trial --- examples/specialty_plots/ishikawa.py | 257 +++++++++++++-------------- 1 file changed, 126 insertions(+), 131 deletions(-) diff --git a/examples/specialty_plots/ishikawa.py b/examples/specialty_plots/ishikawa.py index 5e052dd8fe30..f3b096ba6530 100644 --- a/examples/specialty_plots/ishikawa.py +++ b/examples/specialty_plots/ishikawa.py @@ -1,23 +1,23 @@ -# ============================================================================= -# Importing -# ============================================================================= -# Python Libraries -from typing import List +""" +=============== +Ishikawa Diagrams +=============== -import numpy as np +Ishikawa Diagrams are useful for visualizing the effect to many cause relationships -from pathlib import Path +""" +from typing import List +import numpy as np +from pathlib import Path import matplotlib.pyplot as plt import matplotlib -# ============================================================================= -# AUX FUNCTIONS -# ============================================================================= -def drawSpines(data:dict,ax:plt.Axes,parentSpine:None,alpha_ps:float=60.0,alpha_ss:float=0,primary_spines_rel_xpos: List[float] = [np.nan], - spine_color:str="black",recLevel:int=1,MAX_RECURSION_LEVEL:int=4,DEBUG:bool=False): - ''' +def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60.0, alpha_ss: float = 0, + primary_spines_rel_xpos: List[float] = [np.nan], + spine_color: str = "black", reclevel: int = 1, max_recursion_level: int = 4, debug: bool = False): + """ Draw an Ishikawa spine with recursion Parameters @@ -26,7 +26,7 @@ def drawSpines(data:dict,ax:plt.Axes,parentSpine:None,alpha_ps:float=60.0,alpha_ dictionary structure containing problem, causes... ax : plt.Axes Matplotlib current axes - parentSpine : None + parentspine : None Parent spine object alpha_ps : float, optional First spine angle using during recursion. The default is 60.0. @@ -36,108 +36,108 @@ def drawSpines(data:dict,ax:plt.Axes,parentSpine:None,alpha_ps:float=60.0,alpha_ Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. spine_color : str, optional Spine color. The default is "black". - recLevel : int, optional + reclevel : int, optional Current recursion level. The default is 1. - MAX_RECURSION_LEVEL : int, optional + max_recursion_level : int, optional Maximum recursion level set. The default is 4. - DEBUG : bool, optional + debug : bool, optional Print debug variables. The default is False. Raises ------ AttributeError - Maximum recursion level reached or passed a bad parentSpine object format. + Maximum recursion level reached or passed a bad parentspine object format. Returns ------- None. - ''' - #stop recursion if maximum level reached - if recLevel > MAX_RECURSION_LEVEL: + """ + # stop recursion if maximum level reached + if reclevel > max_recursion_level: raise AttributeError('Max Recursion Level Reached') - if isinstance(data, dict): # switch to correct angle depending on recursion level - if recLevel % 2 != 0: + if reclevel % 2 != 0: alpha = alpha_ps else: alpha = alpha_ss - - if isinstance(parentSpine,matplotlib.lines.Line2D): - #calculate parent data - ([xpb,xpe],[ypb,ype]) = parentSpine.get_data() - elif isinstance(parentSpine, matplotlib.text.Annotation): - xpb,ypb = parentSpine.xy - xpe,ype = parentSpine._x, parentSpine._y#parentSpine._arrow_relpos + + if isinstance(parentspine, matplotlib.lines.Line2D): + # calculate parent data + ([xpb, xpe], [ypb, ype]) = parentspine.get_data() + elif isinstance(parentspine, matplotlib.text.Annotation): + xpb, ypb = parentspine.xy + xpe, ype = parentspine._x, parentspine._y # parentspine._arrow_relpos else: raise AttributeError('Wrong Spine Graphical Element') - - plen = np.sqrt((xpe-xpb)**2+(ype-ypb)**2) - palpha = np.round(np.degrees(np.arctan2(ype-ypb,xpe-xpb))) - - - # calculate spacing - pairs = len(list(data.keys())) // 2 + len(list(data.keys())) % 2 +1 #calculate couple pairs, at least 1 pair to start at middle branch - spacing = (plen) / pairs - - # checking of main spines - - - spine_count=0 - s=spacing - #draw spine + + plen = np.sqrt((xpe - xpb) ** 2 + (ype - ypb) ** 2) + palpha = np.round(np.degrees(np.arctan2(ype - ypb, xpe - xpb))) + + # calculate spine spacing + # calculate couple pairs, at least 1 pair to start at middle branch + pairs = len(list(data.keys())) // 2 + len(list(data.keys())) % 2 + 1 + spacing = plen / pairs + + spine_count = 0 + s = spacing + # draw spine for k in data.keys(): # calculate arrow position in the graph - if recLevel==1: #fix primary spines spacing - if len(primary_spines_rel_xpos)==len(list(data.keys())): #check of list of float is already covered in function def + # fix primary spines spacing + if reclevel == 1: + if len(primary_spines_rel_xpos) == len(list(data.keys())): x_end = primary_spines_rel_xpos[spine_count] else: - x_end = xpe-(s/1.5)*np.cos(np.radians(palpha)) - y_end = ype-s*np.sin(np.radians(palpha)) - - x_start = x_end -s*np.cos(np.radians(alpha)) - y_start = y_end -s*np.sin(np.radians(alpha)) + x_end = xpe - (s / 1.5) * np.cos(np.radians(palpha)) + y_end = ype - s * np.sin(np.radians(palpha)) + + x_start = x_end - s * np.cos(np.radians(alpha)) + y_start = y_end - s * np.sin(np.radians(alpha)) else: - x_end = xpe-s*np.cos(np.radians(palpha)) - y_end = ype-s*np.sin(np.radians(palpha)) - - x_start = x_end -s*np.cos(np.radians(alpha)) - y_start = y_end -s*np.sin(np.radians(alpha)) - if DEBUG == True: - print(f'k: {k}, x_start: {x_start}, y_start: {y_start}, x_end: {x_end}, y_end: {y_end}, alpha: {alpha} \n recLevel: {recLevel}, s:{s}, plen:{plen}, palpha:{palpha}') - - + x_end = xpe - s * np.cos(np.radians(palpha)) + y_end = ype - s * np.sin(np.radians(palpha)) + + x_start = x_end - s * np.cos(np.radians(alpha)) + y_start = y_end - s * np.sin(np.radians(alpha)) + if debug: + print( + f'k: {k}, x_start: {x_start}, y_start: {y_start}, x_end: {x_end}, y_end: {y_end}, alpha: {alpha} \n reclevel: {reclevel}, s:{s}, plen:{plen}, palpha:{palpha}') + # draw arrow arc - if recLevel==1: + if reclevel == 1: props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) - spine = ax.annotate(str.upper(k), xy=(x_end, y_end), xytext=(x_start, y_start),arrowprops=dict(arrowstyle="->",facecolor=spine_color),bbox=props,weight='bold') + spine = ax.annotate(str.upper(k), xy=(x_end, y_end), xytext=(x_start, y_start), + arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props, weight='bold') else: props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) - spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start),arrowprops=dict(arrowstyle="->",facecolor=spine_color),bbox=props) + spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start), + arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props) # Call recursion to draw subspines - drawSpines(data=data[k],ax=ax, - parentSpine=spine,alpha_ps=alpha_ps,alpha_ss=alpha_ss,spine_color=spine_color, - recLevel=recLevel+1,MAX_RECURSION_LEVEL=MAX_RECURSION_LEVEL) #no primary_spines_rel_xpos is needed to be passed on recursion - + drawspines(data=data[k], ax=ax, parentspine=spine, alpha_ps=alpha_ps, alpha_ss=alpha_ss, + spine_color=spine_color, reclevel=reclevel + 1, + max_recursion_level=max_recursion_level) + # no primary_spines_rel_xpos is needed to be passed on recursion # next spine settings - same level alpha *= -1 spine_count += 1 - if spine_count %2 ==0: - s= s+spacing + if spine_count % 2 == 0: + s = s + spacing return None -def ishikawaplot(data,figSize=(20,10),left_margin:float=0.05,right_margin:float=0.05, alpha_ps:float=60.0,alpha_ss= 0.0, primary_spines_rel_xpos: List[float] = [np.nan], - pd_width:int=0.1,spine_color:str="black") -> plt.figure: - ''' - + +def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin: float = 0.05, alpha_ps: float = 60.0, + alpha_ss=0.0, primary_spines_rel_xpos: List[float] = [np.nan], + pd_width: int = 0.1, spine_color: str = "black") -> plt.figure: + """ Parameters ---------- data : TYPE DESCRIPTION. - figSize : TYPE, optional + figsize : TYPE, optional Matplotlib figure size. The default is (20,10). left_margin : float, optional Left spacing from frame border. The default is 0.05. @@ -159,78 +159,73 @@ def ishikawaplot(data,figSize=(20,10),left_margin:float=0.05,right_margin:float= fig : matplotlib.pyplot.figure Figure object containing the Ishikawa plot - ''' - - fig = plt.figure(figsize=figSize) - ax = fig.gca() - - #format axis - ax.set_xlim(0,1.0) - ax.set_ylim(0,1.0) + """ + + fig = plt.figure(figsize=figsize) + ax = fig.gca() + + # format axis + ax.set_xlim(0, 1.0) + ax.set_ylim(0, 1.0) ax.axis('off') - - #draw main spine - mainSpine = ax.axhline(y=0.5,xmin=left_margin,xmax=1-right_margin-pd_width,color=spine_color) - - #draw fish head + # draw main spine + main_spine = ax.axhline(y=0.5, xmin=left_margin, xmax=1 - right_margin - pd_width, color=spine_color) + + # draw fish head props = dict(boxstyle='round', facecolor='wheat', alpha=1.0) # add problem tag - ax.text((1-right_margin-pd_width), 0.5, str.upper(list(data.keys())[0]), fontsize=12,weight='bold',verticalalignment='center',horizontalalignment='left', bbox=props) - - #draw fish tail - x = (0.0,0.0,left_margin) - y = (0.5-left_margin,0.5+left_margin,0.5) - ax.fill(x,y,color=spine_color) - - #draw spines with recursion - drawSpines(data=data[list(data.keys())[0]],ax=ax,parentSpine=mainSpine,alpha_ps = alpha_ps, alpha_ss = alpha_ss, spine_color=spine_color,primary_spines_rel_xpos=primary_spines_rel_xpos) - - - plt.tight_layout() + ax.text((1 - right_margin - pd_width), 0.5, str.upper(list(data.keys())[0]), fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='left', bbox=props) + + # draw fish tail + x = (0.0, 0.0, left_margin) + y = (0.5 - left_margin, 0.5 + left_margin, 0.5) + ax.fill(x, y, color=spine_color) + + # draw spines with recursion + drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, alpha_ps=alpha_ps, alpha_ss=alpha_ss, + primary_spines_rel_xpos=primary_spines_rel_xpos, spine_color=spine_color) + + plt.tight_layout() return fig + # ============================================================================= # Rooting # ============================================================================= current_dir = Path(__file__).parent output_path = current_dir / "Ishikawa.jpeg" - # ============================================================================= # USER DATA # ============================================================================= -data = {'problem': {'machine': {'cause1':''}, - 'process': {'cause2': {'subcause1':'','subcause2':''} - }, - 'man': {'cause3': {'subcause3':'','subcause4':''} - }, - 'design': {'cause4': {'subcause5':'','subcause6':''} - } - - } - } +data = {'problem': {'machine': {'cause1': ''}, + 'process': {'cause2': {'subcause1': '', 'subcause2': ''} + }, + 'man': {'cause3': {'subcause3': '', 'subcause4': ''} + }, + 'design': {'cause4': {'subcause5': '', 'subcause6': ''} + } + + } + } -# ============================================================================= -# MAIN CALL -# ============================================================================= if __name__ == '__main__': - - #try also without opt primary_spines rel_xpos - fig = ishikawaplot(data,figSize=(20,10),primary_spines_rel_xpos=[0.8,0.7,0.6,0.4]) - + # try also without opt primary_spines rel_xpos + fig = ishikawaplot(data, figsize=(20, 10), primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) + fig.show() - - fig.savefig(output_path, - #dpi=800, - format=None, - #metadata=None, - bbox_inches=None, - pad_inches=0.0, - facecolor='auto', - edgecolor='auto', - orientation='landscape', - transparent=False, - backend=None) - \ No newline at end of file + + fig.savefig(output_path, + # dpi=800, + format=None, + # metadata=None, + bbox_inches=None, + pad_inches=0.0, + facecolor='auto', + edgecolor='auto', + orientation='landscape', + transparent=False, + backend=None) From 2ef7f46257286df1547e71b72fe490af0ba9da74 Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sun, 19 Feb 2023 11:30:45 +0100 Subject: [PATCH 03/40] second fixing style typo --- examples/specialty_plots/ishikawa.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/specialty_plots/ishikawa.py b/examples/specialty_plots/ishikawa.py index f3b096ba6530..57e07498398b 100644 --- a/examples/specialty_plots/ishikawa.py +++ b/examples/specialty_plots/ishikawa.py @@ -191,15 +191,12 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin return fig -# ============================================================================= # Rooting -# ============================================================================= current_dir = Path(__file__).parent output_path = current_dir / "Ishikawa.jpeg" -# ============================================================================= + # USER DATA -# ============================================================================= data = {'problem': {'machine': {'cause1': ''}, 'process': {'cause2': {'subcause1': '', 'subcause2': ''} }, From 2018c75bc256ebb13e99f6ead09c75bb798c7c9e Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sun, 19 Feb 2023 11:48:06 +0100 Subject: [PATCH 04/40] fix flake8 configuration --- .flake8 | 1 + 1 file changed, 1 insertion(+) diff --git a/.flake8 b/.flake8 index 3fba9604fab8..357e1c2f57ad 100644 --- a/.flake8 +++ b/.flake8 @@ -92,6 +92,7 @@ per-file-ignores = examples/lines_bars_and_markers/marker_reference.py: E402 examples/misc/print_stdout_sgskip.py: E402 examples/misc/table_demo.py: E201 + examples/specialty_plots/ishikawa.py: E501 examples/style_sheets/bmh.py: E501 examples/style_sheets/plot_solarizedlight2.py: E501 examples/subplots_axes_and_figures/demo_constrained_layout.py: E402 From 8147bf4edb565a7ab14fabae590a3faead81451f Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Tue, 21 Feb 2023 18:18:24 +0100 Subject: [PATCH 05/40] - revised comment style according to flake8 set rules - improved example description docstring - removed __main__ call - debug print converted into logging.debug strings - removed sohw fig and save fig and related Path library - removed exception from flake8 conf file --- .flake8 | 1 - examples/specialty_plots/ishikawa.py | 88 +++++++++++++++------------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/.flake8 b/.flake8 index 357e1c2f57ad..3fba9604fab8 100644 --- a/.flake8 +++ b/.flake8 @@ -92,7 +92,6 @@ per-file-ignores = examples/lines_bars_and_markers/marker_reference.py: E402 examples/misc/print_stdout_sgskip.py: E402 examples/misc/table_demo.py: E201 - examples/specialty_plots/ishikawa.py: E501 examples/style_sheets/bmh.py: E501 examples/style_sheets/plot_solarizedlight2.py: E501 examples/subplots_axes_and_figures/demo_constrained_layout.py: E402 diff --git a/examples/specialty_plots/ishikawa.py b/examples/specialty_plots/ishikawa.py index 57e07498398b..ce0826850803 100644 --- a/examples/specialty_plots/ishikawa.py +++ b/examples/specialty_plots/ishikawa.py @@ -3,20 +3,26 @@ Ishikawa Diagrams =============== -Ishikawa Diagrams are useful for visualizing the effect to many cause relationships - +Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect +diagrams are useful for visualizing the effect to many cause relationships """ +import logging from typing import List -import numpy as np -from pathlib import Path + import matplotlib.pyplot as plt +import numpy as np + import matplotlib +_log = logging.getLogger(__name__) + -def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60.0, alpha_ss: float = 0, +def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60.0, + alpha_ss: float = 0, primary_spines_rel_xpos: List[float] = [np.nan], - spine_color: str = "black", reclevel: int = 1, max_recursion_level: int = 4, debug: bool = False): + spine_color: str = "black", reclevel: int = 1, + max_recursion_level: int = 4, debug: bool = False): """ Draw an Ishikawa spine with recursion @@ -33,7 +39,8 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 alpha_ss : float, optional Secondary spine angle using during recursion. The default is 0. primary_spines_rel_xpos : List[float], optional - Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. + Relative X-position of primary spines, a list of float is accepted. + The default is [np.nan]. spine_color : str, optional Spine color. The default is "black". reclevel : int, optional @@ -103,20 +110,29 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 x_start = x_end - s * np.cos(np.radians(alpha)) y_start = y_end - s * np.sin(np.radians(alpha)) if debug: - print( - f'k: {k}, x_start: {x_start}, y_start: {y_start}, x_end: {x_end}, y_end: {y_end}, alpha: {alpha} \n reclevel: {reclevel}, s:{s}, plen:{plen}, palpha:{palpha}') + _log.debug( + f'k: {k}, x_start: {x_start}, y_start: {y_start}, ' + f'x_end: {x_end} y_end: {y_end}, alpha: {alpha} \n ' + f'reclevel: {reclevel}, ' + f's:{s}, plen:{plen}, palpha:{palpha}') # draw arrow arc if reclevel == 1: props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) - spine = ax.annotate(str.upper(k), xy=(x_end, y_end), xytext=(x_start, y_start), - arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props, weight='bold') + spine = ax.annotate(str.upper(k), xy=(x_end, y_end), + xytext=(x_start, y_start), + arrowprops=dict(arrowstyle="->", + facecolor=spine_color), + bbox=props, weight='bold') else: props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start), - arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props) + arrowprops=dict(arrowstyle="->", + facecolor=spine_color), + bbox=props) # Call recursion to draw subspines - drawspines(data=data[k], ax=ax, parentspine=spine, alpha_ps=alpha_ps, alpha_ss=alpha_ss, + drawspines(data=data[k], ax=ax, parentspine=spine, + alpha_ps=alpha_ps, alpha_ss=alpha_ss, spine_color=spine_color, reclevel=reclevel + 1, max_recursion_level=max_recursion_level) # no primary_spines_rel_xpos is needed to be passed on recursion @@ -128,7 +144,8 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 return None -def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin: float = 0.05, alpha_ps: float = 60.0, +def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, + right_margin: float = 0.05, alpha_ps: float = 60.0, alpha_ss=0.0, primary_spines_rel_xpos: List[float] = [np.nan], pd_width: int = 0.1, spine_color: str = "black") -> plt.figure: """ @@ -148,7 +165,8 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin alpha_ss : TYPE, optional Secondary spine angle using during recursion. The default is 0.0. primary_spines_rel_xpos : list, optional - Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. + Relative X-position of primary spines, a list of float is accepted. + The default is [np.nan]. pd_width : int, optional Problem description box relative width. The default is 0.1. spine_color : str, optional @@ -170,12 +188,15 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin ax.axis('off') # draw main spine - main_spine = ax.axhline(y=0.5, xmin=left_margin, xmax=1 - right_margin - pd_width, color=spine_color) + main_spine = ax.axhline(y=0.5, xmin=left_margin, + xmax=1 - right_margin - pd_width, + color=spine_color) # draw fish head props = dict(boxstyle='round', facecolor='wheat', alpha=1.0) # add problem tag - ax.text((1 - right_margin - pd_width), 0.5, str.upper(list(data.keys())[0]), fontsize=12, weight='bold', + ax.text((1 - right_margin - pd_width), 0.5, str.upper(list(data.keys())[0]), + fontsize=12, weight='bold', verticalalignment='center', horizontalalignment='left', bbox=props) # draw fish tail @@ -184,18 +205,15 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin ax.fill(x, y, color=spine_color) # draw spines with recursion - drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, alpha_ps=alpha_ps, alpha_ss=alpha_ss, - primary_spines_rel_xpos=primary_spines_rel_xpos, spine_color=spine_color) + drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, + alpha_ps=alpha_ps, alpha_ss=alpha_ss, + primary_spines_rel_xpos=primary_spines_rel_xpos, + spine_color=spine_color) plt.tight_layout() return fig -# Rooting -current_dir = Path(__file__).parent -output_path = current_dir / "Ishikawa.jpeg" - - # USER DATA data = {'problem': {'machine': {'cause1': ''}, 'process': {'cause2': {'subcause1': '', 'subcause2': ''} @@ -208,21 +226,7 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin } } - -if __name__ == '__main__': - # try also without opt primary_spines rel_xpos - fig = ishikawaplot(data, figsize=(20, 10), primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) - - fig.show() - - fig.savefig(output_path, - # dpi=800, - format=None, - # metadata=None, - bbox_inches=None, - pad_inches=0.0, - facecolor='auto', - edgecolor='auto', - orientation='landscape', - transparent=False, - backend=None) +# Ishikawa plot generation +# try also without opt primary_spines rel_xpos +fig = ishikawaplot(data, figsize=(20, 10), + primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) From c7b911161cec64f312f4c3da52bd6acd36b10756 Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Wed, 22 Feb 2023 09:53:49 +0100 Subject: [PATCH 06/40] fixed file docstring for title overlay sphinx error --- examples/specialty_plots/ishikawa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/specialty_plots/ishikawa.py b/examples/specialty_plots/ishikawa.py index ce0826850803..b7e016672207 100644 --- a/examples/specialty_plots/ishikawa.py +++ b/examples/specialty_plots/ishikawa.py @@ -1,7 +1,7 @@ """ -=============== +================= Ishikawa Diagrams -=============== +================= Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect diagrams are useful for visualizing the effect to many cause relationships From e9ebc6693d5f523ae1787c85fa25ca4bbe96193c Mon Sep 17 00:00:00 2001 From: devRD Date: Wed, 22 Feb 2023 23:45:37 +0530 Subject: [PATCH 07/40] Fix unmatched offsetText label color --- lib/matplotlib/axis.py | 8 ++++++-- lib/matplotlib/tests/test_axes.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 6da0999e92bc..575f5f61fdcb 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -2259,7 +2259,9 @@ def _init(self): transform=mtransforms.blended_transform_factory( self.axes.transAxes, mtransforms.IdentityTransform()), fontsize=mpl.rcParams['xtick.labelsize'], - color=mpl.rcParams['xtick.color'], + color=mpl.rcParams['xtick.color'] if + mpl.rcParams['xtick.labelcolor'] == 'inherit' else + mpl.rcParams['xtick.labelcolor'], ) self.offset_text_position = 'bottom' @@ -2519,7 +2521,9 @@ def _init(self): transform=mtransforms.blended_transform_factory( self.axes.transAxes, mtransforms.IdentityTransform()), fontsize=mpl.rcParams['ytick.labelsize'], - color=mpl.rcParams['ytick.color'], + color=mpl.rcParams['ytick.color'] if + mpl.rcParams['ytick.labelcolor'] == 'inherit' else + mpl.rcParams['ytick.labelcolor'], ) self.offset_text_position = 'left' diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index b12b5d0bab46..62722b116e96 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7811,6 +7811,36 @@ def test_ytickcolor_is_not_yticklabelcolor(): assert tick.label1.get_color() == 'blue' +def test_xticklabelcolor_if_not_xtickcolor(): + plt.rcParams['xtick.labelcolor'] = 'blue' + ax = plt.axes() + ticks = ax.xaxis.get_major_ticks() + for tick in ticks: + assert tick.label1.get_color() == 'blue' + + plt.rcParams['xtick.color'] = 'yellow' + plt.rcParams['xtick.labelcolor'] = 'inherit' + ax = plt.axes() + ticks = ax.xaxis.get_major_ticks() + for tick in ticks: + assert tick.label1.get_color() == 'yellow' + + +def test_yticklabelcolor_if_not_ytickcolor(): + plt.rcParams['ytick.labelcolor'] = 'green' + ax = plt.axes() + ticks = ax.yaxis.get_major_ticks() + for tick in ticks: + assert tick.label1.get_color() == 'green' + + plt.rcParams['ytick.color'] = 'red' + plt.rcParams['ytick.labelcolor'] = 'inherit' + ax = plt.axes() + ticks = ax.yaxis.get_major_ticks() + for tick in ticks: + assert tick.label1.get_color() == 'red' + + @pytest.mark.parametrize('size', [size for size in mfont_manager.font_scalings if size is not None] + [8, 10, 12]) @mpl.style.context('default') From ddaa7ce6bd3f6ebf6ce1bcb9328d4f1bce66d8ae Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 6 Feb 2023 10:57:59 -0800 Subject: [PATCH 08/40] GitHub: inactive label [skip ci] Co-authored-by: Kyle Sunden Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- .github/workflows/stale.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000000..7bd007e4a52a --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,25 @@ +name: 'Label inactive PRs' +on: + schedule: + - cron: '30 1 * * *' + +jobs: + stale: + if: github.repository == 'matplotlib/matplotlib' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v7 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + operations-per-run: 30 + stale-pr-message: 'Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it.' + stale-pr-label: 'inactive' + days-before-pr-stale: 60 + days-before-pr-close: -1 + stale-issue-message: 'This issue has been marked "inactive" because it has been 365 days since the last comment. If this issue is still present in recent Matplotlib releases, or the feature request is still wanted, please leave a comment and this label will be removed. If there are no updates in another 30 days, this issue will be automatically closed, but you are free to re-open or create a new issue if needed. We value issue reports, and this procedure is meant to help us resurface and prioritize issues that have not been addressed yet, not make them disappear. Thanks for your help!' + stale-issue-label: 'inactive' + days-before-issue-stale: 365 + days-before-issue-close: 30 + ascending: true + exempt-issue-labels: "keep" + exempt-pr-label: "keep,status: orphaned PR" From 0e55ef1b5280730ec094ae77b43d5d9a5c3dae92 Mon Sep 17 00:00:00 2001 From: devRD Date: Fri, 24 Feb 2023 11:51:00 +0530 Subject: [PATCH 09/40] Add conditional before set labelcolor call --- lib/matplotlib/axis.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 575f5f61fdcb..0e98b76a79e8 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -2253,15 +2253,18 @@ def _init(self): ) self.label_position = 'bottom' + if mpl.rcParams['xtick.labelcolor'] == 'inherit': + tick_color = mpl.rcParams['xtick.color'] + else: + tick_color = mpl.rcParams['xtick.labelcolor'] + self.offsetText.set( x=1, y=0, verticalalignment='top', horizontalalignment='right', transform=mtransforms.blended_transform_factory( self.axes.transAxes, mtransforms.IdentityTransform()), fontsize=mpl.rcParams['xtick.labelsize'], - color=mpl.rcParams['xtick.color'] if - mpl.rcParams['xtick.labelcolor'] == 'inherit' else - mpl.rcParams['xtick.labelcolor'], + color=tick_color ) self.offset_text_position = 'bottom' @@ -2514,6 +2517,12 @@ def _init(self): mtransforms.IdentityTransform(), self.axes.transAxes), ) self.label_position = 'left' + + if mpl.rcParams['ytick.labelcolor'] == 'inherit': + tick_color = mpl.rcParams['ytick.color'] + else: + tick_color = mpl.rcParams['ytick.labelcolor'] + # x in axes coords, y in display coords(!). self.offsetText.set( x=0, y=0.5, @@ -2521,9 +2530,7 @@ def _init(self): transform=mtransforms.blended_transform_factory( self.axes.transAxes, mtransforms.IdentityTransform()), fontsize=mpl.rcParams['ytick.labelsize'], - color=mpl.rcParams['ytick.color'] if - mpl.rcParams['ytick.labelcolor'] == 'inherit' else - mpl.rcParams['ytick.labelcolor'], + color=tick_color ) self.offset_text_position = 'left' From 53691a5414763a8a1d53e627f377cbc2533051cd Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Feb 2023 23:04:01 -0500 Subject: [PATCH 10/40] Clean up legend loc parameter documentation As we now have different docs for the *loc* parameter to `Figure/Axes.legend`, we can replace the ambiguous 'axes/figure' with the correct type. And also remove 'best' from the `Figure.legend` version. Note, I did not remove 'best' from the numerical table, as that would be annoying to do, so I just annotated it. Fixes ##25323 --- lib/matplotlib/figure.py | 1 - lib/matplotlib/legend.py | 103 ++++++++++++++++++--------------------- 2 files changed, 48 insertions(+), 56 deletions(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 10a407232827..e1c995d6a1de 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -1102,7 +1102,6 @@ def legend(self, *args, **kwargs): ---------------- %(_legend_kw_figure)s - See Also -------- .Axes.legend diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index ff6abdb95844..c25b519fdb03 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -158,8 +158,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): same height, set to ``[0.5]``. markerscale : float, default: :rc:`legend.markerscale` - The relative size of legend markers compared with the originally - drawn ones. + The relative size of legend markers compared to the originally drawn ones. markerfirst : bool, default: True If *True*, legend marker is placed to the left of the legend label. @@ -254,52 +253,55 @@ def _update_bbox_to_anchor(self, loc_in_canvas): """ _loc_doc_base = """ -loc : str or pair of floats, {0} +loc : str or pair of floats, default: {default} The location of the legend. - The strings - ``'upper left', 'upper right', 'lower left', 'lower right'`` - place the legend at the corresponding corner of the axes/figure. + The strings ``'upper left'``, ``'upper right'``, ``'lower left'``, + ``'lower right'`` place the legend at the corresponding corner of the + {parent}. - The strings - ``'upper center', 'lower center', 'center left', 'center right'`` - place the legend at the center of the corresponding edge of the - axes/figure. - - The string ``'center'`` places the legend at the center of the axes/figure. - - The string ``'best'`` places the legend at the location, among the nine - locations defined so far, with the minimum overlap with other drawn - artists. This option can be quite slow for plots with large amounts of - data; your plotting speed may benefit from providing a specific location. + The strings ``'upper center'``, ``'lower center'``, ``'center left'``, + ``'center right'`` place the legend at the center of the corresponding edge + of the {parent}. + The string ``'center'`` places the legend at the center of the {parent}. +{best} The location can also be a 2-tuple giving the coordinates of the lower-left - corner of the legend in axes coordinates (in which case *bbox_to_anchor* + corner of the legend in {parent} coordinates (in which case *bbox_to_anchor* will be ignored). For back-compatibility, ``'center right'`` (but no other location) can also - be spelled ``'right'``, and each "string" locations can also be given as a + be spelled ``'right'``, and each "string" location can also be given as a numeric value: - =============== ============= - Location String Location Code - =============== ============= - 'best' 0 - 'upper right' 1 - 'upper left' 2 - 'lower left' 3 - 'lower right' 4 - 'right' 5 - 'center left' 6 - 'center right' 7 - 'lower center' 8 - 'upper center' 9 - 'center' 10 - =============== ============= - {1}""" - -_legend_kw_axes_st = (_loc_doc_base.format("default: :rc:`legend.loc`", '') + - _legend_kw_doc_base) + ================== ============= + Location String Location Code + ================== ============= + 'best' (Axes only) 0 + 'upper right' 1 + 'upper left' 2 + 'lower left' 3 + 'lower right' 4 + 'right' 5 + 'center left' 6 + 'center right' 7 + 'lower center' 8 + 'upper center' 9 + 'center' 10 + ================== ============= + {outside}""" + +_loc_doc_best = """ + The string ``'best'`` places the legend at the location, among the nine + locations defined so far, with the minimum overlap with other drawn + artists. This option can be quite slow for plots with large amounts of + data; your plotting speed may benefit from providing a specific location. +""" + +_legend_kw_axes_st = ( + _loc_doc_base.format(parent='axes', default=':rc:`legend.loc`', + best=_loc_doc_best, outside='') + + _legend_kw_doc_base) _docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st) _outside_doc = """ @@ -314,21 +316,23 @@ def _update_bbox_to_anchor(self, loc_in_canvas): :doc:`/tutorials/intermediate/legend_guide` for more details. """ -_legend_kw_figure_st = (_loc_doc_base.format("default: 'upper right'", - _outside_doc) + - _legend_kw_doc_base) +_legend_kw_figure_st = ( + _loc_doc_base.format(parent='figure', default="'upper right'", + best='', outside=_outside_doc) + + _legend_kw_doc_base) _docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st) _legend_kw_both_st = ( - _loc_doc_base.format("default: 'best' for axes, 'upper right' for figures", - _outside_doc) + + _loc_doc_base.format(parent='axes/figure', + default=":rc:`legend.loc` for Axes, 'upper right' for Figure", + best=_loc_doc_best, outside=_outside_doc) + _legend_kw_doc_base) _docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st) class Legend(Artist): """ - Place a legend on the axes at location loc. + Place a legend on the figure/axes. """ # 'best' is only implemented for axes legends @@ -407,17 +411,6 @@ def __init__( List of `.Artist` objects added as legend entries. .. versionadded:: 3.7 - - Notes - ----- - Users can specify any arbitrary location for the legend using the - *bbox_to_anchor* keyword argument. *bbox_to_anchor* can be a - `.BboxBase` (or derived there from) or a tuple of 2 or 4 floats. - See `set_bbox_to_anchor` for more detail. - - The legend location can be specified by setting *loc* with a tuple of - 2 floats, which is interpreted as the lower-left corner of the legend - in the normalized axes coordinate. """ # local import only to avoid circularity from matplotlib.axes import Axes From 264e7d37d2ee89c6019af4e5743653f4748448e1 Mon Sep 17 00:00:00 2001 From: devRD Date: Sat, 25 Feb 2023 11:25:28 +0530 Subject: [PATCH 11/40] Update test to check offset text color --- lib/matplotlib/tests/test_axes.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 62722b116e96..c31edec05308 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7811,34 +7811,26 @@ def test_ytickcolor_is_not_yticklabelcolor(): assert tick.label1.get_color() == 'blue' -def test_xticklabelcolor_if_not_xtickcolor(): +def test_xaxis_offsetText_color(): plt.rcParams['xtick.labelcolor'] = 'blue' ax = plt.axes() - ticks = ax.xaxis.get_major_ticks() - for tick in ticks: - assert tick.label1.get_color() == 'blue' + assert ax.xaxis.offsetText.get_color() == 'blue' plt.rcParams['xtick.color'] = 'yellow' plt.rcParams['xtick.labelcolor'] = 'inherit' ax = plt.axes() - ticks = ax.xaxis.get_major_ticks() - for tick in ticks: - assert tick.label1.get_color() == 'yellow' + assert ax.xaxis.offsetText.get_color() == 'yellow' -def test_yticklabelcolor_if_not_ytickcolor(): +def test_yaxis_offsetText_color(): plt.rcParams['ytick.labelcolor'] = 'green' ax = plt.axes() - ticks = ax.yaxis.get_major_ticks() - for tick in ticks: - assert tick.label1.get_color() == 'green' + assert ax.yaxis.offsetText.get_color() == 'green' plt.rcParams['ytick.color'] = 'red' plt.rcParams['ytick.labelcolor'] = 'inherit' ax = plt.axes() - ticks = ax.yaxis.get_major_ticks() - for tick in ticks: - assert tick.label1.get_color() == 'red' + assert ax.yaxis.offsetText.get_color() == 'red' @pytest.mark.parametrize('size', [size for size in mfont_manager.font_scalings From a3c564f2653d1f65306687a79f66e84589510ace Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sat, 25 Feb 2023 08:45:23 +0100 Subject: [PATCH 12/40] - added wikipedia link to ishikawa desc doc string - reformatted function def to work on axes object - fix plt.tight_layout leaving the user to choose - reduced fisize still mantaining a decent rendering - added swotplot.py for SWOT plots with the same logic, design pattern and style --- examples/specialty_plots/ishikawa.py | 29 ++--- examples/specialty_plots/swotplot.py | 180 +++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 examples/specialty_plots/swotplot.py diff --git a/examples/specialty_plots/ishikawa.py b/examples/specialty_plots/ishikawa.py index b7e016672207..d5fe8e0ff80e 100644 --- a/examples/specialty_plots/ishikawa.py +++ b/examples/specialty_plots/ishikawa.py @@ -5,6 +5,7 @@ Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect diagrams are useful for visualizing the effect to many cause relationships +Source: https://en.wikipedia.org/wiki/Ishikawa_diagram """ import logging @@ -144,18 +145,18 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 return None -def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, +def ishikawaplot(data: dict, ax: plt.Axes, left_margin: float = 0.05, right_margin: float = 0.05, alpha_ps: float = 60.0, alpha_ss=0.0, primary_spines_rel_xpos: List[float] = [np.nan], - pd_width: int = 0.1, spine_color: str = "black") -> plt.figure: + pd_width: int = 0.1, spine_color: str = "black") -> None: """ Parameters ---------- - data : TYPE - DESCRIPTION. - figsize : TYPE, optional - Matplotlib figure size. The default is (20,10). + data : dict + Plot data structure. + ax : matplotlib.pyplot.Axes + Axes in which to drow the plot. left_margin : float, optional Left spacing from frame border. The default is 0.05. right_margin : float, optional @@ -174,14 +175,9 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, Returns ------- - fig : matplotlib.pyplot.figure - Figure object containing the Ishikawa plot + None """ - - fig = plt.figure(figsize=figsize) - ax = fig.gca() - # format axis ax.set_xlim(0, 1.0) ax.set_ylim(0, 1.0) @@ -210,8 +206,7 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, primary_spines_rel_xpos=primary_spines_rel_xpos, spine_color=spine_color) - plt.tight_layout() - return fig + return None # USER DATA @@ -227,6 +222,8 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, } # Ishikawa plot generation +fig = plt.figure(figsize=(12, 6), layout='constrained') +ax = fig.gca() + # try also without opt primary_spines rel_xpos -fig = ishikawaplot(data, figsize=(20, 10), - primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) +ishikawaplot(data, ax, primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) diff --git a/examples/specialty_plots/swotplot.py b/examples/specialty_plots/swotplot.py new file mode 100644 index 000000000000..8fcdf9a9fba5 --- /dev/null +++ b/examples/specialty_plots/swotplot.py @@ -0,0 +1,180 @@ +""" +============ +SWOT Diagram +============ + +SWOT analysis (or SWOT matrix) is a strategic planning and strategic management +technique used to help a person or organization identify Strengths, Weaknesses, +Opportunities, and Threats related to business competition or project planning. +It is sometimes called situational assessment or situational analysis. +Additional acronyms using the same components include TOWS and WOTS-UP. +Source: https://en.wikipedia.org/wiki/SWOT_analysis + +""" +import logging +from typing import Tuple + +import matplotlib.pyplot as plt + +_log = logging.getLogger(__name__) + + +def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, + q_x: Tuple[float, float, float, float], + q_y: Tuple[float, float, float, float], + q_title_pos: Tuple[float, float] = (0.0, 0.0), + q_title_props: dict = dict(boxstyle='round', + facecolor="white", + alpha=1.0), + q_point_props: dict = dict(boxstyle='round', + facecolor="white", + alpha=1.0) + ) -> None: + """ + Draw a quadrant of the SWOT plot + + Parameters + ---------- + q_data : dict + Data structure passed to populate the quadrant. + Format 'key':Tuple(float,float) + ax : plt.Axes + Matplotlib current axes + q_color : str + Quadrant color. + q_x : Tuple[float] + Quadrant X relative coordinates. Format: Tuple(float,float,float,float) + q_y : Tuple[float] + Quadrant Y relative coordinates. Format: Tuple(float,float,float,float) + q_title_pos : Tuple[float,float] + Plot title relative position + q_title_props : dict, optional + Title box style properties. The default is dict(boxstyle='round', + facecolor="white", alpha=1.0). + q_point_props : dict, optional + Point box style properties. The default is dict(boxstyle='round', + facecolor="white", alpha=1.0). + + Returns + ------- + None + + """ + # draw filled rectangle + ax.fill(q_x, q_y, color=q_color) + + # draw quadrant title + ax.text(x=q_title_pos[0], y=q_title_pos[1], + s=str.upper(list(q_data.keys())[0]), fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='center', + bbox=q_title_props) + + # fill quadrant with points + for k, v in q_data[list(q_data.keys())[0]].items(): + ax.text(x=v[0], y=v[1], s=k, fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='center', + bbox=q_point_props) + _log.debug(f"k: {k}, v: {v}") + + return None + + +def swotplot(p_data: dict, ax: plt.Axes, + s_color: str = "forestgreen", w_color: str = "gold", + o_color: str = "skyblue", t_color: str = "firebrick", + left_margin: float = 0.05, right_margin: float = 0.05, + top_margin: float = 0.05, bottom_margin: float = 0.05, + ) -> None: + """ + Draw SWOT plot + + Parameters + ---------- + p_data : dict + Data structure passed to populate the plot. + ax : matplotlib.pyplot.Axes + axes in which to draw the plot + s_color : float, optional + Strength quadrant color. + w_color : float, optional + Weakness quadrant color. + o_color : float, optional + Opportunity quadrant color. + t_color : float, optional + Threat quadrant color. + left_margin : float, optional + Left spacing from frame border. The default is 0.05. + right_margin : float, optional + Right spacing from frame border. The default is 0.05. + top_margin : float, optional + Top spacing from frame border. The default is 0.05. + bottom_margin : float, optional + Bottom spacing from frame border. The default is 0.05. + + + Returns + ------- + None + + """ + # format axis + ax.set_xlim(0, 1.0) + ax.set_ylim(0, 1.0) + ax.axis('off') + + # draw s quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[0], + p_data.items())), + ax=ax, + q_color=s_color, q_x=(left_margin, left_margin, 0.5, 0.5), + q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), + q_title_pos=(0.25 + left_margin / 2, 1 - top_margin)) + + # draw w quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[1], + p_data.items())), + ax=ax, + q_color=w_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), + q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), + q_title_pos=(0.25 + (1 - right_margin) / 2, 1 - top_margin)) + + # draw o quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[2], + p_data.items())), + ax=ax, + q_color=o_color, q_x=(left_margin, left_margin, 0.5, 0.5), + q_y=(bottom_margin, 0.5, 0.5, bottom_margin), + q_title_pos=(0.25 + left_margin / 2, bottom_margin)) + + # draw t quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[3], + p_data.items())), + ax=ax, + q_color=t_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), + q_y=(bottom_margin, 0.5, 0.5, bottom_margin), + q_title_pos=(0.25 + (1 - right_margin) / 2, bottom_margin)) + + return None + + +# USER DATA +# relative x_pos,y_pos make it easy to manage particular cases such as +# soft categorization +data = {'strength': {'SW automation': (0.3, 0.6), + 'teamwork': (0.4, 0.5) + }, + 'weakness': {'delayed payment': (0.5, 0.7), + 'hard to learn': (0.7, 0.9) + }, + 'opportunity': {'dedicated training': (0.3, 0.3), + 'just-in-time development': (0.2, 0.1) + }, + 'threat': {'project de-prioritization': (0.7, 0.3), + 'external consultancy': (0.5, 0.1) + }, + } + +# SWOT plot generation +fig = plt.figure(figsize=(12, 6), layout='constrained') +ax = fig.gca() +swotplot(data, ax) From dfaa99155274400e445d78324a9a00614a44f392 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 23 Feb 2023 22:03:23 +0100 Subject: [PATCH 13/40] Make draggable legends picklable. --- lib/matplotlib/offsetbox.py | 4 +++- lib/matplotlib/tests/test_pickle.py | 12 +++++++++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 1dee8a23d94c..a46b9cd4319d 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -1505,7 +1505,6 @@ def __init__(self, ref_artist, use_blit=False): if not ref_artist.pickable(): ref_artist.set_picker(True) self.got_artist = False - self.canvas = self.ref_artist.figure.canvas self._use_blit = use_blit and self.canvas.supports_blit self.cids = [ self.canvas.callbacks._connect_picklable( @@ -1514,6 +1513,9 @@ def __init__(self, ref_artist, use_blit=False): 'button_release_event', self.on_release), ] + # A property, not an attribute, to maintain picklability. + canvas = property(lambda self: self.ref_artist.figure.canvas) + def on_motion(self, evt): if self._check_still_parented() and self.got_artist: dx = evt.x - self.mouse_x diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index ec6bdcc2fe14..a31927d59634 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -1,6 +1,7 @@ from io import BytesIO import ast import pickle +import pickletools import numpy as np import pytest @@ -88,6 +89,7 @@ def _generate_complete_test_figure(fig_ref): plt.subplot(3, 3, 9) plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4) + plt.legend(draggable=True) @mpl.style.context("default") @@ -95,9 +97,13 @@ def _generate_complete_test_figure(fig_ref): def test_complete(fig_test, fig_ref): _generate_complete_test_figure(fig_ref) # plotting is done, now test its pickle-ability - pkl = BytesIO() - pickle.dump(fig_ref, pkl, pickle.HIGHEST_PROTOCOL) - loaded = pickle.loads(pkl.getbuffer()) + pkl = pickle.dumps(fig_ref, pickle.HIGHEST_PROTOCOL) + # FigureCanvasAgg is picklable and GUI canvases are generally not, but there should + # be no reference to the canvas in the pickle stream in either case. In order to + # keep the test independent of GUI toolkits, run it with Agg and check that there's + # no reference to FigureCanvasAgg in the pickle stream. + assert "FigureCanvasAgg" not in [arg for op, arg, pos in pickletools.genops(pkl)] + loaded = pickle.loads(pkl) loaded.canvas.draw() fig_test.set_size_inches(loaded.get_size_inches()) From f8350143662a9bb76abbd8f4b9d1bfdb4c1318fe Mon Sep 17 00:00:00 2001 From: Oscar Gustafsson Date: Sat, 25 Feb 2023 20:35:50 +0100 Subject: [PATCH 14/40] Fix doc issues identified by velin --- lib/matplotlib/backend_bases.py | 8 ++++++-- lib/matplotlib/backend_managers.py | 6 ++---- lib/matplotlib/colorbar.py | 2 +- lib/matplotlib/colors.py | 14 ++++++++++---- lib/matplotlib/figure.py | 4 ++-- lib/matplotlib/gridspec.py | 4 ++++ lib/matplotlib/image.py | 4 +++- lib/matplotlib/layout_engine.py | 5 ++++- lib/matplotlib/text.py | 3 +++ lib/matplotlib/transforms.py | 2 +- lib/matplotlib/widgets.py | 2 +- 11 files changed, 37 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 4f5cd61dbe0b..9ef707f4e39a 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -313,9 +313,11 @@ def draw_gouraud_triangles(self, gc, triangles_array, colors_array, Parameters ---------- - points : (N, 3, 2) array-like + gc : `.GraphicsContextBase` + The graphics context. + triangles_array : (N, 3, 2) array-like Array of *N* (x, y) points for the triangles. - colors : (N, 3, 4) array-like + colors_array : (N, 3, 4) array-like Array of *N* RGBA colors for each point of the triangles. transform : `matplotlib.transforms.Transform` An affine transform to apply to the points. @@ -611,6 +613,8 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): Parameters ---------- + gc : `.GraphicsContextBase` + The graphics context. x : float The x location of the text in display coords. y : float diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index 5366e8e611f4..ac74ff97a4e8 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -238,10 +238,8 @@ def add_tool(self, name, tool, *args, **kwargs): tool : type Class of the tool to be added. A subclass will be used instead if one was registered for the current canvas class. - - Notes - ----- - args and kwargs get passed directly to the tools constructor. + *args, **kwargs + Passed to the *tool*'s constructor. See Also -------- diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 966eb0760b47..14c7c1e58b9a 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -1003,7 +1003,7 @@ def _set_scale(self, scale, **kwargs): Parameters ---------- - value : {"linear", "log", "symlog", "logit", ...} or `.ScaleBase` + scale : {"linear", "log", "symlog", "logit", ...} or `.ScaleBase` The axis scale type to apply. **kwargs diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index a6c0d09adcd8..b9626d913269 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1311,7 +1311,7 @@ def __call__(self, value, clip=None): ---------- value Data to normalize. - clip : bool + clip : bool, optional If ``None``, defaults to ``self.clip`` (which defaults to ``False``). @@ -1450,7 +1450,7 @@ def autoscale_None(self, A): def __call__(self, value, clip=None): """ - Map value to the interval [0, 1]. The clip argument is unused. + Map value to the interval [0, 1]. The *clip* argument is unused. """ result, is_scalar = self.process_value(value) self.autoscale_None(result) # sets self.vmin, self.vmax if None @@ -1499,6 +1499,10 @@ def __init__(self, vcenter=0, halfrange=None, clip=False): *vcenter* + *halfrange* is ``1.0`` in the normalization. Defaults to the largest absolute difference to *vcenter* for the values in the dataset. + clip : bool, default: False + If ``True`` values falling outside the range ``[vmin, vmax]``, + are mapped to 0 or 1, whichever is closer, and masked values are + set to 1. If ``False`` masked values remain masked. Examples -------- @@ -2423,7 +2427,8 @@ def shade(self, data, cmap, norm=None, blend_mode='overlay', vmin=None, full illumination or shadow (and clipping any values that move beyond 0 or 1). Note that this is not visually or mathematically the same as vertical exaggeration. - Additional kwargs are passed on to the *blend_mode* function. + **kwargs + Additional kwargs are passed on to the *blend_mode* function. Returns ------- @@ -2484,7 +2489,8 @@ def shade_rgb(self, rgb, elevation, fraction=1., blend_mode='hsv', The x-spacing (columns) of the input *elevation* grid. dy : number, optional The y-spacing (rows) of the input *elevation* grid. - Additional kwargs are passed on to the *blend_mode* function. + **kwargs + Additional kwargs are passed on to the *blend_mode* function. Returns ------- diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 10a407232827..b4c38368bfe0 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -949,7 +949,7 @@ def clear(self, keep_observers=False): Parameters ---------- - keep_observers: bool, default: False + keep_observers : bool, default: False Set *keep_observers* to True if, for example, a gui widget is tracking the Axes in the figure. """ @@ -989,7 +989,7 @@ def clf(self, keep_observers=False): Parameters ---------- - keep_observers: bool, default: False + keep_observers : bool, default: False Set *keep_observers* to True if, for example, a gui widget is tracking the Axes in the figure. """ diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 25cd9ae78f41..11dc05b0e9d2 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -447,6 +447,10 @@ def tight_layout(self, figure, renderer=None, Parameters ---------- + figure : `.Figure` + The figure. + renderer : `.RendererBase` subclass, optional + The renderer to be used. pad : float Padding between the figure edge and the edges of subplots, as a fraction of the font-size. diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 7b066bdaf7b4..bc3e35bb7d2c 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -1047,8 +1047,10 @@ def __init__(self, ax, *, interpolation='nearest', **kwargs): """ Parameters ---------- + ax : `~.axes.Axes` + The axes the image will belong to. interpolation : {'nearest', 'bilinear'}, default: 'nearest' - + The interpolation scheme used in the resampling. **kwargs All other keyword arguments are identical to those of `.AxesImage`. """ diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py index 185be9857abb..a3eab93da329 100644 --- a/lib/matplotlib/layout_engine.py +++ b/lib/matplotlib/layout_engine.py @@ -167,7 +167,10 @@ def execute(self, fig): ---------- fig : `.Figure` to perform layout on. - See also: `.figure.Figure.tight_layout` and `.pyplot.tight_layout`. + See Also + -------- + .figure.Figure.tight_layout + .pyplot.tight_layout """ info = self._params renderer = fig._get_renderer() diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 0f874ba33db7..58d071d01e32 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -1626,6 +1626,9 @@ def draggable(self, state=None, use_blit=False): state : bool or None - True or False: set the draggability. - None: toggle the draggability. + use_blit : bool, default: False + Use blitting for faster image composition. For details see + :ref:`func-animation`. Returns ------- diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index cfb9877ad31f..a31e58da8dd7 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -613,7 +613,7 @@ def padded(self, w_pad, h_pad=None): ---------- w_pad : float Width pad - h_pad: float, optional + h_pad : float, optional Height pad. Defaults to *w_pad*. """ diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 35f3eec3e79e..eb4b97da279f 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2512,7 +2512,7 @@ def remove_state(self, state): Parameters ---------- - value : str + state : str Must be a supported state of the selector. See the `state_modifier_keys` parameters for details. From 9bf681e1230d62760545e408e4156e137af75599 Mon Sep 17 00:00:00 2001 From: xtanion Date: Mon, 27 Feb 2023 03:29:28 +0530 Subject: [PATCH 15/40] removing typecasting method to float --- lib/matplotlib/contour.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 6516aa7c2ecb..def2b773b87b 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -1447,8 +1447,8 @@ def _contour_args(self, args, kwargs): else: raise _api.nargs_error(fn, takes="from 1 to 4", given=nargs) z = ma.masked_invalid(z, copy=False) - self.zmax = float(z.max()) - self.zmin = float(z.min()) + self.zmax = z.max().astype(float) + self.zmin = z.min().astype(float) if self.logscale and self.zmin <= 0: z = ma.masked_where(z <= 0, z) _api.warn_external('Log scale: values of z <= 0 have been masked') From da467694721c3592936dffe5f2c97f019708b88e Mon Sep 17 00:00:00 2001 From: xtanion Date: Mon, 27 Feb 2023 16:26:18 +0530 Subject: [PATCH 16/40] adding tests and changing typecasts --- lib/matplotlib/contour.py | 2 +- lib/matplotlib/tests/test_contour.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index def2b773b87b..90c5dd5feba0 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -1452,7 +1452,7 @@ def _contour_args(self, args, kwargs): if self.logscale and self.zmin <= 0: z = ma.masked_where(z <= 0, z) _api.warn_external('Log scale: values of z <= 0 have been masked') - self.zmin = float(z.min()) + self.zmin = z.min().astype(float) self._process_contour_level_args(args, z.dtype) return (x, y, z) diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 58101a9adc24..c27e7029c696 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -715,3 +715,9 @@ def test_bool_autolevel(): assert plt.tricontour(x, y, z).levels.tolist() == [.5] assert plt.tricontourf(x, y, z.tolist()).levels.tolist() == [0, .5, 1] assert plt.tricontourf(x, y, z).levels.tolist() == [0, .5, 1] + +def test_all_nan(): + x = np.array([[np.nan, np.nan], [np.nan, np.nan]]) + assert_array_almost_equal(plt.contour(x).levels.tolist(), + [-1e-13, -7.5e-14, -5e-14, -2.4e-14, 0.0, + 2.4e-14, 5e-14, 7.5e-14, 1e-13]) \ No newline at end of file From 0d7bd7c38e350ace6d3cc31806de5686753cd2e0 Mon Sep 17 00:00:00 2001 From: xtanion Date: Mon, 27 Feb 2023 16:42:01 +0530 Subject: [PATCH 17/40] flake8 fix --- lib/matplotlib/tests/test_contour.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index c27e7029c696..51359244598d 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -716,8 +716,9 @@ def test_bool_autolevel(): assert plt.tricontourf(x, y, z.tolist()).levels.tolist() == [0, .5, 1] assert plt.tricontourf(x, y, z).levels.tolist() == [0, .5, 1] + def test_all_nan(): x = np.array([[np.nan, np.nan], [np.nan, np.nan]]) assert_array_almost_equal(plt.contour(x).levels.tolist(), [-1e-13, -7.5e-14, -5e-14, -2.4e-14, 0.0, - 2.4e-14, 5e-14, 7.5e-14, 1e-13]) \ No newline at end of file + 2.4e-14, 5e-14, 7.5e-14, 1e-13]) From a00680eadbf370e83d6866427b77382182efca0b Mon Sep 17 00:00:00 2001 From: xtanion Date: Mon, 27 Feb 2023 20:09:02 +0530 Subject: [PATCH 18/40] removing redundant `to_list` conversion --- lib/matplotlib/tests/test_contour.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 51359244598d..d56a4c9a972a 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -719,6 +719,6 @@ def test_bool_autolevel(): def test_all_nan(): x = np.array([[np.nan, np.nan], [np.nan, np.nan]]) - assert_array_almost_equal(plt.contour(x).levels.tolist(), + assert_array_almost_equal(plt.contour(x).levels, [-1e-13, -7.5e-14, -5e-14, -2.4e-14, 0.0, 2.4e-14, 5e-14, 7.5e-14, 1e-13]) From 3bdc020a3fbc45d6ef9dc6a81c0f96d0ee40f384 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Mon, 27 Feb 2023 15:27:45 +0000 Subject: [PATCH 19/40] fix typo [skip ci] --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 7bd007e4a52a..a6c7e28e94da 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -22,4 +22,4 @@ jobs: days-before-issue-close: 30 ascending: true exempt-issue-labels: "keep" - exempt-pr-label: "keep,status: orphaned PR" + exempt-pr-labels: "keep,status: orphaned PR" From ea2e9eaa02fcf94d55824c1b76b3366e88e081ee Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Sun, 26 Feb 2023 12:17:28 +0100 Subject: [PATCH 20/40] Support pickling of figures with aligned x/y labels. --- lib/matplotlib/cbook.py | 13 +++++++++++++ lib/matplotlib/tests/test_pickle.py | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index c9699b2e21f5..1a64331e201d 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -788,6 +788,19 @@ class Grouper: def __init__(self, init=()): self._mapping = {weakref.ref(x): [weakref.ref(x)] for x in init} + def __getstate__(self): + return { + **vars(self), + # Convert weak refs to strong ones. + "_mapping": {k(): [v() for v in vs] for k, vs in self._mapping.items()}, + } + + def __setstate__(self, state): + vars(self).update(state) + # Convert strong refs to weak ones. + self._mapping = {weakref.ref(k): [*map(weakref.ref, vs)] + for k, vs in self._mapping.items()} + def __contains__(self, item): return weakref.ref(item) in self._mapping diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index ec6bdcc2fe14..bdcedee37308 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -58,6 +58,7 @@ def _generate_complete_test_figure(fig_ref): # Ensure lists also pickle correctly. plt.subplot(3, 3, 1) plt.plot(list(range(10))) + plt.ylabel("hello") plt.subplot(3, 3, 2) plt.contourf(data, hatches=['//', 'ooo']) @@ -68,6 +69,7 @@ def _generate_complete_test_figure(fig_ref): plt.subplot(3, 3, 4) plt.imshow(data) + plt.ylabel("hello\nworld!") plt.subplot(3, 3, 5) plt.pcolor(data) @@ -89,6 +91,8 @@ def _generate_complete_test_figure(fig_ref): plt.subplot(3, 3, 9) plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4) + fig_ref.align_ylabels() # Test handling of _align_label_groups Groupers. + @mpl.style.context("default") @check_figures_equal(extensions=["png"]) From a0d25af903c8dcb6b329042212cf900f553fa714 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 27 Feb 2023 19:54:32 -0500 Subject: [PATCH 21/40] Fix RangeSlider.set_val when outside existing value The clipping in the setter ensures that min <= max, but if you are setting both values outside the existing range, this will incorrectly set new minimum to old maximum and vice versa. The tests also passed accidentally because all the new ranges overlapped with the old, and did not trigger this issue. Fixes #25338 --- lib/matplotlib/tests/test_widgets.py | 8 ++++---- lib/matplotlib/widgets.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 92b87a96b3d2..5706df46e328 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1297,12 +1297,12 @@ def handle_positions(slider): else: return [h.get_xdata()[0] for h in slider._handles] - slider.set_val((0.2, 0.6)) - assert_allclose(slider.val, (0.2, 0.6)) - assert_allclose(handle_positions(slider), (0.2, 0.6)) + slider.set_val((0.4, 0.6)) + assert_allclose(slider.val, (0.4, 0.6)) + assert_allclose(handle_positions(slider), (0.4, 0.6)) box = slider.poly.get_extents().transformed(ax.transAxes.inverted()) - assert_allclose(box.get_points().flatten()[idx], [0.2, .25, 0.6, .75]) + assert_allclose(box.get_points().flatten()[idx], [0.4, .25, 0.6, .75]) slider.set_val((0.2, 0.1)) assert_allclose(slider.val, (0.1, 0.2)) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index eb4b97da279f..c25644dbe6b8 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -702,7 +702,7 @@ def __init__( valmin, valmax, valfmt, dragging, valstep) # Set a value to allow _value_in_bounds() to work. - self.val = [valmin, valmax] + self.val = (valmin, valmax) if valinit is None: # Place at the 25th and 75th percentiles extent = valmax - valmin @@ -947,9 +947,9 @@ def set_val(self, val): """ val = np.sort(val) _api.check_shape((2,), val=val) - vmin, vmax = val - vmin = self._min_in_bounds(vmin) - vmax = self._max_in_bounds(vmax) + # Reset value to allow _value_in_bounds() to work. + self.val = (self.valmin, self.valmax) + vmin, vmax = self._value_in_bounds(val) self._update_selection_poly(vmin, vmax) if self.orientation == "vertical": self._handles[0].set_ydata([vmin]) From ff59e4695c64f985ba1ea02e930cc601c60954aa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 27 Feb 2023 22:19:23 -0500 Subject: [PATCH 22/40] TST: Increase test_set_line_coll_dash_image tolerance slightly. --- lib/matplotlib/tests/test_lines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 7eecf5675a4b..b75d3c01b28e 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -185,7 +185,7 @@ def test_set_drawstyle(): @image_comparison( ['line_collection_dashes'], remove_text=True, style='mpl20', - tol=0.62 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) + tol=0.65 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) def test_set_line_coll_dash_image(): fig, ax = plt.subplots() np.random.seed(0) From 790bcc293cf65bc8a4a4dfa955ad25e587a4673e Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Tue, 28 Feb 2023 14:25:59 +0000 Subject: [PATCH 23/40] FIX: use wrapped text in Text._get_layout [skip circle] --- lib/matplotlib/tests/test_text.py | 16 ++++++++++++++++ lib/matplotlib/text.py | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 65aff05b035b..c80fefd37e14 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -701,6 +701,22 @@ def test_wrap(): 'times.') +def test_get_window_extent_wrapped(): + # Test that a long title that wraps to two lines has the same vertical + # extent as an explicit two line title. + + fig1 = plt.figure(figsize=(3, 3)) + fig1.suptitle("suptitle that is clearly too long in this case", wrap=True) + window_extent_test = fig1._suptitle.get_window_extent() + + fig2 = plt.figure(figsize=(3, 3)) + fig2.suptitle("suptitle that is clearly\ntoo long in this case") + window_extent_ref = fig2._suptitle.get_window_extent() + + assert window_extent_test.y0 == window_extent_ref.y0 + assert window_extent_test.y1 == window_extent_ref.y1 + + def test_long_word_wrap(): fig = plt.figure(figsize=(6, 4)) text = fig.text(9.5, 8, 'Alonglineoftexttowrap', wrap=True) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 58d071d01e32..f1e300b838e2 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -367,7 +367,7 @@ def _get_layout(self, renderer): of a rotated text when necessary. """ thisx, thisy = 0.0, 0.0 - lines = self.get_text().split("\n") # Ensures lines is not empty. + lines = self._get_wrapped_text().split("\n") # Ensures lines is not empty. ws = [] hs = [] From b85b46a38e39a31eb0c760cec9cec532b2f42af0 Mon Sep 17 00:00:00 2001 From: Ian Hunt-Isaak Date: Wed, 1 Mar 2023 00:12:57 -0500 Subject: [PATCH 24/40] link to ipympl docs instead of github --- doc/users/explain/interactive.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/users/explain/interactive.rst b/doc/users/explain/interactive.rst index 83d1ee2835a6..942b682fb04c 100644 --- a/doc/users/explain/interactive.rst +++ b/doc/users/explain/interactive.rst @@ -269,7 +269,7 @@ Jupyter Notebooks / JupyterLab cells. To get interactive figures in the 'classic' notebook or Jupyter lab, -use the `ipympl `__ backend +use the `ipympl `__ backend (must be installed separately) which uses the **ipywidget** framework. If ``ipympl`` is installed use the magic: From c38c4055b71239e6170eb03a379cd4fe004003d0 Mon Sep 17 00:00:00 2001 From: Greg Lucas Date: Sun, 26 Feb 2023 19:20:50 -0700 Subject: [PATCH 25/40] MNT: Use WeakKeyDictionary and WeakSet in Grouper Rather than handling the weakrefs ourselves, just use the builtin WeakKeyDictionary instead. This will automatically remove dead references meaning we can remove the clean() method. --- .../deprecations/25352-GL.rst | 4 ++ lib/matplotlib/axes/_base.py | 2 - lib/matplotlib/cbook.py | 42 +++++++------------ lib/matplotlib/tests/test_cbook.py | 7 ++-- lib/mpl_toolkits/mplot3d/axes3d.py | 3 -- 5 files changed, 23 insertions(+), 35 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/25352-GL.rst diff --git a/doc/api/next_api_changes/deprecations/25352-GL.rst b/doc/api/next_api_changes/deprecations/25352-GL.rst new file mode 100644 index 000000000000..e7edd57a6453 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/25352-GL.rst @@ -0,0 +1,4 @@ +``Grouper.clean()`` +~~~~~~~~~~~~~~~~~~~ + +with no replacement. The Grouper class now cleans itself up automatically. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 8e348fea4675..7893d7434f0d 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1363,8 +1363,6 @@ def __clear(self): self.xaxis.set_clip_path(self.patch) self.yaxis.set_clip_path(self.patch) - self._shared_axes["x"].clean() - self._shared_axes["y"].clean() if self._sharex is not None: self.xaxis.set_visible(xaxis_visible) self.patch.set_visible(patch_visible) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 1a64331e201d..3c97e26f6316 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -786,61 +786,53 @@ class Grouper: """ def __init__(self, init=()): - self._mapping = {weakref.ref(x): [weakref.ref(x)] for x in init} + self._mapping = weakref.WeakKeyDictionary( + {x: weakref.WeakSet([x]) for x in init}) def __getstate__(self): return { **vars(self), # Convert weak refs to strong ones. - "_mapping": {k(): [v() for v in vs] for k, vs in self._mapping.items()}, + "_mapping": {k: set(v) for k, v in self._mapping.items()}, } def __setstate__(self, state): vars(self).update(state) # Convert strong refs to weak ones. - self._mapping = {weakref.ref(k): [*map(weakref.ref, vs)] - for k, vs in self._mapping.items()} + self._mapping = weakref.WeakKeyDictionary( + {k: weakref.WeakSet(v) for k, v in self._mapping.items()}) def __contains__(self, item): - return weakref.ref(item) in self._mapping + return item in self._mapping + @_api.deprecated("3.8", alternative="none, you no longer need to clean a Grouper") def clean(self): """Clean dead weak references from the dictionary.""" - mapping = self._mapping - to_drop = [key for key in mapping if key() is None] - for key in to_drop: - val = mapping.pop(key) - val.remove(key) def join(self, a, *args): """ Join given arguments into the same set. Accepts one or more arguments. """ mapping = self._mapping - set_a = mapping.setdefault(weakref.ref(a), [weakref.ref(a)]) + set_a = mapping.setdefault(a, weakref.WeakSet([a])) for arg in args: - set_b = mapping.get(weakref.ref(arg), [weakref.ref(arg)]) + set_b = mapping.get(arg, weakref.WeakSet([arg])) if set_b is not set_a: if len(set_b) > len(set_a): set_a, set_b = set_b, set_a - set_a.extend(set_b) + set_a.update(set_b) for elem in set_b: mapping[elem] = set_a - self.clean() - def joined(self, a, b): """Return whether *a* and *b* are members of the same set.""" - self.clean() - return (self._mapping.get(weakref.ref(a), object()) - is self._mapping.get(weakref.ref(b))) + return (self._mapping.get(a, object()) is self._mapping.get(b)) def remove(self, a): - self.clean() - set_a = self._mapping.pop(weakref.ref(a), None) + set_a = self._mapping.pop(a, None) if set_a: - set_a.remove(weakref.ref(a)) + set_a.remove(a) def __iter__(self): """ @@ -848,16 +840,14 @@ def __iter__(self): The iterator is invalid if interleaved with calls to join(). """ - self.clean() unique_groups = {id(group): group for group in self._mapping.values()} for group in unique_groups.values(): - yield [x() for x in group] + yield [x for x in group] def get_siblings(self, a): """Return all of the items joined with *a*, including itself.""" - self.clean() - siblings = self._mapping.get(weakref.ref(a), [weakref.ref(a)]) - return [x() for x in siblings] + siblings = self._mapping.get(a, [a]) + return [x for x in siblings] class GrouperView: diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index aa5c999b7079..da9c187a323a 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -1,7 +1,6 @@ import itertools import pickle -from weakref import ref from unittest.mock import patch, Mock from datetime import datetime, date, timedelta @@ -590,11 +589,11 @@ class Dummy: mapping = g._mapping for o in objs: - assert ref(o) in mapping + assert o in mapping - base_set = mapping[ref(objs[0])] + base_set = mapping[objs[0]] for o in objs[1:]: - assert mapping[ref(o)] is base_set + assert mapping[o] is base_set def test_flatiter(): diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index cb31aca6459e..67f438f107dd 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -640,7 +640,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, _tight = self._tight = bool(tight) if scalex and self.get_autoscalex_on(): - self._shared_axes["x"].clean() x0, x1 = self.xy_dataLim.intervalx xlocator = self.xaxis.get_major_locator() x0, x1 = xlocator.nonsingular(x0, x1) @@ -653,7 +652,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self.set_xbound(x0, x1) if scaley and self.get_autoscaley_on(): - self._shared_axes["y"].clean() y0, y1 = self.xy_dataLim.intervaly ylocator = self.yaxis.get_major_locator() y0, y1 = ylocator.nonsingular(y0, y1) @@ -666,7 +664,6 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, self.set_ybound(y0, y1) if scalez and self.get_autoscalez_on(): - self._shared_axes["z"].clean() z0, z1 = self.zz_dataLim.intervalx zlocator = self.zaxis.get_major_locator() z0, z1 = zlocator.nonsingular(z0, z1) From 7528257eb22a6788cf858e7888848376698e144e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 1 Mar 2023 18:24:21 +0100 Subject: [PATCH 26/40] Remove unused menu field from macos NavigationToolbar2. The attribute is never referenced in the entire file; it appears to be a leftover of old style navigation toolbars, which were removed in 28effb4 and earlier. --- src/_macosx.m | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_macosx.m b/src/_macosx.m index 1fd987bb4825..30fdf8fbd75c 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -865,7 +865,6 @@ - (void)save_figure:(id)sender; typedef struct { PyObject_HEAD - NSPopUpButton* menu; NSTextView* messagebox; NavigationToolbar2Handler* handler; int height; From f2bcaefe768255a31312d9f8b0578f3fe2cd364f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 27 Feb 2023 18:59:42 -0500 Subject: [PATCH 27/40] Disable discarded animation warning on save --- lib/matplotlib/animation.py | 14 +++++++++----- lib/matplotlib/tests/test_animation.py | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 3a5d0eaf6bbf..b4cf9e26ab18 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -992,6 +992,15 @@ def func(current_frame: int, total_frames: int) -> Any is a `.MovieWriter`, a `RuntimeError` will be raised. """ + all_anim = [self] + if extra_anim is not None: + all_anim.extend(anim for anim in extra_anim + if anim._fig is self._fig) + + # Disable "Animation was deleted without rendering" warning. + for anim in all_anim: + anim._draw_was_started = True + if writer is None: writer = mpl.rcParams['animation.writer'] elif (not isinstance(writer, str) and @@ -1030,11 +1039,6 @@ def func(current_frame: int, total_frames: int) -> Any if metadata is not None: writer_kwargs['metadata'] = metadata - all_anim = [self] - if extra_anim is not None: - all_anim.extend(anim for anim in extra_anim - if anim._fig is self._fig) - # If we have the name of a writer, instantiate an instance of the # registered class. if isinstance(writer, str): diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index 9f41ff5cc69c..49e6a374ea57 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -514,5 +514,5 @@ def test_movie_writer_invalid_path(anim): else: match_str = re.escape("[Errno 2] No such file or directory: '/foo") with pytest.raises(FileNotFoundError, match=match_str): - _ = anim.save("/foo/bar/aardvark/thiscannotreallyexist.mp4", - writer=animation.FFMpegFileWriter()) + anim.save("/foo/bar/aardvark/thiscannotreallyexist.mp4", + writer=animation.FFMpegFileWriter()) From ad0bac9482bb1d0d17ec2575bc3f2938f6579824 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 2 Mar 2023 02:55:23 -0500 Subject: [PATCH 28/40] BLD: Pre-download Qhull license to put in wheels We couldn't work out what exactly changed to stop the license being included, so work around that by pre-downloading it. Fixes #25212 --- .github/workflows/cibuildwheel.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index b776979b67ed..82da1890660f 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -61,6 +61,14 @@ jobs: with: fetch-depth: 0 + # Something changed somewhere that prevents the downloaded-at-build-time + # licenses from being included in built wheels, so pre-download them so + # that they exist before the build and are included. + - name: Pre-download bundled licenses + run: > + curl -Lo LICENSE/LICENSE_QHULL + https://github.com/qhull/qhull/raw/2020.2/COPYING.txt + - name: Build wheels for CPython 3.11 uses: pypa/cibuildwheel@v2.12.0 env: From 775411298857fa8db31daa1c2bebad75912522c3 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 2 Mar 2023 15:23:49 -0500 Subject: [PATCH 29/40] Pin sphinx themes more strictly --- requirements/doc/doc-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 5b21c338782c..1c237ea7abb8 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -13,8 +13,8 @@ ipython ipywidgets numpydoc>=1.0 packaging>=20 -pydata-sphinx-theme>=0.12.0 -mpl-sphinx-theme>=3.7.0 +pydata-sphinx-theme~=0.12.0 +mpl-sphinx-theme~=3.7.0 pyyaml sphinxcontrib-svg2pdfconverter>=1.1.0 sphinx-gallery>=0.10 From a3f41149319db721cc233b98139c0c4aa53502a1 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 2 Mar 2023 21:20:43 -0500 Subject: [PATCH 30/40] Tk: Fix size of spacers when changing display DPI Fixes #25365 --- lib/matplotlib/backends/_backend_tk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 5da9d491ebaf..4ed5a1ee6850 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -677,8 +677,8 @@ def _rescale(self): # Text-only button is handled by the font setting instead. pass elif isinstance(widget, tk.Frame): - widget.configure(height='22p', pady='1p') - widget.pack_configure(padx='4p') + widget.configure(height='18p') + widget.pack_configure(padx='3p') elif isinstance(widget, tk.Label): pass # Text is handled by the font setting instead. else: From bf5bd7681726ef9716d34098b46d2a292528d92a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 2 Mar 2023 21:28:41 -0500 Subject: [PATCH 31/40] Clean up Curve ArrowStyle docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit By combining A/B parameters, to match the style of the CurveBracket, CurveFilled, etc. Also, tweak the example image so that it is not scaled down in the docs, and explicitly shows 0°. --- .../angles_on_bracket_arrows.py | 60 ++++++++++--------- lib/matplotlib/patches.py | 33 ++++------ 2 files changed, 42 insertions(+), 51 deletions(-) diff --git a/galleries/examples/text_labels_and_annotations/angles_on_bracket_arrows.py b/galleries/examples/text_labels_and_annotations/angles_on_bracket_arrows.py index bb536cd5d2c3..a23c7adc3897 100644 --- a/galleries/examples/text_labels_and_annotations/angles_on_bracket_arrows.py +++ b/galleries/examples/text_labels_and_annotations/angles_on_bracket_arrows.py @@ -23,37 +23,39 @@ def get_point_of_rotated_vertical(origin, line_length, degrees): origin[1] + line_length * np.cos(rad)] -fig, ax = plt.subplots(figsize=(8, 7)) -ax.set(xlim=(0, 6), ylim=(-1, 4)) +fig, ax = plt.subplots() +ax.set(xlim=(0, 6), ylim=(-1, 5)) ax.set_title("Orientation of the bracket arrows relative to angleA and angleB") -for i, style in enumerate(["]-[", "|-|"]): - for j, angle in enumerate([-40, 60]): - y = 2*i + j - arrow_centers = ((1, y), (5, y)) - vlines = ((1, y + 0.5), (5, y + 0.5)) - anglesAB = (angle, -angle) - bracketstyle = f"{style}, angleA={anglesAB[0]}, angleB={anglesAB[1]}" - bracket = FancyArrowPatch(*arrow_centers, arrowstyle=bracketstyle, - mutation_scale=42) - ax.add_patch(bracket) - ax.text(3, y + 0.05, bracketstyle, ha="center", va="bottom") - ax.vlines([i[0] for i in vlines], [y, y], [i[1] for i in vlines], - linestyles="--", color="C0") - # Get the top coordinates for the drawn patches at A and B - patch_tops = [get_point_of_rotated_vertical(center, 0.5, angle) - for center, angle in zip(arrow_centers, anglesAB)] - # Define the connection directions for the annotation arrows - connection_dirs = (1, -1) if angle > 0 else (-1, 1) - # Add arrows and annotation text - arrowstyle = "Simple, tail_width=0.5, head_width=4, head_length=8" - for vline, dir, patch_top, angle in zip(vlines, connection_dirs, - patch_tops, anglesAB): - kw = dict(connectionstyle=f"arc3,rad={dir * 0.5}", - arrowstyle=arrowstyle, color="C0") - ax.add_patch(FancyArrowPatch(vline, patch_top, **kw)) - ax.text(vline[0] - dir * 0.15, y + 0.3, f'{angle}°', ha="center", - va="center") +style = ']-[' +for i, angle in enumerate([-40, 0, 60]): + y = 2*i + arrow_centers = ((1, y), (5, y)) + vlines = ((1, y + 0.5), (5, y + 0.5)) + anglesAB = (angle, -angle) + bracketstyle = f"{style}, angleA={anglesAB[0]}, angleB={anglesAB[1]}" + bracket = FancyArrowPatch(*arrow_centers, arrowstyle=bracketstyle, + mutation_scale=42) + ax.add_patch(bracket) + ax.text(3, y + 0.05, bracketstyle, ha="center", va="bottom", fontsize=14) + ax.vlines([line[0] for line in vlines], [y, y], [line[1] for line in vlines], + linestyles="--", color="C0") + # Get the top coordinates for the drawn patches at A and B + patch_tops = [get_point_of_rotated_vertical(center, 0.5, angle) + for center, angle in zip(arrow_centers, anglesAB)] + # Define the connection directions for the annotation arrows + connection_dirs = (1, -1) if angle > 0 else (-1, 1) + # Add arrows and annotation text + arrowstyle = "Simple, tail_width=0.5, head_width=4, head_length=8" + for vline, dir, patch_top, angle in zip(vlines, connection_dirs, + patch_tops, anglesAB): + kw = dict(connectionstyle=f"arc3,rad={dir * 0.5}", + arrowstyle=arrowstyle, color="C0") + ax.add_patch(FancyArrowPatch(vline, patch_top, **kw)) + ax.text(vline[0] - dir * 0.15, y + 0.7, f'{angle}°', ha="center", + va="center") + +plt.show() # %% # diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index fb6fec46622b..e60a14f592fb 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -3197,29 +3197,18 @@ def __init__(self, head_length=.4, head_width=.2, widthA=1., widthB=1., Parameters ---------- head_length : float, default: 0.4 - Length of the arrow head, relative to *mutation_scale*. + Length of the arrow head, relative to *mutation_size*. head_width : float, default: 0.2 - Width of the arrow head, relative to *mutation_scale*. - widthA : float, default: 1.0 - Width of the bracket at the beginning of the arrow - widthB : float, default: 1.0 - Width of the bracket at the end of the arrow - lengthA : float, default: 0.2 - Length of the bracket at the beginning of the arrow - lengthB : float, default: 0.2 - Length of the bracket at the end of the arrow - angleA : float, default 0 - Orientation of the bracket at the beginning, as a - counterclockwise angle. 0 degrees means perpendicular - to the line. - angleB : float, default 0 - Orientation of the bracket at the beginning, as a - counterclockwise angle. 0 degrees means perpendicular - to the line. - scaleA : float, default *mutation_size* - The mutation_size for the beginning bracket - scaleB : float, default *mutation_size* - The mutation_size for the end bracket + Width of the arrow head, relative to *mutation_size*. + widthA, widthB : float, default: 1.0 + Width of the bracket. + lengthA, lengthB : float, default: 0.2 + Length of the bracket. + angleA, angleB : float, default: 0 + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. + scaleA, scaleB : float, default: *mutation_size* + The scale of the brackets. """ self.head_length, self.head_width = head_length, head_width From d9315e95a3b1e16b13cfa801aa1bee2f5767c800 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Fri, 3 Mar 2023 16:59:21 +0000 Subject: [PATCH 32/40] "Inactive" workflow: bump operations-per-run [skip ci] --- .github/workflows/stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a6c7e28e94da..b73e517f698a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/stale@v7 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - operations-per-run: 30 + operations-per-run: 50 stale-pr-message: 'Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it.' stale-pr-label: 'inactive' days-before-pr-stale: 60 From 42777afbc0e091a379defe2574acfe7eb3275ea5 Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sat, 18 Feb 2023 08:59:26 +0100 Subject: [PATCH 33/40] added Ishikawa plot in response to issue #25222 add organizational charts to supported plots --- .../examples/specialty_plots/ishikawa.py | 236 ++++++++++++++++++ .../examples/specialty_plots/swotplot.py | 180 +++++++++++++ 2 files changed, 416 insertions(+) create mode 100644 galleries/examples/specialty_plots/ishikawa.py create mode 100644 galleries/examples/specialty_plots/swotplot.py diff --git a/galleries/examples/specialty_plots/ishikawa.py b/galleries/examples/specialty_plots/ishikawa.py new file mode 100644 index 000000000000..5e052dd8fe30 --- /dev/null +++ b/galleries/examples/specialty_plots/ishikawa.py @@ -0,0 +1,236 @@ +# ============================================================================= +# Importing +# ============================================================================= +# Python Libraries +from typing import List + +import numpy as np + +from pathlib import Path + +import matplotlib.pyplot as plt +import matplotlib + +# ============================================================================= +# AUX FUNCTIONS +# ============================================================================= + +def drawSpines(data:dict,ax:plt.Axes,parentSpine:None,alpha_ps:float=60.0,alpha_ss:float=0,primary_spines_rel_xpos: List[float] = [np.nan], + spine_color:str="black",recLevel:int=1,MAX_RECURSION_LEVEL:int=4,DEBUG:bool=False): + ''' + Draw an Ishikawa spine with recursion + + Parameters + ---------- + data : dict + dictionary structure containing problem, causes... + ax : plt.Axes + Matplotlib current axes + parentSpine : None + Parent spine object + alpha_ps : float, optional + First spine angle using during recursion. The default is 60.0. + alpha_ss : float, optional + Secondary spine angle using during recursion. The default is 0. + primary_spines_rel_xpos : List[float], optional + Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. + spine_color : str, optional + Spine color. The default is "black". + recLevel : int, optional + Current recursion level. The default is 1. + MAX_RECURSION_LEVEL : int, optional + Maximum recursion level set. The default is 4. + DEBUG : bool, optional + Print debug variables. The default is False. + + Raises + ------ + AttributeError + Maximum recursion level reached or passed a bad parentSpine object format. + + Returns + ------- + None. + + ''' + #stop recursion if maximum level reached + if recLevel > MAX_RECURSION_LEVEL: + raise AttributeError('Max Recursion Level Reached') + + + if isinstance(data, dict): + # switch to correct angle depending on recursion level + if recLevel % 2 != 0: + alpha = alpha_ps + else: + alpha = alpha_ss + + if isinstance(parentSpine,matplotlib.lines.Line2D): + #calculate parent data + ([xpb,xpe],[ypb,ype]) = parentSpine.get_data() + elif isinstance(parentSpine, matplotlib.text.Annotation): + xpb,ypb = parentSpine.xy + xpe,ype = parentSpine._x, parentSpine._y#parentSpine._arrow_relpos + else: + raise AttributeError('Wrong Spine Graphical Element') + + plen = np.sqrt((xpe-xpb)**2+(ype-ypb)**2) + palpha = np.round(np.degrees(np.arctan2(ype-ypb,xpe-xpb))) + + + # calculate spacing + pairs = len(list(data.keys())) // 2 + len(list(data.keys())) % 2 +1 #calculate couple pairs, at least 1 pair to start at middle branch + spacing = (plen) / pairs + + # checking of main spines + + + spine_count=0 + s=spacing + #draw spine + for k in data.keys(): + # calculate arrow position in the graph + if recLevel==1: #fix primary spines spacing + if len(primary_spines_rel_xpos)==len(list(data.keys())): #check of list of float is already covered in function def + x_end = primary_spines_rel_xpos[spine_count] + else: + x_end = xpe-(s/1.5)*np.cos(np.radians(palpha)) + y_end = ype-s*np.sin(np.radians(palpha)) + + x_start = x_end -s*np.cos(np.radians(alpha)) + y_start = y_end -s*np.sin(np.radians(alpha)) + else: + x_end = xpe-s*np.cos(np.radians(palpha)) + y_end = ype-s*np.sin(np.radians(palpha)) + + x_start = x_end -s*np.cos(np.radians(alpha)) + y_start = y_end -s*np.sin(np.radians(alpha)) + if DEBUG == True: + print(f'k: {k}, x_start: {x_start}, y_start: {y_start}, x_end: {x_end}, y_end: {y_end}, alpha: {alpha} \n recLevel: {recLevel}, s:{s}, plen:{plen}, palpha:{palpha}') + + + # draw arrow arc + if recLevel==1: + props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) + spine = ax.annotate(str.upper(k), xy=(x_end, y_end), xytext=(x_start, y_start),arrowprops=dict(arrowstyle="->",facecolor=spine_color),bbox=props,weight='bold') + else: + props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) + spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start),arrowprops=dict(arrowstyle="->",facecolor=spine_color),bbox=props) + # Call recursion to draw subspines + drawSpines(data=data[k],ax=ax, + parentSpine=spine,alpha_ps=alpha_ps,alpha_ss=alpha_ss,spine_color=spine_color, + recLevel=recLevel+1,MAX_RECURSION_LEVEL=MAX_RECURSION_LEVEL) #no primary_spines_rel_xpos is needed to be passed on recursion + + # next spine settings - same level + alpha *= -1 + spine_count += 1 + if spine_count %2 ==0: + s= s+spacing + return None + +def ishikawaplot(data,figSize=(20,10),left_margin:float=0.05,right_margin:float=0.05, alpha_ps:float=60.0,alpha_ss= 0.0, primary_spines_rel_xpos: List[float] = [np.nan], + pd_width:int=0.1,spine_color:str="black") -> plt.figure: + ''' + + + Parameters + ---------- + data : TYPE + DESCRIPTION. + figSize : TYPE, optional + Matplotlib figure size. The default is (20,10). + left_margin : float, optional + Left spacing from frame border. The default is 0.05. + right_margin : float, optional + Right spacing from frame border. The default is 0.05. + alpha_ps : float, optional + First spine angle using during recursion. The default is 60.0. + alpha_ss : TYPE, optional + Secondary spine angle using during recursion. The default is 0.0. + primary_spines_rel_xpos : list, optional + Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. + pd_width : int, optional + Problem description box relative width. The default is 0.1. + spine_color : str, optional + Spine color. The default is "black". + + Returns + ------- + fig : matplotlib.pyplot.figure + Figure object containing the Ishikawa plot + + ''' + + fig = plt.figure(figsize=figSize) + ax = fig.gca() + + #format axis + ax.set_xlim(0,1.0) + ax.set_ylim(0,1.0) + ax.axis('off') + + + #draw main spine + mainSpine = ax.axhline(y=0.5,xmin=left_margin,xmax=1-right_margin-pd_width,color=spine_color) + + #draw fish head + props = dict(boxstyle='round', facecolor='wheat', alpha=1.0) + # add problem tag + ax.text((1-right_margin-pd_width), 0.5, str.upper(list(data.keys())[0]), fontsize=12,weight='bold',verticalalignment='center',horizontalalignment='left', bbox=props) + + #draw fish tail + x = (0.0,0.0,left_margin) + y = (0.5-left_margin,0.5+left_margin,0.5) + ax.fill(x,y,color=spine_color) + + #draw spines with recursion + drawSpines(data=data[list(data.keys())[0]],ax=ax,parentSpine=mainSpine,alpha_ps = alpha_ps, alpha_ss = alpha_ss, spine_color=spine_color,primary_spines_rel_xpos=primary_spines_rel_xpos) + + + plt.tight_layout() + return fig + +# ============================================================================= +# Rooting +# ============================================================================= +current_dir = Path(__file__).parent +output_path = current_dir / "Ishikawa.jpeg" + + +# ============================================================================= +# USER DATA +# ============================================================================= +data = {'problem': {'machine': {'cause1':''}, + 'process': {'cause2': {'subcause1':'','subcause2':''} + }, + 'man': {'cause3': {'subcause3':'','subcause4':''} + }, + 'design': {'cause4': {'subcause5':'','subcause6':''} + } + + } + } + + +# ============================================================================= +# MAIN CALL +# ============================================================================= +if __name__ == '__main__': + + #try also without opt primary_spines rel_xpos + fig = ishikawaplot(data,figSize=(20,10),primary_spines_rel_xpos=[0.8,0.7,0.6,0.4]) + + fig.show() + + fig.savefig(output_path, + #dpi=800, + format=None, + #metadata=None, + bbox_inches=None, + pad_inches=0.0, + facecolor='auto', + edgecolor='auto', + orientation='landscape', + transparent=False, + backend=None) + \ No newline at end of file diff --git a/galleries/examples/specialty_plots/swotplot.py b/galleries/examples/specialty_plots/swotplot.py new file mode 100644 index 000000000000..e3f30452833b --- /dev/null +++ b/galleries/examples/specialty_plots/swotplot.py @@ -0,0 +1,180 @@ +""" +============ +SWOT Diagram +============ + +SWOT analysis (or SWOT matrix) is a strategic planning and strategic management +technique used to help a person or organization identify Strengths, Weaknesses, +Opportunities, and Threats related to business competition or project planning. +It is sometimes called situational assessment or situational analysis. +Additional acronyms using the same components include TOWS and WOTS-UP. +Source: https://en.wikipedia.org/wiki/SWOT_analysis + +""" +import logging +from typing import Tuple + +import matplotlib.pyplot as plt + +_log = logging.getLogger(__name__) + + +def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, + q_x: Tuple[float,float,float,float], + q_y: Tuple[float,float,float,float], + q_title_pos: Tuple[float,float] = (0.0, 0.0), + q_title_props: dict = dict(boxstyle='round', + facecolor="white", + alpha=1.0), + q_point_props: dict = dict(boxstyle='round', + facecolor="white", + alpha=1.0) + ) -> None: + """ + Draw a quadrant of the SWOT plot + + Parameters + ---------- + q_data : dict + Data structure passed to populate the quadrant. + Format 'key':Tuple(float,float) + ax : plt.Axes + Matplotlib current axes + q_color : str + Quadrant color. + q_x : Tuple[float] + Quadrant X relative coordinates. Format: Tuple(float,float,float,float) + q_y : Tuple[float] + Quadrant Y relative coordinates. Format: Tuple(float,float,float,float) + q_title_pos : Tuple[float,float] + Plot title relative position + q_title_props : dict, optional + Title box style properties. The default is dict(boxstyle='round', + facecolor="white", alpha=1.0). + q_point_props : dict, optional + Point box style properties. The default is dict(boxstyle='round', + facecolor="white", alpha=1.0). + + Returns + ------- + None + + """ + # draw filled rectangle + ax.fill(q_x, q_y, color=q_color) + + # draw quadrant title + ax.text(x=q_title_pos[0], y=q_title_pos[1], + s=str.upper(list(q_data.keys())[0]), fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='center', + bbox=q_title_props) + + # fill quadrant with points + for k, v in q_data[list(q_data.keys())[0]].items(): + ax.text(x=v[0], y=v[1], s=k, fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='center', + bbox=q_point_props) + _log.debug(f"k: {k}, v: {v}") + + return None + + +def swotplot(p_data: dict, ax: plt.Axes, + s_color: str = "forestgreen", w_color: str = "gold", + o_color: str = "skyblue", t_color: str = "firebrick", + left_margin: float = 0.05, right_margin: float = 0.05, + top_margin: float = 0.05, bottom_margin: float = 0.05, + ) -> None: + """ + Draw SWOT plot + + Parameters + ---------- + p_data : dict + Data structure passed to populate the plot. + ax : matplotlib.pyplot.Axes + axes in which to draw the plot + s_color : float, optional + Strength quadrant color. + w_color : float, optional + Weakness quadrant color. + o_color : float, optional + Opportunity quadrant color. + t_color : float, optional + Threat quadrant color. + left_margin : float, optional + Left spacing from frame border. The default is 0.05. + right_margin : float, optional + Right spacing from frame border. The default is 0.05. + top_margin : float, optional + Top spacing from frame border. The default is 0.05. + bottom_margin : float, optional + Bottom spacing from frame border. The default is 0.05. + + + Returns + ------- + None + + """ + # format axis + ax.set_xlim(0, 1.0) + ax.set_ylim(0, 1.0) + ax.axis('off') + + # draw s quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[0], + p_data.items())), + ax=ax, + q_color=s_color, q_x=(left_margin, left_margin, 0.5, 0.5), + q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), + q_title_pos=(0.25 + left_margin / 2, 1 - top_margin)) + + # draw w quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[1], + p_data.items())), + ax=ax, + q_color=w_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), + q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), + q_title_pos=(0.25 + (1 - right_margin) / 2, 1 - top_margin)) + + # draw o quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[2], + p_data.items())), + ax=ax, + q_color=o_color, q_x=(left_margin, left_margin, 0.5, 0.5), + q_y=(bottom_margin, 0.5, 0.5, bottom_margin), + q_title_pos=(0.25 + left_margin / 2, bottom_margin)) + + # draw t quadrant + draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[3], + p_data.items())), + ax=ax, + q_color=t_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), + q_y=(bottom_margin, 0.5, 0.5, bottom_margin), + q_title_pos=(0.25 + (1 - right_margin) / 2, bottom_margin)) + + return None + + +# USER DATA +# relative x_pos,y_pos make it easy to manage particular cases such as +# soft categorization +data = {'strength': {'SW automation': (0.3, 0.6), + 'teamwork': (0.4, 0.5) + }, + 'weakness': {'delayed payment': (0.5, 0.7), + 'hard to learn': (0.7, 0.9) + }, + 'opportunity': {'dedicated training': (0.3, 0.3), + 'just-in-time development': (0.2, 0.1) + }, + 'threat': {'project de-prioritization': (0.7, 0.3), + 'external consultancy': (0.5, 0.1) + }, + } + +# SWOT plot generation +fig = plt.figure(figsize=(12, 6), layout='constrained') +ax = fig.gca() +swotplot(data, ax) From 9cd719c991af1b17690d9267b33ec5b96481be5c Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sun, 19 Feb 2023 11:12:49 +0100 Subject: [PATCH 34/40] fixing code style for flake8 first trial --- .../examples/specialty_plots/ishikawa.py | 257 +++++++++--------- 1 file changed, 126 insertions(+), 131 deletions(-) diff --git a/galleries/examples/specialty_plots/ishikawa.py b/galleries/examples/specialty_plots/ishikawa.py index 5e052dd8fe30..f3b096ba6530 100644 --- a/galleries/examples/specialty_plots/ishikawa.py +++ b/galleries/examples/specialty_plots/ishikawa.py @@ -1,23 +1,23 @@ -# ============================================================================= -# Importing -# ============================================================================= -# Python Libraries -from typing import List +""" +=============== +Ishikawa Diagrams +=============== -import numpy as np +Ishikawa Diagrams are useful for visualizing the effect to many cause relationships -from pathlib import Path +""" +from typing import List +import numpy as np +from pathlib import Path import matplotlib.pyplot as plt import matplotlib -# ============================================================================= -# AUX FUNCTIONS -# ============================================================================= -def drawSpines(data:dict,ax:plt.Axes,parentSpine:None,alpha_ps:float=60.0,alpha_ss:float=0,primary_spines_rel_xpos: List[float] = [np.nan], - spine_color:str="black",recLevel:int=1,MAX_RECURSION_LEVEL:int=4,DEBUG:bool=False): - ''' +def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60.0, alpha_ss: float = 0, + primary_spines_rel_xpos: List[float] = [np.nan], + spine_color: str = "black", reclevel: int = 1, max_recursion_level: int = 4, debug: bool = False): + """ Draw an Ishikawa spine with recursion Parameters @@ -26,7 +26,7 @@ def drawSpines(data:dict,ax:plt.Axes,parentSpine:None,alpha_ps:float=60.0,alpha_ dictionary structure containing problem, causes... ax : plt.Axes Matplotlib current axes - parentSpine : None + parentspine : None Parent spine object alpha_ps : float, optional First spine angle using during recursion. The default is 60.0. @@ -36,108 +36,108 @@ def drawSpines(data:dict,ax:plt.Axes,parentSpine:None,alpha_ps:float=60.0,alpha_ Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. spine_color : str, optional Spine color. The default is "black". - recLevel : int, optional + reclevel : int, optional Current recursion level. The default is 1. - MAX_RECURSION_LEVEL : int, optional + max_recursion_level : int, optional Maximum recursion level set. The default is 4. - DEBUG : bool, optional + debug : bool, optional Print debug variables. The default is False. Raises ------ AttributeError - Maximum recursion level reached or passed a bad parentSpine object format. + Maximum recursion level reached or passed a bad parentspine object format. Returns ------- None. - ''' - #stop recursion if maximum level reached - if recLevel > MAX_RECURSION_LEVEL: + """ + # stop recursion if maximum level reached + if reclevel > max_recursion_level: raise AttributeError('Max Recursion Level Reached') - if isinstance(data, dict): # switch to correct angle depending on recursion level - if recLevel % 2 != 0: + if reclevel % 2 != 0: alpha = alpha_ps else: alpha = alpha_ss - - if isinstance(parentSpine,matplotlib.lines.Line2D): - #calculate parent data - ([xpb,xpe],[ypb,ype]) = parentSpine.get_data() - elif isinstance(parentSpine, matplotlib.text.Annotation): - xpb,ypb = parentSpine.xy - xpe,ype = parentSpine._x, parentSpine._y#parentSpine._arrow_relpos + + if isinstance(parentspine, matplotlib.lines.Line2D): + # calculate parent data + ([xpb, xpe], [ypb, ype]) = parentspine.get_data() + elif isinstance(parentspine, matplotlib.text.Annotation): + xpb, ypb = parentspine.xy + xpe, ype = parentspine._x, parentspine._y # parentspine._arrow_relpos else: raise AttributeError('Wrong Spine Graphical Element') - - plen = np.sqrt((xpe-xpb)**2+(ype-ypb)**2) - palpha = np.round(np.degrees(np.arctan2(ype-ypb,xpe-xpb))) - - - # calculate spacing - pairs = len(list(data.keys())) // 2 + len(list(data.keys())) % 2 +1 #calculate couple pairs, at least 1 pair to start at middle branch - spacing = (plen) / pairs - - # checking of main spines - - - spine_count=0 - s=spacing - #draw spine + + plen = np.sqrt((xpe - xpb) ** 2 + (ype - ypb) ** 2) + palpha = np.round(np.degrees(np.arctan2(ype - ypb, xpe - xpb))) + + # calculate spine spacing + # calculate couple pairs, at least 1 pair to start at middle branch + pairs = len(list(data.keys())) // 2 + len(list(data.keys())) % 2 + 1 + spacing = plen / pairs + + spine_count = 0 + s = spacing + # draw spine for k in data.keys(): # calculate arrow position in the graph - if recLevel==1: #fix primary spines spacing - if len(primary_spines_rel_xpos)==len(list(data.keys())): #check of list of float is already covered in function def + # fix primary spines spacing + if reclevel == 1: + if len(primary_spines_rel_xpos) == len(list(data.keys())): x_end = primary_spines_rel_xpos[spine_count] else: - x_end = xpe-(s/1.5)*np.cos(np.radians(palpha)) - y_end = ype-s*np.sin(np.radians(palpha)) - - x_start = x_end -s*np.cos(np.radians(alpha)) - y_start = y_end -s*np.sin(np.radians(alpha)) + x_end = xpe - (s / 1.5) * np.cos(np.radians(palpha)) + y_end = ype - s * np.sin(np.radians(palpha)) + + x_start = x_end - s * np.cos(np.radians(alpha)) + y_start = y_end - s * np.sin(np.radians(alpha)) else: - x_end = xpe-s*np.cos(np.radians(palpha)) - y_end = ype-s*np.sin(np.radians(palpha)) - - x_start = x_end -s*np.cos(np.radians(alpha)) - y_start = y_end -s*np.sin(np.radians(alpha)) - if DEBUG == True: - print(f'k: {k}, x_start: {x_start}, y_start: {y_start}, x_end: {x_end}, y_end: {y_end}, alpha: {alpha} \n recLevel: {recLevel}, s:{s}, plen:{plen}, palpha:{palpha}') - - + x_end = xpe - s * np.cos(np.radians(palpha)) + y_end = ype - s * np.sin(np.radians(palpha)) + + x_start = x_end - s * np.cos(np.radians(alpha)) + y_start = y_end - s * np.sin(np.radians(alpha)) + if debug: + print( + f'k: {k}, x_start: {x_start}, y_start: {y_start}, x_end: {x_end}, y_end: {y_end}, alpha: {alpha} \n reclevel: {reclevel}, s:{s}, plen:{plen}, palpha:{palpha}') + # draw arrow arc - if recLevel==1: + if reclevel == 1: props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) - spine = ax.annotate(str.upper(k), xy=(x_end, y_end), xytext=(x_start, y_start),arrowprops=dict(arrowstyle="->",facecolor=spine_color),bbox=props,weight='bold') + spine = ax.annotate(str.upper(k), xy=(x_end, y_end), xytext=(x_start, y_start), + arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props, weight='bold') else: props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) - spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start),arrowprops=dict(arrowstyle="->",facecolor=spine_color),bbox=props) + spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start), + arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props) # Call recursion to draw subspines - drawSpines(data=data[k],ax=ax, - parentSpine=spine,alpha_ps=alpha_ps,alpha_ss=alpha_ss,spine_color=spine_color, - recLevel=recLevel+1,MAX_RECURSION_LEVEL=MAX_RECURSION_LEVEL) #no primary_spines_rel_xpos is needed to be passed on recursion - + drawspines(data=data[k], ax=ax, parentspine=spine, alpha_ps=alpha_ps, alpha_ss=alpha_ss, + spine_color=spine_color, reclevel=reclevel + 1, + max_recursion_level=max_recursion_level) + # no primary_spines_rel_xpos is needed to be passed on recursion # next spine settings - same level alpha *= -1 spine_count += 1 - if spine_count %2 ==0: - s= s+spacing + if spine_count % 2 == 0: + s = s + spacing return None -def ishikawaplot(data,figSize=(20,10),left_margin:float=0.05,right_margin:float=0.05, alpha_ps:float=60.0,alpha_ss= 0.0, primary_spines_rel_xpos: List[float] = [np.nan], - pd_width:int=0.1,spine_color:str="black") -> plt.figure: - ''' - + +def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin: float = 0.05, alpha_ps: float = 60.0, + alpha_ss=0.0, primary_spines_rel_xpos: List[float] = [np.nan], + pd_width: int = 0.1, spine_color: str = "black") -> plt.figure: + """ Parameters ---------- data : TYPE DESCRIPTION. - figSize : TYPE, optional + figsize : TYPE, optional Matplotlib figure size. The default is (20,10). left_margin : float, optional Left spacing from frame border. The default is 0.05. @@ -159,78 +159,73 @@ def ishikawaplot(data,figSize=(20,10),left_margin:float=0.05,right_margin:float= fig : matplotlib.pyplot.figure Figure object containing the Ishikawa plot - ''' - - fig = plt.figure(figsize=figSize) - ax = fig.gca() - - #format axis - ax.set_xlim(0,1.0) - ax.set_ylim(0,1.0) + """ + + fig = plt.figure(figsize=figsize) + ax = fig.gca() + + # format axis + ax.set_xlim(0, 1.0) + ax.set_ylim(0, 1.0) ax.axis('off') - - #draw main spine - mainSpine = ax.axhline(y=0.5,xmin=left_margin,xmax=1-right_margin-pd_width,color=spine_color) - - #draw fish head + # draw main spine + main_spine = ax.axhline(y=0.5, xmin=left_margin, xmax=1 - right_margin - pd_width, color=spine_color) + + # draw fish head props = dict(boxstyle='round', facecolor='wheat', alpha=1.0) # add problem tag - ax.text((1-right_margin-pd_width), 0.5, str.upper(list(data.keys())[0]), fontsize=12,weight='bold',verticalalignment='center',horizontalalignment='left', bbox=props) - - #draw fish tail - x = (0.0,0.0,left_margin) - y = (0.5-left_margin,0.5+left_margin,0.5) - ax.fill(x,y,color=spine_color) - - #draw spines with recursion - drawSpines(data=data[list(data.keys())[0]],ax=ax,parentSpine=mainSpine,alpha_ps = alpha_ps, alpha_ss = alpha_ss, spine_color=spine_color,primary_spines_rel_xpos=primary_spines_rel_xpos) - - - plt.tight_layout() + ax.text((1 - right_margin - pd_width), 0.5, str.upper(list(data.keys())[0]), fontsize=12, weight='bold', + verticalalignment='center', horizontalalignment='left', bbox=props) + + # draw fish tail + x = (0.0, 0.0, left_margin) + y = (0.5 - left_margin, 0.5 + left_margin, 0.5) + ax.fill(x, y, color=spine_color) + + # draw spines with recursion + drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, alpha_ps=alpha_ps, alpha_ss=alpha_ss, + primary_spines_rel_xpos=primary_spines_rel_xpos, spine_color=spine_color) + + plt.tight_layout() return fig + # ============================================================================= # Rooting # ============================================================================= current_dir = Path(__file__).parent output_path = current_dir / "Ishikawa.jpeg" - # ============================================================================= # USER DATA # ============================================================================= -data = {'problem': {'machine': {'cause1':''}, - 'process': {'cause2': {'subcause1':'','subcause2':''} - }, - 'man': {'cause3': {'subcause3':'','subcause4':''} - }, - 'design': {'cause4': {'subcause5':'','subcause6':''} - } - - } - } +data = {'problem': {'machine': {'cause1': ''}, + 'process': {'cause2': {'subcause1': '', 'subcause2': ''} + }, + 'man': {'cause3': {'subcause3': '', 'subcause4': ''} + }, + 'design': {'cause4': {'subcause5': '', 'subcause6': ''} + } + + } + } -# ============================================================================= -# MAIN CALL -# ============================================================================= if __name__ == '__main__': - - #try also without opt primary_spines rel_xpos - fig = ishikawaplot(data,figSize=(20,10),primary_spines_rel_xpos=[0.8,0.7,0.6,0.4]) - + # try also without opt primary_spines rel_xpos + fig = ishikawaplot(data, figsize=(20, 10), primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) + fig.show() - - fig.savefig(output_path, - #dpi=800, - format=None, - #metadata=None, - bbox_inches=None, - pad_inches=0.0, - facecolor='auto', - edgecolor='auto', - orientation='landscape', - transparent=False, - backend=None) - \ No newline at end of file + + fig.savefig(output_path, + # dpi=800, + format=None, + # metadata=None, + bbox_inches=None, + pad_inches=0.0, + facecolor='auto', + edgecolor='auto', + orientation='landscape', + transparent=False, + backend=None) From 95fe3b1e5b2fa5a61958538e5c1e2bbc4b0d4fc1 Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sun, 19 Feb 2023 11:30:45 +0100 Subject: [PATCH 35/40] second fixing style typo --- galleries/examples/specialty_plots/ishikawa.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/galleries/examples/specialty_plots/ishikawa.py b/galleries/examples/specialty_plots/ishikawa.py index f3b096ba6530..57e07498398b 100644 --- a/galleries/examples/specialty_plots/ishikawa.py +++ b/galleries/examples/specialty_plots/ishikawa.py @@ -191,15 +191,12 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin return fig -# ============================================================================= # Rooting -# ============================================================================= current_dir = Path(__file__).parent output_path = current_dir / "Ishikawa.jpeg" -# ============================================================================= + # USER DATA -# ============================================================================= data = {'problem': {'machine': {'cause1': ''}, 'process': {'cause2': {'subcause1': '', 'subcause2': ''} }, From 9effc905245229f40758f3d0754f29461d30048f Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Tue, 21 Feb 2023 18:18:24 +0100 Subject: [PATCH 36/40] - revised comment style according to flake8 set rules - improved example description docstring - removed __main__ call - debug print converted into logging.debug strings - removed sohw fig and save fig and related Path library - removed exception from flake8 conf file --- .flake8 | 2 +- .../examples/specialty_plots/ishikawa.py | 88 ++++++++++--------- 2 files changed, 47 insertions(+), 43 deletions(-) diff --git a/.flake8 b/.flake8 index 450b743e87b1..87ece8254ddc 100644 --- a/.flake8 +++ b/.flake8 @@ -94,4 +94,4 @@ per-file-ignores = galleries/examples/user_interfaces/pylab_with_gtk3_sgskip.py: E402 galleries/examples/user_interfaces/pylab_with_gtk4_sgskip.py: E402 galleries/examples/userdemo/pgf_preamble_sgskip.py: E402 -force-check = True +force-check = True \ No newline at end of file diff --git a/galleries/examples/specialty_plots/ishikawa.py b/galleries/examples/specialty_plots/ishikawa.py index 57e07498398b..ce0826850803 100644 --- a/galleries/examples/specialty_plots/ishikawa.py +++ b/galleries/examples/specialty_plots/ishikawa.py @@ -3,20 +3,26 @@ Ishikawa Diagrams =============== -Ishikawa Diagrams are useful for visualizing the effect to many cause relationships - +Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect +diagrams are useful for visualizing the effect to many cause relationships """ +import logging from typing import List -import numpy as np -from pathlib import Path + import matplotlib.pyplot as plt +import numpy as np + import matplotlib +_log = logging.getLogger(__name__) + -def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60.0, alpha_ss: float = 0, +def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60.0, + alpha_ss: float = 0, primary_spines_rel_xpos: List[float] = [np.nan], - spine_color: str = "black", reclevel: int = 1, max_recursion_level: int = 4, debug: bool = False): + spine_color: str = "black", reclevel: int = 1, + max_recursion_level: int = 4, debug: bool = False): """ Draw an Ishikawa spine with recursion @@ -33,7 +39,8 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 alpha_ss : float, optional Secondary spine angle using during recursion. The default is 0. primary_spines_rel_xpos : List[float], optional - Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. + Relative X-position of primary spines, a list of float is accepted. + The default is [np.nan]. spine_color : str, optional Spine color. The default is "black". reclevel : int, optional @@ -103,20 +110,29 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 x_start = x_end - s * np.cos(np.radians(alpha)) y_start = y_end - s * np.sin(np.radians(alpha)) if debug: - print( - f'k: {k}, x_start: {x_start}, y_start: {y_start}, x_end: {x_end}, y_end: {y_end}, alpha: {alpha} \n reclevel: {reclevel}, s:{s}, plen:{plen}, palpha:{palpha}') + _log.debug( + f'k: {k}, x_start: {x_start}, y_start: {y_start}, ' + f'x_end: {x_end} y_end: {y_end}, alpha: {alpha} \n ' + f'reclevel: {reclevel}, ' + f's:{s}, plen:{plen}, palpha:{palpha}') # draw arrow arc if reclevel == 1: props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) - spine = ax.annotate(str.upper(k), xy=(x_end, y_end), xytext=(x_start, y_start), - arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props, weight='bold') + spine = ax.annotate(str.upper(k), xy=(x_end, y_end), + xytext=(x_start, y_start), + arrowprops=dict(arrowstyle="->", + facecolor=spine_color), + bbox=props, weight='bold') else: props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start), - arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props) + arrowprops=dict(arrowstyle="->", + facecolor=spine_color), + bbox=props) # Call recursion to draw subspines - drawspines(data=data[k], ax=ax, parentspine=spine, alpha_ps=alpha_ps, alpha_ss=alpha_ss, + drawspines(data=data[k], ax=ax, parentspine=spine, + alpha_ps=alpha_ps, alpha_ss=alpha_ss, spine_color=spine_color, reclevel=reclevel + 1, max_recursion_level=max_recursion_level) # no primary_spines_rel_xpos is needed to be passed on recursion @@ -128,7 +144,8 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 return None -def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin: float = 0.05, alpha_ps: float = 60.0, +def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, + right_margin: float = 0.05, alpha_ps: float = 60.0, alpha_ss=0.0, primary_spines_rel_xpos: List[float] = [np.nan], pd_width: int = 0.1, spine_color: str = "black") -> plt.figure: """ @@ -148,7 +165,8 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin alpha_ss : TYPE, optional Secondary spine angle using during recursion. The default is 0.0. primary_spines_rel_xpos : list, optional - Relative X-position of primary spines, a list of float is accepted. The default is [np.nan]. + Relative X-position of primary spines, a list of float is accepted. + The default is [np.nan]. pd_width : int, optional Problem description box relative width. The default is 0.1. spine_color : str, optional @@ -170,12 +188,15 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin ax.axis('off') # draw main spine - main_spine = ax.axhline(y=0.5, xmin=left_margin, xmax=1 - right_margin - pd_width, color=spine_color) + main_spine = ax.axhline(y=0.5, xmin=left_margin, + xmax=1 - right_margin - pd_width, + color=spine_color) # draw fish head props = dict(boxstyle='round', facecolor='wheat', alpha=1.0) # add problem tag - ax.text((1 - right_margin - pd_width), 0.5, str.upper(list(data.keys())[0]), fontsize=12, weight='bold', + ax.text((1 - right_margin - pd_width), 0.5, str.upper(list(data.keys())[0]), + fontsize=12, weight='bold', verticalalignment='center', horizontalalignment='left', bbox=props) # draw fish tail @@ -184,18 +205,15 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin ax.fill(x, y, color=spine_color) # draw spines with recursion - drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, alpha_ps=alpha_ps, alpha_ss=alpha_ss, - primary_spines_rel_xpos=primary_spines_rel_xpos, spine_color=spine_color) + drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, + alpha_ps=alpha_ps, alpha_ss=alpha_ss, + primary_spines_rel_xpos=primary_spines_rel_xpos, + spine_color=spine_color) plt.tight_layout() return fig -# Rooting -current_dir = Path(__file__).parent -output_path = current_dir / "Ishikawa.jpeg" - - # USER DATA data = {'problem': {'machine': {'cause1': ''}, 'process': {'cause2': {'subcause1': '', 'subcause2': ''} @@ -208,21 +226,7 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, right_margin } } - -if __name__ == '__main__': - # try also without opt primary_spines rel_xpos - fig = ishikawaplot(data, figsize=(20, 10), primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) - - fig.show() - - fig.savefig(output_path, - # dpi=800, - format=None, - # metadata=None, - bbox_inches=None, - pad_inches=0.0, - facecolor='auto', - edgecolor='auto', - orientation='landscape', - transparent=False, - backend=None) +# Ishikawa plot generation +# try also without opt primary_spines rel_xpos +fig = ishikawaplot(data, figsize=(20, 10), + primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) From 07135075b31f7451bb94c937b6e09d5fff040061 Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Wed, 22 Feb 2023 09:53:49 +0100 Subject: [PATCH 37/40] fixed file docstring for title overlay sphinx error --- galleries/examples/specialty_plots/ishikawa.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/galleries/examples/specialty_plots/ishikawa.py b/galleries/examples/specialty_plots/ishikawa.py index ce0826850803..b7e016672207 100644 --- a/galleries/examples/specialty_plots/ishikawa.py +++ b/galleries/examples/specialty_plots/ishikawa.py @@ -1,7 +1,7 @@ """ -=============== +================= Ishikawa Diagrams -=============== +================= Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect diagrams are useful for visualizing the effect to many cause relationships From 965ab5b40abcdc39b59adf019be49566e332920c Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sat, 4 Mar 2023 10:47:06 +0100 Subject: [PATCH 38/40] - fix PR rebase - added wikipedia link to ishikawa desc doc string - reformatted function def to work on axes object - fix plt.tight_layout leaving the user to choose - reduced fisize still mantaining a decent rendering - added swotplot.py for SWOT plots with the same logic, design pattern and style --- .../examples/specialty_plots/ishikawa.py | 29 +++++++------- .../examples/specialty_plots/swotplot.py | 38 +++++++++---------- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/galleries/examples/specialty_plots/ishikawa.py b/galleries/examples/specialty_plots/ishikawa.py index b7e016672207..d5fe8e0ff80e 100644 --- a/galleries/examples/specialty_plots/ishikawa.py +++ b/galleries/examples/specialty_plots/ishikawa.py @@ -5,6 +5,7 @@ Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect diagrams are useful for visualizing the effect to many cause relationships +Source: https://en.wikipedia.org/wiki/Ishikawa_diagram """ import logging @@ -144,18 +145,18 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 return None -def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, +def ishikawaplot(data: dict, ax: plt.Axes, left_margin: float = 0.05, right_margin: float = 0.05, alpha_ps: float = 60.0, alpha_ss=0.0, primary_spines_rel_xpos: List[float] = [np.nan], - pd_width: int = 0.1, spine_color: str = "black") -> plt.figure: + pd_width: int = 0.1, spine_color: str = "black") -> None: """ Parameters ---------- - data : TYPE - DESCRIPTION. - figsize : TYPE, optional - Matplotlib figure size. The default is (20,10). + data : dict + Plot data structure. + ax : matplotlib.pyplot.Axes + Axes in which to drow the plot. left_margin : float, optional Left spacing from frame border. The default is 0.05. right_margin : float, optional @@ -174,14 +175,9 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, Returns ------- - fig : matplotlib.pyplot.figure - Figure object containing the Ishikawa plot + None """ - - fig = plt.figure(figsize=figsize) - ax = fig.gca() - # format axis ax.set_xlim(0, 1.0) ax.set_ylim(0, 1.0) @@ -210,8 +206,7 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, primary_spines_rel_xpos=primary_spines_rel_xpos, spine_color=spine_color) - plt.tight_layout() - return fig + return None # USER DATA @@ -227,6 +222,8 @@ def ishikawaplot(data, figsize=(20, 10), left_margin: float = 0.05, } # Ishikawa plot generation +fig = plt.figure(figsize=(12, 6), layout='constrained') +ax = fig.gca() + # try also without opt primary_spines rel_xpos -fig = ishikawaplot(data, figsize=(20, 10), - primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) +ishikawaplot(data, ax, primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) diff --git a/galleries/examples/specialty_plots/swotplot.py b/galleries/examples/specialty_plots/swotplot.py index e3f30452833b..8fcdf9a9fba5 100644 --- a/galleries/examples/specialty_plots/swotplot.py +++ b/galleries/examples/specialty_plots/swotplot.py @@ -5,8 +5,8 @@ SWOT analysis (or SWOT matrix) is a strategic planning and strategic management technique used to help a person or organization identify Strengths, Weaknesses, -Opportunities, and Threats related to business competition or project planning. -It is sometimes called situational assessment or situational analysis. +Opportunities, and Threats related to business competition or project planning. +It is sometimes called situational assessment or situational analysis. Additional acronyms using the same components include TOWS and WOTS-UP. Source: https://en.wikipedia.org/wiki/SWOT_analysis @@ -20,9 +20,9 @@ def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, - q_x: Tuple[float,float,float,float], - q_y: Tuple[float,float,float,float], - q_title_pos: Tuple[float,float] = (0.0, 0.0), + q_x: Tuple[float, float, float, float], + q_y: Tuple[float, float, float, float], + q_title_pos: Tuple[float, float] = (0.0, 0.0), q_title_props: dict = dict(boxstyle='round', facecolor="white", alpha=1.0), @@ -36,23 +36,23 @@ def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, Parameters ---------- q_data : dict - Data structure passed to populate the quadrant. + Data structure passed to populate the quadrant. Format 'key':Tuple(float,float) ax : plt.Axes Matplotlib current axes q_color : str - Quadrant color. + Quadrant color. q_x : Tuple[float] - Quadrant X relative coordinates. Format: Tuple(float,float,float,float) + Quadrant X relative coordinates. Format: Tuple(float,float,float,float) q_y : Tuple[float] Quadrant Y relative coordinates. Format: Tuple(float,float,float,float) q_title_pos : Tuple[float,float] Plot title relative position q_title_props : dict, optional - Title box style properties. The default is dict(boxstyle='round', + Title box style properties. The default is dict(boxstyle='round', facecolor="white", alpha=1.0). q_point_props : dict, optional - Point box style properties. The default is dict(boxstyle='round', + Point box style properties. The default is dict(boxstyle='round', facecolor="white", alpha=1.0). Returns @@ -60,7 +60,7 @@ def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, None """ - # draw filled rectangle + # draw filled rectangle ax.fill(q_x, q_y, color=q_color) # draw quadrant title @@ -68,7 +68,7 @@ def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, s=str.upper(list(q_data.keys())[0]), fontsize=12, weight='bold', verticalalignment='center', horizontalalignment='center', bbox=q_title_props) - + # fill quadrant with points for k, v in q_data[list(q_data.keys())[0]].items(): ax.text(x=v[0], y=v[1], s=k, fontsize=12, weight='bold', @@ -87,11 +87,11 @@ def swotplot(p_data: dict, ax: plt.Axes, ) -> None: """ Draw SWOT plot - + Parameters ---------- p_data : dict - Data structure passed to populate the plot. + Data structure passed to populate the plot. ax : matplotlib.pyplot.Axes axes in which to draw the plot s_color : float, optional @@ -124,7 +124,7 @@ def swotplot(p_data: dict, ax: plt.Axes, # draw s quadrant draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[0], - p_data.items())), + p_data.items())), ax=ax, q_color=s_color, q_x=(left_margin, left_margin, 0.5, 0.5), q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), @@ -132,7 +132,7 @@ def swotplot(p_data: dict, ax: plt.Axes, # draw w quadrant draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[1], - p_data.items())), + p_data.items())), ax=ax, q_color=w_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), @@ -140,7 +140,7 @@ def swotplot(p_data: dict, ax: plt.Axes, # draw o quadrant draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[2], - p_data.items())), + p_data.items())), ax=ax, q_color=o_color, q_x=(left_margin, left_margin, 0.5, 0.5), q_y=(bottom_margin, 0.5, 0.5, bottom_margin), @@ -148,7 +148,7 @@ def swotplot(p_data: dict, ax: plt.Axes, # draw t quadrant draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[3], - p_data.items())), + p_data.items())), ax=ax, q_color=t_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), q_y=(bottom_margin, 0.5, 0.5, bottom_margin), @@ -158,7 +158,7 @@ def swotplot(p_data: dict, ax: plt.Axes, # USER DATA -# relative x_pos,y_pos make it easy to manage particular cases such as +# relative x_pos,y_pos make it easy to manage particular cases such as # soft categorization data = {'strength': {'SW automation': (0.3, 0.6), 'teamwork': (0.4, 0.5) From 709653c6fefcf27864bcb8718fc310bb65559753 Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sat, 4 Mar 2023 10:52:54 +0100 Subject: [PATCH 39/40] - fix examples folder --- examples/specialty_plots/ishikawa.py | 229 --------------------------- examples/specialty_plots/swotplot.py | 180 --------------------- 2 files changed, 409 deletions(-) delete mode 100644 examples/specialty_plots/ishikawa.py delete mode 100644 examples/specialty_plots/swotplot.py diff --git a/examples/specialty_plots/ishikawa.py b/examples/specialty_plots/ishikawa.py deleted file mode 100644 index d5fe8e0ff80e..000000000000 --- a/examples/specialty_plots/ishikawa.py +++ /dev/null @@ -1,229 +0,0 @@ -""" -================= -Ishikawa Diagrams -================= - -Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect -diagrams are useful for visualizing the effect to many cause relationships -Source: https://en.wikipedia.org/wiki/Ishikawa_diagram - -""" -import logging -from typing import List - -import matplotlib.pyplot as plt -import numpy as np - -import matplotlib - -_log = logging.getLogger(__name__) - - -def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60.0, - alpha_ss: float = 0, - primary_spines_rel_xpos: List[float] = [np.nan], - spine_color: str = "black", reclevel: int = 1, - max_recursion_level: int = 4, debug: bool = False): - """ - Draw an Ishikawa spine with recursion - - Parameters - ---------- - data : dict - dictionary structure containing problem, causes... - ax : plt.Axes - Matplotlib current axes - parentspine : None - Parent spine object - alpha_ps : float, optional - First spine angle using during recursion. The default is 60.0. - alpha_ss : float, optional - Secondary spine angle using during recursion. The default is 0. - primary_spines_rel_xpos : List[float], optional - Relative X-position of primary spines, a list of float is accepted. - The default is [np.nan]. - spine_color : str, optional - Spine color. The default is "black". - reclevel : int, optional - Current recursion level. The default is 1. - max_recursion_level : int, optional - Maximum recursion level set. The default is 4. - debug : bool, optional - Print debug variables. The default is False. - - Raises - ------ - AttributeError - Maximum recursion level reached or passed a bad parentspine object format. - - Returns - ------- - None. - - """ - # stop recursion if maximum level reached - if reclevel > max_recursion_level: - raise AttributeError('Max Recursion Level Reached') - - if isinstance(data, dict): - # switch to correct angle depending on recursion level - if reclevel % 2 != 0: - alpha = alpha_ps - else: - alpha = alpha_ss - - if isinstance(parentspine, matplotlib.lines.Line2D): - # calculate parent data - ([xpb, xpe], [ypb, ype]) = parentspine.get_data() - elif isinstance(parentspine, matplotlib.text.Annotation): - xpb, ypb = parentspine.xy - xpe, ype = parentspine._x, parentspine._y # parentspine._arrow_relpos - else: - raise AttributeError('Wrong Spine Graphical Element') - - plen = np.sqrt((xpe - xpb) ** 2 + (ype - ypb) ** 2) - palpha = np.round(np.degrees(np.arctan2(ype - ypb, xpe - xpb))) - - # calculate spine spacing - # calculate couple pairs, at least 1 pair to start at middle branch - pairs = len(list(data.keys())) // 2 + len(list(data.keys())) % 2 + 1 - spacing = plen / pairs - - spine_count = 0 - s = spacing - # draw spine - for k in data.keys(): - # calculate arrow position in the graph - # fix primary spines spacing - if reclevel == 1: - if len(primary_spines_rel_xpos) == len(list(data.keys())): - x_end = primary_spines_rel_xpos[spine_count] - else: - x_end = xpe - (s / 1.5) * np.cos(np.radians(palpha)) - y_end = ype - s * np.sin(np.radians(palpha)) - - x_start = x_end - s * np.cos(np.radians(alpha)) - y_start = y_end - s * np.sin(np.radians(alpha)) - else: - x_end = xpe - s * np.cos(np.radians(palpha)) - y_end = ype - s * np.sin(np.radians(palpha)) - - x_start = x_end - s * np.cos(np.radians(alpha)) - y_start = y_end - s * np.sin(np.radians(alpha)) - if debug: - _log.debug( - f'k: {k}, x_start: {x_start}, y_start: {y_start}, ' - f'x_end: {x_end} y_end: {y_end}, alpha: {alpha} \n ' - f'reclevel: {reclevel}, ' - f's:{s}, plen:{plen}, palpha:{palpha}') - - # draw arrow arc - if reclevel == 1: - props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) - spine = ax.annotate(str.upper(k), xy=(x_end, y_end), - xytext=(x_start, y_start), - arrowprops=dict(arrowstyle="->", - facecolor=spine_color), - bbox=props, weight='bold') - else: - props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) - spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start), - arrowprops=dict(arrowstyle="->", - facecolor=spine_color), - bbox=props) - # Call recursion to draw subspines - drawspines(data=data[k], ax=ax, parentspine=spine, - alpha_ps=alpha_ps, alpha_ss=alpha_ss, - spine_color=spine_color, reclevel=reclevel + 1, - max_recursion_level=max_recursion_level) - # no primary_spines_rel_xpos is needed to be passed on recursion - # next spine settings - same level - alpha *= -1 - spine_count += 1 - if spine_count % 2 == 0: - s = s + spacing - return None - - -def ishikawaplot(data: dict, ax: plt.Axes, left_margin: float = 0.05, - right_margin: float = 0.05, alpha_ps: float = 60.0, - alpha_ss=0.0, primary_spines_rel_xpos: List[float] = [np.nan], - pd_width: int = 0.1, spine_color: str = "black") -> None: - """ - - Parameters - ---------- - data : dict - Plot data structure. - ax : matplotlib.pyplot.Axes - Axes in which to drow the plot. - left_margin : float, optional - Left spacing from frame border. The default is 0.05. - right_margin : float, optional - Right spacing from frame border. The default is 0.05. - alpha_ps : float, optional - First spine angle using during recursion. The default is 60.0. - alpha_ss : TYPE, optional - Secondary spine angle using during recursion. The default is 0.0. - primary_spines_rel_xpos : list, optional - Relative X-position of primary spines, a list of float is accepted. - The default is [np.nan]. - pd_width : int, optional - Problem description box relative width. The default is 0.1. - spine_color : str, optional - Spine color. The default is "black". - - Returns - ------- - None - - """ - # format axis - ax.set_xlim(0, 1.0) - ax.set_ylim(0, 1.0) - ax.axis('off') - - # draw main spine - main_spine = ax.axhline(y=0.5, xmin=left_margin, - xmax=1 - right_margin - pd_width, - color=spine_color) - - # draw fish head - props = dict(boxstyle='round', facecolor='wheat', alpha=1.0) - # add problem tag - ax.text((1 - right_margin - pd_width), 0.5, str.upper(list(data.keys())[0]), - fontsize=12, weight='bold', - verticalalignment='center', horizontalalignment='left', bbox=props) - - # draw fish tail - x = (0.0, 0.0, left_margin) - y = (0.5 - left_margin, 0.5 + left_margin, 0.5) - ax.fill(x, y, color=spine_color) - - # draw spines with recursion - drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, - alpha_ps=alpha_ps, alpha_ss=alpha_ss, - primary_spines_rel_xpos=primary_spines_rel_xpos, - spine_color=spine_color) - - return None - - -# USER DATA -data = {'problem': {'machine': {'cause1': ''}, - 'process': {'cause2': {'subcause1': '', 'subcause2': ''} - }, - 'man': {'cause3': {'subcause3': '', 'subcause4': ''} - }, - 'design': {'cause4': {'subcause5': '', 'subcause6': ''} - } - - } - } - -# Ishikawa plot generation -fig = plt.figure(figsize=(12, 6), layout='constrained') -ax = fig.gca() - -# try also without opt primary_spines rel_xpos -ishikawaplot(data, ax, primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) diff --git a/examples/specialty_plots/swotplot.py b/examples/specialty_plots/swotplot.py deleted file mode 100644 index 8fcdf9a9fba5..000000000000 --- a/examples/specialty_plots/swotplot.py +++ /dev/null @@ -1,180 +0,0 @@ -""" -============ -SWOT Diagram -============ - -SWOT analysis (or SWOT matrix) is a strategic planning and strategic management -technique used to help a person or organization identify Strengths, Weaknesses, -Opportunities, and Threats related to business competition or project planning. -It is sometimes called situational assessment or situational analysis. -Additional acronyms using the same components include TOWS and WOTS-UP. -Source: https://en.wikipedia.org/wiki/SWOT_analysis - -""" -import logging -from typing import Tuple - -import matplotlib.pyplot as plt - -_log = logging.getLogger(__name__) - - -def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, - q_x: Tuple[float, float, float, float], - q_y: Tuple[float, float, float, float], - q_title_pos: Tuple[float, float] = (0.0, 0.0), - q_title_props: dict = dict(boxstyle='round', - facecolor="white", - alpha=1.0), - q_point_props: dict = dict(boxstyle='round', - facecolor="white", - alpha=1.0) - ) -> None: - """ - Draw a quadrant of the SWOT plot - - Parameters - ---------- - q_data : dict - Data structure passed to populate the quadrant. - Format 'key':Tuple(float,float) - ax : plt.Axes - Matplotlib current axes - q_color : str - Quadrant color. - q_x : Tuple[float] - Quadrant X relative coordinates. Format: Tuple(float,float,float,float) - q_y : Tuple[float] - Quadrant Y relative coordinates. Format: Tuple(float,float,float,float) - q_title_pos : Tuple[float,float] - Plot title relative position - q_title_props : dict, optional - Title box style properties. The default is dict(boxstyle='round', - facecolor="white", alpha=1.0). - q_point_props : dict, optional - Point box style properties. The default is dict(boxstyle='round', - facecolor="white", alpha=1.0). - - Returns - ------- - None - - """ - # draw filled rectangle - ax.fill(q_x, q_y, color=q_color) - - # draw quadrant title - ax.text(x=q_title_pos[0], y=q_title_pos[1], - s=str.upper(list(q_data.keys())[0]), fontsize=12, weight='bold', - verticalalignment='center', horizontalalignment='center', - bbox=q_title_props) - - # fill quadrant with points - for k, v in q_data[list(q_data.keys())[0]].items(): - ax.text(x=v[0], y=v[1], s=k, fontsize=12, weight='bold', - verticalalignment='center', horizontalalignment='center', - bbox=q_point_props) - _log.debug(f"k: {k}, v: {v}") - - return None - - -def swotplot(p_data: dict, ax: plt.Axes, - s_color: str = "forestgreen", w_color: str = "gold", - o_color: str = "skyblue", t_color: str = "firebrick", - left_margin: float = 0.05, right_margin: float = 0.05, - top_margin: float = 0.05, bottom_margin: float = 0.05, - ) -> None: - """ - Draw SWOT plot - - Parameters - ---------- - p_data : dict - Data structure passed to populate the plot. - ax : matplotlib.pyplot.Axes - axes in which to draw the plot - s_color : float, optional - Strength quadrant color. - w_color : float, optional - Weakness quadrant color. - o_color : float, optional - Opportunity quadrant color. - t_color : float, optional - Threat quadrant color. - left_margin : float, optional - Left spacing from frame border. The default is 0.05. - right_margin : float, optional - Right spacing from frame border. The default is 0.05. - top_margin : float, optional - Top spacing from frame border. The default is 0.05. - bottom_margin : float, optional - Bottom spacing from frame border. The default is 0.05. - - - Returns - ------- - None - - """ - # format axis - ax.set_xlim(0, 1.0) - ax.set_ylim(0, 1.0) - ax.axis('off') - - # draw s quadrant - draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[0], - p_data.items())), - ax=ax, - q_color=s_color, q_x=(left_margin, left_margin, 0.5, 0.5), - q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), - q_title_pos=(0.25 + left_margin / 2, 1 - top_margin)) - - # draw w quadrant - draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[1], - p_data.items())), - ax=ax, - q_color=w_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), - q_y=(0.5, 1 - top_margin, 1 - top_margin, 0.5), - q_title_pos=(0.25 + (1 - right_margin) / 2, 1 - top_margin)) - - # draw o quadrant - draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[2], - p_data.items())), - ax=ax, - q_color=o_color, q_x=(left_margin, left_margin, 0.5, 0.5), - q_y=(bottom_margin, 0.5, 0.5, bottom_margin), - q_title_pos=(0.25 + left_margin / 2, bottom_margin)) - - # draw t quadrant - draw_quadrant(q_data=dict(filter(lambda i: i[0] in list(p_data.keys())[3], - p_data.items())), - ax=ax, - q_color=t_color, q_x=(0.5, 0.5, 1 - right_margin, 1 - right_margin), - q_y=(bottom_margin, 0.5, 0.5, bottom_margin), - q_title_pos=(0.25 + (1 - right_margin) / 2, bottom_margin)) - - return None - - -# USER DATA -# relative x_pos,y_pos make it easy to manage particular cases such as -# soft categorization -data = {'strength': {'SW automation': (0.3, 0.6), - 'teamwork': (0.4, 0.5) - }, - 'weakness': {'delayed payment': (0.5, 0.7), - 'hard to learn': (0.7, 0.9) - }, - 'opportunity': {'dedicated training': (0.3, 0.3), - 'just-in-time development': (0.2, 0.1) - }, - 'threat': {'project de-prioritization': (0.7, 0.3), - 'external consultancy': (0.5, 0.1) - }, - } - -# SWOT plot generation -fig = plt.figure(figsize=(12, 6), layout='constrained') -ax = fig.gca() -swotplot(data, ax) From 69d76bf2401df4e5d3fdc1c0c62fc33730a4b86f Mon Sep 17 00:00:00 2001 From: "roberto.bodo" Date: Sat, 4 Mar 2023 12:27:26 +0100 Subject: [PATCH 40/40] - change plt.figure with plt.subplots - integrated hypot call instead sqrt formula - fix dict iteration - debug eliminated - alpha vars renaming - docstrings minor fixes --- .../examples/specialty_plots/ishikawa.py | 60 +++++++++---------- .../examples/specialty_plots/swotplot.py | 17 ++---- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/galleries/examples/specialty_plots/ishikawa.py b/galleries/examples/specialty_plots/ishikawa.py index d5fe8e0ff80e..97ce113590d5 100644 --- a/galleries/examples/specialty_plots/ishikawa.py +++ b/galleries/examples/specialty_plots/ishikawa.py @@ -8,7 +8,6 @@ Source: https://en.wikipedia.org/wiki/Ishikawa_diagram """ -import logging from typing import List import matplotlib.pyplot as plt @@ -16,14 +15,13 @@ import matplotlib -_log = logging.getLogger(__name__) - -def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60.0, - alpha_ss: float = 0, +def drawspines(data: dict, ax: plt.Axes, parentspine: None, + primary_spine_angle: float = 60.0, + secondary_spine_angle: float = 0, primary_spines_rel_xpos: List[float] = [np.nan], spine_color: str = "black", reclevel: int = 1, - max_recursion_level: int = 4, debug: bool = False): + max_recursion_level: int = 4): """ Draw an Ishikawa spine with recursion @@ -35,9 +33,9 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 Matplotlib current axes parentspine : None Parent spine object - alpha_ps : float, optional + primary_spine_angle : float, optional First spine angle using during recursion. The default is 60.0. - alpha_ss : float, optional + secondary_spine_angle : float, optional Secondary spine angle using during recursion. The default is 0. primary_spines_rel_xpos : List[float], optional Relative X-position of primary spines, a list of float is accepted. @@ -48,8 +46,6 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 Current recursion level. The default is 1. max_recursion_level : int, optional Maximum recursion level set. The default is 4. - debug : bool, optional - Print debug variables. The default is False. Raises ------ @@ -68,9 +64,9 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 if isinstance(data, dict): # switch to correct angle depending on recursion level if reclevel % 2 != 0: - alpha = alpha_ps + alpha = primary_spine_angle else: - alpha = alpha_ss + alpha = secondary_spine_angle if isinstance(parentspine, matplotlib.lines.Line2D): # calculate parent data @@ -81,22 +77,22 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 else: raise AttributeError('Wrong Spine Graphical Element') - plen = np.sqrt((xpe - xpb) ** 2 + (ype - ypb) ** 2) + plen = np.hypot(xpe - xpb, ype - ypb) palpha = np.round(np.degrees(np.arctan2(ype - ypb, xpe - xpb))) # calculate spine spacing # calculate couple pairs, at least 1 pair to start at middle branch - pairs = len(list(data.keys())) // 2 + len(list(data.keys())) % 2 + 1 + pairs = np.ceil(len(data) / 2) + 1 spacing = plen / pairs spine_count = 0 s = spacing # draw spine - for k in data.keys(): + for problem, cause in data.items(): # calculate arrow position in the graph # fix primary spines spacing if reclevel == 1: - if len(primary_spines_rel_xpos) == len(list(data.keys())): + if len(primary_spines_rel_xpos) == len(data): x_end = primary_spines_rel_xpos[spine_count] else: x_end = xpe - (s / 1.5) * np.cos(np.radians(palpha)) @@ -110,30 +106,26 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 x_start = x_end - s * np.cos(np.radians(alpha)) y_start = y_end - s * np.sin(np.radians(alpha)) - if debug: - _log.debug( - f'k: {k}, x_start: {x_start}, y_start: {y_start}, ' - f'x_end: {x_end} y_end: {y_end}, alpha: {alpha} \n ' - f'reclevel: {reclevel}, ' - f's:{s}, plen:{plen}, palpha:{palpha}') # draw arrow arc if reclevel == 1: props = dict(boxstyle='round', facecolor='lightsteelblue', alpha=1.0) - spine = ax.annotate(str.upper(k), xy=(x_end, y_end), + spine = ax.annotate(problem.upper(), xy=(x_end, y_end), xytext=(x_start, y_start), arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props, weight='bold') else: props = dict(boxstyle='round', facecolor='lavender', alpha=1.0) - spine = ax.annotate(k, xy=(x_end, y_end), xytext=(x_start, y_start), + spine = ax.annotate(problem, xy=(x_end, y_end), + xytext=(x_start, y_start), arrowprops=dict(arrowstyle="->", facecolor=spine_color), bbox=props) # Call recursion to draw subspines - drawspines(data=data[k], ax=ax, parentspine=spine, - alpha_ps=alpha_ps, alpha_ss=alpha_ss, + drawspines(data=cause, ax=ax, parentspine=spine, + primary_spine_angle=primary_spine_angle, + secondary_spine_angle=secondary_spine_angle, spine_color=spine_color, reclevel=reclevel + 1, max_recursion_level=max_recursion_level) # no primary_spines_rel_xpos is needed to be passed on recursion @@ -146,8 +138,10 @@ def drawspines(data: dict, ax: plt.Axes, parentspine: None, alpha_ps: float = 60 def ishikawaplot(data: dict, ax: plt.Axes, left_margin: float = 0.05, - right_margin: float = 0.05, alpha_ps: float = 60.0, - alpha_ss=0.0, primary_spines_rel_xpos: List[float] = [np.nan], + right_margin: float = 0.05, + primary_spine_angle: float = 60.0, + secondary_spine_angle: float = 0.0, + primary_spines_rel_xpos: List[float] = [np.nan], pd_width: int = 0.1, spine_color: str = "black") -> None: """ @@ -161,9 +155,9 @@ def ishikawaplot(data: dict, ax: plt.Axes, left_margin: float = 0.05, Left spacing from frame border. The default is 0.05. right_margin : float, optional Right spacing from frame border. The default is 0.05. - alpha_ps : float, optional + primary_spine_angle : float, optional First spine angle using during recursion. The default is 60.0. - alpha_ss : TYPE, optional + secondary_spine_angle : float, optional Secondary spine angle using during recursion. The default is 0.0. primary_spines_rel_xpos : list, optional Relative X-position of primary spines, a list of float is accepted. @@ -202,7 +196,8 @@ def ishikawaplot(data: dict, ax: plt.Axes, left_margin: float = 0.05, # draw spines with recursion drawspines(data=data[list(data.keys())[0]], ax=ax, parentspine=main_spine, - alpha_ps=alpha_ps, alpha_ss=alpha_ss, + primary_spine_angle=primary_spine_angle, + secondary_spine_angle=secondary_spine_angle, primary_spines_rel_xpos=primary_spines_rel_xpos, spine_color=spine_color) @@ -222,8 +217,7 @@ def ishikawaplot(data: dict, ax: plt.Axes, left_margin: float = 0.05, } # Ishikawa plot generation -fig = plt.figure(figsize=(12, 6), layout='constrained') -ax = fig.gca() +fig, ax = plt.subplots(figsize=(12, 6), layout='constrained') # try also without opt primary_spines rel_xpos ishikawaplot(data, ax, primary_spines_rel_xpos=[0.8, 0.7, 0.6, 0.4]) diff --git a/galleries/examples/specialty_plots/swotplot.py b/galleries/examples/specialty_plots/swotplot.py index 8fcdf9a9fba5..4251c3938232 100644 --- a/galleries/examples/specialty_plots/swotplot.py +++ b/galleries/examples/specialty_plots/swotplot.py @@ -11,13 +11,10 @@ Source: https://en.wikipedia.org/wiki/SWOT_analysis """ -import logging from typing import Tuple import matplotlib.pyplot as plt -_log = logging.getLogger(__name__) - def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, q_x: Tuple[float, float, float, float], @@ -42,10 +39,10 @@ def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, Matplotlib current axes q_color : str Quadrant color. - q_x : Tuple[float] - Quadrant X relative coordinates. Format: Tuple(float,float,float,float) - q_y : Tuple[float] - Quadrant Y relative coordinates. Format: Tuple(float,float,float,float) + q_x : Tuple[float, float, float, float] + Quadrant X coordinates. + q_y : Tuple[float, float, float, float] + Quadrant Y coordinates. q_title_pos : Tuple[float,float] Plot title relative position q_title_props : dict, optional @@ -74,7 +71,6 @@ def draw_quadrant(q_data: dict, ax: plt.Axes, q_color: str, ax.text(x=v[0], y=v[1], s=k, fontsize=12, weight='bold', verticalalignment='center', horizontalalignment='center', bbox=q_point_props) - _log.debug(f"k: {k}, v: {v}") return None @@ -111,7 +107,6 @@ def swotplot(p_data: dict, ax: plt.Axes, bottom_margin : float, optional Bottom spacing from frame border. The default is 0.05. - Returns ------- None @@ -175,6 +170,6 @@ def swotplot(p_data: dict, ax: plt.Axes, } # SWOT plot generation -fig = plt.figure(figsize=(12, 6), layout='constrained') -ax = fig.gca() +fig, ax = plt.subplots(figsize=(12, 6), layout='constrained') + swotplot(data, ax)