Matplotlib - Viewlims



Viewlims or View limits, refer to the range of data that is displayed within a plot along the x and y axes. Viewlims are useful for interactive data visualization because they allow users to dynamically adjust the display of data.

Matplotlib provides various methods and tools for setting and updating viewlims interactively, enabling users to explore and analyze data effectively. In general, view limits are automatically determined based on the data being plotted in matplotlib plots.

This tutorial will see step by setp implementation of creating interactive zoom functionality in a Matplotlib plot with viewlims to create dynamic visualizations coresponding to the user interactions.

Creating an Interactive Zoom Plot

Create a class that regenerates a fractal set (the Mandelbrot set is a famous fractal in mathematics) as we zoom in, allowing us to observe increasing detail. Additionally, we'll display a box in the left panel to show the area to which we are zoomed.

This class defines the compute_image method to calculate the Mandelbrot set based on the provided bounds. And the axes_update method updates the plot based on the current view limits. It ensures that the Mandelbrot set is recalculated and redrawn whenever the view limits change.

class InteractiveFractal:

   def __init__(self, h=500, w=500, niter=50, radius=2., power=2):
      self.height = h
      self.width = w
      self.niter = niter
      self.radius = radius
      self.power = power

   def compute_image(self, xstart, xend, ystart, yend):
      self.x = np.linspace(xstart, xend, self.width)
      self.y = np.linspace(ystart, yend, self.height).reshape(-1, 1)
      c = self.x + 1.0j * self.y
      threshold_time = np.zeros((self.height, self.width))
      z = np.zeros(threshold_time.shape, dtype=complex)
      mask = np.ones(threshold_time.shape, dtype=bool)
      for i in range(self.niter):
         z[mask] = z[mask]**self.power + c[mask]
         mask = (np.abs(z) < self.radius)
         threshold_time += mask
      return threshold_time

   def axes_update(self, ax):
      ax.set_autoscale_on(False) 
        
      self.width, self.height = \
         np.round(ax.patch.get_window_extent().size).astype(int)
        
      vl = ax.viewLim
      extent = vl.x0, vl.x1, vl.y0, vl.y1

      im = ax.images[-1]
      im.set_data(self.compute_image(*extent))
      im.set_extent(extent)
      ax.figure.canvas.draw_idle()

Updating View Limits

We'll create a class UpdateRectangle to create a rectangle that represents the area to which we are zoomed in the Mandelbrot set. This class extends the Rectangle class and is used to update the rectangle representing the zoom area as the view limits change. As we zoom in on the left panel, the rectangle will update its shape to match the bounds of the axes.

class UpdateRectangle(Rectangle):
   def __call__(self, ax):
      self.set_bounds(*ax.viewLim.bounds)
      ax.figure.canvas.draw_idle()

Connecting Callbacks

Callbacks are connected to the xlim_changed and ylim_changed events of the second subplot (ax2). These callbacks trigger the update of the rectangle and the Mandelbrot set whenever the view limits change.

# Connect for changing the view limits
ax2.callbacks.connect('xlim_changed', rect)
ax2.callbacks.connect('ylim_changed', rect)

ax2.callbacks.connect('xlim_changed', md.ax_update)
ax2.callbacks.connect('ylim_changed', md.ax_update)

Here is the complete code

The Mandelbrot set is initially plotted in two subplots (ax1 and ax2). The rectangle representing the zoom area is added to ax1, and callbacks are connected to ax2 to handle view limit changes.

Example

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.patches import Rectangle

class UpdateRectangle(Rectangle):
   def __call__(self, ax):
      self.set_bounds(*ax.viewLim.bounds)
      ax.figure.canvas.draw_idle()

class InteractiveFractal:

   def __init__(self, h=500, w=500, niter=50, radius=2., power=2):
      self.height = h
      self.width = w
      self.niter = niter
      self.radius = radius
      self.power = power

   def compute_image(self, xstart, xend, ystart, yend):
      self.x = np.linspace(xstart, xend, self.width)
      self.y = np.linspace(ystart, yend, self.height).reshape(-1, 1)
      c = self.x + 1.0j * self.y
      threshold_time = np.zeros((self.height, self.width))
      z = np.zeros(threshold_time.shape, dtype=complex)
      mask = np.ones(threshold_time.shape, dtype=bool)
      for i in range(self.niter):
         z[mask] = z[mask]**self.power + c[mask]
         mask = (np.abs(z) < self.radius)
         threshold_time += mask
      return threshold_time

   def axes_update(self, ax):
      ax.set_autoscale_on(False) 
        
      self.width, self.height = \
         np.round(ax.patch.get_window_extent().size).astype(int)
        
      vl = ax.viewLim
      extent = vl.x0, vl.x1, vl.y0, vl.y1

      im = ax.images[-1]
      im.set_data(self.compute_image(*extent))
      im.set_extent(extent)
      ax.figure.canvas.draw_idle()

md = InteractiveFractal()
Z = md.compute_image(-2., 0.5, -1.25, 1.25)

fig1, (ax1, ax2) = plt.subplots(1, 2, figsize=(7, 4))
ax1.imshow(Z, origin='lower',
   extent=(md.x.min(), md.x.max(), md.y.min(), md.y.max()))
ax2.imshow(Z, origin='lower',
   extent=(md.x.min(), md.x.max(), md.y.min(), md.y.max()))

rect = UpdateRectangle(
   [0, 0], 0, 0, facecolor='none', edgecolor='black', linewidth=1.0)
rect.set_bounds(*ax2.viewLim.bounds)
ax1.add_patch(rect)

# Connect for changing the view limits
ax2.callbacks.connect('xlim_changed', rect)
ax2.callbacks.connect('ylim_changed', rect)

ax2.callbacks.connect('xlim_changed', md.axes_update)
ax2.callbacks.connect('ylim_changed', md.axes_update)
ax2.set_title("Zoom here")

plt.show()
Output

On executing the above program you will get the following output −

Viewlims Example 1 Observe the below video to see how the viewlims works here − Viewlims gif file
Advertisements