3

I am trying to get a scatter plot of 2 different variables, one set of scatter points is height values for a projectile under the effect of air resistant, and one is a projectile with no air resistance. I can get this working with 1 set of scatterpoints, but not both of them. I'm using the mplcursors library to show annotation labels when hovering over a point. This is the relevant code:

import numpy as np
from matplotlib import pyplot as plt
import mplcursors

# ...

fig, ax = plt.subplots()
nair_scatter = ax.scatter(nairRangeValues, nairHeightValues, c="blue", label="No air resistance", s=3)
air_scatter = ax.scatter(airRangeValues, airHeightValues, c="red", label="Air resistance", s=3)
ax.legend()
plt.xlabel("Range", size=10)
plt.ylabel("Height", size=10)

crs = mplcursors.cursor(ax,hover=True)
crs.connect("add", lambda sel: sel.annotation.set_text(
        'Range {} m\nHeight {} m\nVelocity {} m/s at angle {} degrees\nDisplacement {} m\nTime of flight {} s' .format(
        (sel.target[0]), (sel.target[1]),
        (airVelocityValues[get_index(airRangeValues, sel.target[0])]),
        (airAngleValues[get_index(airRangeValues, sel.target[0])]),
        (airDisplacementValues[get_index(airRangeValues, sel.target[0])]),
        (airTimeValues[get_index(airRangeValues, sel.target[0])]) ) ) )

crs2 = mplcursors.cursor(ax,hover=True)
crs2.connect("add", lambda ok: ok.annotation.set_text(
        'Range {} m\nHeight {} m' .format(ok.target[0], ok.target[1])))
plt.show()

There are a few problems with this. Firstly, it gives me a massive error and says StopIteration at the end. The other one is that it shows the correct label on 1 set of scatter points, but also shows the crs2 values for the same scatterplot, not the other one. I have no idea how to allow them to be able to be unique to each scatter point set, if anyone can help id appreciate it.

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
  • sorry, the height and range are the values for each scatterpoint, one is for air resistant, one is for no air resistance. –  Jan 18 '20 at 11:18
  • some extra code that might be relevant: fig, ax = plt.subplots() ax.scatter(nairRangeValues, nairHeightValues, c="blue", label="No air resistance", s=3) ax.scatter(airRangeValues, airHeightValues, c="red", label="Air resistance", s=3) ax.legend() plt.title(str(Title), size = 15) plt.xlabel("Range", size = 10) plt.ylabel("Height", size = 10) –  Jan 18 '20 at 11:19
  • 2
    Please **edit** your question rather than adding details as comments. – petezurich Jan 18 '20 at 11:21
  • I dont know how to do that –  Jan 18 '20 at 11:35

1 Answers1

4

Instead of connecting the cursors to the ax, try connecting them only to the scatter plot they belong to. The first parameter of mplcursors.cursor is meant for just that.

You'll notice that when you move to a point from the other set, that the annotation of the previous one is not removed. Therefore, I added some code to remove the other annotation when a new one is opened.

Note that you can directly access the index via sel.target.index. Calling get_index(airRangeValues, ...) with a point of the wrong set could be a cause of the error you encountered.

Here's some code to demonstrate the principles. The backgroundcolor of the annotations is set differently to better illustrate which cursor is being displayed. Also, the alpha is changed to ease reading the text.

import numpy as np
from matplotlib import pyplot as plt
import mplcursors

def cursor1_annotations(sel):
    sel.annotation.set_text(
        'Cursor One:\nRange {:.2f} m\nHeight {:.2f} m\nindex: {}'.format(sel.target[0], sel.target[1], sel.target.index))
    sel.annotation.get_bbox_patch().set(fc="powderblue", alpha=0.9)
    for s in crs2.selections:
        crs2.remove_selection(s)

def cursor2_annotations(sel):
    sel.annotation.set_text(
        'Cursor Two:\nRange {:.2f} m\nHeight {:.2f} m\nindex: {}'.format(sel.target[0], sel.target[1], sel.target.index))
    sel.annotation.get_bbox_patch().set(fc="lightsalmon", alpha=0.9)
    for s in crs1.selections:
        crs1.remove_selection(s)

N = 100
nairRangeValues = np.random.normal(30, 10, N)
nairHeightValues = np.random.uniform(40, 100, N)
airRangeValues = np.random.normal(40, 10, N)
airHeightValues = np.random.uniform(50, 120, N)

fig, ax = plt.subplots()

nair_scatter = ax.scatter(nairRangeValues, nairHeightValues, c="blue", label="No air resistance", s=3)
air_scatter = ax.scatter(airRangeValues, airHeightValues, c="red", label="Air resistance", s=3)
ax.legend()
plt.xlabel("Range", size=10)
plt.ylabel("Height", size=10)

crs1 = mplcursors.cursor(nair_scatter, hover=True)
crs1.connect("add", cursor1_annotations)
crs2 = mplcursors.cursor(air_scatter, hover=True)
crs2.connect("add", cursor2_annotations)

plt.show()

example plot

PS: Something similar can also be achieved using only one cursor and adding a test. In that case there is no need to manually remove the other cursor:

def cursor_annotations(sel):
    if sel.artist == nair_scatter:
        sel.annotation.set_text(
            'Cursor One:\nRange {:.2f} m\nHeight {:.2f} m\nindex: {}'.format(sel.target[0], sel.target[1], sel.target.index))
        sel.annotation.get_bbox_patch().set(fc="powderblue", alpha=0.9)
    else:
        sel.annotation.set_text(
            'Cursor Two:\nRange {:.2f} m\nHeight {:.2f} m\nindex: {}'.format(sel.target[0], sel.target[1], sel.target.index))
        sel.annotation.get_bbox_patch().set(fc="lightsalmon", alpha=0.9)

crs = mplcursors.cursor([nair_scatter, air_scatter], hover=True)
crs.connect("add", cursor_annotations)
JohanC
  • 71,591
  • 8
  • 33
  • 66
  • Wow, just wow man, that solution is so awesome and even colour coded for the values, thank you! –  Jan 19 '20 at 00:53
  • You could further improve the scatter plot by color-coding the dots using a third variable, for example `ax.scatter(airRangeValues, airHeightValues, c=airVelocityValues, cmap='Reds', ...)`. You could even use the size of the dots as a fourth dimension. – JohanC Jan 19 '20 at 01:03
  • Im not exactly sure what you mean by that, but also I just want to ask, is it possible that when im hovering over a set of scatter points, if I click, the label stays there until i click off it? Ie, i find a point on air resistant values, click it, then move to non air resistant values and click that and they both show? –  Jan 19 '20 at 01:21
  • Just remove the code `for s in crs2.selections: crs2.remove_selection(s)` in the two functions. That way the annotations stay until you hover over one of the same color, or you right-click it away. – JohanC Jan 19 '20 at 01:24
  • okay, thanks that helps alot, but say i wanted it to only show values if i hadn't clicked on a value or something if that makes sense? As in i hover over the values of air resistance, and it shows them and what not, and once i click, it shows that value i clicked on and until i right click, hovering over values does not change what it shows, and same for non air resistance values? –  Jan 19 '20 at 01:31
  • I don't think that would be easy with the current interface of mplcursors. But maybe you could just set `hover=False`? – JohanC Jan 19 '20 at 02:51
  • Brilliant, thanks alot man, and yeah youre too right, the function i made called get_index did cause an issue, but this solution you have given me is just incredibly helpful, thanks –  Jan 19 '20 at 04:13
  • This might also be a bit too much to do, but now that it works that when i click on each of them the labels both stay there, how can i get the labels to not overlap so you can clearly see both of them? –  Jan 19 '20 at 04:36
  • You can click on a label and move it around, not only to avoid the other label but also to avoid other information on the plot. There is nothing to automatically avoid the overlap. – JohanC Jan 19 '20 at 04:40
  • Ah, cool that works, there just a small bug with it, when both of them overlap, if i click to move one of them, it spazzes and both of the lables flash on and off and move at the same time, could i somehow get the label in front to be the only one to move? –  Jan 19 '20 at 04:48