Repository: matplotlib/matplotlib
Pull request: add ishikawa diagram to examples
## PR summary
Closes #25222. Adds a fishbone plot to the specialty plots example in the docs. This PR is a continuation of https://github.com/matplotlib/matplotlib/pull/25248 except the plot is different. It's a simple representation of a fishbone plot which takes the input data as a dictionary of nested lists or tuples. The diagram will scale depending on the number of arguments that are passed to it.

<a href='https://postimages.org/' target='_blank'><img src='https://i.postimg.cc/8CgGPgvP/ishikawa.png' border='0' alt='ishikawa'/></a>
## PR checklist
<!-- Please mark any checkboxes that do not apply to this PR as [N/A].-->

- [x] "closes #0000" is in the body of the PR description to [link the related issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue)
- [ ] new and changed code is [tested](https://matplotlib.org/devdocs/devel/testing.html)
- [ ] *Plotting related* features are demonstrated in an [example](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-examples-and-tutorials)
- [ ] *New Features* and *API Changes* are noted with a [directive and release note](https://matplotlib.org/devdocs/devel/coding_guide.html#new-features-and-api-changes)
- [ ] Documentation complies with [general](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-rest-pages) and [docstring](https://matplotlib.org/devdocs/devel/documenting_mpl.html#writing-docstrings) guidelines

<!--
Thank you so much for your PR!  To help us review your contribution, please
consider the following points:

- A development guide is available at https://matplotlib.org/devdocs/devel/index.html.

- Help with git and github is available at https://matplotlib.org/devdocs/devel/development_workflow.html

- Create a separate branch for your changes and open the PR from this branch. Please avoid working on `main`.

- The PR title should summarize the changes, for example "Raise ValueError on
  non-numeric input to set_xlim".  Avoid non-descriptive titles such as
  "Addresses issue #8576".

- The summary should provide at least 1-2 sentences describing the pull request
  in detail (Why is this change required?  What problem does it solve?) and
  link to any relevant issues.

- If you are contributing fixes to docstrings, please pay attention to
  https://matplotlib.org/stable/devel/documenting_mpl.html#formatting-conventions.  In particular,
  note the difference between using single backquotes, double backquotes, and
  asterisks in the markup.

We understand that PRs can sometimes be overwhelming, especially as the
reviews start coming in.  Please let us know if the reviews are unclear or
the recommended next step seems overly demanding, if you would like help in
addressing a reviewer's comments, or if you have been waiting too long to hear
back on your PR.
-->


Problem statement:
[ENH]: add organizational charts to supported plots
### Problem

I would like to efficiently use matplotlib to produce also organizational charts like the Ishikawa (fishbone) diagram to include also the cause-effect relationships into professional and academical documents. For the Ishikawa case, it could be interesting to modify the treeplot by using a fractal layout for the spines. 

Thank you for your consideration. 

### Proposed solution

_No response_

Hints from issue discussion:
Thanks for your interest in improving Matplotlib!  

That type of chart is super-specialized and I would expect better suited to an external library.  We don't provide a tree plot, either, though https://github.com/erdogant/treeplot does.  If the external library proved popular, and/or there was a compelling reason it needed to be in core, it would certainly be possible to consider.  
This could also maybe be a gallery entry at  https://networkx.org/documentation/stable/auto_examples/index.html but you'd have to ask the networkx folk
Think about this more, it is definitely something that should be developed elsewhere.  The API for this will be extremely broad, so best to refine it first.  Please feel free to approach us again if there is a package that should be subsumed, or if it is not possible to make what you want without changing Matplotlib internals.  
Thank you @jklymak for the support. I will try to ask the same to some dedicated projects or I will implement it for specific cases little by little. 
Regards,
Robert
PS: For one that will read the post in the future, I have implemented only the Ishikawa plot from scratch by using only matplotlib and numpy. 
You can find the code here: [fishbone diagram using Python or graphviz](https://stackoverflow.com/questions/51348536/fishbone-diagram-using-python-or-graphviz)
Great!  Your s/o post seems modest sized so consider adding to our examples?

Commit message: remove redundant line

Patch to review:
diff --git a/galleries/examples/specialty_plots/ishikawa_diagram.py b/galleries/examples/specialty_plots/ishikawa_diagram.py
new file mode 100644
index 000000000000..976fdc516887
--- /dev/null
+++ b/galleries/examples/specialty_plots/ishikawa_diagram.py
@@ -0,0 +1,200 @@
+"""
+================
+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 matplotlib.pyplot as plt
+
+from matplotlib.patches import Polygon, Wedge
+
+# Create the fishbone diagram
+fig, ax = plt.subplots(figsize=(10, 6), layout='constrained')
+ax.set_xlim(-5, 5)
+ax.set_ylim(-5, 5)
+ax.axis('off')
+COLOR = 'C0'
+
+
+def problems(data: list,
+             problem_x: float, problem_y: float,
+             prob_angle_x: float, prob_angle_y: float):
+    """
+    Draw each problem section of the Ishikawa plot.
+
+    Parameters
+    ----------
+    data : indexable object
+        The input data (can be list or tuple).
+    problem_x, problem_y : float, optional
+        The `X` and `Y` positions of the problem arrows (`Y` defaults to zero).
+    prob_angle_x, prob_angle_y : float, optional
+        The angle of the problem annotations. They are angled towards
+        the tail of the plot.
+
+    Returns
+    -------
+    None.
+
+    """
+    ax.annotate(str.upper(data[0]), xy=(problem_x, problem_y),
+                xytext=(prob_angle_x, prob_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=COLOR,
+                          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 : indexible object
+        The input data (can be list or tuple). 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
+
+    Returns
+    -------
+    None.
+
+    """
+    for index, cause in enumerate(data[1]):
+        # First cause annotation is placed in the middle of the problems arrow
+        # and each subsequent cause is plotted above or below it in succession.
+
+        # [<x pos>, [<y pos top>, <y pos bottom>]]
+        coords = [[0, [0, 0]],
+                  [0.23, [0.5, -0.5]],
+                  [-0.46, [-1, 1]],
+                  [0.69, [1.5, -1.5]],
+                  [-0.92, [-2, 2]],
+                  [1.15, [2.5, -2.5]]]
+        if top:
+            cause_y += coords[index][1][0]
+        else:
+            cause_y += coords[index][1][1]
+        cause_x -= coords[index][0]
+
+        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(*args):
+    """
+    Place each section in its correct place by changing
+    the coordinates on each loop.
+
+    Parameters
+    ----------
+    *args : indexable object
+        The input data (can be list or tuple). ValueError is
+        raised if more than six arguments are passed.
+
+    Returns
+    -------
+    None.
+
+    """
+    second_sections = []
+    third_sections = []
+    # Resize diagram to automatically scale in response to the number
+    # of problems in the input data.
+    if len(args) == 1 or len(args) == 2:
+        spine_length = (-2.1, 2)
+        head_pos = (2, 0)
+        tail_pos = ((-2.8, 0.8), (-2.8, -0.8), (-2.0, -0.01))
+        first_section = [1.6, 0.8]
+    elif len(args) == 3 or len(args) == 4:
+        spine_length = (-3.1, 3)
+        head_pos = (3, 0)
+        tail_pos = ((-3.8, 0.8), (-3.8, -0.8), (-3.0, -0.01))
+        first_section = [2.6, 1.8]
+        second_sections = [-0.4, -1.2]
+    else:  # num == 5 or 6
+        spine_length = (-4.1, 4)
+        head_pos = (4, 0)
+        tail_pos = ((-4.8, 0.8), (-4.8, -0.8), (-4.0, -0.01))
+        first_section = [3.5, 2.7]
+        second_sections = [1, 0.2]
+        third_sections = [-1.5, -2.3]
+
+    # Change the coordinates of the annotations on each loop.
+    for index, problem in enumerate(args):
+        top_row = True
+        cause_arrow_y = 1.7
+        if index % 2 != 0:  # Plot problems below the spine.
+            top_row = False
+            y_prob_angle = -16
+            cause_arrow_y = -1.7
+        else:  # Plot problems above the spine.
+            y_prob_angle = 16
+        # Plot the 3 sections in pairs along the main spine.
+        if index in (0, 1):
+            prob_arrow_x = first_section[0]
+            cause_arrow_x = first_section[1]
+        elif index in (2, 3):
+            prob_arrow_x = second_sections[0]
+            cause_arrow_x = second_sections[1]
+        else:
+            prob_arrow_x = third_sections[0]
+            cause_arrow_x = third_sections[1]
+        if index > 5:
+            raise ValueError(f'Maximum number of problems is 6, you have entered '
+                             f'{len(args)}')
+
+        # draw main spine
+        ax.plot(spine_length, [0, 0], color=COLOR, linewidth=2)
+        # draw fish head
+        ax.text(head_pos[0] + 0.1, head_pos[1] - 0.05, 'PROBLEM', fontsize=10,
+                color='white')
+        semicircle = Wedge(head_pos, 1, 270, 90, fc=COLOR)
+        ax.add_patch(semicircle)
+        # draw fishtail
+        triangle = Polygon(tail_pos, fc=COLOR)
+        ax.add_patch(triangle)
+
+        problems(problem, prob_arrow_x, 0, -12, y_prob_angle)
+        causes(problem, cause_arrow_x, cause_arrow_y, top=top_row)
+
+
+# Input data
+method = ['Method', ['Time consumption', 'Cost', 'Procedures',
+                     'Inefficient process', 'Sampling']]
+machine = ['Machine', ['Faulty equipment', 'Compatibility']]
+material = ['Material', ['Poor-quality input', 'Raw materials', 'Supplier',
+                         'Shortage']]
+measure = ['Measurement', ['Calibration', 'Performance', 'Wrong measurements']]
+env = ['Environment', ['Bad conditions']]
+people = ['People', ['Lack of training', 'Managers', 'Labor shortage',
+                     'Procedures', 'Sales strategy']]
+
+draw_body(method, machine, material, measure, env, people)
+plt.show()


Code review comments:
diff_hunk: @@ -0,0 +1,200 @@
+"""
+================
+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 matplotlib.pyplot as plt
+
+from matplotlib.patches import Polygon, Wedge
+
+# Create the fishbone diagram
+fig, ax = plt.subplots(figsize=(10, 6), layout='constrained')
+ax.set_xlim(-5, 5)
+ax.set_ylim(-5, 5)
+ax.axis('off')
+COLOR = 'C0'
+
+
+def problems(data: list,
+             problem_x: float, problem_y: float,
+             prob_angle_x: float, prob_angle_y: float):
+    """
+    Draw each problem section of the Ishikawa plot.
+
+    Parameters
+    ----------
+    data : indexable object
+        The input data (can be list or tuple).
+    problem_x, problem_y : float, optional
+        The `X` and `Y` positions of the problem arrows (`Y` defaults to zero).
+    prob_angle_x, prob_angle_y : float, optional
+        The angle of the problem annotations. They are angled towards
+        the tail of the plot.
+
+    Returns
+    -------
+    None.
+
+    """
+    ax.annotate(str.upper(data[0]), xy=(problem_x, problem_y),
+                xytext=(prob_angle_x, prob_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=COLOR,
+                          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 : indexible object
+        The input data (can be list or tuple). 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
+
+    Returns
+    -------
+    None.
+
+    """
+    for index, cause in enumerate(data[1]):
+        # First cause annotation is placed in the middle of the problems arrow
+        # and each subsequent cause is plotted above or below it in succession.
+
+        # [<x pos>, [<y pos top>, <y pos bottom>]]
+        coords = [[0, [0, 0]],
+                  [0.23, [0.5, -0.5]],
+                  [-0.46, [-1, 1]],
+                  [0.69, [1.5, -1.5]],
+                  [-0.92, [-2, 2]],
+                  [1.15, [2.5, -2.5]]]
+        if top:
+            cause_y += coords[index][1][0]
+        else:
+            cause_y += coords[index][1][1]
+        cause_x -= coords[index][0]
+
+        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(*args):
+    """
+    Place each section in its correct place by changing
+    the coordinates on each loop.
+
+    Parameters
+    ----------
+    *args : indexable object
+        The input data (can be list or tuple). ValueError is
+        raised if more than six arguments are passed.
+
+    Returns
+    -------
+    None.
+
+    """
+    second_sections = []
+    third_sections = []
+    # Resize diagram to automatically scale in response to the number
+    # of problems in the input data.
+    if len(args) == 1 or len(args) == 2:
+        spine_length = (-2.1, 2)
+        head_pos = (2, 0)
+        tail_pos = ((-2.8, 0.8), (-2.8, -0.8), (-2.0, -0.01))
+        first_section = [1.6, 0.8]
+    elif len(args) == 3 or len(args) == 4:
+        spine_length = (-3.1, 3)
+        head_pos = (3, 0)
+        tail_pos = ((-3.8, 0.8), (-3.8, -0.8), (-3.0, -0.01))
+        first_section = [2.6, 1.8]
+        second_sections = [-0.4, -1.2]
+    else:  # num == 5 or 6
+        spine_length = (-4.1, 4)
+        head_pos = (4, 0)
+        tail_pos = ((-4.8, 0.8), (-4.8, -0.8), (-4.0, -0.01))
+        first_section = [3.5, 2.7]
+        second_sections = [1, 0.2]
+        third_sections = [-1.5, -2.3]
+
+    # Change the coordinates of the annotations on each loop.
+    for index, problem in enumerate(args):
+        top_row = True
+        cause_arrow_y = 1.7
+        if index % 2 != 0:  # Plot problems below the spine.
+            top_row = False
+            y_prob_angle = -16
+            cause_arrow_y = -1.7
+        else:  # Plot problems above the spine.
+            y_prob_angle = 16
+        # Plot the 3 sections in pairs along the main spine.
+        if index in (0, 1):
+            prob_arrow_x = first_section[0]
+            cause_arrow_x = first_section[1]
+        elif index in (2, 3):
+            prob_arrow_x = second_sections[0]
+            cause_arrow_x = second_sections[1]
+        else:
+            prob_arrow_x = third_sections[0]
+            cause_arrow_x = third_sections[1]
+        if index > 5:
+            raise ValueError(f'Maximum number of problems is 6, you have entered '
+                             f'{len(args)}')
+
+        # draw main spine
+        ax.plot(spine_length, [0, 0], color=COLOR, linewidth=2)
+        # draw fish head
+        ax.text(head_pos[0] + 0.1, head_pos[1] - 0.05, 'PROBLEM', fontsize=10,
+                color='white')
+        semicircle = Wedge(head_pos, 1, 270, 90, fc=COLOR)
+        ax.add_patch(semicircle)
+        # draw fishtail
+        triangle = Polygon(tail_pos, fc=COLOR)
+        ax.add_patch(triangle)
+
+        problems(problem, prob_arrow_x, 0, -12, y_prob_angle)
+        causes(problem, cause_arrow_x, cause_arrow_y, top=top_row)
+
+
+# Input data
+method = ['Method', ['Time consumption', 'Cost', 'Procedures',
+                     'Inefficient process', 'Sampling']]
+machine = ['Machine', ['Faulty equipment', 'Compatibility']]
+material = ['Material', ['Poor-quality input', 'Raw materials', 'Supplier',
+                         'Shortage']]
+measure = ['Measurement', ['Calibration', 'Performance', 'Wrong measurements']]
+env = ['Environment', ['Bad conditions']]
+people = ['People', ['Lack of training', 'Managers', 'Labor shortage',
+                     'Procedures', 'Sales strategy']]
path: galleries/examples/specialty_plots/ishikawa_diagram.py
text: @user1:
This seems like a dictionary - is there an advantage to this format? 

@user2:
Indeed, you can make this a single dict (problem: list of causes) and pass it to draw_body().

@author:
There's no advantage to it. I chose lists over a dictionary because @user3 disapproved of that format [in the old PR.](https://github.com/matplotlib/matplotlib/pull/25248#pullrequestreview-1316508821)

@user2:
I have not followed the whole topic, but my impression was that the no-dict comment refers to passing all data around through all functions as dict.

A dict as the top-level structure defining the diagram is much more concise:

```
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'],
}
```

diff_hunk: @@ -0,0 +1,200 @@
+"""
+================
+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 matplotlib.pyplot as plt
+
+from matplotlib.patches import Polygon, Wedge
+
+# Create the fishbone diagram
+fig, ax = plt.subplots(figsize=(10, 6), layout='constrained')
+ax.set_xlim(-5, 5)
+ax.set_ylim(-5, 5)
+ax.axis('off')
+COLOR = 'C0'
path: galleries/examples/specialty_plots/ishikawa_diagram.py
text: @user1:
What about using a non-default color to make it clear your doing this so that you use a consistent color through out? 

@user2:
You can use "tab:blue" to get the same color by a fixed name.

@user1:
yeah so my point is use a color that's not the matplotlib default 'cause that's gonna make it crystal clear that the point of this variable is to keep the color in sync (and the blue isn't an artifact of it being the default)

@user2:
I like keeping the default colors in the examples. This makes for a homogeneous visual impression and "brand identity". If there is a dedicated color configuration, its correct usage is implied. We don't have to choose another color just for the purpose of proving that this is properly applied.

@author:
To be honest, I only made that variable to make it easier to experiment with different colours. I can just as easily remove it since it's only being used in 3 places.