Matplotlib setting `axes` object with `imshow` causes y-axis to become variable - python-3.x

Description
I have began refactoring some code based on the future warning of matplotlib, to re-use the initially defined axes object. However, I noticed that whenever I was re-using my axes object, the image size would be variable. Since, I have managed to isolate the problem to the axes.imshow method as after using imshow, the y-axis of any subsequent drawing on that axes has a y-axis that seems to rescale.
The feeling I have is that the y-axis scale is retained from the initial image that is plotted using imshow (I thought that axes.clear should reset this). Specifically in the below examples, shuffling plots some data spanning ~ 9.90 to 10.10 but because the original image spanned form 0 to 50 the y-axis is barely visible.
Below are first two screenshots of the expected and then 'bugged' behaviour, followed by an MVCE that has two sections that can be toggled to get the expected or 'bugged' behaviour:
Images
Splash without imshow:
Screen after 'Foo -> Shuffle' (Expected behaviour):
Splash with imshow:
Screen after 'Foo -> Shuffle' (unexpected behaviour):
MVCE
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg
)
import tkinter as tk
from matplotlib import image, figure
from numpy import random, linspace
from os import path, getcwd
from pylab import get_cmap
class Foo(object):
#classmethod
def run(cls):
root = tk.Tk()
Foo(root)
root.mainloop()
def __init__(self, master):
# Figure & canvas
self.fig = figure.Figure(figsize=(5,5))
self.axes = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, master=master)
self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=tk.YES)
# Splash image (This 'bugs')
Z = random.random((50,50))
self.axes.imshow(Z, cmap=get_cmap("Spectral"), interpolation='nearest')
self.canvas.draw()
# Dummy start data (This Works)
#self.axes.plot(random.normal(10,0.05,100))
#self.canvas.draw()
# MENU
menu = tk.Menu(master)
master.config(menu=menu)
test_menu = tk.Menu(menu, tearoff=0)
menu.add_cascade(label="Foo", menu=test_menu)
test_menu.add_command(label="Shuffle",
command=self.shuffle)
test_menu.add_command(label="Add",
command=self.add)
def add(self):
x_data = linspace(0,10, 1000)
y_data = random.normal(x_data)
self.axes.plot(x_data, y_data)
self.canvas.draw()
def shuffle(self):
self.axes.clear()
self.axes.plot(random.normal(10,0.05,100))
self.canvas.draw()
if __name__ == "__main__":
Foo.run()
Question
What is going on here, specifically what is causing the image to appear so differently and what can be done about it?

When no argument is given for aspect, it defaults to None. From the documentation:
If None, default to rc image.aspect value
Therefore if no argument is given to imshow, it will use whatever the rcParam for "image.aspect" is, which you can find by doing:
print (plt.rcParams["image.aspect"]) # default is "equal"
A fix to your problem would be to set it to "auto" in your shuffle function using axes.set_aspect():
def shuffle(self):
self.axes.clear()
self.axes.plot(random.normal(10,0.05,100))
self.axes.set_aspect("auto")
self.canvas.draw()
If you don't mind changing the aspect ratio of imshow, there is also an aspect= argument:
self.axes.imshow(Z, cmap=get_cmap("Spectral"), interpolation='nearest', aspect="auto")

Related

How to customize labels of a heatmap created using pyqtgraph?

I'm transitioning from tkinter with matplotlib to PyQt with pyqtgraph and wish to create a simple heatmap, with labels coming from a list of strings. I have no experience with PyQt nor pyqtgraph, and I'm having a hard time tracking down the means by which I can add the labels and adjust the fontsize. Here's what I have so far:
import numpy as np
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QHBoxLayout
import pyqtgraph as pg
pg.setConfigOption('background', 'lightgray')
from pyqtgraph import PlotWidget
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Heatmap")
self.setStyleSheet("background-color: lightgray;")
self.resize(500,500)
layout = QHBoxLayout() # using since I will later add widgets to this row
graphWidget = pg.ImageView()
graphWidget.setImage(np.random.rand(5,5))
colors = [(0, 0, 0),(4, 5, 61),(84, 42, 55),(15, 87, 60),(208, 17, 141),(255,
255, 255)]
cmap = pg.ColorMap(pos=np.linspace(0.0, 1.0, 6), color=colors)
graphWidget.setColorMap(cmap)
layout.addWidget(graphWidget)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
The output image looks like this:
What I'd like to do is the following:
Starting with a list of strings, Labels=['a','b','c','d','e'], place the
labels along the horizontal and vertical axes, starting with 'a','a' in the lower
left and ending with 'e','e' in the top right. The labels should be centered with
respect to their corresponding squares.
Be able to adjust the font family and size of my labels.
Completely remove everything to the right of the heatmap itself and use a simple
static colorbar as I did using matplotlib.
I will later be adding a slider for animation, along with other features, later.

Window size incorrect on matplotlib animation

Trying to get an animation of a rotating arrow in a Jupyter notebook.
Can't get the window size and circle display correct.
I'm trying to get an animation of a rotating arrow in matplotlib. This is part of a jupyter engineering mechanics book I'm building for my students.
The idea of the question is that the animation shows what the two dimensional force balance is of multiple vectors on a node (the black dot in the code).
The animation is based on the following three sources:
1) Drawing a shape
2) Matplotlib animation
3) Arrow animation
get_ipython().run_line_magic('matplotlib', 'inline')
import matplotlib.pyplot as plt
import numpy as np
import matplotlib.patches as patches
from matplotlib import animation, rc
from IPython.display import HTML
from math import degrees,radians,cos,sin,atan,acos,sqrt
# Create figure
fig, ax = plt.subplots()
# Axes labels and title are established
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_ylim(-100,100) #<---- This window size is not displayed
ax.set_xlim(-100,100) #<---- This window size is not displayed
ax.set_aspect('equal', adjustable='box')
#the circle
circle = plt.Circle((0, 0), radius=10, fc='black')
plt.gca().add_patch(circle) #<---- The circle is not displayed
#arrow1 (more arrows will me added)
arrow1x=[]
arrow1y=[]
arrow1dx=[]
arrow1dy=[]
for t in range(1000):
if t <= 250:
arrow1x.append(0)
arrow1y.append(0)
arrow1dx.append(t/250*100)
arrow1dy.append(0)
elif t <= 500:
arrow1x.append(0)
arrow1y.append(0)
arrow1dx.append(100)
arrow1dy.append(0)
elif t <= 750:
arrow1x.append(0)
arrow1y.append(0)
arrow1dx.append(100*cos(radians((t-500)/250*180.)))
arrow1dy.append(100*sin(radians((t-500)/250*180.)))
else:
arrow1x.append(0)
arrow1y.append(0)
arrow1dx.append((100-100*(t-750)/250)*-sin(radians((t-750)/250*180.)))
arrow1dy.append((100-100*(t-750)/250)*-sin(radians((t-750)/250*180.)))
patch = patches.Arrow(arrow1x[0], arrow1y[0], arrow1dx[0], arrow1dy[0])
#the animation (I have no idea how this works:)
def init():
ax.add_patch(patch)
return patch,
def animate(t):
ax.clear()
patch = plt.Arrow(arrow1x[t], arrow1y[t], arrow1dx[t], arrow1dy[t])
ax.add_patch(patch)
return patch,
anim = animation.FuncAnimation(fig, animate, init_func=init,
frames=1000, interval=20,
blit=True)
HTML(anim.to_jshtml())
As a result of this code I would like to see a square screen with range (-100 x 100,-100 y 100), the black node and the arrow.
What I'm seeing is a square screen (0 x 1,0 y 1), the rotating arrow, and no black dot.
There is no error output in jupyter which makes this really difficult to follow. Additionally the code takes really long to compile, which is also something that is not desired for a webpage, if this keeps taking so long I think i should look in a pre-compiled image (any tips for that perhaps ?).
Thus for some reason the window size and the dot are not adopted, but as far as I'm seeing the code from the sources is adopted as depicted on the webpages.
You took inappropriate part of "Arrow animation". Since you have static elements on your plot, you don't want to fully clear your ax: you should remove one patch during execution of animate function. Just replace ax.clear() with the next lines:
global patch
ax.patches.remove(patch)

Trying to get matplotlib to display images as an animation

I have a class that imports matplotlib when needed and functions as a callback that receives a NumPy array, which should be shown in the next rendered frame. I need to dump this in a window as an animation on the screen. The current code is:
import matplotlib.pyplot as plt
import numpy as np
class Renderer(object):
def __init__(self):
self._image = None
def __call__(self, buffer):
if not self._image:
self._image = plt.imshow(buffer, animated=True)
else:
self._image.set_data(buffer)
plt.draw()
renderer = Renderer()
for _ in range(100):
renderer(
np.random.randint(low=0, high=255, size=(240, 320, 3), dtype=np.uint8))
There's some pretty heavy computation doing simulations that are generating each frame, so I don't worry that the frame rate will be too high.
Currently, the code does absolutely nothing, i.e. nothing appears on screen. Does anyone have an idea how to do an animation with the library?
UPDATE: Regarding context, so in my use case an instance of Renderer gets passed down to a layer of code that generates pixels and draws them on the screen by calling the Renderer object. In other words, when something should be drawn is out of my control, I also can't control the frame rate and don't know the time interval between the frames. For this reason what I really need from an API point-of-view is just a way to dump a bunch of pixels on the screen right now.
The FuncAnimation approach has the problem that getting the frames to it would require changing the Renderer callback to put frames on a queue from where the generator generating frames would pull them. FuncAnimation also seems to require me to know the time interval between frames a priori, which I don't know and isn't necessarily constant.
Just as usual for animations, use interactive mode (plt.ion()) or a FuncAnimation.
Interactive mode (plt.ion())
import matplotlib.pyplot as plt
import numpy as np
class Renderer(object):
def __init__(self):
self._image = None
def __call__(self, buffer):
if not self._image:
self._image = plt.imshow(buffer, animated=True)
else:
self._image.set_data(buffer)
plt.pause(0.01)
plt.draw()
renderer = Renderer()
plt.ion()
for _ in range(100):
renderer(
np.random.randint(low=0, high=255, size=(240, 320, 3), dtype=np.uint8))
plt.ioff()
plt.show()
Animation with FuncAnimaton
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import numpy as np
class Renderer(object):
def __init__(self):
self._image = None
def __call__(self, buffer):
if not self._image:
self._image = plt.imshow(buffer, animated=True)
else:
self._image.set_data(buffer)
renderer = Renderer()
fig, ax = plt.subplots()
def update(i):
renderer(
np.random.randint(low=0, high=255, size=(240, 320, 3), dtype=np.uint8))
ani = FuncAnimation(fig, update, frames=100, interval=10)
plt.show()

Responsive text in Matplotlib in Python

I am developing a simple graph visualizer using networkX and Matplotlib in Python. I also have some buttons plotted with text in them. As a whole the design is responsive which means that the graph and the buttons scale when I resize the window. However, the text size remains the same which makes the whole visualizer look very bad when not resized enough. Do you know how I can make the text also responsive?
Thank you in advance!!!
You update the fontsize of a matplotlib.text.Text using text.set_fontsize(). You can use a "resize_event" to call a function that sets a new fontsize. In order to do this with every text in a plot, it might be helpful to define a class that stores initial figure height and fontsizes and updates the fontsizes once the figure is resized, scaled by the new figure height divided by the initial one.
You may then also define a minimal readable fontsize, below which the text should not be resized.
A full example:
import matplotlib.pyplot as plt
import numpy as np
class TextResizer():
def __init__(self, texts, fig=None, minimal=4):
if not fig: fig = plt.gcf()
self.fig=fig
self.texts = texts
self.fontsizes = [t.get_fontsize() for t in self.texts]
_, self.windowheight = fig.get_size_inches()*fig.dpi
self.minimal= minimal
def __call__(self, event=None):
scale = event.height / self.windowheight
for i in range(len(self.texts)):
newsize = np.max([int(self.fontsizes[i]*scale), self.minimal])
self.texts[i].set_fontsize(newsize)
fontsize=11
text = plt.text(0.7, 0.6, "Some text", fontsize=fontsize,
bbox={'facecolor':'skyblue', 'alpha':0.5, 'pad':10})
cid = plt.gcf().canvas.mpl_connect("resize_event", TextResizer([text]))
plt.show()

Display Matplotlib spines in Tkinter

I know how to display spines in Matplotlib. I know how to display a Matplotlib subplot in Tkinter too. But I would like to know how to put the spines in this subplot in Tkinter.
Here is the code to display a subplot in Tkinter :
import matplotlib
matplotlib.use('TkAgg')
from numpy import arange, sin, pi
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import sys
if sys.version_info[0] < 3:
import Tkinter as Tk
else:
import tkinter as Tk
def destroy(e): sys.exit()
root = Tk.Tk()
root.wm_title("Embedding in TK")
f = Figure(figsize=(5,4), dpi=100)
a = f.add_subplot(111)
t = arange(0.0,3.0,0.01)
s = sin(2*pi*t)
a.plot(t,s)
a.set_title('Tk embedding')
a.set_xlabel('X axis label')
a.set_ylabel('Y label')
# a tk.DrawingArea
canvas = FigureCanvasTkAgg(f, master=root)
canvas.show()
canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
#toolbar = NavigationToolbar2TkAgg( canvas, root )
#toolbar.update()
canvas._tkcanvas.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
button = Tk.Button(master=root, text='Quit', command=sys.exit)
button.pack(side=Tk.BOTTOM)
Tk.mainloop()`
Here is the code to display spines in Matplotlib :
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
image = np.random.uniform(size=(10, 10))
ax.imshow(image, cmap=plt.cm.gray, interpolation='nearest')
ax.set_title('dropped spines')
# Move left and bottom spines outward by 10 points
ax.spines['left'].set_position(('outward', 10))
ax.spines['bottom'].set_position(('outward', 10))
# Hide the right and top spines
ax.spines['right'].set_visible(False)
ax.spines['top'].set_visible(False)
# Only show ticks on the left and bottom spines
ax.yaxis.set_ticks_position('left')
ax.xaxis.set_ticks_position('bottom')
plt.show()
Where you use ax.set_title('...') in your second code block, you use a.set_title('...') in your first block. This pretty much gives away that the methods you can call on ax, you can also call on a.
Simply use the same code as in the second block, but replace ax with a, and it should work fine.
According to the docs, ax and a are not exactly the same objects. Figure.add_subplot() returns an Axes instance, and pyplot.subplots() returns an Axis object as second output parameter. However, since
The Axes contains most of the figure elements: Axis...
you can edit the spines in the same way from both.

Resources