pythonmatplotlibmplot3d

Implementing panning in matplotlib 3D plot


I have a 3D plot that I embed in tkinter and I'm trying to make interactive by binding mouse events to rotation, zooming and panning. I implemented the rotation and zooming function by referring to their axes3d source code and modifying the elev and azim or x, y and z limits respectively myself.

However, there is the panning part that is not implemented in the source code. It simply said

#        elif self.button_pressed == 2:
            # pan view
            # project xv, yv, zv -> xw, yw, zw
            # pan
#            pass

So I tried to implement it myself but I can't get it right. Here's what I have, if put into their source code:

The problem I'm having is how to project the 2 dx and dy values of mouse movement into the 3 dxx, dyy and dzz values of the 3d axes. What I have right now is wrong, although I can't really figure out why. It just doesn't always pan the way I want to, except for some values of elev and azim, such as when elev=0 and azim=0 or when only either one is at multiples of 90. When both elev and azim are at non-zero multiples of 90, it doesn't pan or pans the opposite way for some axes. When it's at any other value, it doesn't project to all the axes correctly such that it appears that I'm panning the whole figure left/right/up/down.

        elif self.button_pressed == 2:
            # get the x and y pixel coords
            if dx == 0 and dy == 0:
                return
            # how to convert dx dy -> dxx dyy dzz? this is wrong
            minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
            elev, azim = np.deg2rad(self.elev), np.deg2rad(self.azim)
            dxx = (maxx-minx) * ( (dx/w) * np.sin(azim) + (dy/h) * np.sin(elev) )
            dyy = (maxy-miny) * ( - (dx/w) * np.cos(azim) )
            dzz = (maxz-minz) * ( - (dy/h) * np.cos(elev) )
            # pan
            self.set_xlim3d(minx + dxx, maxx + dxx)
            self.set_ylim3d(miny + dyy, maxy + dyy)
            self.set_zlim3d(minz + dzz, maxz + dzz)
            self.get_proj()
            self.figure.canvas.draw_idle()

edit: the latest matplotlib source code would already have the panning implemented.


Solution

  • somehow this works

            elif self.button_pressed == 2:
                # get the x and y pixel coords
                if dx == 0 and dy == 0:
                    return
                # convert dx dy -> dxx dyy dzz
                minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
                elev, azim = np.deg2rad(self.elev), np.deg2rad(self.azim)
                dxe = (dy/h) * np.sin(elev)
                dye = - (dx/w)
                dze = - (dy/h) * np.cos(elev)
                dxx = (maxx-minx) * ( dxe * np.cos(azim) - dye * np.sin(azim) )
                dyy = (maxy-miny) * ( dye * np.cos(azim) + dxe * np.sin(azim) )
                dzz = (maxz-minz) * ( dze )
                # pan
                self.set_xlim3d(minx + dxx, maxx + dxx)
                self.set_ylim3d(miny + dyy, maxy + dyy)
                self.set_zlim3d(minz + dzz, maxz + dzz)
                self.get_proj()
                self.figure.canvas.draw_idle()
    

    I noticed that before it worked only when either one of elev or azim is 0, so I thought that maybe I need to project through elev first then azim.