Hi @jmmease,
Thanks so much for your reply.
The problem I was having was both with the initial generation time and interaction.
I had tried to use heatmapgl
but that didn’t help with generation time and also did not seem to help much with the interaction.
I had a look at the Datashader
case study you suggested, and in there I found exactly what I was looking for: a way to install a callback from pan/zoom to a function.
Based on this, and after a few more tricks, I finally managed to obtain what I wanted, even without the need to use PNG images and the Datashader
module, just pure plotly
.
I can now make a large image and plot a low-resolution of it until I zoom all the way down to the original data.
I post my solution below, in case anyone else finds it useful.
# In[ ]:
from plotly.offline import init_notebook_mode, iplot
init_notebook_mode(connected=True)
import plotly.graph_objs as go
import numpy as np
# In[ ]:
# Define image maximum resolution to be displayed
res = 128
# Define full resolution image
N = 2000
M = 1000
# xx and yy are the edges of the pixels in the full image
xx = np.arange(N+1, dtype=np.float64)
yy = np.arange(M+1, dtype=np.float64)
x, y = np.meshgrid(xx, yy)
b = N/40.0
c = M/2.0
r = np.sqrt(((x[:-1]-c)/b)**2 + ((y[:-1]-c)/b)**2)
# zz is the heatmap values
zz = np.sin(r)
# xc and yc are the pixels centers
xc = 0.5 * (xx[1:] + xx[:-1])
yc = 0.5 * (yy[1:] + yy[:-1])
# Store the limits of the full resolution array for the colorbar
cmin = np.amin(zz)
cmax = np.amax(zz)
# In[ ]:
def resample_image(x_range, y_range):
# Find indices of xx and yy that are shown in current range
x_in_range = np.where(np.logical_and(xx >= x_range[0], xx <= x_range[1]))
y_in_range = np.where(np.logical_and(yy >= y_range[0], yy <= y_range[1]))
# xmin, xmax... here are array indices, not float coordinates
xmin = x_in_range[0][0]
xmax = x_in_range[0][-1]
ymin = y_in_range[0][0]
ymax = y_in_range[0][-1]
# here we perform a trick so that the edges of the displayed image is not greyed out
# if the zoom area slices a pixel in half, only the pixel inside the view area will be shown
# and the outer edge between that last pixel edge and the edge of the view frame area will
# be empty. So we extend the selected area with an additional pixel, if the selected area
# is inside the global limits of the full resolution array
xmin -= int(xmin > 0)
xmax += int(xmax < len(xx)-1)
ymin -= int(ymin > 0)
ymax += int(ymax < len(yy)-1)
# Local coordinate arrays
xxx = xx[xmin:xmax+1]
yyy = yy[ymin:ymax+1]
# Count the number of pixels in the current view
nx_view = xmax-xmin
ny_view = ymax-ymin
# Define x and y edges for histogramming
# If the number of pixels in the view area is larger than the max allowed resolution
# we create some custom pixels
if nx_view > res:
xe = np.linspace(xxx[0],xxx[-1],res)
else:
xe = xxx
if ny_view > res:
ye = np.linspace(yyy[0],yyy[-1],res)
else:
ye = yyy
# Optimize if no re-sampling is required
if (nx_view < self.resolution) and (ny_view < self.resolution):
z1 = self.z[ymin:ymax,xmin:xmax]
else:
xg, yg = np.meshgrid(xc[xmin:xmax], yc[ymin:ymax])
xv = np.ravel(xg)
yv = np.ravel(yg)
zv = np.ravel(zz[ymin:ymax,xmin:xmax])
# Histogram the data to make a low-resolution image
# Using weights in the second histogram allows us to then do z1/z0 to obtain the
# averaged data inside the coarse pixels
z0, yedges1, xedges1 = np.histogram2d(yv, xv, bins=(ye,xe))
z1, yedges1, xedges1 = np.histogram2d(yv, xv, bins=(ye,xe), weights=zv)
z1 /= z0
# Here we perform another trick. If we plot simply the local arrays in plotly, the reset axes
# or home functionality will be lost because plotly will now think that the data that eixsts
# is only the small window shown after a zoom. So we add a one-pixel padding area to the local
# z array. The size of that padding extends from the edges of the initial full resolution array
# (e.g. x=0, y=0) up to the edge of the view area. These large (and probably elongated) pixels
# add very little data and will not show in the view area but allow plotly to recover the full
# axes limits if we double-click on the plot
if xmin > 0:
xe = np.concatenate([xx[0:1], xe])
if xmax < len(xx)-1:
xe = np.concatenate([xe, xx[-1:]])
if ymin > 0:
ye = np.concatenate([yy[0:1], ye])
if ymax < len(yy)-1:
ye = np.concatenate([ye, yy[-1:]])
imin = int(xmin>0)
imax = int(xmax<(len(xx)-1))
jmin = int(ymin>0)
jmax = int(ymax<(len(yy)-1))
# the local z array
zzz = np.zeros([len(ye)-1, len(xe)-1])
zzz[jmin:len(ye)-jmax-1,imin:len(xe)-imax-1] = z1
return xe, ye, zzz
# In[ ]:
# Make an initial low-resolution sampling of the image for plotting
x_init, y_init, z_init = resample_image([xx[0], xx[-1]], [yy[0], yy[-1]])
trace = dict(type='heatmap', x=x_init, y=y_init, z=z_init)
data=[trace]
f = go.FigureWidget(data=data)
# In[ ]:
# The function bound to the on_change callback
def update_image(layout, x_range, y_range):
x_upd, y_upd, z_upd = resample_image(x_range, y_range)
# Using f.update allows us here to update all x, y and z at the same time
# We also apply the global colorbar limits to avoid the autoscaling of the colorbar as we zoom in
# and out
f.update({'data': [{'type':'heatmap', 'x':x_upd, 'y':y_upd, 'z':z_upd, 'zmin':cmin, 'zmax':cmax}]})
# In[ ]:
# Add a callback to update the view area
f.layout.on_change(update_image, 'xaxis.range', 'yaxis.range')
# In[ ]:
# Plot the Figure
f