How to have a fast crosshair mouse cursor for subplots in matplotlib? - python-3.x

In this video of backtrader's matplotlib implementation https://youtu.be/m6b4Ti4P2HA?t=2008 I can see that a default and very fast and CPU saving crosshair mouse cursor seems to exist in matplotlib.
I would like to have the same kind of mouse cursor for a simple multi subplot plot in matplotlib like this:
import numpy as np
import matplotlib
matplotlib.use('QT5Agg')
matplotlib.rcParams['figure.figsize'] = (20.0, 22.0)
import matplotlib.pyplot as plt
fig = plt.figure()
ax1 = plt.subplot(2, 1, 1)
ax2 = plt.subplot(2, 1, 2, sharex=ax1)
ax1.plot(np.array(np.random.rand(100)))
ax2.plot(np.array(np.random.rand(100)))
plt.show()
So, if I am with my mouse in the lower subplot, I want to see directly and very precisely, which value of x/y in the lower plot corresponds to which value pair in the upper plot.
I have found other solutions to do this but they seem to be very slow compared to the implementation in the video.

You can create a crosshair cursor via mplcursors. sel.extras.append() takes care that the old cursor is removed when a new is drawn. With sel.annotation.set_text you can adapt the popup annotation shown. To leave out the annotation, use sel.annotation.set_visible(False). To find the corresponding y-value in the other subplot, np.interp with the data extracted from the curve can be used.
import numpy as np
import matplotlib.pyplot as plt
import mplcursors
def crosshair(sel):
x, y2 = sel.target
y1 = np.interp( sel.target[0], plot1.get_xdata(), plot1.get_ydata() )
sel.annotation.set_text(f'x: {x:.2f}\ny1: {y1:.2f}\ny2: {y2:.2f}')
# sel.annotation.set_visible(False)
hline1 = ax1.axhline(y1, color='k', ls=':')
vline1 = ax1.axvline(x, color='k', ls=':')
vline2 = ax2.axvline(x, color='k', ls=':')
hline2 = ax2.axhline(y2, color='k', ls=':')
sel.extras.append(hline1)
sel.extras.append(vline1)
sel.extras.append(hline2)
sel.extras.append(vline2)
fig = plt.figure(figsize=(15, 10))
ax1 = plt.subplot(2, 1, 1)
ax2 = plt.subplot(2, 1, 2, sharex=ax1)
plot1, = ax1.plot(np.array(np.random.uniform(-1, 1, 100).cumsum()))
plot2, = ax2.plot(np.array(np.random.uniform(-1, 1, 100).cumsum()))
cursor = mplcursors.cursor(plot2, hover=True)
cursor.connect('add', crosshair)
plt.show()
Here is an alternative implementation that stores the data in global variables and moves the lines (instead of deleting and recreating them):
import numpy as np
import matplotlib.pyplot as plt
import mplcursors
def crosshair(sel):
x = sel.target[0]
y1 = np.interp(x, plot1x, plot1y)
y2 = np.interp(x, plot2x, plot2y)
sel.annotation.set_visible(False)
hline1.set_ydata([y1])
vline1.set_xdata([x])
hline2.set_ydata([y2])
vline2.set_xdata([x])
hline1.set_visible(True)
vline1.set_visible(True)
hline2.set_visible(True)
vline2.set_visible(True)
fig = plt.figure(figsize=(15, 10))
ax1 = plt.subplot(2, 1, 1)
ax2 = plt.subplot(2, 1, 2, sharex=ax1)
plot1, = ax1.plot(np.array(np.random.uniform(-1, 1, 100).cumsum()))
plot2, = ax2.plot(np.array(np.random.uniform(-1, 1, 100).cumsum()))
plot1x = plot1.get_xdata()
plot1y = plot1.get_ydata()
plot2x = plot2.get_xdata()
plot2y = plot2.get_ydata()
hline1 = ax1.axhline(plot1y[0], color='k', ls=':', visible=False)
vline1 = ax1.axvline(plot1x[0], color='k', ls=':', visible=False)
hline2 = ax2.axhline(plot2y[0], color='k', ls=':', visible=False)
vline2 = ax2.axvline(plot2x[0], color='k', ls=':', visible=False)
cursor = mplcursors.cursor([plot1, plot2], hover=True)
cursor.connect('add', crosshair)
plt.show()

Sorry for the late answer, but I was horrified by how much code was suggested above, when there is this one-liner on matplotlib to do a simple crosshair accross different axes. It won't show your labels but it's CPU-light.
from matplotlib.widgets import MultiCursor
cursor = MultiCursor(fig.canvas, (ax[0], ax[1]), color='r',lw=0.5, horizOn=True, vertOn=True)

Related

How to change x,y in plt to lon, lat in cartopy?

I want to change x, y to lon, lat (Picture below yellow marker) when I click some point in map and get x, y.
How can I change it? Help me please...
import cartopy.crs as ccrs
import matplotlib.pyplot as plt
import numpy as np
import cartopy
def mouse_click(event):
x, y = event.xdata, event.ydata
print(x, y)
proj = ccrs.LambertConformal(central_longitude=125, central_latitude=35, false_easting=400000,false_northing=400000,
standard_parallels=(46, 49))
fig = plt.figure(figsize=(16.535433, 11.692913))
ax = fig.add_subplot(1,1,1, projection=proj)
ax.set_extent((79, 156, 10, 66))
gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, linewidth=0.2,color='black', alpha=0.5,
linestyle=(3, (20, 5)), xlocs=np.arange(-180, 200, 10), x_inline=False, y_inline=False)
#xlabel_style = {'rotation': 0},ylabel_style = {'rotation': 0})
ax.coastlines(resolution='50m', alpha=1, linewidth=0.5)
ax.add_feature(cartopy.feature.BORDERS, alpha=1,linewidth=0.5)
ax.add_feature(cartopy.feature.LAKES, alpha=0.4)
plt.connect('button_press_event', mouse_click)
plt.show()
I tried to find about it but It's hard to understanding.
I found some way.
Change the projection.
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1], projection=ccrs.PlateCarree())
ax.add_feature(cartopy.feature.LAND, facecolor='black')
ax.set_global()
In LambertConformal,x,y is different with lon,lat. But in PlateCarree, they are same.

Change colorbar limits without changing the values of the data it represents in scatter

I'm trying to change a colorbar attached to a scatter plot so that the minimum and maximum of the colorbar are the minimum and maximum of the data, but I want the data to be centred at zero as I'm using a colormap with white at zero. Here is my example
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 1, 61)
y = np.linspace(0, 1, 61)
C = np.linspace(-10, 50, 61)
M = np.abs(C).max() # used for vmin and vmax
fig, ax = plt.subplots(1, 1, figsize=(5,3), dpi=150)
sc=ax.scatter(x, y, c=C, marker='o', edgecolor='k', vmin=-M, vmax=M, cmap=plt.cm.RdBu_r)
cbar=fig.colorbar(sc, ax=ax, label='$R - R_0$ (mm)')
ax.set_xlabel('x')
ax.set_ylabel('y')
As you can see from the attached figure, the colorbar goes down to -M, where as I want the bar to just go down to -10, but if I let vmin=-10 then the colorbar won't be zerod at white. Normally, setting vmin to +/- M when using contourf the colorbar automatically sorts to how I want. This sort of behaviour is what I expect when contourf uses levels=np.linspace(-M,M,61) rather than setting it with vmin and vmax with levels=62. An example showing the default contourf colorbar behaviour I want in my scatter example is shown below
plt.figure(figsize=(6,5), dpi=150)
plt.contourf(x, x, np.reshape(np.linspace(-10, 50, 61*61), (61,61)),
levels=62, vmin=-M, vmax=M, cmap=plt.cm.RdBu_r)
plt.colorbar(label='$R - R_0$ (mm)')
Does anyone have any thoughts? I found this link which I thought might solve the problem, but when executing the cbar.outline.set_ydata line I get this error AttributeError: 'Polygon' object has no attribute 'set_ydata' .
EDIT a little annoyed that someone has closed this question without allowing me to clarify any questions they might have, as none of the proposed solutions are what I'm asking for.
As for Normalize.TwoSlopeNorm, I do not want to rescale the smaller negative side to use the entire colormap range, I just want the colorbar attached to the side of my graph to stop at -10.
This link also does not solve my issue, as it's the TwoSlopeNorm solution again.
After changing the ylim of the colorbar, the rectangle formed by the surrounding spines is too large. You can make this outline invisible. And then add a new rectangular border:
import numpy as np
import matplotlib.pyplot as plt
x = np.linspace(0, 1, 61)
y = np.linspace(0, 1, 61)
C = np.linspace(-10, 50, 61)
M = np.abs(C).max() # used for vmin and vmax
fig, ax = plt.subplots(1, 1, figsize=(5, 3), dpi=150)
sc = ax.scatter(x, y, c=C, marker='o', edgecolor='k', vmin=-M, vmax=M, cmap=plt.cm.RdBu_r)
cbar = fig.colorbar(sc, ax=ax, label='$R - R_0$ (mm)')
cb_ymin = C.min()
cb_ymax = C.max()
cb_xmin, cb_xmax = cbar.ax.get_xlim()
cbar.ax.set_ylim(cb_ymin, cb_ymax)
cbar.outline.set_visible(False) # hide the surrounding spines, which are too large after set_ylim
cbar.ax.add_patch(plt.Rectangle((cb_xmin, cb_ymin), cb_xmax - cb_xmin, cb_ymax - cb_ymin,
fc='none', ec='black', clip_on=False))
plt.show()
Another approach until v3.5 is released is to make a custom colormap that does what you want (see also https://matplotlib.org/stable/tutorials/colors/colormap-manipulation.html#sphx-glr-tutorials-colors-colormap-manipulation-py)
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.cm as cm
from matplotlib.colors import ListedColormap
fig, axs = plt.subplots(2, 1)
X = np.random.randn(32, 32) + 2
pc = axs[0].pcolormesh(X, vmin=-6, vmax=6, cmap='RdBu_r')
fig.colorbar(pc, ax=axs[0])
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.cm as cm
from matplotlib.colors import ListedColormap
fig, axs = plt.subplots(2, 1)
X = np.random.randn(32, 32) + 2
pc = axs[0].pcolormesh(X, vmin=-6, vmax=6, cmap='RdBu_r')
fig.colorbar(pc, ax=axs[0])
def keep_center_colormap(vmin, vmax, center=0):
vmin = vmin - center
vmax = vmax - center
dv = max(-vmin, vmax) * 2
N = int(256 * dv / (vmax-vmin))
RdBu_r = cm.get_cmap('RdBu_r', N)
newcolors = RdBu_r(np.linspace(0, 1, N))
beg = int((dv / 2 + vmin)*N / dv)
end = N - int((dv / 2 - vmax)*N / dv)
newmap = ListedColormap(newcolors[beg:end])
return newmap
newmap = keep_center_colormap(-2, 6, center=0)
pc = axs[1].pcolormesh(X, vmin=-2, vmax=6, cmap=newmap)
fig.colorbar(pc, ax=axs[1])
plt.show()

Adjust hspace one-sided for matplotlib subplots

My question is based on this question:
Adjust hspace for some of the subplots
Which adjusts the top plot of a number of subplots and increases the difference in hspace. I want to increase the hspace between two plots within the subplots (in my case: between plot 3 and plot4 from the top).
Here is my example:
import numpy as np
import matplotlib.pyplot as plt
noise = np.random.rand(300)
gs_top = plt.GridSpec(9, 1, hspace=0.5)
gs_base = plt.GridSpec(9, 1, hspace=0)
fig = plt.figure()
fig.patch.set_facecolor('white')
ax0 = fig.add_subplot(gs_base[0,:])
ax1 = fig.add_subplot(gs_base[1,:])
ax2 = fig.add_subplot(gs_top[2,:])
ax3 = fig.add_subplot(gs_base[3,:])
ax4 = fig.add_subplot(gs_base[4,:])
ax5 = fig.add_subplot(gs_base[5,:])
ax0.plot(noise)
ax1.plot(noise)
ax2.plot(noise)
ax3.plot(noise)
ax4.plot(noise)
ax5.plot(noise)
In the example it is shown that the hspace increases between plot 3 and 4. However, I don't want to increase the space between plot 2 and plot 3.
How can I adjust the hspace variable only on one side?
Found the answer after manipulating google by asking with various word combinations. Found this: Stackoverflow answer
In short (dirty way):
Adding a seperate axis and make it invisible.
Example:
import numpy as np
import matplotlib.pyplot as plt
noise = np.random.rand(300)
gs_base = plt.GridSpec(7, 1, hspace=0, height_ratios=[1, 1, 1, 0.8, 1,1,1])
fig = plt.figure()
fig.patch.set_facecolor('white')
ax0 = fig.add_subplot(gs_base[0,:])
ax1 = fig.add_subplot(gs_base[1,:])
ax2 = fig.add_subplot(gs_base[2,:])
ax3 = fig.add_subplot(gs_base[3,:])
ax3.set_visible(False)
ax4 = fig.add_subplot(gs_base[4,:])
ax5 = fig.add_subplot(gs_base[5,:])
ax6 = fig.add_subplot(gs_base[6,:])
ax0.plot(noise)
ax1.plot(noise)
ax2.plot(noise)
ax4.plot(noise)
ax5.plot(noise)
ax6.plot(noise)
In long (correct way):
Couldn't figure it out for the moment.

Matplotlib: how to get color bars that are one on top of each other as opposed to side by side?

I have the following code:
import matplotlib.pyplot as plt
import numpy as np
img1 = np.zeros([512,512])
img2 = np.zeros([512,512])
plt.figure(figsize=(10,10))
plt.imshow(img1, cmap='inferno')
plt.axis('off')
cba = plt.colorbar(shrink=0.25)
cba.ax.set_ylabel('Events / counts', fontsize=14)
cba.ax.tick_params(labelsize=12)
plt.imshow(img2, cmap='turbo', alpha=0.5)
plt.axis('off')
cba = plt.colorbar(shrink=0.25)
cba.ax.set_ylabel('Lifetime / ns)', fontsize=14)
cba.ax.tick_params(labelsize=12)
plt.tight_layout()
plt.show()
which produces the following output:
My question is, how can I get color bars that are on top of one another as opposed to next to each other? Ideally, I would like to get something like this:
You can grab the position of the ax and use it to create new axes for the colorbars. Here is an example:
import numpy as np
import matplotlib.pyplot as plt
from scipy import ndimage
data = ndimage.gaussian_filter(np.random.randn(512, 512), sigma=15, mode='nearest') * 20
fig, ax = plt.subplots()
im1 = ax.imshow(data, vmin=-1, vmax=0, cmap='viridis')
data[data < 0] = np.nan
im2 = ax.imshow(data, vmin=0.001, vmax=1, cmap='Reds_r')
ax.axis('off')
pos = ax.get_position()
bar_h = (pos.y1 - pos.y0) * 0.5 # 0.5 joins the two bars, e.g. 0.48 separates them a bit
ax_cbar1 = fig.add_axes([pos.x1 + 0.02, pos.y0, 0.03, bar_h])
cbar1 = fig.colorbar(im1, cax=ax_cbar1, orientation='vertical')
ax_cbar2 = fig.add_axes([pos.x1 + 0.02, pos.y1 - bar_h, 0.03, bar_h])
cbar2 = fig.colorbar(im2, cax=ax_cbar2, orientation='vertical')
plt.show()

matplotlib notebook cursor coordinates on graph with double y axis

The issue I would like you to figure out is about the coordinantes appearence on matplotlib graph with a double y axis. First of all a code on Jupyter Notebook which draws a graph with two lines and only one y axis (for some unknown reasons I have to run it two times in order to make it working correctly)
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.mlab as mlab
from IPython.display import display
from IPython.core.display import display, HTML #display multiple output on a cell
display(HTML("<style>.container { width:100% !important; }</style>")) # improve cells horizontal size
from IPython.core.interactiveshell import InteractiveShell # It saves you having to repeatedly type "Display"
InteractiveShell.ast_node_interactivity = "all"
%matplotlib notebook
x = np.arange(0, 10, 0.01)
y1 = np.sin(np.pi*x)/(np.pi*x)
y2 = abs(np.tan(0.1*np.pi*x))
plt.figure()
plt.plot(x, y1)
plt.plot(x, y2)
plt.ylim(0, 3)
plt.grid()
plt.show()
The present figure provides the two lines with cursor coordinates on the right bottom part of the graph.
The following code
import pandas as pd
import os
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.mlab as mlab
from IPython.display import display
from IPython.core.display import display, HTML #display multiple output on a cell
display(HTML("<style>.container { width:100% !important; }</style>")) # improve cells horizontal size
from IPython.core.interactiveshell import InteractiveShell # It saves you having to repeatedly type "Display"
InteractiveShell.ast_node_interactivity = "all"
%matplotlib notebook
x = np.arange(0, 10, 0.01)
y1 = np.sin(np.pi*x)/(np.pi*x)
y2 = abs(np.tan(0.1*np.pi*x))
# Create some mock data
fig, ax1 = plt.subplots()
plt.grid()
color = 'tab:red'
ax1.set_xlabel('Time (days from 24 February)')
ax1.set_ylabel('Death cases/Intensive care', color=color)
#ax1.set_xlim(0, 15)
#ax1.set_ylim(0, 900)
ax1.plot(x, y1, '-', color=color, label = 'Left hand scale')
ax1.tick_params(axis='y', labelcolor=color)
ax1.legend(loc = 'upper left')
ax2 = ax1.twinx()
color = 'tab:blue'
ax2.set_ylabel('Total cases/currently positive', color=color) # we already handled the x-label with ax1
ax2.plot(x, y2, '-', color=color, label = 'Right hand scale')
ax2.set_ylim(0, 20)
ax2.tick_params(axis='y', labelcolor=color)
ax2.legend(loc = 'lower right')
fig.tight_layout()
plt.show()
Shows the following graph
Which shows a graph with TWO y scales, one red on the left side and one blue on the right side. The problem here is that in the left bottom side of the picture there are the cursor coordinates related to the right scale and nothing about the left one. Is there a way to show up both the two scales?
Depending on your precise needs, mplcursors seems helpful. Mplcursors allows a lot of ways to customize, for example you can show both y-values together with the current x. Or you could suppress the annotation and only write in the status bar.
Setting hover=True constantly displays the plotted values when the mouse hovers over a curve. Default, the values would only be displayed when clicking.
import matplotlib.pyplot as plt
import numpy as np
import mplcursors
# Create some test data
x = np.arange(0, 10, 0.01)
y1 = np.sin(np.pi * x) / (np.pi * x)
y2 = abs(np.tan(0.1 * np.pi * x))
fig, ax1 = plt.subplots()
plt.grid()
color = 'tab:red'
ax1.set_xlabel('Time (days from 24 February)')
ax1.set_ylabel('Death cases/Intensive care', color=color)
lines1 = ax1.plot(x, y1, '-', color=color, label='Left hand scale')
ax1.tick_params(axis='y', labelcolor=color)
ax1.legend(loc='upper left')
ax2 = ax1.twinx()
color = 'tab:blue'
ax2.set_ylabel('Total cases/currently positive', color=color) # we already handled the x-label with ax1
lines2 = ax2.plot(x, y2, '-', color=color, label='Right hand scale')
ax2.set_ylim(0, 20)
ax2.tick_params(axis='y', labelcolor=color)
ax2.legend(loc='lower right')
cursor1 = mplcursors.cursor(lines1, hover=True)
cursor2 = mplcursors.cursor(lines2, hover=True)
fig.tight_layout()
plt.show()

Resources