Event Pick to update BarChart Attributes - python

New to the forum!
I’m trying to create an interactive barchart for a homework problem – I am wondering where I am going wrong with out using some one else's solution (like this awesome code here!)
I click on the chart to generate a reference line with a new y value and to change the color of the bar. For simplicity, I’m debugging using just two colors and comparing to the mean (when y >mean, y<mean). Aside from the two codes below, I've tried to clear the chart and re-draw it within the onclick function and to write a separate function, although not sure how to call it... Any guidance would be much appreciated - I'm not sure how the pieces fit together, so its hard to break it down for troubleshooting.
df=pd.DataFrame({'mean':[40000,50000,20000,60000,3000],'CI':[4000,4000,3000,1000,200]},index=['A','B','C','D','E'])
df=df.T
fig, ax = plt.subplots()
bars = ax.bar([1,2,3,4,5], df.loc['mean'])
#Set horizontal line
hline = ax.axhline(y=20000, c='red', linestyle='--')
ax.set_xticks([1,2,3,4,5])
ax.set_xticklabels(df.columns)
def onclick(event):
hline.set_ydata([event.ydata, event.ydata])
df2.loc['y']=event.ydata
for val in df2.loc['y']:
if df2.loc['y'] < df2.loc['mean']:
col.append('red')
else:
col.append('white')
fig.canvas.mpl_connect('button_press_event', onclick)
Also tried
def onclick(event):
#provide y data, based on where clicking. Note to self: 'xdata' would give slanted line
hline.set_ydata([event.ydata, event.ydata])
df2.loc['y']=event.ydata
for bar in bars:
if event.ydata < df2.loc['mean']:
bar.set_color('red')
else:
bar.set_color('white')
return result

The main problem is that you never redraw the canvas, so every change you communicate to matplotlib will not appear in the figure generated by the backend. You also have to update the properties of the rectangles representing the bars - you tried this with bar.set_color() in one of the versions which changes both facecolor and edgecolor, intended or otherwise.
import pandas as pd
import matplotlib.pyplot as plt
df=pd.DataFrame({'mean':[40000,50000,20000,60000,3000],'CI':[4000,4000,3000,1000,200]},index=['A','B','C','D','E'])
df2=df.T
fig, ax = plt.subplots()
bars = ax.bar(range(df2.loc['mean'].size), df2.loc['mean'])
#Set horizontal line
hline = ax.axhline(y=20000, c='red', linestyle='--')
ax.set_xticks(range(df2.columns.size), df2.columns)
def onclick(event):
#update hline position
hline.set_ydata([event.ydata])
#change all rectangles that represent the bars
for bar in bars:
#retrieve bar height and compare
if bar.get_height() > event.ydata:
#to set the color
bar.set_color("red")
else:
bar.set_color("white")
#redraw the figure to make changes visible
fig.canvas.draw_idle()
fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()
output:

Related

Preserve content of fig after show() in matplotlib?

I'm creating a violinplot of some data and afterwards I render a scatterplot with individual data points (red points in example) to three subplots.
Since the generation of the violinplot is relatively time consuming, I'm generating the violinplot only once, then add the scatterplot for one data row, write the result file, remove the scatterplots from the axes and add the scatterplots for the next row.
Everything works, but I would like to add the option, to show() each plot prior to saving it.
If I'm using plt.show(), the figure is shown correctly, but afterwards the figure seems to be cleared and in the next iteration I'm getting the plot without the violin plots.
Is there any way to preserve the content of the figure after plt.show()?
In short, my code is
fig = generate_plot(ws, show=False) #returns the fig instance of the violin plot
#if I do plt.show() here (or in "generate_plot()"), the violin plots are gone.
ax1, ax3, ax2 = fig.get_axes()
scatter1 = ax1.scatter(...) #draw scatter plot for first axes
[...] #same vor every axis
plt.savefig(...)
scatter1.remove()
I was thinking that a possible option is to use the event loop to advance through the plots. The following would define an updating function, which changes only the scatter points, draws the image and saves it. We can manage this via a class with a callback on the key_press - such then when you hit Space the next image is shown; upon pressing Space on the last image, the plot is closed.
import matplotlib
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
import numpy as np
class NextPlotter(object):
def __init__(self, fig, func, n):
self.__dict__.update(locals())
self.i = 0
self.cid = self.fig.canvas.mpl_connect("key_press_event", self.adv)
def adv(self, evt):
if evt.key == " " and self.i < self.n:
self.func(self.i)
self.i+=1
elif self.i >= self.n:
plt.close("all")
#Start of code:
# Create data
pos = [1, 2, 4, 5, 7, 8]
data = [np.random.normal(0, std, size=100) for std in pos]
data2 = [np.random.rayleigh(std, size=100) for std in pos]
scatterdata = np.random.normal(0, 5, size=(10,len(pos)))
#Create plot
fig, axes = plt.subplots(ncols=2)
axes[0].violinplot(data, pos, points=40, widths=0.9,
showmeans=True, showextrema=True, showmedians=True)
axes[1].violinplot(data2, pos, points=40, widths=0.9,
showmeans=True, showextrema=True, showmedians=True)
scatter = axes[0].scatter(pos, scatterdata[0,:], c="crimson", s=60)
scatter2 = axes[1].scatter(pos, scatterdata[1,:], c="crimson", s=60)
# define updating function
def update(i):
scatter.set_offsets(np.c_[pos,scatterdata[2*i,:]])
scatter2.set_offsets(np.c_[pos,scatterdata[2*i+1,:]])
fig.canvas.draw()
plt.savefig("plot{i}.png".format(i=i))
# instantiate NextPlotter; press <space> to advance to the next image
c = NextPlotter(fig, update, len(scatterdata)//2)
plt.show()
A workaround could be to not remove the scatterplot.
Why not keep the scatter plot axis, and just update the data for that set of axis?
You will most likely need a plt.draw() after update of scatter plot data to force a new rendering.
I found a way to draw figures interactively here. plt.ion() and block the process with input() seems to be important.
import matplotlib.pyplot as plt
plt.ion()
fig = plt.figure()
ax = plt.subplot(1,1,1)
ax.set_xlim([-1, 5])
ax.set_ylim([-1, 5])
ax.grid('on')
for i in range(5):
lineObject = ax.plot(i,i,'ro')
fig.savefig('%02d.png'%i)
# plt.draw() # not necessary?
input()
lineObject[0].remove()
I also tried to block the process with time.sleep(1), but it does not work at all.

How to check if colorbar exists on figure

Question: Is there a way to check if a color bar already exists?
I am making many plots with a loop. The issue is that the color bar is drawn every iteration!
If I could determine if the color bar exists then I can put the color bar function in an if statement.
if cb_exists:
# do nothing
else:
plt.colorbar() #draw the colorbar
If I use multiprocessing to make the figures, is it possible to prevent multiple color bars from being added?
import numpy as np
import matplotlib.pyplot as plt
import multiprocessing
def plot(number):
a = np.random.random([5,5])*number
plt.pcolormesh(a)
plt.colorbar()
plt.savefig('this_'+str(number))
# I want to make a 50 plots
some_list = range(0,50)
num_proc = 5
p = multiprocessing.Pool(num_proc)
temps = p.map(plot, some_list)
I realize I can clear the figure with plt.clf() and plt.cla() before plotting the next iteration. But, I have data on my basemap layer I don't want to re-plot (that adds to the time it takes to create the plot). So, if I could remove the colorbar and add a new one I'd save some time.
Is is actually not easy to remove a colorbar from a plot and later draw a new one to it.
The best solution I can come up with at the moment is the following, which assumes that there is only one axes present in the plot. Now, if there was a second axis, it must be the colorbar beeing present. So by checking how many axes we find on the plot, we can judge upon whether or not there is a colorbar.
Here we also mind the user's wish not to reference any named objects from outside. (Which does not makes much sense, as we need to use plt anyways, but hey.. so was the question)
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
im = ax.pcolormesh(np.array(np.random.rand(2,2) ))
ax.plot(np.cos(np.linspace(0.2,1.8))+0.9, np.sin(np.linspace(0.2,1.8))+0.9, c="k", lw=6)
ax.set_title("Title")
cbar = plt.colorbar(im)
cbar.ax.set_ylabel("Label")
for i in range(10):
# inside this loop we should not access any variables defined outside
# why? no real reason, but questioner asked for it.
#draw new colormesh
im = plt.gcf().gca().pcolormesh(np.random.rand(2,2))
#check if there is more than one axes
if len(plt.gcf().axes) > 1:
# if so, then the last axes must be the colorbar.
# we get its extent
pts = plt.gcf().axes[-1].get_position().get_points()
# and its label
label = plt.gcf().axes[-1].get_ylabel()
# and then remove the axes
plt.gcf().axes[-1].remove()
# then we draw a new axes a the extents of the old one
cax= plt.gcf().add_axes([pts[0][0],pts[0][1],pts[1][0]-pts[0][0],pts[1][1]-pts[0][1] ])
# and add a colorbar to it
cbar = plt.colorbar(im, cax=cax)
cbar.ax.set_ylabel(label)
# unfortunately the aspect is different between the initial call to colorbar
# without cax argument. Try to reset it (but still it's somehow different)
cbar.ax.set_aspect(20)
else:
plt.colorbar(im)
plt.show()
In general a much better solution would be to operate on the objects already present in the plot and only update them with the new data. Thereby, we suppress the need to remove and add axes and find a much cleaner and faster solution.
import matplotlib.pyplot as plt
import numpy as np
fig, ax = plt.subplots()
im = ax.pcolormesh(np.array(np.random.rand(2,2) ))
ax.plot(np.cos(np.linspace(0.2,1.8))+0.9, np.sin(np.linspace(0.2,1.8))+0.9, c="k", lw=6)
ax.set_title("Title")
cbar = plt.colorbar(im)
cbar.ax.set_ylabel("Label")
for i in range(10):
data = np.array(np.random.rand(2,2) )
im.set_array(data.flatten())
cbar.set_clim(vmin=data.min(),vmax=data.max())
cbar.draw_all()
plt.draw()
plt.show()
Update:
Actually, the latter approach of referencing objects from outside even works together with the multiprocess approach desired by the questioner.
So, here is a code that updates the figure, without the need to delete the colorbar.
import matplotlib.pyplot as plt
import numpy as np
import multiprocessing
import time
fig, ax = plt.subplots()
im = ax.pcolormesh(np.array(np.random.rand(2,2) ))
ax.plot(np.cos(np.linspace(0.2,1.8))+0.9, np.sin(np.linspace(0.2,1.8))+0.9, c="w", lw=6)
ax.set_title("Title")
cbar = plt.colorbar(im)
cbar.ax.set_ylabel("Label")
tx = ax.text(0.2,0.8, "", fontsize=30, color="w")
tx2 = ax.text(0.2,0.2, "", fontsize=30, color="w")
def do(number):
start = time.time()
tx.set_text(str(number))
data = np.array(np.random.rand(2,2)*(number+1) )
im.set_array(data.flatten())
cbar.set_clim(vmin=data.min(),vmax=data.max())
tx2.set_text("{m:.2f} < {ma:.2f}".format(m=data.min(), ma= data.max() ))
cbar.draw_all()
plt.draw()
plt.savefig("multiproc/{n}.png".format(n=number))
stop = time.time()
return np.array([number, start, stop])
if __name__ == "__main__":
multiprocessing.freeze_support()
some_list = range(0,50)
num_proc = 5
p = multiprocessing.Pool(num_proc)
nu = p.map(do, some_list)
nu = np.array(nu)
plt.close("all")
fig, ax = plt.subplots(figsize=(16,9))
ax.barh(nu[:,0], nu[:,2]-nu[:,1], height=np.ones(len(some_list)), left=nu[:,1], align="center")
plt.show()
(The code at the end shows a timetable which allows to see that multiprocessing has indeed taken place)
If you can access to axis and image information, colorbar can be retrieved
as a property of the image (or the mappable to which associate colorbar).
Following a previous answer (How to retrieve colorbar instance from figure in matplotlib), an example could be:
ax=plt.gca() #plt.gca() for current axis, otherwise set appropriately.
im=ax.images #this is a list of all images that have been plotted
if im[-1].colorbar is None: #in this case I assume to be interested to the last one plotted, otherwise use the appropriate index or loop over
plt.colorbar() #plot a new colorbar
Note that an image without colorbar returns None to im[-1].colorbar
One approach is:
initially (prior to having any color bar drawn), set a variable
colorBarPresent = False
in the method for drawing the color bar, check to see if it's already drawn. If not, draw it and set the colorBarPresent variable True:
def drawColorBar():
if colorBarPresent:
# leave the function and don't draw the bar again
else:
# draw the color bar
colorBarPresent = True
There is an indirect way of guessing (with reasonable accuracy for most applications, I think) whether an Axes instance is home to a color bar. Depending on whether it is a horizontal or vertical color bar, either the X axis or Y axis (but not both) will satisfy all of these conditions:
No ticks
No tick labels
No axis label
Axis range is (0, 1)
So here's a function for you:
def is_colorbar(ax):
"""
Guesses whether a set of Axes is home to a colorbar
:param ax: Axes instance
:return: bool
True if the x xor y axis satisfies all of the following and thus looks like it's probably a colorbar:
No ticks, no tick labels, no axis label, and range is (0, 1)
"""
xcb = (len(ax.get_xticks()) == 0) and (len(ax.get_xticklabels()) == 0) and (len(ax.get_xlabel()) == 0) and \
(ax.get_xlim() == (0, 1))
ycb = (len(ax.get_yticks()) == 0) and (len(ax.get_yticklabels()) == 0) and (len(ax.get_ylabel()) == 0) and \
(ax.get_ylim() == (0, 1))
return xcb != ycb # != is effectively xor in this case, since xcb and ycb are both bool
Thanks to this answer for the cool != xor trick: https://stackoverflow.com/a/433161/6605826
With this function, you can see if a colorbar exists by:
colorbar_exists = any([is_colorbar(ax) for ax in np.atleast_1d(gcf().axes).flatten()])
or if you're sure the colorbar will always be last, you can get off easy with:
colorbar_exists = is_colorbar(gcf().axes[-1])

Update Existent Matplotlib Subplot with a user input

I am currently trying to implement a 'zoom' functionality into my code. By this I mean I would like to have two subplots side by side, one of which contains the initial data and the other which contains a 'zoomed in' plot which is decided by user input.
Currently, I can create two subplots side by side, but after calling for the user input, instead of updating the second subplot, my script is creating an entirely new figure below and not updating the second subplot. It is important that the graph containing data is plotted first so the user can choose the value for the input accordingly.
def plot_func(data):
plot_this = data
plt.close('all')
fig = plt.figure()
#Subplot 1
ax1 = fig.add_subplot(1,2,1)
ax1.plot(plot_this)
plt.show()
zoom = input("Where would you like to zoom to: ")
zoom_in = plot_this[0:int(zoom)]
#Subplot 2
ax2 = fig.add_subplot(1,2,2)
ax2.plot(zoom_in)
plt.show()
The code above is a simplified version of what I am hoping to do. Display a subplot, and let the user enter an input based on that subplot. Then either edit a subplot that is already created or create a new one that is next to the first. Again it is crucial that the 'zoomed in' subplot is alongside the first opposed to below.
I think it is not very convenient for the user to type in numbers for zooming. The more standard way would be mouse interaction as already provided by the various matplotlib tools.
There is no standard tool for zooming in a different plot, but we can easily provide this functionality using matplotlib.widgets.RectangleSelector as shown in the code below.
We need to plot the same data in two subplots and connect the RectangleSelector to one of the subplots (ax). Every time a selection is made, the data coordinates of the selection in the first subplot are simply used as axis-limits on the second subplot, effectiveliy proving zoom-in (or magnification) functionality.
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import RectangleSelector
def onselect(eclick, erelease):
#http://matplotlib.org/api/widgets_api.html
xlim = np.sort(np.array([erelease.xdata,eclick.xdata ]))
ylim = np.sort(np.array([erelease.ydata,eclick.ydata ]))
ax2.set_xlim(xlim)
ax2.set_ylim(ylim)
def toggle_selector(event):
# press escape to return to non-zoomed plot
if event.key in ['escape'] and toggle_selector.RS.active:
ax2.set_xlim(ax.get_xlim())
ax2.set_ylim(ax.get_ylim())
x = np.arange(100)/(100.)*7.*np.pi
y = np.sin(x)**2
fig = plt.figure()
ax = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
#plot identical data in both axes
ax.plot(x,y, lw=2)
ax.plot([5,14,21],[.3,.6,.1], marker="s", color="red", ls="none")
ax2.plot(x,y, lw=2)
ax2.plot([5,14,21],[.3,.6,.1], marker="s", color="red", ls="none")
ax.set_title("Select region with your mouse.\nPress escape to deactivate zoom")
ax2.set_title("Zoomed Plot")
toggle_selector.RS = RectangleSelector(ax, onselect, drawtype='box', interactive=True)
fig.canvas.mpl_connect('key_press_event', toggle_selector)
plt.show()
%matplotlib inline
import mpld3
mpld3.enable_notebook()

Update color of an already plotted graph?

I wonder if it is possible to update a parameter such as the line color of an already plotted graph that doesn't wrap on destroying the graph and creating another one.
Example: I plot a graph then I create a few horizontal green lines on it by clicking. Now I want to change the blue main line of the graph to the color red without losing the horizontal green lines that were created.
Something like:
import matplotlib.pyplot as plt
c = None
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot([1,2,3],[1,2,3], color = c)
def onclick(event):
plt.ion()
plt.hlines(event.ydata,event.xdata-0.1,event.xdata+0.1,
colors='green',linestyle='solid')
cid = fig.canvas.mpl_connect('button_press_event', onclick)
def change_color():
c = 'r'
# ???
plt.show()
change_color() # running this function will update the plot line color to red
You need to capture the artist created by the hlines call:
fig, ax = plt.subplots()
arts = ax.hlines([.5, .75], 0, 1, lw=5)
Which returns a LineCollection object. You can programtically modify it
arts.set_color(['sage', 'purple'])
and to get the window to update you will need to call
fig.canvas.draw()
(this last bit is no longer true on master when at the repl with pyplot imported)
I did something a bit fancier here and used hlines to draw more than one line and set more than one color, but it works the same with only one line as well.

In Python with Matplotlib how to check if a subplot is empty in the figure

I have some graphs created with NetworkX and show them on screen using Matplotlib. Specifically, since I don't know in advance how many graphs I need to show, I create a subplot on the figure on fly. That works fine. However, at some point in the script, some subplots are removed from the figure and the figure is shown with some empty subplots. I would like to avoid it, but I was not able to retrieve the subplots that are empty in the figure. Here is my code:
#instantiate a figure with size 12x12
fig = plt.figure(figsize=(12,12))
#when a graph is created, also a subplot is created:
ax = plt.subplot(3,4,count+1)
#and the graph is drawn inside it: N.B.: pe is the graph to be shown
nx.draw(pe, positions, labels=positions, font_size=8, font_weight='bold', node_color='yellow', alpha=0.5)
#many of them are created..
#under some conditions a subplot needs to be deleted, and so..
#condition here....and then retrieve the subplot to deleted. The graph contains the id of the ax in which it is shown.
for ax in fig.axes:
if id(ax) == G.node[shape]['idax']:
fig.delaxes(ax)
until here works fine, but when I show the figure, the result looks like this:
you can notice that there are two empty subplots there.. at the second position and at the fifth. How can I avoid it? Or.. how can I re-organize the subplots in such a way that there are no more blanks in the figure?
Any help is apreciated! Thanks in advance.
So to do this I would keep a list of axes and when I delete the contents of one I would swap it out with a full one. I think the example below solved the problem (or at least gives an idea of how to solve it):
import matplotlib.pyplot as plt
# this is just a helper class to keep things clean
class MyAxis(object):
def __init__(self,ax,fig):
# this flag tells me if there is a plot in these axes
self.empty = False
self.ax = ax
self.fig = fig
self.pos = self.ax.get_position()
def del_ax(self):
# delete the axes
self.empty = True
self.fig.delaxes(self.ax)
def swap(self,other):
# swap the positions of two axes
#
# THIS IS THE IMPORTANT BIT!
#
new_pos = other.ax.get_position()
self.ax.set_position(new_pos)
other.ax.set_position(self.pos)
self.pos = new_pos
def main():
# generate a figure and 10 subplots in a grid
fig, axes = plt.subplots(ncols=5,nrows=2)
# get these as a list of MyAxis objects
my_axes = [MyAxis(ax,fig) for ax in axes.ravel()]
for ax in my_axes:
# plot some random stuff
ax.ax.plot(range(10))
# delete a couple of axes
my_axes[0].del_ax()
my_axes[6].del_ax()
# count how many axes are dead
dead = sum([ax.empty for ax in my_axes])
# swap the dead plots for full plots in a row wise fashion
for kk in range(dead):
for ii,ax1 in enumerate(my_axes[kk:]):
if ax1.empty:
print ii,"dead"
for jj,ax2 in enumerate(my_axes[::-1][kk:]):
if not ax2.empty:
print "replace with",jj
ax1.swap(ax2)
break
break
plt.draw()
plt.show()
if __name__ == "__main__":
main()
The extremely ugly for loop construct is really just a placeholder to give an example of how the axes can be swapped.

Categories