Update color of an already plotted graph? - python

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.

Related

Event Pick to update BarChart Attributes

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:

Returning matplotlib plot/figure from a function and saving it later

As the title states I want to return a plt or figure (still not sure what the difference between the two things are) using matplotlib. The main idea behind it is so I can save the plt/figure later.
import seaborn as sns
from matplotlib import pyplot as plt
def graph(df, id):
# size of the graph
xlims = (-180, 180)
ylims = (-180, 180)
# dictate the colors of the scatter plot based on the grouping of hot or cold
color_dict = {'COLD': 'blue',
'HOT': 'red'}
title_name = f"{id}"
ax = sns.scatterplot(data=df, hue='GRP', x='X_GRID', y='Y_GRID',
legend=False, palette=color_dict)
ax.set_title(title_name)
ax.set(xlim=xlims)
ax.set(ylim=ylims)
if show_grid:
# pass in the prev graph so I can overlay grid
ax = self.__get_grid( ax)
circle1 = plt.Circle(xy=(0, 0), radius=150, color='black', fill=False, zorder=3)
ax.add_patch(circle1)
ax.set_aspect('equal')
plt.axis('off')
plt.savefig(title_name + '_in_ftn.png')
fig = plt.figure()
plt.clf()
return (fig, title_name + '.png')
plots = []
# dfs is just a tuple of df, id for example purposes
for df, id in dfs:
plots.append(graph(df, id))
for plot, file_name in plots:
plot.savefig(file_name)
plot.clf()
When using plot.savefig(filename) it saves, but the saved file is blank which is wrong. Am I not properly returning the object I want to save? If not what should I return to be able to save it?
I kind of having it work, but not really. I am currently saving two figures for testing purposes. For some reason when I use the fig=plt.figure() and saving it outside the function the title of the figure and the filename are different (even though they should be the same since the only difference is .png)
However, when saving it inside the function the title name of the figure and the filename name are the same.
You code has multiple issues that I'll try to discuss here:
Your confusion around plt
First of all, there is no such thing as "a plt". plt is the custom name you are giving to the matplotlib.pyplot module when you are importing it with the line import matplotlib.pyplot as plt. You are basically just renaming the module with an easy to type abbreviation. If you had just written import matplotlib, you would have to write matplotlib.pyplot.axis('off') instead of plt.axis('off').
Mix of procedural and object oriented approach
You are using a mix of the procedural and object oriented approach for matplotlib.
Either you call your methods on the axis object (ax) or you can call functions that implicitly handle the axis and figure. For example you could either create and axis and then call ax.plot(...) or instead use plt.plot(...), which implicitly creates the figure and axis. In your case, you mainly use the object oriented approach on the axis object that is returned by the seaborn function. However, you should use ax.axis('off') instead of plt.axis('off').
You create a new blank figure
When you are calling the seaborn function sns.scatterplot, you are implicitly creating a matplotlib figure and axis object. You catch that axis object in the variable ax. You then use plt.savefig to save your image in the function, which works by implicitly getting the figure corresponding to the currently used axis object. However, you are then creating a new figure by calling fig = plt.figure(), which is of course blank, and then returning it. What you should do, is getting the figure currently used by the axis object you are working with. You can get it by calling fig = plt.gcf() (which stands for "get current figure") and would be the procedural approach, or better use fig = ax.get_figure()
What you should do instead is something like this:
import seaborn as sns
from matplotlib import pyplot as plt
def graph(df, id):
# size of the graph
xlims = (-180, 180)
ylims = (-180, 180)
# dictate the colors of the scatter plot based on the grouping of hot or cold
color_dict = {'COLD': 'blue',
'HOT': 'red'}
title_name = f"{id}"
ax = sns.scatterplot(data=df, hue='GRP', x='X_GRID', y='Y_GRID',
legend=False, palette=color_dict)
ax.set_title(title_name)
ax.set(xlim=xlims)
ax.set(ylim=ylims)
if show_grid:
# pass in the prev graph so I can overlay grid
ax = self.__get_grid( ax)
circle1 = plt.Circle(xy=(0, 0), radius=150, color='black', fill=False, zorder=3)
ax.add_patch(circle1)
ax.set_aspect('equal')
ax.axis('off')
fig = ax.get_figure()
fig.savefig(title_name + '_in_ftn.png')
return (fig, title_name + '.png')

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])

Patches I add to my graph are not opaque with alpha=1. Why?

I would like to add a rectangle over a graph. Through all the documentation I've found, the rectangle should be opaque by default, with transparency controlled by an alpha argument. However, I can't get the rectangle to show up as opaque, even with alpha = 1. Am I doing something wrong, or is there something else I need to know about the way that graphs interact with patches?
Here is a toy example:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from pylab import *
x = np.arange(10)
y = x
fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, y)
rect = patches.Rectangle( ( 2,3 ), 2, 2, alpha = 1, ec = "gray", fc = "CornflowerBlue", visible = True)
ax.add_patch(rect)
plt.show()
From the documentation:
Within an axes, the order that the
various lines, markers, text,
collections, etc appear is determined
by the
matplotlib.artist.Artist.set_zorder()
property. The default order is
patches, lines, text, with collections
of lines and collections of patches
appearing at the same level as regular
lines and patches, respectively.
So patches will be drawn below lines by default. You can change the order by specifying the zorder of the rectangle:
# note alpha is None and visible is True by default
rect = patches.Rectangle((2, 3), 2, 2, ec="gray", fc="CornflowerBlue", zorder=10)
You can check the zorder of the line on your plot by changing ax.plot(x, y) to lines = ax.plot(x, y) and add a new line of code: print lines[0].zorder. When I did this, the zorder for the line was 2. Therefore, the rectangle will need a zorder > 2 to obscure the line.
Your choice of facecolor (CornflowerBlue) has an appearance of being semi-opaque, but in reality the color you are seeing is correct for alpha = 1. Try a different color like 'blue' instead. Matplotlib does appear to be placing the rectangular patch below the line, but I don't think that's a transparency issue.

Categories