99Source: https://en.wikipedia.org/wiki/Ishikawa_diagram
1010
1111"""
12+ import math
13+
1214import matplotlib .pyplot as plt
1315
1416from matplotlib .patches import Polygon , Wedge
1517
16- # Create the fishbone diagram
1718fig , ax = plt .subplots (figsize = (10 , 6 ), layout = 'constrained' )
1819ax .set_xlim (- 5 , 5 )
1920ax .set_ylim (- 5 , 5 )
2223
2324def problems (data : str ,
2425 problem_x : float , problem_y : float ,
25- prob_angle_x : float , prob_angle_y : float ):
26+ angle_x : float , angle_y : float ):
2627 """
2728 Draw each problem section of the Ishikawa plot.
2829
2930 Parameters
3031 ----------
3132 data : str
32- The category name.
33+ The name of the problem category .
3334 problem_x, problem_y : float, optional
3435 The `X` and `Y` positions of the problem arrows (`Y` defaults to zero).
35- prob_angle_x, prob_angle_y : float, optional
36- The angle of the problem annotations. They are angled towards
36+ angle_x, angle_y : float, optional
37+ The angle of the problem annotations. They are always angled towards
3738 the tail of the plot.
3839
3940 Returns
@@ -42,8 +43,8 @@ def problems(data: str,
4243
4344 """
4445 ax .annotate (str .upper (data ), xy = (problem_x , problem_y ),
45- xytext = (prob_angle_x , prob_angle_y ),
46- fontsize = '10' ,
46+ xytext = (angle_x , angle_y ),
47+ fontsize = 10 ,
4748 color = 'white' ,
4849 weight = 'bold' ,
4950 xycoords = 'data' ,
@@ -56,7 +57,8 @@ def problems(data: str,
5657 pad = 0.8 ))
5758
5859
59- def causes (data : list , cause_x : float , cause_y : float ,
60+ def causes (data : list ,
61+ cause_x : float , cause_y : float ,
6062 cause_xytext = (- 9 , - 0.3 ), top : bool = True ):
6163 """
6264 Place each cause to a position relative to the problems
@@ -72,34 +74,33 @@ def causes(data: list, cause_x: float, cause_y: float,
7274 cause_xytext : tuple, optional
7375 Adjust to set the distance of the cause text from the problem
7476 arrow in fontsize units.
75- top : bool
77+ top : bool, default: True
78+ Determines whether the next cause annotation will be
79+ plotted above or below the previous one.
7680
7781 Returns
7882 -------
7983 None.
8084
8185 """
8286 for index , cause in enumerate (data ):
83- # First cause annotation is placed in the middle of the problems arrow
87+ # [<x pos>, <y pos>]
88+ coords = [[0.02 , 0 ],
89+ [0.23 , 0.5 ],
90+ [- 0.46 , - 1 ],
91+ [0.69 , 1.5 ],
92+ [- 0.92 , - 2 ],
93+ [1.15 , 2.5 ]]
94+
95+ # First 'cause' annotation is placed in the middle of the 'problems' arrow
8496 # and each subsequent cause is plotted above or below it in succession.
85-
86- # [<x pos>, [<y pos top>, <y pos bottom>]]
87- coords = [[0 , [0 , 0 ]],
88- [0.23 , [0.5 , - 0.5 ]],
89- [- 0.46 , [- 1 , 1 ]],
90- [0.69 , [1.5 , - 1.5 ]],
91- [- 0.92 , [- 2 , 2 ]],
92- [1.15 , [2.5 , - 2.5 ]]]
93- if top :
94- cause_y += coords [index ][1 ][0 ]
95- else :
96- cause_y += coords [index ][1 ][1 ]
9797 cause_x -= coords [index ][0 ]
98+ cause_y += coords [index ][1 ] if top else - coords [index ][1 ]
9899
99100 ax .annotate (cause , xy = (cause_x , cause_y ),
100101 horizontalalignment = 'center' ,
101102 xytext = cause_xytext ,
102- fontsize = '9' ,
103+ fontsize = 9 ,
103104 xycoords = 'data' ,
104105 textcoords = 'offset fontsize' ,
105106 arrowprops = dict (arrowstyle = "->" ,
@@ -108,82 +109,74 @@ def causes(data: list, cause_x: float, cause_y: float,
108109
109110def draw_body (data : dict ):
110111 """
111- Place each section in its correct place by changing
112+ Place each problem section in its correct place by changing
112113 the coordinates on each loop.
113114
114115 Parameters
115116 ----------
116117 data : dict
117- The input data (can be list or tuple ). ValueError is
118- raised if more than six arguments are passed.
118+ The input data (can be a dict of lists or tuples ). ValueError
119+ is raised if more than six arguments are passed.
119120
120121 Returns
121122 -------
122123 None.
123124
124125 """
125- second_sections = []
126- third_sections = []
127- # Resize diagram to automatically scale in response to the number
128- # of problems in the input data.
129- if len (data ) == 1 or len (data ) == 2 :
130- spine_length = (- 2.1 , 2 )
131- head_pos = (2 , 0 )
132- tail_pos = ((- 2.8 , 0.8 ), (- 2.8 , - 0.8 ), (- 2.0 , - 0.01 ))
133- first_section = [1.6 , 0.8 ]
134- elif len (data ) == 3 or len (data ) == 4 :
135- spine_length = (- 3.1 , 3 )
136- head_pos = (3 , 0 )
137- tail_pos = ((- 3.8 , 0.8 ), (- 3.8 , - 0.8 ), (- 3.0 , - 0.01 ))
138- first_section = [2.6 , 1.8 ]
139- second_sections = [- 0.4 , - 1.2 ]
140- else : # len(data) == 5 or 6
141- spine_length = (- 4.1 , 4 )
142- head_pos = (4 , 0 )
143- tail_pos = ((- 4.8 , 0.8 ), (- 4.8 , - 0.8 ), (- 4.0 , - 0.01 ))
144- first_section = [3.5 , 2.7 ]
145- second_sections = [1 , 0.2 ]
146- third_sections = [- 1.5 , - 2.3 ]
147-
148- # Change the coordinates of the annotations on each loop.
126+ # Set the length of the spine according to the number of 'problem' categories.
127+ length = (math .ceil (len (data ) / 2 )) - 1
128+ draw_spine (- 2 - length , 2 + length )
129+
130+ # Change the coordinates of the 'problem' annotations after each one is rendered.
131+ offset = 0
132+ prob_section = [1.55 , 0.8 ]
149133 for index , problem in enumerate (data .values ()):
150- top_row = True
151- cause_arrow_y = 1.7
152- if index % 2 != 0 : # Plot problems below the spine.
153- top_row = False
154- y_prob_angle = - 16
155- cause_arrow_y = - 1.7
156- else : # Plot problems above the spine.
157- y_prob_angle = 16
158- # Plot the 3 sections in pairs along the main spine.
159- if index in (0 , 1 ):
160- prob_arrow_x = first_section [0 ]
161- cause_arrow_x = first_section [1 ]
162- elif index in (2 , 3 ):
163- prob_arrow_x = second_sections [0 ]
164- cause_arrow_x = second_sections [1 ]
165- else :
166- prob_arrow_x = third_sections [0 ]
167- cause_arrow_x = third_sections [1 ]
134+ plot_above = index % 2 == 0
135+ cause_arrow_y = 1.7 if plot_above else - 1.7
136+ y_prob_angle = 16 if plot_above else - 16
137+
138+ # Plot each section in pairs along the main spine.
139+ prob_arrow_x = prob_section [0 ] + length + offset
140+ cause_arrow_x = prob_section [1 ] + length + offset
141+ if not plot_above :
142+ offset -= 2.5
168143 if index > 5 :
169144 raise ValueError (f'Maximum number of problems is 6, you have entered '
170145 f'{ len (data )} ' )
171146
172- # draw main spine
173- ax .plot (spine_length , [0 , 0 ], color = 'tab:blue' , linewidth = 2 )
174- # draw fish head
175- ax .text (head_pos [0 ] + 0.1 , head_pos [1 ] - 0.05 , 'PROBLEM' , fontsize = 10 ,
176- weight = 'bold' , color = 'white' )
177- semicircle = Wedge (head_pos , 1 , 270 , 90 , fc = 'tab:blue' )
178- ax .add_patch (semicircle )
179- # draw fishtail
180- triangle = Polygon (tail_pos , fc = 'tab:blue' )
181- ax .add_patch (triangle )
182- # Pass each category name to the problems function as a string on each loop.
183147 problems (list (data .keys ())[index ], prob_arrow_x , 0 , - 12 , y_prob_angle )
184- # Start the cause function with the first annotation being plotted at
185- # the cause_arrow_x, cause_arrow_y coordinates.
186- causes (problem , cause_arrow_x , cause_arrow_y , top = top_row )
148+ causes (problem , cause_arrow_x , cause_arrow_y , top = plot_above )
149+
150+
151+ def draw_spine (xmin : int , xmax : int ):
152+ """
153+ Draw main spine, head and tail.
154+
155+ Parameters
156+ ----------
157+ xmin : int
158+ The default position of the head of the spine's
159+ x-coordinate.
160+ xmax : int
161+ The default position of the tail of the spine's
162+ x-coordinate.
163+
164+ Returns
165+ -------
166+ None.
167+
168+ """
169+ # draw main spine
170+ ax .plot ([xmin - 0.1 , xmax ], [0 , 0 ], color = 'tab:blue' , linewidth = 2 )
171+ # draw fish head
172+ ax .text (xmax + 0.1 , - 0.05 , 'PROBLEM' , fontsize = 10 ,
173+ weight = 'bold' , color = 'white' )
174+ semicircle = Wedge ((xmax , 0 ), 1 , 270 , 90 , fc = 'tab:blue' )
175+ ax .add_patch (semicircle )
176+ # draw fish tail
177+ tail_pos = [[xmin - 0.8 , 0.8 ], [xmin - 0.8 , - 0.8 ], [xmin , - 0.01 ]]
178+ triangle = Polygon (tail_pos , fc = 'tab:blue' )
179+ ax .add_patch (triangle )
187180
188181
189182# Input data
0 commit comments