Skip to content

[ENH]: Improve plt.arrow default head shape #22379

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

Closed
ianhi opened this issue Feb 2, 2022 · 6 comments
Closed

[ENH]: Improve plt.arrow default head shape #22379

ianhi opened this issue Feb 2, 2022 · 6 comments

Comments

@ianhi
Copy link
Contributor

ianhi commented Feb 2, 2022

Problem

The default for the head width/length of the fancy arrow patch are not very good. See for example: https://philbull.wordpress.com/2012/04/05/drawing-arrows-in-matplotlib/

This means that in order to get a usable arrow you need to mess around with options such as head_length which can be tricky to learn about.

Proposed solution

Use a new heuristic for head sizing as a default behavior.

@kmdalton suggested the below function norm_arrow which you can see gives better defaults in two basic test cases.

import matplotlib.pyplot as plt
import numpy as np

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

lims = (
    [-1, 1],
    [-10,10],
)

def norm_arrow(ax, x1, y1, x2, y2, head_scale=0.1, head_width=None, **kwargs):
    norm = np.sqrt((x2-x1)*(x2-x1) + (y2-y1)*(y2-y1))
    l = head_scale*norm
    w = head_width if head_width is not None else 0.5*l
    ax.arrow(x1, y1, x2, y2, head_width=w, head_length=l, length_includes_head=True, **kwargs)

axes[0][1].set_title("Norm based default")
axes[0][0].set_title("Current Default")

for axs, lim in zip(axes, lims):
    axs[0].set_ylim(lim)
    axs[0].set_xlim(lim)
    axs[1].set_ylim(lim)
    axs[1].set_xlim(lim)

    axs[0].arrow( 0, 0, lim[1]/2, lim[1]/2, fc="k", ec="k" )
    norm_arrow(axs[1], 0, 0, lim[1]/2, lim[1]/2, fc="k", ec="k" )

plt.show()

image

@kmdalton
Copy link

kmdalton commented Feb 2, 2022

The function should actually be the following:

def norm_arrow(ax, x1, y1, x2, y2, head_scale=0.1, head_width=None, **kwargs):
    dx,dy = x2-x1,y2-y1
    norm = np.sqrt(dx*dx + dy*dy)
    l = head_scale*norm
    w = head_width if head_width is not None else 0.5*l
    ax.arrow(x1, y1, dx, dy, head_width=w, head_length=l, length_includes_head=True, **kwargs)

The original version only works for arrows that start at the origin (oops).

@jklymak
Copy link
Member

jklymak commented Feb 2, 2022

Plt.arrow is definitely broken and we have considered deprecating it.

But scaling arrow heads by the length of the arrow does not strike me as a very good solution either. For one arrow it looks good, but different length arrows will have different arrow heads. Also non-equal aspect ratio axes will have different head sizes depending on if the arrow points in one direction or the other.

Rather the arrowhead should dimensioned and drawn in physical units, which I am pretty sure is what happens with plt.annotate.

@jklymak
Copy link
Member

jklymak commented Feb 3, 2022

Crossref #20387 for discussion of deprecating arrow.

@jklymak
Copy link
Member

jklymak commented Feb 3, 2022

See #22382 for the purposed deprecation...

@kmdalton
Copy link

kmdalton commented Feb 3, 2022

I concur that plt.annotate draws nicer arrows. However, it has a number of drawbacks. In particular it does not

  • add handles for plt.legend based on label=
  • use the axes prop_cycler to set the color
  • appropriately set xlim and ylim

@ianhi
Copy link
Contributor Author

ianhi commented Feb 5, 2022

closing in favor of #22390

@ianhi ianhi closed this as completed Feb 5, 2022
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

4 participants