32 votes

matplotlib : comment annoter un point sur un nuage de points en plaçant automatiquement une flèche ?

Si je fais un nuage de points avec matplotlib :

plt.scatter(randn(100),randn(100))
# set x, y lims
plt.xlim([...])
plt.ylim([...])

Je voudrais annoter un point donné (x, y) avec une flèche pointant vers elle et une étiquette. Je sais que cela peut être fait avec annotate mais j'aimerais que la flèche et son étiquette soient placées de manière "optimale" de telle sorte que, si cela est possible (compte tenu des échelles/limites actuelles de l'axe), la flèche et l'étiquette ne se chevauchent pas avec les autres points. par exemple, si vous voulez étiqueter un point aberrant. existe-t-il un moyen de faire cela ? cela ne doit pas être parfait, mais juste un placement intelligent de la flèche/étiquette, compte tenu uniquement de l'échelle/des limites de l'axe. (x,y) coordonnées du point à étiqueter. merci.

42voto

Joe Kington Points 68089

En gros, non, il n'y en a pas.

Les moteurs de mise en page qui gèrent le placement d'étiquettes de carte de manière similaire sont étonnamment complexes et dépassent la portée de matplotlib. (Les intersections de boîtes englobantes sont en fait un moyen assez pauvre de décider où placer les étiquettes. Quel est l'intérêt d'écrire une tonne de code pour quelque chose qui ne fonctionnera que dans un cas sur 1000) ?

En outre, en raison de la quantité de rendu de texte complexe que fait matplotlib (par exemple, latex), il est impossible de déterminer l'étendue du texte sans le rendre entièrement d'abord (ce qui est plutôt lent).

Cependant, dans de nombreux cas, vous constaterez que l'utilisation d'une boîte transparente derrière votre étiquette placée avec annotate constitue une solution de rechange adéquate.

Par exemple

import numpy as np
import matplotlib.pyplot as plt

np.random.seed(1)
x, y = np.random.random((2,500))

fig, ax = plt.subplots()
ax.plot(x, y, 'bo')

# The key option here is `bbox`. I'm just going a bit crazy with it.
ax.annotate('Something', xy=(x[0], y[0]), xytext=(-20,20), 
            textcoords='offset points', ha='center', va='bottom',
            bbox=dict(boxstyle='round,pad=0.2', fc='yellow', alpha=0.3),
            arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0.5', 
                            color='red'))

plt.show()

enter image description here

10voto

Phlya Points 2313

Utilisez adjustText (je l'ai écrit).

Étiquetons les 10 premiers points. Le seul paramètre que j'ai modifié est la diminution de la force de répulsion des points, car ils sont nombreux et nous voulons que l'algorithme prenne un peu plus de temps et place les annotations plus soigneusement.

import numpy as np
import matplotlib.pyplot as plt
from adjustText import adjust_text
np.random.seed(1)
x, y = np.random.random((2,500))

fig, ax = plt.subplots()
ax.plot(x, y, 'bo')
ts = []
for i in range(10):
    ts.append(plt.text(x[i], y[i], 'Something'+str(i)))
adjust_text(ts, x=x, y=y, force_points=0.1, arrowprops=dict(arrowstyle='->', 
color='red'))
plt.show()

enter image description here Ce n'est pas idéal, mais les points sont vraiment denses ici et parfois il n'y a aucun moyen de placer le texte près de sa cible sans en chevaucher aucun. Mais tout cela est automatique et facile à utiliser, et ne laisse pas non plus les étiquettes se chevaucher.

PS Il utilise les intersections de boîtes englobantes, mais plutôt avec succès je dirais !

1voto

Xopi García Points 111

Un autre exemple utilisant awesome Phlya Le paquet de l'entreprise est basé sur Texte d'ajustement_mtcars :

from adjustText import adjust_text
import matplotlib.pyplot as plt

mtcars = pd.read_csv(
    "https://gist.githubusercontent.com/seankross/a412dfbd88b3db70b74b/raw/5f23f993cd87c283ce766e7ac6b329ee7cc2e1d1/mtcars.csv"
)

def plot_mtcars(adjust=False, force_points=1, *args, **kwargs):
    # plt.figure(figsize=(9, 6))
    plt.scatter(mtcars["wt"], mtcars["mpg"], s=15, c="r", edgecolors=(1, 1, 1, 0))
    texts = []
    for x, y, s in zip(mtcars["wt"], mtcars["mpg"], mtcars["model"]):
        texts.append(plt.text(x, y, s, size=9))
    plt.xlabel("wt")
    plt.ylabel("mpg")
    if adjust:
        plt.title(
            "force_points: %.1f\n adjust_text required %s iterations"
            % (
                force_points,
                adjust_text(
                    texts,
                    force_points=force_points,
                    arrowprops=dict(arrowstyle="-", color="k", lw=0.5),
                    **kwargs,
                ),
            )
        )
    else:
        plt.title("Original")
    return plt

fig = plt.figure(figsize=(12, 12))

force_points = [0.5, 1, 2, 4]
for index, k in enumerate(force_points):
    fig.add_subplot(2, 2, index + 1)
    plot_mtcars(adjust=True, force_points=k)

enter image description here

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X