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 ,
2425problem_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- Thecategory 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 arealways angled towards
3738 the tail of the plot.
3839
3940 Returns
@@ -42,8 +43,8 @@ def problems(data: str,
4243
4344 """
4445ax .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 ,
4748color = 'white' ,
4849weight = 'bold' ,
4950xycoords = 'data' ,
@@ -56,7 +57,8 @@ def problems(data: str,
5657pad = 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 ,
6062cause_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 """
8286for 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 ]
9797cause_x -= coords [index ][0 ]
98+ cause_y += coords [index ][1 ]if top else - coords [index ][1 ]
9899
99100ax .annotate (cause ,xy = (cause_x ,cause_y ),
100101horizontalalignment = 'center' ,
101102xytext = cause_xytext ,
102- fontsize = '9' ,
103+ fontsize = 9 ,
103104xycoords = 'data' ,
104105textcoords = 'offset fontsize' ,
105106arrowprops = 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 eachproblem 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 belist ortuple ). ValueError is
118- raised if more than six arguments are passed.
118+ The input data (can bea dict of lists ortuples ). 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 ]
149133for 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
168143if index > 5 :
169144raise ValueError (f'Maximum number of problems is 6, you have entered '
170145f'{ 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.
183147problems (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