I am working on an interactive plotting application which requires users to select data points from a matplotlib scatter plot. For clarity, I would like to be able to alter the colour and shape of a plotted point when it is clicked on (or selected by any means).
As the matplotlib.collections.PathCollection
class has a set_facecolors
method, altering the color of the points is relatively simple. However, I cannot see a similar way to update the marker shape.
Is there a way to do this?
A barebones illustration of the problem:
import numpy as np
import matplotlib.pyplot as plt
x = np.random.normal(0,1.0,100)
y = np.random.normal(0,1.0,100)
scatter_plot = plt.scatter(x, y, facecolor="b", marker="o")
#update the colour
new_facecolors = ["r","g"]*50
scatter_plot.set_facecolors(new_facecolors)
#update the marker?
#new_marker = ["o","s"]*50
#scatter_plot.???(new_marker) #<--how do I access the marker shapes?
plt.show()
Any ideas?
If what you are really after is highlighting the point selected by the user, then you could superimpose another dot (with dot = ax.scatter(...)
) on top of the point selected. Later, in response to user clicks, you could then use dot.set_offsets((x, y))
to change the location of the dot.
Joe Kington has written a wonderful example (DataCursor
) of how to add an annotation displaying the data coordinates when a user clicks on on artist (such as a scatter plot).
Here is a derivative example (FollowDotCursor
) which highlights and annotates data points when a user hovers the mouse over a point.
With the DataCursor
the data coordinates displayed are where the user clicks -- which might not be exactly the same coordinates as the underlying data.
With the FollowDotCursor
the data coordinate displayed is always a point in the underlying data which is nearest the mouse.
import numpy as np
import matplotlib.pyplot as plt
import scipy.spatial as spatial
def fmt(x, y):
return 'x: {x:0.2f}\ny: {y:0.2f}'.format(x=x, y=y)
class FollowDotCursor(object):
"""Display the x,y location of the nearest data point.
"""
def __init__(self, ax, x, y, tolerance=5, formatter=fmt, offsets=(-20, 20)):
try:
x = np.asarray(x, dtype='float')
except (TypeError, ValueError):
x = np.asarray(mdates.date2num(x), dtype='float')
y = np.asarray(y, dtype='float')
self._points = np.column_stack((x, y))
self.offsets = offsets
self.scale = x.ptp()
self.scale = y.ptp() / self.scale if self.scale else 1
self.tree = spatial.cKDTree(self.scaled(self._points))
self.formatter = formatter
self.tolerance = tolerance
self.ax = ax
self.fig = ax.figure
self.ax.xaxis.set_label_position('top')
self.dot = ax.scatter(
[x.min()], [y.min()], s=130, color='green', alpha=0.7)
self.annotation = self.setup_annotation()
plt.connect('motion_notify_event', self)
def scaled(self, points):
points = np.asarray(points)
return points * (self.scale, 1)
def __call__(self, event):
ax = self.ax
# event.inaxes is always the current axis. If you use twinx, ax could be
# a different axis.
if event.inaxes == ax:
x, y = event.xdata, event.ydata
elif event.inaxes is None:
return
else:
inv = ax.transData.inverted()
x, y = inv.transform([(event.x, event.y)]).ravel()
annotation = self.annotation
x, y = self.snap(x, y)
annotation.xy = x, y
annotation.set_text(self.formatter(x, y))
self.dot.set_offsets((x, y))
bbox = ax.viewLim
event.canvas.draw()
def setup_annotation(self):
"""Draw and hide the annotation box."""
annotation = self.ax.annotate(
'', xy=(0, 0), ha = 'right',
xytext = self.offsets, textcoords = 'offset points', va = 'bottom',
bbox = dict(
boxstyle='round,pad=0.5', fc='yellow', alpha=0.75),
arrowprops = dict(
arrowstyle='->', connectionstyle='arc3,rad=0'))
return annotation
def snap(self, x, y):
"""Return the value in self.tree closest to x, y."""
dist, idx = self.tree.query(self.scaled((x, y)), k=1, p=1)
try:
return self._points[idx]
except IndexError:
# IndexError: index out of bounds
return self._points[0]
x = np.random.normal(0,1.0,100)
y = np.random.normal(0,1.0,100)
fig, ax = plt.subplots()
cursor = FollowDotCursor(ax, x, y, formatter=fmt, tolerance=20)
scatter_plot = plt.scatter(x, y, facecolor="b", marker="o")
#update the colour
new_facecolors = ["r","g"]*50
scatter_plot.set_facecolors(new_facecolors)
plt.show()
Pretty sure there is no way to do this. scatter
has turned your data into a collection of paths and no longer has the meta-data you would need to do this (ie, it knows nothing about the semantics of why it is drawing a shape, it just has a list of shapes to draw).
You can also update the colors with set_array
(as PathCollection
is a sub-class of ScalerMappable
).
If you want to do this (and have a reasonably small number of points) you can manage the paths by hand.
The other (simpler) option is to use two (or several, one for each shape/color combination you want) Line2D
objects (as you are not in this example scaling the size of the markers) with linestyle='none'
. The picker event on Line2D
objects will give you back which point you were nearest.
Sorry this is rambley.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With