9
9
Source: https://en.wikipedia.org/wiki/Ishikawa_diagram
10
10
11
11
"""
12
+ import math
13
+
12
14
import matplotlib .pyplot as plt
13
15
14
16
from matplotlib .patches import Polygon , Wedge
15
17
16
- # Create the fishbone diagram
17
18
fig , ax = plt .subplots (figsize = (10 , 6 ), layout = 'constrained' )
18
19
ax .set_xlim (- 5 , 5 )
19
20
ax .set_ylim (- 5 , 5 )
22
23
23
24
def problems (data : str ,
24
25
problem_x : float , problem_y : float ,
25
- prob_angle_x : float , prob_angle_y : float ):
26
+ angle_x : float , angle_y : float ):
26
27
"""
27
28
Draw each problem section of the Ishikawa plot.
28
29
29
30
Parameters
30
31
----------
31
32
data : str
32
- The category name.
33
+ The name of the problem category .
33
34
problem_x, problem_y : float, optional
34
35
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
37
38
the tail of the plot.
38
39
39
40
Returns
@@ -42,8 +43,8 @@ def problems(data: str,
42
43
43
44
"""
44
45
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 ,
47
48
color = 'white' ,
48
49
weight = 'bold' ,
49
50
xycoords = 'data' ,
@@ -56,7 +57,8 @@ def problems(data: str,
56
57
pad = 0.8 ))
57
58
58
59
59
- def causes (data : list , cause_x : float , cause_y : float ,
60
+ def causes (data : list ,
61
+ cause_x : float , cause_y : float ,
60
62
cause_xytext = (- 9 , - 0.3 ), top : bool = True ):
61
63
"""
62
64
Place each cause to a position relative to the problems
@@ -72,34 +74,33 @@ def causes(data: list, cause_x: float, cause_y: float,
72
74
cause_xytext : tuple, optional
73
75
Adjust to set the distance of the cause text from the problem
74
76
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.
76
80
77
81
Returns
78
82
-------
79
83
None.
80
84
81
85
"""
82
86
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
84
96
# 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 ]
97
97
cause_x -= coords [index ][0 ]
98
+ cause_y += coords [index ][1 ] if top else - coords [index ][1 ]
98
99
99
100
ax .annotate (cause , xy = (cause_x , cause_y ),
100
101
horizontalalignment = 'center' ,
101
102
xytext = cause_xytext ,
102
- fontsize = '9' ,
103
+ fontsize = 9 ,
103
104
xycoords = 'data' ,
104
105
textcoords = 'offset fontsize' ,
105
106
arrowprops = dict (arrowstyle = "->" ,
@@ -108,82 +109,74 @@ def causes(data: list, cause_x: float, cause_y: float,
108
109
109
110
def draw_body (data : dict ):
110
111
"""
111
- Place each section in its correct place by changing
112
+ Place each problem section in its correct place by changing
112
113
the coordinates on each loop.
113
114
114
115
Parameters
115
116
----------
116
117
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.
119
120
120
121
Returns
121
122
-------
122
123
None.
123
124
124
125
"""
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 ]
149
133
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
168
143
if index > 5 :
169
144
raise ValueError (f'Maximum number of problems is 6, you have entered '
170
145
f'{ len (data )} ' )
171
146
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.
183
147
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 )
187
180
188
181
189
182
# Input data
0 commit comments