1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
|
"""
================
Ishikawa Diagram
================
Ishikawa Diagrams, fishbone diagrams, herringbone diagrams, or cause-and-effect
diagrams are used to identify problems in a system by showing how causes and
effects are linked.
Source: https://en.wikipedia.org/wiki/Ishikawa_diagram
"""
import math
import matplotlib.pyplot as plt
from matplotlib.patches import Polygon, Wedge
fig, ax = plt.subplots(figsize=(10, 6), layout='constrained')
ax.set_xlim(-5, 5)
ax.set_ylim(-5, 5)
ax.axis('off')
def problems(data: str,
problem_x: float, problem_y: float,
angle_x: float, angle_y: float):
"""
Draw each problem section of the Ishikawa plot.
Parameters
----------
data : str
The name of the problem category.
problem_x, problem_y : float, optional
The `X` and `Y` positions of the problem arrows (`Y` defaults to zero).
angle_x, angle_y : float, optional
The angle of the problem annotations. They are always angled towards
the tail of the plot.
Returns
-------
None.
"""
ax.annotate(str.upper(data), xy=(problem_x, problem_y),
xytext=(angle_x, angle_y),
fontsize=10,
color='white',
weight='bold',
xycoords='data',
verticalalignment='center',
horizontalalignment='center',
textcoords='offset fontsize',
arrowprops=dict(arrowstyle="->", facecolor='black'),
bbox=dict(boxstyle='square',
facecolor='tab:blue',
pad=0.8))
def causes(data: list,
cause_x: float, cause_y: float,
cause_xytext=(-9, -0.3), top: bool = True):
"""
Place each cause to a position relative to the problems
annotations.
Parameters
----------
data : indexable object
The input data. IndexError is
raised if more than six arguments are passed.
cause_x, cause_y : float
The `X` and `Y` position of the cause annotations.
cause_xytext : tuple, optional
Adjust to set the distance of the cause text from the problem
arrow in fontsize units.
top : bool, default: True
Determines whether the next cause annotation will be
plotted above or below the previous one.
Returns
-------
None.
"""
for index, cause in enumerate(data):
# [<x pos>, <y pos>]
coords = [[0.02, 0],
[0.23, 0.5],
[-0.46, -1],
[0.69, 1.5],
[-0.92, -2],
[1.15, 2.5]]
# First 'cause' annotation is placed in the middle of the 'problems' arrow
# and each subsequent cause is plotted above or below it in succession.
cause_x -= coords[index][0]
cause_y += coords[index][1] if top else -coords[index][1]
ax.annotate(cause, xy=(cause_x, cause_y),
horizontalalignment='center',
xytext=cause_xytext,
fontsize=9,
xycoords='data',
textcoords='offset fontsize',
arrowprops=dict(arrowstyle="->",
facecolor='black'))
def draw_body(data: dict):
"""
Place each problem section in its correct place by changing
the coordinates on each loop.
Parameters
----------
data : dict
The input data (can be a dict of lists or tuples). ValueError
is raised if more than six arguments are passed.
Returns
-------
None.
"""
# Set the length of the spine according to the number of 'problem' categories.
length = (math.ceil(len(data) / 2)) - 1
draw_spine(-2 - length, 2 + length)
# Change the coordinates of the 'problem' annotations after each one is rendered.
offset = 0
prob_section = [1.55, 0.8]
for index, problem in enumerate(data.values()):
plot_above = index % 2 == 0
cause_arrow_y = 1.7 if plot_above else -1.7
y_prob_angle = 16 if plot_above else -16
# Plot each section in pairs along the main spine.
prob_arrow_x = prob_section[0] + length + offset
cause_arrow_x = prob_section[1] + length + offset
if not plot_above:
offset -= 2.5
if index > 5:
raise ValueError(f'Maximum number of problems is 6, you have entered '
f'{len(data)}')
problems(list(data.keys())[index], prob_arrow_x, 0, -12, y_prob_angle)
causes(problem, cause_arrow_x, cause_arrow_y, top=plot_above)
def draw_spine(xmin: int, xmax: int):
"""
Draw main spine, head and tail.
Parameters
----------
xmin : int
The default position of the head of the spine's
x-coordinate.
xmax : int
The default position of the tail of the spine's
x-coordinate.
Returns
-------
None.
"""
# draw main spine
ax.plot([xmin - 0.1, xmax], [0, 0], color='tab:blue', linewidth=2)
# draw fish head
ax.text(xmax + 0.1, - 0.05, 'PROBLEM', fontsize=10,
weight='bold', color='white')
semicircle = Wedge((xmax, 0), 1, 270, 90, fc='tab:blue')
ax.add_patch(semicircle)
# draw fish tail
tail_pos = [[xmin - 0.8, 0.8], [xmin - 0.8, -0.8], [xmin, -0.01]]
triangle = Polygon(tail_pos, fc='tab:blue')
ax.add_patch(triangle)
# Input data
categories = {
'Method': ['Time consumption', 'Cost', 'Procedures', 'Inefficient process',
'Sampling'],
'Machine': ['Faulty equipment', 'Compatibility'],
'Material': ['Poor-quality input', 'Raw materials', 'Supplier',
'Shortage'],
'Measurement': ['Calibration', 'Performance', 'Wrong measurements'],
'Environment': ['Bad conditions'],
'People': ['Lack of training', 'Managers', 'Labor shortage',
'Procedures', 'Sales strategy']
}
draw_body(categories)
plt.show()
|