I have trouble understanding how mplcursors cursors work. Let me give an example.
import pandas as pd
import matplotlib.pyplot as plt
import mplcursors
%matplotlib qt5
def random_data(rows_count):
data = []
for i in range(rows_count):
row = {}
row["x"] = np.random.uniform()
row["y"] = np.random.uniform()
if (i%2 == 0):
row["type"] = "sith"
row["color"] = "red"
else:
row["type"] = "jedi"
row["color"] = "blue"
data.append(row)
return pd.DataFrame(data)
data_df = random_data(30)
fig, ax = plt.subplots(figsize=(8,8))
ax = plt.gca()
types = ["jedi","sith"]
for scat_type in types:
local_data_df = data_df.loc[data_df["type"] == scat_type]
scat = ax.scatter(local_data_df["x"],
local_data_df["y"],
c=local_data_df["color"],
label=scat_type)
cursor = mplcursors.cursor(scat, hover=mplcursors.HoverMode.Transient)
@cursor.connect("add")
def on_add(sel):
annotation = (local_data_df.iloc[sel.index]["type"]+
"\n"+str(local_data_df.iloc[sel.index]["x"])+
"\n"+str(local_data_df.iloc[sel.index]["y"]))
sel.annotation.set(text=annotation)
ax.legend()
plt.title("a battle of Force users")
plt.xlabel("x")
plt.ylabel("y")
plt.xlim(-1, 2)
plt.ylim(-1, 2)
ax.set_aspect('equal', adjustable='box')
plt.show()
This code is supposed to generate a DataFrame such that each row has random properties x, y, a type which is jedi or sith, and a color which is blue or red, depending on if the row is a jedi or a sith, then scatterplot the jedis in their color, attach to them a cursor, and then scatterplot the siths in their color, and attach to them another cursor, and display a legend box telling the reader that blue points correspond to jedi rows and red ones to sith ones.
However, when hovering points, the annotations say that all the points are sith and the coordinates do not look good.
I would like to understand why the code does not do what I would like it to do.
Just to clarify: I call .scatter() for each type (jedi or sith) and then try to attach a cursor to each of the plots because I have tried calling scatter on the whole data_df, but then the .legend() does not display what I want.
I hope that the answer you give me will be enough for me to be able to write a code that displays the jedi and the sith points, shows the right annotations and the right legend box.
There are a lot of strange things going on.
One of the confusions is that having the variable
local_data_dfinside aforloop would create a variable that would only be local to one cycle of the loop. Instead, it is just a global variable that gets overridden for each cycle. Similarly, defining the functionon_addinside theforloop doesn't make it local. Alsoon_addwill be global and overridden by each cycle of theforloop.Another confusion is that the connected function would have access to local variables from another function or loop. Instead, such local variables get inaccessible once the function or loop has finished.
Further, not that
sel.indexwill not be the index into the dataframe, but into the points of the scatter plot. You can reset the index of the "local df" to have it similar to the waysel.indexis ordered.To mimic your local variable, you can add extra data to the
scatobject. E.g.scat.my_data = local_dfwill add that variable to the global object that contains the scatter element (thePathCollectionthat contains all information matplotlib needs to represent the scatter points). Although the variablescatgets overridden, there is onePathCollectionfor each of the calls toax.scatter. (You can also access these viaax.collections).Here is a rewrite of your code, trying to stay as close as possible to the original: