Open In App

Cross-Hair Cursor in Matplotlib

Last Updated : 28 Nov, 2024
Comments
Improve
Suggest changes
Like Article
Like
Report

A cross-hair cursor is a precision tool designed to enhance accuracy by marking a specific point on the screen with two intersecting perpendicular lines. Commonly used in design, gaming, and analysis, the cross-hair cursor enables exact placement, measurement, or alignment.

Example: Imagine analyzing a graph in Matplotlib and needing to pinpoint the exact intersection of a sine wave with a specific value on the x-axis. With a cross-hair cursor, you can dynamically trace the coordinates and accurately identify the intersection point, making data analysis more precise and intuitive1. In data analysis, a cross-hair cursor can help pinpoint specific points on a graph. Here's an implementation in Matplotlib:

Python
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y = np.sin(x)

fig, ax = plt.subplots()
ax.plot(x, y, label="Sine Wave")
ax.set_title("Graph with Cross-Hair Cursor")
ax.set_xlabel("X-axis")
ax.set_ylabel("Y-axis")
ax.legend()

# Adding cross-hair functionality
def on_move(event):
    if event.inaxes:
        ax.lines = ax.lines[:1]  # Clear old cross-hair
        ax.axvline(event.xdata, color='red', linestyle='--')  # Vertical line
        ax.axhline(event.ydata, color='blue', linestyle='--')  # Horizontal line
        plt.draw()

fig.canvas.mpl_connect('motion_notify_event', on_move)
plt.show()

Output:

Cross-hair
Visualising the simple Cross hair Cursor with the Help of Matplotlib

This code plots a sine wave and adds interactive crosshairs. As the mouse moves, vertical and horizontal dashed lines are drawn at the cursor position using the on_move function. The previous crosshair is cleared to ensure only one is visible at a time. The mouse movement is tracked via mpl_connect to trigger the on_move function. Now, let's move forward to explore the three practical implementations of the crosshair cursor:

  1. A simple cursor that redraws the figure on every mouse move, which may cause some lag.
  2. A cursor that uses blitting to speed up rendering.
  3. A cursor that snaps to the data points for precise positioning.

1. A Simple Cursor in Matplotlib

A cursor in Matplotlib is essentially a visual indicator that can be moved around the plot. It can be used to highlight specific data points or regions of interest. A simple example of how to create a cursor that displays the x and y coordinates of the mouse position as it moves over the plot:

Python
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backend_bases import MouseEvent


class Cursor:
    """
    A cross hair cursor.
    """
    def __init__(self, ax):
        self.ax = ax
        self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
        self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
        # text location in axes coordinates
        self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)

    def set_cross_hair_visible(self, visible):
        need_redraw = self.horizontal_line.get_visible() != visible
        self.horizontal_line.set_visible(visible)
        self.vertical_line.set_visible(visible)
        self.text.set_visible(visible)
        return need_redraw

    def on_mouse_move(self, event):
        if not event.inaxes:
            need_redraw = self.set_cross_hair_visible(False)
            if need_redraw:
                self.ax.figure.canvas.draw()
        else:
            self.set_cross_hair_visible(True)
            x, y = event.xdata, event.ydata
            # update the line positions
            self.horizontal_line.set_ydata([y])
            self.vertical_line.set_xdata([x])
            self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')
            self.ax.figure.canvas.draw()


x = np.arange(0, 1, 0.01)
y = np.sin(2 * 2 * np.pi * x)

fig, ax = plt.subplots()
ax.set_title('Simple cursor')
ax.plot(x, y, 'o')
cursor = Cursor(ax)
fig.canvas.mpl_connect('motion_notify_event', cursor.on_mouse_move)

# Simulate a mouse move to (0.5, 0.5), needed for online docs
t = ax.transData
MouseEvent(
    "motion_notify_event", ax.figure.canvas, *t.transform((0.5, 0.5))
)._process()

Output:

Screenshot-2024-11-27-124053
Simple Cursor

2. Blitting Cursor - For speedup of the rendering

This technique involves storing the rendered plot as a background image, with only the modified elements (like the crosshair lines and text) being re-rendered. These updated elements are then combined with the background using blitting.

This method offers a significant performance boost. However, it requires extra setup, as the background must be stored without the crosshair lines (via create_new_background()). Additionally, a new background must be generated whenever the figure changes, which is handled by connecting to the draw_event.

Python
class BlittedCursor:
    """
    A cross-hair cursor using blitting for faster redraw.
    """
    def __init__(self, ax):
        self.ax = ax
        self.background = None
        self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
        self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
        # text location in axes coordinates
        self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)
        self._creating_background = False
        ax.figure.canvas.mpl_connect('draw_event', self.on_draw)

    def on_draw(self, event):
        self.create_new_background()

    def set_cross_hair_visible(self, visible):
        need_redraw = self.horizontal_line.get_visible() != visible
        self.horizontal_line.set_visible(visible)
        self.vertical_line.set_visible(visible)
        self.text.set_visible(visible)
        return need_redraw

    def create_new_background(self):
        if self._creating_background:
            # discard calls triggered from within this function
            return
        self._creating_background = True
        self.set_cross_hair_visible(False)
        self.ax.figure.canvas.draw()
        self.background = self.ax.figure.canvas.copy_from_bbox(self.ax.bbox)
        self.set_cross_hair_visible(True)
        self._creating_background = False

    def on_mouse_move(self, event):
        if self.background is None:
            self.create_new_background()
        if not event.inaxes:
            need_redraw = self.set_cross_hair_visible(False)
            if need_redraw:
                self.ax.figure.canvas.restore_region(self.background)
                self.ax.figure.canvas.blit(self.ax.bbox)
        else:
            self.set_cross_hair_visible(True)
            # update the line positions
            x, y = event.xdata, event.ydata
            self.horizontal_line.set_ydata([y])
            self.vertical_line.set_xdata([x])
            self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')

            self.ax.figure.canvas.restore_region(self.background)
            self.ax.draw_artist(self.horizontal_line)
            self.ax.draw_artist(self.vertical_line)
            self.ax.draw_artist(self.text)
            self.ax.figure.canvas.blit(self.ax.bbox)


x = np.arange(0, 1, 0.01)
y = np.sin(2 * 2 * np.pi * x)

fig, ax = plt.subplots()
ax.set_title('Blitted cursor')
ax.plot(x, y, 'o')
blitted_cursor = BlittedCursor(ax)
fig.canvas.mpl_connect('motion_notify_event', blitted_cursor.on_mouse_move)

# Simulate a mouse move to (0.5, 0.5), needed for online docs
t = ax.transData
MouseEvent(
    "motion_notify_event", ax.figure.canvas, *t.transform((0.5, 0.5))
)._process()

Output:

Screenshot-2024-11-27-154226
A cursor that uses blitting

3. Cursor - snaps to data points

  • This cursor locks its position to the data points of a Line2D object.
  • To minimize unnecessary redraws, the index of the last selected data point is stored in self._last_index. A redraw occurs only when the mouse moves sufficiently far to select a new data point. This helps reduce lag caused by frequent redraws. For further performance improvements, blitting could be implemented on top of this approach.

Snapping Behavior: The cursor is not free to move anywhere within the plot. Instead, it jumps from one data point to the next. This behavior is useful when you want to examine the values at specific data points precisely.

Python
class SnappingCursor:
    """
    A cross-hair cursor that snaps to the data point of a line, which is
    closest to the *x* position of the cursor.

    For simplicity, this assumes that *x* values of the data are sorted.
    """
    def __init__(self, ax, line):
        self.ax = ax
        self.horizontal_line = ax.axhline(color='k', lw=0.8, ls='--')
        self.vertical_line = ax.axvline(color='k', lw=0.8, ls='--')
        self.x, self.y = line.get_data()
        self._last_index = None
        # text location in axes coords
        self.text = ax.text(0.72, 0.9, '', transform=ax.transAxes)

    def set_cross_hair_visible(self, visible):
        need_redraw = self.horizontal_line.get_visible() != visible
        self.horizontal_line.set_visible(visible)
        self.vertical_line.set_visible(visible)
        self.text.set_visible(visible)
        return need_redraw

    def on_mouse_move(self, event):
        if not event.inaxes:
            self._last_index = None
            need_redraw = self.set_cross_hair_visible(False)
            if need_redraw:
                self.ax.figure.canvas.draw()
        else:
            self.set_cross_hair_visible(True)
            x, y = event.xdata, event.ydata
            index = min(np.searchsorted(self.x, x), len(self.x) - 1)
            if index == self._last_index:
                return  # still on the same data point. Nothing to do.
            self._last_index = index
            x = self.x[index]
            y = self.y[index]
            # update the line positions
            self.horizontal_line.set_ydata([y])
            self.vertical_line.set_xdata([x])
            self.text.set_text(f'x={x:1.2f}, y={y:1.2f}')
            self.ax.figure.canvas.draw()


x = np.arange(0, 1, 0.01)
y = np.sin(2 * 2 * np.pi * x)

fig, ax = plt.subplots()
ax.set_title('Snapping cursor')
line, = ax.plot(x, y, 'o')
snap_cursor = SnappingCursor(ax, line)
fig.canvas.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move)

# Simulate a mouse move to (0.5, 0.5), needed for online docs
t = ax.transData
MouseEvent(
    "motion_notify_event", ax.figure.canvas, *t.transform((0.5, 0.5))
)._process()

plt.show()

Output:

SnappingCursor
A cursor that snaps to data points.

To optimize performance, the cursor only redraws when the mouse moves a significant distance, reducing unnecessary redraws and potential lag.

Interactive Visualization with Crosshairs in Matplotlib

Now lets move forward to explore the additional functionalities of Cross-hair cursor.

1. Using the Cursor Widget with axhline and axvline

The Cursor widget is an interactive tool that displays horizontal and vertical lines that move with the mouse pointer. This method is significant because it allows users to dynamically track values on the plot without cluttering the visualization with static lines. It enhances user interaction by providing immediate feedback on data points.

Python
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Cursor
matplotlib.use('TkAgg')  # Use TkAgg backend for GUI support


# Generate random data for the scatter plot
np.random.seed(0)
x = np.random.rand(50) * 10  # 50 random values between 0 and 10 for the x-axis
y = np.random.rand(50) * 10  # 50 random values between 0 and 10 for the y-axis

# Create a figure and axis
fig, ax = plt.subplots()

# Scatter plot the data
ax.scatter(x, y, color='blue', label="Random Data Points")
ax.set_title("Interactive Scatter Plot with Cross-Hair Cursor")
ax.set_xlabel("X-axis")
ax.set_ylabel("Y-axis")
ax.legend()

# Create the Cursor widget with blitting enabled
cursor = Cursor(ax, useblit=True, color='red', linewidth=2)

# Show the plot
plt.show()


Output:

Widgets-
Cursor Widgets

2. Drawing Static Crosshairs

You can create static crosshairs using axhline() and axvline(), which draw horizontal and vertical lines across the axes at specified y and x values, respectively. This method is significant for providing clear reference lines at specific data points or thresholds, which can help in comparative analysis.

Python
import matplotlib.pyplot as plt
import numpy as np

# Generate random data for the scatter plot
np.random.seed(0)
x = np.random.rand(50) * 10  # 50 random values between 0 and 10 for the x-axis
y = np.random.rand(50) * 10  # 50 random values between 0 and 10 for the y-axis

# Create a figure and axis
fig, ax = plt.subplots()

# Scatter plot the data
ax.scatter(x, y, color='blue', label="Random Data Points")
ax.set_title("Scatter Plot with Static Crosshairs")
ax.set_xlabel("X-axis")
ax.set_ylabel("Y-axis")
ax.legend()

# Draw static crosshairs at the specified position (e.g., at x=5, y=5)
x_crosshair = 5  # X position for the vertical line
y_crosshair = 5  # Y position for the horizontal line

# Draw the horizontal and vertical lines (static crosshairs)
ax.axhline(y=y_crosshair, color='purple', linestyle='--', linewidth=2, label=f'Y={y_crosshair}')
ax.axvline(x=x_crosshair, color='purple', linestyle='--', linewidth=2, label=f'X={x_crosshair}')

# Add a label or text for clarity (static text near the crosshair)
ax.text(x_crosshair + 0.2, y_crosshair, f'({x_crosshair}, {y_crosshair})', color='purple')

# Display the plot
plt.show()


Output:

Screenshot-2024-11-28-130533
Static Crosshairs

The lines are drawn at fixed coordinates, specifically at x_crosshair = 5 and y_crosshair = 5.

  • The horizontal line is created using ax.axhline(y=y_crosshair, ...), where y_crosshair specifies the y-coordinate for the horizontal line.
  • Similarly, the vertical line is created using ax.axvline(x=x_crosshair, ...), where x_crosshair specifies the x-coordinate for the vertical line.
  • These two lines intersect at the point (5, 5) on the plot, forming the static crosshairs

3. Custom Crosshair Function

Creating a custom function to draw crosshairs can be beneficial for more complex visualizations. This method allows for greater flexibility in terms of styling and positioning of the lines. It is significant because it enables users to tailor the appearance of crosshairs according to specific needs or aesthetic preferences.

Below is the Pseudocode:

def draw_crosshair(x, y):    
ax.axhline(y=y, color='purple', linestyle=':')
ax.axvline(x=x, color='purple', linestyle=':')

Key Features of the Cross-Hair Cursor

  • Customizable Appearance: The crosshairs can be adjusted in terms of color, thickness, and style, making them suitable for different lighting conditions or specific tasks. This helps improve visibility and reduces strain during long periods of use.
  • Dynamic Updates: The crosshairs update in real-time based on user input, such as mouse movements or snapping to a grid. This ensures precise, immediate feedback for accurate adjustments.
  • Versatile Applications: The crosshair feature is useful in a wide range of fields like gaming, design, CAD, and data analysis, each with tailored functionality to meet specific needs. Its versatility makes it an essential tool for tasks requiring precision.

Cross-Hair Cursor in Matplotlib - Key Takeaways

  1. A cross-hair cursor provides pixel-level accuracy and is crucial in fields like gaming, design, and data analysis.
  2. Implementation can be achieved using libraries like Pygame, Matplotlib, or Tkinter, depending on the application context.
  3. Customization and dynamic functionality make it a vital tool for improving productivity and accuracy.

Next Article

Similar Reads