Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Draw curved lines to connect points in matplotlib

So I am trying to plot curved lines to join points, here is the code I am using:-

def hanging_line(point1, point2):
    a = (point2[1] - point1[1])/(np.cosh(point2[0]) - np.cosh(point1[0]))
    b = point1[1] - a*np.cosh(point1[0])
    x = np.linspace(point1[0], point2[0], 100)
    y = a*np.cosh(x) + b

    return (x,y)

n_teams = 4
n_weeks = 4

fig, ax = plt.subplots(figsize=(6,6))

t = np.array([
    [1, 2, 4, 3],
    [4, 3, 3, 2],
    [3, 4, 1, 4],
    [2, 1, 2, 1]
])

fig.patch.set_facecolor('#1b1b1b')

for nw in range(n_weeks):
    ax.scatter([nw] * n_weeks, t[:, nw], marker='o', color='#4F535C', s=100, zorder=2)
    
ax.axis('off')
    
for team in t:
    x1, x2 = 0, 1
    
    for rank in range(0, len(team) - 1):
        y1 = n_weeks - team[rank] + 1
        y2 = n_weeks - team[rank + 1] + 1
        x, y = hanging_line([x1, y1], [x2, y2])
        ax.plot(x, y, color='#4F535C', zorder=1)
        x1 += 1
        x2 += 1

The code is producing the following output:- enter image description here

But I want the curved lines to look somewhat like this: enter image description here

What changes should I have to do in my code to get the required result?

like image 591
slothfulwave612 Avatar asked Aug 24 '20 11:08

slothfulwave612


People also ask

How do I mark points in Matplotlib?

Initialize a list for x and y with a single value. Limit X and Y axis range for 0 to 5. Lay out a grid in the current line style. Plot x and y using plot() method with marker="o", markeredgecolor="red", markerfacecolor="green".

How do you plot a curve in Matplotlib?

The matplotlib.pyplot.plot () function by default produces a curve by joining two adjacent points in the data with a straight line, and hence the matplotlib.pyplot.plot () function does not produce a smooth curve for a small range of data points.

How to add lines to paired data points in Matplotlib?

Adding lines to paired data points can be extremely helpful in understanding the relationship between two variables with respect to a third variable. Mainly we use Matplotlib’s plot () function and scatter () function to make scatter plot and add lines to paired data points.

How to connect points of scatter plot in Matplotlib?

To connect these points of scatter plot in order, call matplotlib.pyplot.plot (x, y) keeping x and y the same as ones passed into scatter () function. On the axes, sketch the curve y = x^3 – 2 You must show the coordinates of the y-intercept.

How do you plot a smooth spline curve in Python?

It plots a smooth spline curve by first determining the spline curve’s coefficients using the scipy.interpolate.make_interp_spline (). We use the given data points to estimate the coefficients for the spline curve, and then we use the coefficients to determine the y-values for very closely spaced x-values to make the curve appear smooth.


Video Answer


2 Answers

Here is an approach using bezier curves.

The sequence [...., i-indent, i, i + 0.8, ...] will put control points at each integer position i and some space before and after. The plot below used indent=0.8; indent=0 would create straight lines; with indent>1 the curves would be intersecting more. Other variations will make the curves more or less "cornered".

import matplotlib.pyplot as plt
from matplotlib.path import Path
import matplotlib.patches as patches
import numpy as np

n_teams = 4
n_weeks = 4
t = np.array([[1, 2, 4, 3],
              [4, 3, 3, 2],
              [3, 4, 1, 4],
              [2, 1, 2, 1]])
fig, ax = plt.subplots(figsize=(10, 4), facecolor='#1b1b1b')
ax.set_facecolor('#1b1b1b')

indent = 0.8
for tj in t:
    ax.scatter(np.arange(len(tj)), tj, marker='o', color='#4F535C', s=100, zorder=3)
    # create bezier curves
    verts = [(i + d, tij) for i, tij in enumerate(tj) for d in (-indent, 0, indent)][1:-1]
    codes = [Path.MOVETO] + [Path.CURVE4] * (len(verts) - 1)
    path = Path(verts, codes)
    patch = patches.PathPatch(path, facecolor='none', lw=2, edgecolor='#4F535C')
    ax.add_patch(patch)
ax.set_xticks([])
ax.set_yticks([])
ax.autoscale() # sets the xlim and ylim for the added patches
plt.show()

resulting plot

A colored version could look like:

colors = ['crimson', 'skyblue', 'lime', 'gold']
for tj, color in zip(t, colors):
    ax.scatter(np.arange(len(tj)), tj, marker='o', color=color, s=100, zorder=3)
    verts = [(i + d, tij) for i, tij in enumerate(tj) for d in (-indent, 0, indent)][1:-1]
    codes = [Path.MOVETO] + [Path.CURVE4] * (len(verts) - 1)
    path = Path(verts, codes)
    patch = patches.PathPatch(path, facecolor='none', lw=2, edgecolor=color)
    ax.add_patch(patch)

colored version

The following plot compares different values for indent:

comparing indents

like image 54
JohanC Avatar answered Oct 22 '22 21:10

JohanC


You can do this by customizing the connectionstyle argument of FancyArrowPatch. The documentation doesn't explain fraction and angle of bar well, I draw them out by enumeration.

import matplotlib.pyplot as plt


x1, y1 = 0.3, 0.2
x2, y2 = 0.8, 0.6

fig, axs = plt.subplots(2, 2)

axs[0, 0].plot([x1, x2], [y2, y1], ".")
axs[0, 0].annotate("",
            xy=(x1, y2), xycoords='data',
            xytext=(x2, y1), textcoords='data',
            arrowprops=dict(arrowstyle="-", color="0.5",
                            connectionstyle="bar,angle=180,fraction=-0.3",
                            ),
            )


axs[0, 1].plot([x1, x2], [y1, y2], ".")
axs[0, 1].annotate("",
            xy=(x1, y1), xycoords='data',
            xytext=(x2, y2), textcoords='data',
            arrowprops=dict(arrowstyle="-", color="0.5",
                            connectionstyle="bar,angle=180,fraction=-0.3",
                            ),
            )

axs[1, 0].plot([x1, x2], [y2, y1], ".")
axs[1, 0].annotate("",
            xy=(x1, y2), xycoords='data',
            xytext=(x2, y1), textcoords='data',
            arrowprops=dict(arrowstyle="-", color="0.5",
                            connectionstyle="bar,angle=90,fraction=-0.3",
                            ),
            )

axs[1, 1].plot([x1, x2], [y1, y2], ".")
axs[1, 1].annotate("",
            xy=(x1, y1), xycoords='data',
            xytext=(x2, y2), textcoords='data',
            arrowprops=dict(arrowstyle="-", color="0.5",
                            connectionstyle="bar,angle=270,fraction=-0.3",
                            ),
            )

for ax in axs.flat:
    ax.set(xlim=(0, 1), ylim=(0, 1), aspect=1)

fig.tight_layout(pad=0.2)

plt.show()

enter image description here

Annotations — Annotating with Arrow and Connectionstyle Demo for reference.


Back to your problem. I use two if to judge the position of (x1, y1) and (x2, y2) to check which connectionstyle they should use.

import matplotlib.pyplot as plt
import numpy as np

n_teams = 4
n_weeks = 4

fig, ax = plt.subplots(figsize=(6,6))

t = np.array([
    [1, 2, 4, 3],
    [4, 3, 3, 2],
    [3, 4, 1, 4],
    [2, 1, 2, 1]
])

fig.patch.set_facecolor('#1b1b1b')

for nw in range(n_weeks):
    ax.scatter([nw] * n_weeks, t[:, nw], marker='o', color='#4F535C', s=100, zorder=2)
    
ax.axis('off')
    
for team in t:
    x1, x2 = 0, 1
    
    for rank in range(0, len(team) - 1):
        y1 = n_weeks - team[rank] + 1
        y2 = n_weeks - team[rank + 1] + 1

        if (x1 < x2 and y1 > y2):
            ax.annotate("",
                    xy=(x1, y1), xycoords='data',
                    xytext=(x2, y2), textcoords='data',
                    arrowprops=dict(arrowstyle="-", color="0.5",
                                    connectionstyle="bar,angle=180,fraction=-0.2",
                                    ),
                    )

        if (x1 < x2 and y1 < y2):
            ax.annotate("",
                        xy=(x1, y1), xycoords='data',
                        xytext=(x2, y2), textcoords='data',
                        arrowprops=dict(arrowstyle="-", color="0.5",
                                        connectionstyle="bar,angle=270,fraction=-0.4",
                                        ),
                        )

        x1 += 1
        x2 += 1

plt.show()

enter image description here


Here is an example how to enumerate:

import matplotlib.pyplot as plt


x1, y1 = 0.3, 0.3
x2, y2 = 0.6, 0.6

fig, axs = plt.subplots(5, 5)

angle = 0

for ax in axs.flat:
    ax.plot([x1, x2], [y1, y2], ".")
    ax.annotate("",
                xy=(x1, y1), xycoords='data',
                xytext=(x2, y2), textcoords='data',
                arrowprops=dict(arrowstyle="-", color="0.5",
                                connectionstyle=f"bar,angle={angle},fraction=-0.3",
                                ),
                )
    ax.set_title(angle)
    angle += 15
    ax.set(xlim=(0, 1), ylim=(0, 1), aspect=1)

fig.tight_layout(pad=0.2)

plt.show()

enter image description here

like image 40
Ynjxsjmh Avatar answered Oct 22 '22 22:10

Ynjxsjmh