6

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?

weberc2
  • 7,423
  • 4
  • 41
  • 57
ebarr
  • 7,704
  • 1
  • 29
  • 40

2 Answers2

5

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()

enter image description here

Community
  • 1
  • 1
unutbu
  • 842,883
  • 184
  • 1,785
  • 1,677
  • I am looking to change the style of either one or many points as the same time (either using Lasso or a picker) so adding and keeping track of multiple added scatter dots is not ideal. However, I like the idea of tracking annotations and the code snipet works very nicely. – ebarr Mar 17 '13 at 12:39
4

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.

tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • Thanks, I've already had a mess around with using multiple Line2D objects, but for the functionality I'm looking for this solution is not really feasible (or rather, it's feasible but more effort than it is worth). – ebarr Mar 16 '13 at 20:12