Related
When exploring data sets with many points on an xy chart, I can adjust the alpha and/or marker size to give a good quick visual impression of where the points are most densely clustered. However when I zoom in or make the window bigger, the a different alpha and/or marker size is needed to give the same visual impression.
How can I have the alpha value and/or the marker size increase when I make the window bigger or zoom in on the data? I am thinking that if I double the window area I could double the marker size, and/or take the square root of the alpha; and the opposite for zooming.
Note that all points have the same size and alpha. Ideally the solution would work with plot(), but if it can only be done with scatter() that would be helpful also.
You can achieve what you want with matplotlib event handling. You have to catch zoom and resize events separately. It's a bit tricky to account for both at the same time, but not impossible. Below is an example with two subplots, a line plot on the left and a scatter plot on the right. Both zooming (factor) and resizing of the figure (fig_factor) re-scale the points according to the scaling factors in figure size and x- and y- limits. As there are two limits defined -- one for the x and one for the y direction, I used here the respective minima for the two factors. If you'd rather want to scale with the larger factors, change the min to max in both event functions.
from matplotlib import pyplot as plt
import numpy as np
fig, axes = plt.subplots(nrows=1, ncols = 2)
ax1,ax2 = axes
fig_width = fig.get_figwidth()
fig_height = fig.get_figheight()
fig_factor = 1.0
##saving some values
xlim = dict()
ylim = dict()
lines = dict()
line_sizes = dict()
paths = dict()
point_sizes = dict()
## a line plot
x1 = np.linspace(0,np.pi,30)
y1 = np.sin(x1)
lines[ax1] = ax1.plot(x1, y1, 'ro', markersize = 3, alpha = 0.8)
xlim[ax1] = ax1.get_xlim()
ylim[ax1] = ax1.get_ylim()
line_sizes[ax1] = [line.get_markersize() for line in lines[ax1]]
## a scatter plot
x2 = np.random.normal(1,1,30)
y2 = np.random.normal(1,1,30)
paths[ax2] = ax2.scatter(x2,y2, c = 'b', s = 20, alpha = 0.6)
point_sizes[ax2] = paths[ax2].get_sizes()
xlim[ax2] = ax2.get_xlim()
ylim[ax2] = ax2.get_ylim()
def on_resize(event):
global fig_factor
w = fig.get_figwidth()
h = fig.get_figheight()
fig_factor = min(w/fig_width,h/fig_height)
for ax in axes:
lim_change(ax)
def lim_change(ax):
lx = ax.get_xlim()
ly = ax.get_ylim()
factor = min(
(xlim[ax][1]-xlim[ax][0])/(lx[1]-lx[0]),
(ylim[ax][1]-ylim[ax][0])/(ly[1]-ly[0])
)
try:
for line,size in zip(lines[ax],line_sizes[ax]):
line.set_markersize(size*factor*fig_factor)
except KeyError:
pass
try:
paths[ax].set_sizes([s*factor*fig_factor for s in point_sizes[ax]])
except KeyError:
pass
fig.canvas.mpl_connect('resize_event', on_resize)
for ax in axes:
ax.callbacks.connect('xlim_changed', lim_change)
ax.callbacks.connect('ylim_changed', lim_change)
plt.show()
The code has been tested in Pyton 2.7 and 3.6 with matplotlib 2.1.1.
EDIT
Motivated by the comments below and this answer, I created another solution. The main idea here is to only use one type of event, namely draw_event. At first the plots did not update correctly upon zooming. Also ax.draw_artist() followed by a fig.canvas.draw_idle() like in the linked answer did not really solve the problem (however, this might be platform/backend specific). Instead I added an extra call to fig.canvas.draw() whenever the scaling changes (the if statement prevents infinite loops).
In addition, do avoid all the global variables, I wrapped everything into a class called MarkerUpdater. Each Axes instance can be registered separately to the MarkerUpdater instance, so you could also have several subplots in one figure, of which some are updated and some not. I also fixed another bug, where the points in the scatter plot scaled wrongly -- they should scale quadratic, not linear (see here).
Finally, as it was missing from the previous solution, I also added updating for the alpha value of the markers. This is not quite as straight forward as the marker size, because alpha values must not be larger than 1.0. For this reason, in my implementation the alpha value can only be decreased from the original value. Here I implemented it such that the alpha decreases when the figure size is decreased. Note that if no alpha value is provided to the plot command, the artist stores None as alpha value. In this case the automatic alpha tuning is off.
What should be updated in which Axes can be defined with the features keyword -- see below if __name__ == '__main__': for an example how to use MarkerUpdater.
EDIT 2
As pointed out by #ImportanceOfBeingErnest, there was a problem with infinite recursion with my answer when using the TkAgg backend, and apparently problems with the figure not refreshing properly upon zooming (which I couldn't verify, so probably that was implementation dependent). Removing the fig.canvas.draw() and adding ax.draw_artist(ax) within the loop over the Axes instances instead fixed this issue.
EDIT 3
I updated the code to fix an ongoing issue where figure is not updated properly upon a draw_event. The fix was taken from this answer, but modified to also work for several figures.
In terms of an explanation of how the factors are obtained, the MarkerUpdater instance contains a dict that stores for each Axes instance the figure dimensions and the limits of the axes at the time it is added with add_ax. Upon a draw_event, which is for instance triggered when the figure is resized or the user zooms in on the data, the new (current) values for figure size and axes limits are retrieved and a scaling factor is calculated (and stored) such that zooming in and increasing the figure size makes the markers bigger. Because x- and y-dimensions may change at different rates, I use min to pick one of the two calculated factors and always scale against the original size of the figure.
If you want your alpha to scale with a different function, you can easily change the lines that adjust the alpha value. For instance, if you want a power law instead of a linear decrease, you can write path.set_alpha(alpha*facA**n), where n is the power.
from matplotlib import pyplot as plt
import numpy as np
##plt.switch_backend('TkAgg')
class MarkerUpdater:
def __init__(self):
##for storing information about Figures and Axes
self.figs = {}
##for storing timers
self.timer_dict = {}
def add_ax(self, ax, features=[]):
ax_dict = self.figs.setdefault(ax.figure,dict())
ax_dict[ax] = {
'xlim' : ax.get_xlim(),
'ylim' : ax.get_ylim(),
'figw' : ax.figure.get_figwidth(),
'figh' : ax.figure.get_figheight(),
'scale_s' : 1.0,
'scale_a' : 1.0,
'features' : [features] if isinstance(features,str) else features,
}
ax.figure.canvas.mpl_connect('draw_event', self.update_axes)
def update_axes(self, event):
for fig,axes in self.figs.items():
if fig is event.canvas.figure:
for ax, args in axes.items():
##make sure the figure is re-drawn
update = True
fw = fig.get_figwidth()
fh = fig.get_figheight()
fac1 = min(fw/args['figw'], fh/args['figh'])
xl = ax.get_xlim()
yl = ax.get_ylim()
fac2 = min(
abs(args['xlim'][1]-args['xlim'][0])/abs(xl[1]-xl[0]),
abs(args['ylim'][1]-args['ylim'][0])/abs(yl[1]-yl[0])
)
##factor for marker size
facS = (fac1*fac2)/args['scale_s']
##factor for alpha -- limited to values smaller 1.0
facA = min(1.0,fac1*fac2)/args['scale_a']
##updating the artists
if facS != 1.0:
for line in ax.lines:
if 'size' in args['features']:
line.set_markersize(line.get_markersize()*facS)
if 'alpha' in args['features']:
alpha = line.get_alpha()
if alpha is not None:
line.set_alpha(alpha*facA)
for path in ax.collections:
if 'size' in args['features']:
path.set_sizes([s*facS**2 for s in path.get_sizes()])
if 'alpha' in args['features']:
alpha = path.get_alpha()
if alpha is not None:
path.set_alpha(alpha*facA)
args['scale_s'] *= facS
args['scale_a'] *= facA
self._redraw_later(fig)
def _redraw_later(self, fig):
timer = fig.canvas.new_timer(interval=10)
timer.single_shot = True
timer.add_callback(lambda : fig.canvas.draw_idle())
timer.start()
##stopping previous timer
if fig in self.timer_dict:
self.timer_dict[fig].stop()
##storing a reference to prevent garbage collection
self.timer_dict[fig] = timer
if __name__ == '__main__':
my_updater = MarkerUpdater()
##setting up the figure
fig, axes = plt.subplots(nrows = 2, ncols =2)#, figsize=(1,1))
ax1,ax2,ax3,ax4 = axes.flatten()
## a line plot
x1 = np.linspace(0,np.pi,30)
y1 = np.sin(x1)
ax1.plot(x1, y1, 'ro', markersize = 10, alpha = 0.8)
ax3.plot(x1, y1, 'ro', markersize = 10, alpha = 1)
## a scatter plot
x2 = np.random.normal(1,1,30)
y2 = np.random.normal(1,1,30)
ax2.scatter(x2,y2, c = 'b', s = 100, alpha = 0.6)
## scatter and line plot
ax4.scatter(x2,y2, c = 'b', s = 100, alpha = 0.6)
ax4.plot([0,0.5,1],[0,0.5,1],'ro', markersize = 10) ##note: no alpha value!
##setting up the updater
my_updater.add_ax(ax1, ['size']) ##line plot, only marker size
my_updater.add_ax(ax2, ['size']) ##scatter plot, only marker size
my_updater.add_ax(ax3, ['alpha']) ##line plot, only alpha
my_updater.add_ax(ax4, ['size', 'alpha']) ##scatter plot, marker size and alpha
plt.show()
I'm having issues with redrawing the figure here. I allow the user to specify the units in the time scale (x-axis) and then I recalculate and call this function plots(). I want the plot to simply update, not append another plot to the figure.
def plots():
global vlgaBuffSorted
cntr()
result = collections.defaultdict(list)
for d in vlgaBuffSorted:
result[d['event']].append(d)
result_list = result.values()
f = Figure()
graph1 = f.add_subplot(211)
graph2 = f.add_subplot(212,sharex=graph1)
for item in result_list:
tL = []
vgsL = []
vdsL = []
isubL = []
for dict in item:
tL.append(dict['time'])
vgsL.append(dict['vgs'])
vdsL.append(dict['vds'])
isubL.append(dict['isub'])
graph1.plot(tL,vdsL,'bo',label='a')
graph1.plot(tL,vgsL,'rp',label='b')
graph2.plot(tL,isubL,'b-',label='c')
plotCanvas = FigureCanvasTkAgg(f, pltFrame)
toolbar = NavigationToolbar2TkAgg(plotCanvas, pltFrame)
toolbar.pack(side=BOTTOM)
plotCanvas.get_tk_widget().pack(side=TOP)
You essentially have two options:
Do exactly what you're currently doing, but call graph1.clear() and graph2.clear() before replotting the data. This is the slowest, but most simplest and most robust option.
Instead of replotting, you can just update the data of the plot objects. You'll need to make some changes in your code, but this should be much, much faster than replotting things every time. However, the shape of the data that you're plotting can't change, and if the range of your data is changing, you'll need to manually reset the x and y axis limits.
To give an example of the second option:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 6*np.pi, 100)
y = np.sin(x)
# You probably won't need this if you're embedding things in a tkinter plot...
plt.ion()
fig = plt.figure()
ax = fig.add_subplot(111)
line1, = ax.plot(x, y, 'r-') # Returns a tuple of line objects, thus the comma
for phase in np.linspace(0, 10*np.pi, 500):
line1.set_ydata(np.sin(x + phase))
fig.canvas.draw()
fig.canvas.flush_events()
You can also do like the following:
This will draw a 10x1 random matrix data on the plot for 50 cycles of the for loop.
import matplotlib.pyplot as plt
import numpy as np
plt.ion()
for i in range(50):
y = np.random.random([10,1])
plt.plot(y)
plt.draw()
plt.pause(0.0001)
plt.clf()
This worked for me. Repeatedly calls a function updating the graph every time.
import matplotlib.pyplot as plt
import matplotlib.animation as anim
def plot_cont(fun, xmax):
y = []
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
def update(i):
yi = fun()
y.append(yi)
x = range(len(y))
ax.clear()
ax.plot(x, y)
print i, ': ', yi
a = anim.FuncAnimation(fig, update, frames=xmax, repeat=False)
plt.show()
"fun" is a function that returns an integer.
FuncAnimation will repeatedly call "update", it will do that "xmax" times.
This worked for me:
from matplotlib import pyplot as plt
from IPython.display import clear_output
import numpy as np
for i in range(50):
clear_output(wait=True)
y = np.random.random([10,1])
plt.plot(y)
plt.show()
I have released a package called python-drawnow that provides functionality to let a figure update, typically called within a for loop, similar to Matlab's drawnow.
An example usage:
from pylab import figure, plot, ion, linspace, arange, sin, pi
def draw_fig():
# can be arbitrarily complex; just to draw a figure
#figure() # don't call!
plot(t, x)
#show() # don't call!
N = 1e3
figure() # call here instead!
ion() # enable interactivity
t = linspace(0, 2*pi, num=N)
for i in arange(100):
x = sin(2 * pi * i**2 * t / 100.0)
drawnow(draw_fig)
This package works with any matplotlib figure and provides options to wait after each figure update or drop into the debugger.
In case anyone comes across this article looking for what I was looking for, I found examples at
How to visualize scalar 2D data with Matplotlib?
and
http://mri.brechmos.org/2009/07/automatically-update-a-figure-in-a-loop
(on web.archive.org)
then modified them to use imshow with an input stack of frames, instead of generating and using contours on the fly.
Starting with a 3D array of images of shape (nBins, nBins, nBins), called frames.
def animate_frames(frames):
nBins = frames.shape[0]
frame = frames[0]
tempCS1 = plt.imshow(frame, cmap=plt.cm.gray)
for k in range(nBins):
frame = frames[k]
tempCS1 = plt.imshow(frame, cmap=plt.cm.gray)
del tempCS1
fig.canvas.draw()
#time.sleep(1e-2) #unnecessary, but useful
fig.clf()
fig = plt.figure()
ax = fig.add_subplot(111)
win = fig.canvas.manager.window
fig.canvas.manager.window.after(100, animate_frames, frames)
I also found a much simpler way to go about this whole process, albeit less robust:
fig = plt.figure()
for k in range(nBins):
plt.clf()
plt.imshow(frames[k],cmap=plt.cm.gray)
fig.canvas.draw()
time.sleep(1e-6) #unnecessary, but useful
Note that both of these only seem to work with ipython --pylab=tk, a.k.a.backend = TkAgg
Thank you for the help with everything.
All of the above might be true, however for me "online-updating" of figures only works with some backends, specifically wx. You just might try to change to this, e.g. by starting ipython/pylab by ipython --pylab=wx! Good luck!
Based on the other answers, I wrapped the figure's update in a python decorator to separate the plot's update mechanism from the actual plot. This way, it is much easier to update any plot.
def plotlive(func):
plt.ion()
#functools.wraps(func)
def new_func(*args, **kwargs):
# Clear all axes in the current figure.
axes = plt.gcf().get_axes()
for axis in axes:
axis.cla()
# Call func to plot something
result = func(*args, **kwargs)
# Draw the plot
plt.draw()
plt.pause(0.01)
return result
return new_func
Usage example
And then you can use it like any other decorator.
#plotlive
def plot_something_live(ax, x, y):
ax.plot(x, y)
ax.set_ylim([0, 100])
The only constraint is that you have to create the figure before the loop:
fig, ax = plt.subplots()
for i in range(100):
x = np.arange(100)
y = np.full([100], fill_value=i)
plot_something_live(ax, x, y)
I am trying to plot some data with a discrete color bar. I was following the example given (https://gist.github.com/jakevdp/91077b0cae40f8f8244a) but the issue is this example does not work 1-1 with different spacing. For example, the spacing in the example in the link is for only increasing by 1 but my data is increasing by 0.5. You can see the output from the code I have.. Any help with this would be appreciated. I know I am missing something key here but cant figure it out.
import matplotlib.pylab as plt
import numpy as np
def discrete_cmap(N, base_cmap=None):
"""Create an N-bin discrete colormap from the specified input map"""
# Note that if base_cmap is a string or None, you can simply do
# return plt.cm.get_cmap(base_cmap, N)
# The following works for string, None, or a colormap instance:
base = plt.cm.get_cmap(base_cmap)
color_list = base(np.linspace(0, 1, N))
cmap_name = base.name + str(N)
return base.from_list(cmap_name, color_list, N)
num=11
x = np.random.randn(40)
y = np.random.randn(40)
c = np.random.randint(num, size=40)
plt.figure(figsize=(10,7.5))
plt.scatter(x, y, c=c, s=50, cmap=discrete_cmap(num, 'jet'))
plt.colorbar(ticks=np.arange(0,5.5,0.5))
plt.clim(-0.5, num - 0.5)
plt.show()
Not sure what version of matplotlib/pyplot introduced this, but plt.get_cmap now supports an int argument specifying the number of colors you want to get, for discrete colormaps.
This automatically results in the colorbar being discrete.
By the way, pandas has an even better handling of the colorbar.
import numpy as np
from matplotlib import pyplot as plt
plt.style.use('ggplot')
# remove if not using Jupyter/IPython
%matplotlib inline
# choose number of clusters and number of points in each cluster
n_clusters = 5
n_samples = 20
# there are fancier ways to do this
clusters = np.array([k for k in range(n_clusters) for i in range(n_samples)])
# generate the coordinates of the center
# of each cluster by shuffling a range of values
clusters_x = np.arange(n_clusters)
clusters_y = np.arange(n_clusters)
np.random.shuffle(clusters_x)
np.random.shuffle(clusters_y)
# get dicts like cluster -> center coordinate
x_dict = dict(enumerate(clusters_x))
y_dict = dict(enumerate(clusters_y))
# get coordinates of cluster center for each point
x = np.array(list(x_dict[k] for k in clusters)).astype(float)
y = np.array(list(y_dict[k] for k in clusters)).astype(float)
# add noise
x += np.random.normal(scale=0.5, size=n_clusters*n_samples)
y += np.random.normal(scale=0.5, size=n_clusters*n_samples)
### Finally, plot
fig, ax = plt.subplots(figsize=(12,8))
# get discrete colormap
cmap = plt.get_cmap('viridis', n_clusters)
# scatter points
scatter = ax.scatter(x, y, c=clusters, cmap=cmap)
# scatter cluster centers
ax.scatter(clusters_x, clusters_y, c='red')
# add colorbar
cbar = plt.colorbar(scatter)
# set ticks locations (not very elegant, but it works):
# - shift by 0.5
# - scale so that the last value is at the center of the last color
tick_locs = (np.arange(n_clusters) + 0.5)*(n_clusters-1)/n_clusters
cbar.set_ticks(tick_locs)
# set tick labels (as before)
cbar.set_ticklabels(np.arange(n_clusters))
Ok so this is the hack I found for my own question. I am sure there is a better way to do this but this works for what I am doing. Feel free to suggest a better way to do this.
import numpy as np
import matplotlib.pylab as plt
def discrete_cmap(N, base_cmap=None):
"""Create an N-bin discrete colormap from the specified input map"""
# Note that if base_cmap is a string or None, you can simply do
# return plt.cm.get_cmap(base_cmap, N)
# The following works for string, None, or a colormap instance:
base = plt.cm.get_cmap(base_cmap)
color_list = base(np.linspace(0, 1, N))
cmap_name = base.name + str(N)
return base.from_list(cmap_name, color_list, N)
num=11
plt.figure(figsize=(10,7.5))
x = np.random.randn(40)
y = np.random.randn(40)
c = np.random.randint(num, size=40)
plt.scatter(x, y, c=c, s=50, cmap=discrete_cmap(num, 'jet'))
cbar=plt.colorbar(ticks=range(num))
plt.clim(-0.5, num - 0.5)
cbar.ax.set_yticklabels(np.arange(0.0,5.5,0.5))
plt.show()
For some reason I cannot upload the image associated with the code above. I get an error when uploading so not sure how to show the final example. But simply I set the color bar axes for tick labels for a vertical color bar and passed in the labels I want and it produced the correct output.
I would like to draw a grid covering all the sphere on an orthographic projection.
The issue is cells outside the projection are not drawed correctly. This happened with drawgreatcircles as pointed here.
I have also tried to use Polygons as described here, but same problem.
Finally, I have coded a custom check based on Wikipedia. The idea is for each point of each segment, we check cos c (cf Wikipedia) and do not plot it if the cosinus is negative.
My question is : can we do this kind of check with basemap own functions ?
This strategy would not work for other projections.
Also, why is this kind of check not included in Basemap ?
Thanks to your example, I took the data and plotted it with cartopy. The following changes were needed to create the plot:
import cartopy.crs as ccrs
ax =plt.axes(projection=ccrs.Orthographic())
plt.pcolormesh(lons, lats,val, edgecolors='k',
linewidths=1, transform=ccrs.PlateCarree())
ax.coastlines()
ax.gridlines()
plt.show()
This is using pcolormesh so is pretty quick (though your example wasn't that slow on my machine in the first place).
Here is a solution using pcolor :
import pylab as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
nb_lat2 = 20
nb_lat = 2*nb_lat2
nb_lon = 3*(2*(nb_lat+1) - 1)
lats = np.zeros((2*nb_lat, nb_lon))
lons = np.zeros((2*nb_lat, nb_lon))
val = np.zeros((2*nb_lat, nb_lon))
dlat = 90./nb_lat2
for i in range(nb_lat):
nb_lon = 2*(i+1)-1
if ((i+1) > nb_lat2):
nb_lon = 2*(nb_lat - i)-1
dlon = 120./nb_lon
lats[2*i][:] = 90 - i*dlat
lats[2*i+1][:] = 90 - (i+1)*dlat
for j in range(nb_lon):
lons[2*i][j] = j*dlon
lons[2*i+1][j] = j*dlon
for k in range(1,3):
lons[2*i][j + k*nb_lon] = j*dlon + 120.*k
lons[2*i+1][j + k*nb_lon] = j*dlon + 120.*k
lons[2*i][3*nb_lon:] = nb_lon*dlon + 240.
lons[2*i+1][3*nb_lon:] = nb_lon*dlon + 240.
lons = lons - 180
val = lats + lons
# Crash
##m = Basemap(projection='robin',lon_0=0,resolution=None)
#m = Basemap(projection='mill',lon_0=0)
m = Basemap(projection='ortho', lat_0=0,lon_0=0)
x, y = m(lons, lats)
m.pcolor(x,y,val, edgecolors='k', linewidths=1)
m.drawcoastlines()
m.drawparallels(np.arange(-90.,91.,30.))
m.drawmeridians(np.arange(-180.,181.,60.))
plt.show()
This does exactly what I want : drawing rectangles and filling them with one color.
But it is very slow (too slow). A lot of cells are unused : at the end of a latidude line, we set the width of unused cells to 0.
Another issue is some projections crash (Robin for example).
Is it possible to setup plot to show more data when expanded?
Matplotlib plots scale when resized. To show specific area one can use set_xlim and such on axes. I have an ecg-like plot showing realtime data, its y limits are predefined, but I want to see more data along x if I expand window or just have big monitor.
Im using it in a pyside app and I could just change xlim on resize but I want more clean and generic solution.
One way to do this is to implement a handler for resize_event. Here is a short example how this might be done. You can modify it for your needs:
import numpy as np
import matplotlib.pyplot as plt
def onresize(event):
width = event.width
scale_factor = 100.
data_range = width/scale_factor
start, end = plt.xlim()
new_end = start+data_range
plt.xlim((start, new_end))
if __name__ == "__main__":
fig = plt.figure()
ax = fig.add_subplot(111)
t = np.arange(100)
y = np.random.rand(100)
ax.plot(t,y)
plt.xlim((0, 10))
cid = fig.canvas.mpl_connect('resize_event', onresize)
plt.show()