I have a 2-d scatter plot of points, that correspond to images. I was wondering if there's an easy way to display the corresponding image (as a popup or tooltip) when you hover your mouse over each point? I tried plotly but found out you need to manually edit javascript to get the hover event to work. Is there a simple solution just with matplotlib or some other common package?
Find here a complete solution on how to display an image on hover events. It uses a 'motion_notify_event' to detect when the mouse is over a scatter point (hovering). If this is the case, it displays an image annotation with a corresponding image next to the hovered scatter point.
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np; np.random.seed(42)
# Generate data x, y for scatter and an array of images.
x = np.arange(20)
y = np.random.rand(len(x))
arr = np.empty((len(x),10,10))
for i in range(len(x)):
f = np.random.rand(5,5)
arr[i, 0:5,0:5] = f
arr[i, 5:,0:5] =np.flipud(f)
arr[i, 5:,5:] =np.fliplr(np.flipud(f))
arr[i, 0:5:,5:] = np.fliplr(f)
# create figure and plot scatter
fig = plt.figure()
ax = fig.add_subplot(111)
line, = ax.plot(x,y, ls="", marker="o")
# create the annotations box
im = OffsetImage(arr[0,:,:], zoom=5)
xybox=(50., 50.)
ab = AnnotationBbox(im, (0,0), xybox=xybox, xycoords='data',
boxcoords="offset points", pad=0.3, arrowprops=dict(arrowstyle="->"))
# add it to the axes and make it invisible
ax.add_artist(ab)
ab.set_visible(False)
def hover(event):
# if the mouse is over the scatter points
if line.contains(event)[0]:
# find out the index within the array from the event
ind, = line.contains(event)[1]["ind"]
# get the figure size
w,h = fig.get_size_inches()*fig.dpi
ws = (event.x > w/2.)*-1 + (event.x <= w/2.)
hs = (event.y > h/2.)*-1 + (event.y <= h/2.)
# if event occurs in the top or right quadrant of the figure,
# change the annotation box position relative to mouse.
ab.xybox = (xybox[0]*ws, xybox[1]*hs)
# make annotation box visible
ab.set_visible(True)
# place it at the position of the hovered scatter point
ab.xy =(x[ind], y[ind])
# set the image corresponding to that point
im.set_data(arr[ind,:,:])
else:
#if the mouse is not over a scatter point
ab.set_visible(False)
fig.canvas.draw_idle()
# add callback for mouse moves
fig.canvas.mpl_connect('motion_notify_event', hover)
plt.show()
If you want your images to be displayed in RGB, you have to slightly adjust the code. For this example the images need to be at your disk. Instead of using an 3darray for the images where the first dimension only represents the index you need to read the image with the plt.imread and then set_data to the corresponding position in your array containing the image names.
import matplotlib.pyplot as plt
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
import numpy as np; np.random.seed(42)
import os
os.chdir('Path/to/your/images')
# Generate data x, y for scatter and an array of images.
x = np.arange(3)
y = np.random.rand(len(x))
jpg_name_np = np.array(['904646.jpg', '903825.jpg', '905722.jpg']).astype('<U12') # names of your images files
cmap = plt.cm.RdYlGn
# create figure and plot scatter
fig = plt.figure()
ax = fig.add_subplot(111)
#line, = ax.plot(x,y, ls="", marker="o")
line = plt.scatter(x,y,c=heat, s=10, cmap=cmap)
image_path = np.asarray(jpg_name_np)
# create the annotations box
image = plt.imread(image_path[0])
im = OffsetImage(image, zoom=0.1)
xybox=(50., 50.)
ab = AnnotationBbox(im, (0,0), xybox=xybox, xycoords='data',
boxcoords="offset points", pad=0.3, arrowprops=dict(arrowstyle="->"))
# add it to the axes and make it invisible
ax.add_artist(ab)
ab.set_visible(False)
def hover(event):
# if the mouse is over the scatter points
if line.contains(event)[0]:
# find out the index within the array from the event
ind, = line.contains(event)[1]["ind"]
# get the figure size
w,h = fig.get_size_inches()*fig.dpi
ws = (event.x > w/2.)*-1 + (event.x <= w/2.)
hs = (event.y > h/2.)*-1 + (event.y <= h/2.)
# if event occurs in the top or right quadrant of the figure,
# change the annotation box position relative to mouse.
ab.xybox = (xybox[0]*ws, xybox[1]*hs)
# make annotation box visible
ab.set_visible(True)
# place it at the position of the hovered scatter point
ab.xy =(x[ind], y[ind])
# set the image corresponding to that point
im.set_data(plt.imread(image_path[ind]))
else:
#if the mouse is not over a scatter point
ab.set_visible(False)
fig.canvas.draw_idle()
# add callback for mouse moves
fig.canvas.mpl_connect('motion_notify_event', hover)
fig = plt.gcf()
fig.set_size_inches(10.5, 9.5)
plt.show()
Plot when hovering over scatterpoint
Related
I'm trying to construct a grid of black squares, and everytime you click on one it turns white. Now for some reason my code does very weird things:
The coordinates I input doesn't correspond to the array coordinates. I tried to change that by letting i = y - (N-1) and j = x with (x,y) the mouse coordinates. But only the first line will be converted properly (top row of the plot). The rest will be inverted vertically.
When all squares are white the plot automatically reset to black squares.
Here is my code:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import math
N = 3
# Make an empty data set
data = np.zeros((N, N))
# Make a figure + axes
fig, ax = plt.subplots(1, 1, tight_layout=True)
# Draw the boxes
box = ax.imshow(data, cmap='gray', extent=[0, N, 0, N])
# Draw the grid
for x in range(N + 1):
ax.axhline(x, lw=2, color='w', zorder=5)
ax.axvline(x, lw=2, color='w', zorder=5)
# Create interactivity
def on_click(event):
gx = event.xdata
gy = event.ydata
print('x=',gx)
print('y=',gy)
i = int(gy) - N + 1
j = int(gx)
data[i,j] = 1
ax.imshow(data, cmap='gray', extent=[0, N, 0, N])
fig.canvas.draw_idle()
fig = plt.gcf()
fig.canvas.mpl_connect('button_press_event', on_click)
# Turn off the axis labels
ax.axis('off')
plt.show()
Thanks for your help
Well I found the issues:
It was N - 1 - int(gy)
Matplotlib normalizes the values for me when they aren't specified. So a grid of all zeros is the same as a grid of all ones to it (each cell is equal to the mean, in both cases).
I have created a plot function that produces common x and multiple y-axes. It is working fine. I am having trouble setting the y-axes label, ticks, and edge to plot color.
Created plot function
import matplotlib.pyplot as plt
#https://matplotlib.org/stable/gallery/color/named_colors.html
import matplotlib.colors as mcolors
def oscilloscope_plot(df,x='x',y=['y1','y2'],xlbl = ['xlabel','xuni'],
ylbl = [['ylabel1','yuni1'],['ylabel2','yuni2']],
saveformat='Oscilloscope_plot.jpg'):
color_list = list(mcolors.TABLEAU_COLORS)
n = len(y)
fig, axes = plt.subplots(nrows=n,ncols=1,sharex=True)
for i in range(n):
# print('%s'%(y[i][0]))
axes[i] = plt.subplot((n*100)+10+(i+1))
plt.plot(df[x], df[y[i]],'-',color=color_list[i])
axes[i].grid(axis="y")
axes[i].legend(['%s'%(ylbl[i][0])])
# axes[i].set_ylabel('(in %s)'%(ylbl[i][1]), color=color_list[i])
axes[i].spines['left'].set_color(color_list[i])
axes[i].tick_params(axis='y', color=color_list[i])
plt.ylabel('(in %s)'%(ylbl[i][1]), color=color_list[i])
if i==0:
axes[i].spines['bottom'].set_visible(False)
elif i!=(n-1):
plt.setp(axes[i].get_xaxis(), visible=False)
axes[i].spines['top'].set_visible(False)
axes[i].spines['bottom'].set_visible(False)
# axes[i].spines['left'].set_edgecolor(color_list[i])
# axes[i].tick_params(axis='y', color=color_list[i])
else:
plt.setp(axes[i].get_xaxis(), visible=True)
axes[i].spines['top'].set_visible(False)
axes[i].spines['bottom'].set_visible(True)
axes[i].set_xlabel('%s (in %s)'%(xlbl[0],xlbl[1]))
# axes[i].spines['left'].set_edgecolor(color_list[i])
# axes[i].tick_params(axis='y', color=color_list[i])
plt.subplots_adjust(hspace=0.01)
plt.subplots_adjust(left=0.12, right=0.97, top=0.95, bottom=0.15)
temp_name = 'sun_inverterDCparams.png'
plt.savefig(temp_name,bbox_inches='tight',pad_inches=0.1, dpi=250)
plt.show()
Actual code
xdf = pd.DataFrame({'x':[10,20,30,40,50],'y1':[10,20,30,40,50],'y2':[10,20,30,40,50]})
oscilloscope_plot(xdf,x='x',y=['y1','y2'],xlbl=['X','x'],
ylbl=[['Y1','Y2'],['y1','y2'],['y1','y2']])
Present output:
In the below plot, I want to change the color of y axes ticks, label and spines to plot color.
i have a little problem to create a subplot loop.
The following code show my result for one plot.... So it starts with a dayloop than with a hour loop (8 timesteps).
If i run the code i get a nice QUiver plot with the colorbar.
for dd in range(1,15):
day=str(dd)
readfile=fns[files_indizes[dd]]
if dd < 10:
nc_u_comp = NetCDFFile(ROOT+u_comp1+'0'+day+comp)
nc_v_comp = NetCDFFile(ROOT+v_comp1+'0'+day+comp)
else:
nc_u_comp = NetCDFFile(ROOT+u_comp1+day+comp)
nc_v_comp = NetCDFFile(ROOT+v_comp1+day+comp)
time = nc_u_comp.variables['time'][:]
index=readfile.find(comp)
index=index+len(comp)
date=readfile[index-14:index-6]
plt.clf()
for tt in range(0,len(time)):
if tt < 10:
h =str(0)+str(tt)
else:
h=str(tt)
varU=nc_u_comp.variables['u10'][tt,:,:]
varV=nc_v_comp.variables['v10'][tt,:,:]
lat = nc_u_comp.variables['latitude'][:]
lon = nc_u_comp.variables['longitude'][:]
plt.rcParams["figure.figsize"] = [10,10]
#plane projection of the world
#map with box size (defintion on the top)
box = sgeom.box(minx=llcrnrlon, maxx=urcrnrlon, miny=llcrnrlat, maxy=urcrnrlat)
x0, y0, x1, y1 = box.bounds
#Map plot. The middel of the map is central_longitude
#proj = ccrs.PlateCarree(central_longitude=0)
proj=ccrs.PlateCarree()
#Change middelpoint of the map
box_proj = ccrs.PlateCarree(central_longitude=0)
ax2 = plt.axes(projection=proj)
ax2.set_extent([x0, x1, y0, y1], box_proj)
ax2.add_feature(cartopy.feature.BORDERS, linestyle='-', alpha=.5)
ax2.coastlines(resolution='50m')
#Definition of the scale_bar
gl = ax2.gridlines(ccrs.PlateCarree(), \
linestyle='--', alpha=1, linewidth=0.5, draw_labels=True)
gl.xlabels_top = False
gl.ylabels_right = False
gl.xformatter = LONGITUDE_FORMATTER
gl.yformatter = LATITUDE_FORMATTER
magnitude = (varU ** 2 + varV ** 2) ** 0.5
strm =plt.streamplot(lon , lat , varU, varV, linewidth=2, density=2, color=magnitude)
cbar= plt.colorbar()
cbar.set_label('$m/s$')
name='Wind in 10 m '+ date + h+' UTC'
ax2.set_aspect('auto')
plt.title(name, y=1)
Now i want to create an 2x4 Subplot array with a colorbar allocate to the complete Subplot array.
I find some infromation in the internet, but it doesn't run with my code. Maybe someone can help me?
This shows how to plot an array of simple Cartopy maps in 4 rows 2 columns. Also shows how to plot a colorbar to accompany the maps array. Hope it helps.
import numpy as np
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import matplotlib as mpl
# create figure with figsize big enough to accomodate all maps, labels, etc.
fig = plt.figure(figsize=(8, 10), tight_layout=False)
# define plot array's arrangement
columns = 2
rows = 4
# set projection to use
projex = ccrs.PlateCarree()
# set the colormap and norm for
# the colorbar to use
cmap1 = mpl.cm.magma
norm1 = mpl.colors.Normalize(vmin=0, vmax=100)
def plotmymap(axs):
# your plot specs of each map should replace this
img = np.random.randint(100, size=(15, 30)) # 2d array of random values (1-100)
# render image on current axis
plims = plt.imshow(img, extent=[-180,180,-90,90], alpha=0.5, cmap=cmap1, norm=norm1)
axs.set_global()
axs.coastlines()
# add title to the map
axs.set_title("Map_"+str(i))
return plims # for use by colorbar
for i in range(1, columns*rows +1):
# add a subplot into the array of plots
ax = fig.add_subplot(rows, columns, i, projection=projex)
plims = plotmymap(ax) # a simple maps is created on subplot
# add a subplot for vertical colorbar
bottom, top = 0.1, 0.9
left, right = 0.1, 0.8
fig.subplots_adjust(top=top, bottom=bottom, left=left, right=right, hspace=0.15, wspace=0.25)
cbar_ax = fig.add_axes([0.85, bottom, 0.05, top-bottom])
fig.colorbar(plims, cax=cbar_ax) # plot colorbar
plt.show() # this plot all the maps
The resulting plots:
I created a figure and axis using fig = plt.figure() and ax = fig.add_subplot(my_arguments). Then I added a few patches using matplotlib.patches. I transformed each patch by using matplotlib.transforms.Affine2D() to translate and rotate in data coordinates and then convert the transformed coordinates in display coordinates by adding ax.transData() to the end of my Affine2D transformations.
This is a simplified version of the code:
import matplotlib as mpl
import matplotlib.patches as patches
from matplotlib.transforms import Bbox
fig = plt.figure()
ax = fig.add_subplot(111)
# plot anything here
ax.plot(range(10), 'ro')
my_patches = []
# in my code there many patches and therefore the line
# below is actually a list comprehension for each one
my_patches.append(
patches.Rectangle( (1, 2), 10, 20, transform=mpl.transforms.Affine2D() \
.translate(1, 1) \
.rotate_deg_around(1, 2, 35)
+ ax.transData, fill=False, color='blue')
)
# now add a new axis using the coordinates of the patch
patch = my_patches[0]
# get the coords of the lower left corner of the patch
left, bottom = patch.get_xy()
# get its width and height
width, height = patch.get_width(), patch.get_height()
# create a Bbox instance using the coords of the patch
bbox = Bbox.from_bounds(left, bottom, width, height)
# transform from data coords to display coords
disp_coords = ax.transData.transform(bbox)
# transform from display coords to figure coords
fig_coords = fig.transFigure.inverted().transform(disp_coords)
# new axis
ax2 = fig.add_axes(Bbox(fig_coords))
# plot anything else here
ax2.plot(range(10), 'bo')
However, the additional axis is not added to the figure at the same position as the transformed coordinates of the patch (they're close, though). Am I missing something?
I'm uncertain about what the purpose of this code is, so this might not be what you want. But in order for the axes box to appear at coordinates (1,2), you should probably draw the canvas first before working with coordinates obtained from patches.
...
fig.canvas.draw()
left, bottom = patch.get_xy()
...
I was wondering, is it possible to offset the start of the radial axis or move it outside of the graph.
This is what I'm hoping to achieve:
And this is what I have for now.
I have read the documentation and different topics on SO, but I couldn't find anything helpful. Does that mean that it is not even possible if it is not mentioned anywhere.
Thank you in advance.
EDIT (added snippet of a code used to create the plot):
ax = fig.add_subplot(111, projection='polar')
ax.set_theta_zero_location('N')
ax.set_theta_direction(-1)
ax.plot(X,lines[li]*yScalingFactor,label=linelabels[li],color=color,linestyle=ls)
To offset the start of the radial axis:
EDIT: As of Matplotlib 2.2.3 there's a new Axes method called set_rorigin which does exactly that. You call it with the theoretical radial coordinate of the origin. So if you call ax.set_ylim(0, 2) and ax.set_rorigin(-1), the radius of the center circle will be a third of the radius of the plot.
A quick and dirty workaround for Matplotlib < 2.2.3 is to set the lower radial axis limit to a negative value and hide the inner part of the plot behind a circle:
import numpy as np
import matplotlib.pyplot as plt
CIRCLE_RES = 36 # resolution of circle inside
def offset_radial_axis(ax):
x_circle = np.linspace(0, 2*np.pi, CIRCLE_RES)
y_circle = np.zeros_like(x_circle)
ax.fill(x_circle, y_circle, fc='white', ec='black', zorder=2) # circle
ax.set_rmin(-1) # needs to be after ax.fill. No idea why.
ax.set_rticks([tick for tick in ax.get_yticks() if tick >= 0])
# or set the ticks manually (simple)
# or define a custom TickLocator (very flexible)
# or leave out this line if the ticks are fully behind the circle
To add a scale outside the plot:
You can add an extra axes object in the upper half of the other axes and use its yaxis:
X_OFFSET = 0 # to control how far the scale is from the plot (axes coordinates)
def add_scale(ax):
# add extra axes for the scale
rect = ax.get_position()
rect = (rect.xmin-X_OFFSET, rect.ymin+rect.height/2, # x, y
rect.width, rect.height/2) # width, height
scale_ax = ax.figure.add_axes(rect)
# hide most elements of the new axes
for loc in ['right', 'top', 'bottom']:
scale_ax.spines[loc].set_visible(False)
scale_ax.tick_params(bottom=False, labelbottom=False)
scale_ax.patch.set_visible(False) # hide white background
# adjust the scale
scale_ax.spines['left'].set_bounds(*ax.get_ylim())
# scale_ax.spines['left'].set_bounds(0, ax.get_rmax()) # mpl < 2.2.3
scale_ax.set_yticks(ax.get_yticks())
scale_ax.set_ylim(ax.get_rorigin(), ax.get_rmax())
# scale_ax.set_ylim(ax.get_ylim()) # Matplotlib < 2.2.3
Putting it all together:
(The example is taken from the Matplotlib polar plot demo)
r = np.arange(0, 2, 0.01)
theta = 2 * np.pi * r
ax = plt.subplot(111, projection='polar')
ax.plot(theta, r)
ax.grid(True)
ax.set_rorigin(-1)
# offset_radial_axis(ax) # Matplotlib < 2.2.3
add_scale(ax)
ax.set_title("A line plot on an offset polar axis", va='bottom')
plt.show()
I am not sure if the polar plot can be adjusted like that. But here is a work-around, based on the last example given here: Floating Axes.
I have included explanatory comments in the code, if you copy/paste it, it should run as-is:
import mpl_toolkits.axisartist.floating_axes as floating_axes
from matplotlib.projections import PolarAxes
from mpl_toolkits.axisartist.grid_finder import FixedLocator, \
MaxNLocator, DictFormatter
import numpy as np
import matplotlib.pyplot as plt
# generate 100 random data points
# order the theta coordinates
# theta between 0 and 2*pi
theta = np.random.rand(100)*2.*np.pi
theta = np.sort(theta)
# "radius" between 0 and a max value of 40,000
# as roughly in your example
# normalize the r coordinates and offset by 1 (will be clear later)
MAX_R = 40000.
radius = np.random.rand(100)*MAX_R
radius = radius/np.max(radius) + 1.
# initialize figure:
fig = plt.figure()
# set up polar axis
tr = PolarAxes.PolarTransform()
# define angle ticks around the circumference:
angle_ticks = [(0, r"$0$"),
(.25*np.pi, r"$\frac{1}{4}\pi$"),
(.5*np.pi, r"$\frac{1}{2}\pi$"),
(.75*np.pi, r"$\frac{3}{4}\pi$"),
(1.*np.pi, r"$\pi$"),
(1.25*np.pi, r"$\frac{5}{4}\pi$"),
(1.5*np.pi, r"$\frac{3}{2}\pi$"),
(1.75*np.pi, r"$\frac{7}{4}\pi$")]
# set up ticks and spacing around the circle
grid_locator1 = FixedLocator([v for v, s in angle_ticks])
tick_formatter1 = DictFormatter(dict(angle_ticks))
# set up grid spacing along the 'radius'
radius_ticks = [(1., '0.0'),
(1.5, '%i' % (MAX_R/2.)),
(2.0, '%i' % (MAX_R))]
grid_locator2 = FixedLocator([v for v, s in radius_ticks])
tick_formatter2 = DictFormatter(dict(radius_ticks))
# set up axis:
# tr: the polar axis setup
# extremes: theta max, theta min, r max, r min
# the grid for the theta axis
# the grid for the r axis
# the tick formatting for the theta axis
# the tick formatting for the r axis
grid_helper = floating_axes.GridHelperCurveLinear(tr,
extremes=(2.*np.pi, 0, 2, 1),
grid_locator1=grid_locator1,
grid_locator2=grid_locator2,
tick_formatter1=tick_formatter1,
tick_formatter2=tick_formatter2)
ax1 = floating_axes.FloatingSubplot(fig, 111, grid_helper=grid_helper)
fig.add_subplot(ax1)
# create a parasite axes whose transData in RA, cz
aux_ax = ax1.get_aux_axes(tr)
aux_ax.patch = ax1.patch # for aux_ax to have a clip path as in ax
ax1.patch.zorder=0.9 # but this has a side effect that the patch is
# drawn twice, and possibly over some other
# artists. So, we decrease the zorder a bit to
# prevent this.
# plot your data:
aux_ax.plot(theta, radius)
plt.show()
This will generate the following plot:
You'd have to tweak the axis labels to meet your demands.
I scaled the data because otherwise the same issue as with your plot would have occurred - the inner, empty circle would have been scaled to a dot. You might try the scaling with your polar plot and just put custom labels on the radial axis to achieve a similar effect.