Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH]: Band and Sankey Diagramm #28227

Open
Paimard opened this issue May 15, 2024 · 2 comments
Open

[ENH]: Band and Sankey Diagramm #28227

Paimard opened this issue May 15, 2024 · 2 comments

Comments

@Paimard
Copy link

Paimard commented May 15, 2024

Problem

  • I'm always frustrated when I need a band and Sankey diagram when I analyze my data.
  • I would like to know if it's possible to generate these diagrams with Matplotlib.
  • Here is a sample of that.

Sanky Diagram ↓
Sankey_Diagrams_01

Band Diagram ↓
Band

Proposed solution

No response

@tacaswell
Copy link
Member

sankey: https://matplotlib.org/stable/gallery/specialty_plots/sankey_basics.html
streamplot: https://matplotlib.org/stable/gallery/lines_bars_and_markers/stackplot_demo.html#streamgraphs is probably the cloest thing to "band"

To build exact replicas of these I think you need

gradients: https://matplotlib.org/stable/gallery/lines_bars_and_markers/gradient_bar.html
bezier paths: https://matplotlib.org/stable/gallery/event_handling/path_editor.html, https://matplotlib.org/stable/users/explain/artists/paths.html#bezier-example
clipping images (gradients) to paths: https://matplotlib.org/stable/gallery/images_contours_and_fields/image_clip_path.html

Something we don't have (that I know of) is code like

from matplotlib.path import Path


def make_connector_path(x1, y1, y2, w, h, *, scale=0.2):

    h1 = h2 = h

    y3 = y2 + h2
    y4 = y1 + h1
    x2 = x1 + w

    mid1 = x1 * scale + x2 * (1 - scale)
    mid2 = x2 * scale + x1 * (1 - scale)

    pt1 = (x1, y1)
    ctla_1 = (mid1, y1)
    ctla_2 = (mid2, y2)
    pt2 = (x2, y2)
    pt3 = (x2, y3)
    ctlb_1 = (mid2, y3)
    ctlb_2 = (mid1, y4)
    pt4 = (x1, y4)

    pathdata = [
        (Path.MOVETO, pt1),
        (Path.CURVE4, ctla_1),
        (Path.CURVE4, ctla_2),
        (Path.CURVE4, pt2),
        (Path.LINETO, pt3),
        (Path.CURVE4, ctlb_1),
        (Path.CURVE4, ctlb_2),
        (Path.CURVE4, pt4),
        (Path.CLOSEPOLY, pt1),
    ]

    codes, verts = zip(*pathdata)
    path = Path(verts, codes)
    return path

that is parameterized to make generating those connector patches a bit nicer (and you can use that path to clip a gradient). If you assume both ends of the connector are the same height and the curve and the connector is symmetric, you only actually have 5 (position) + 1 (shape) degrees of freedom but need 18 values so there are a lot of ways to parameterize this function. This is what felt right to me sketching it out, but no idea if it is the "right" parameterization.

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backend_bases import MouseButton


from matplotlib.patches import PathPatch

fig, (ax, ax2) = plt.subplots(1, 2, sharex=True, sharey=True)

x1 = -1
w = 3
x2 = x1 + w
y1 = -1
y2 = 0
h = 2
y3 = y2 + h

path = make_connector_path(x1=x1, y1=y1, y2=y2, w=w, h=h)
patch = PathPatch(path, facecolor="green", edgecolor="yellow", alpha=0.5)
ax.add_patch(patch)

(ln,) = ax.plot(*zip(*path.vertices), marker="o", markerfacecolor="r")


im = ax2.imshow(np.tile(range(128), (128, 1)), extent=(x1, x2, y1, y3), aspect='auto')
im.set_clip_path(path, transform=ax2.transData)

ax.set_xlim(-3, 4)
ax.set_ylim(-3, 4)

plt.show()

so


These plots are very specialized and I expect that there is a variety of reasonable ways structure the input data. I suspect a complete def make_me_the_plot(ax, data) function may be out of scope for Matplotlib, but I think that we would be open to taking some more of the pieces.

In terms of both effort I think the path here is:

  • turn the above code into an example in the docs (avoids discussion of the best API as it is an example we expect users to copy-paste-edit), possible expanding to generate those two example images in the OP. This may be a good place to start no matter what to get a sense of what feels too verbose
  • add the "make connector path" function to the core library (only requires sorting out one API)
  • add a PathPatch sub-class that has what ever paramaterization we pick for the function that calls the function at draw time (like Circle or Rectangle but with a fancier underlying Path), but only a solid color
  • add gradient-via-image to that artist (I see this getting messy)
  • add gradient-via-image to PathPatch (I see this getting very messy)
  • add the concept of gradient at the backend level (this is a very heavy lift) and let the backends that native support it (maybe just svg?) do the efficient thing and the rest fallback to gradiant-as-clipped-image.
  • add one-liners for the two OP plots to Matplotlib (this is best done as a third-party stand alone library first and it may be best for it to stay like that forever (so you can depend on things like pandas and have a release cycle independent of upstream))

@OdileVidrine
Copy link
Contributor

https://matplotlib.org/stable/api/sankey_api.html Have you looked at this for making sankey diagrams in matplotlib? Is it missing a feature you want?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants