How to customize labels of a heatmap created using pyqtgraph? - pyqt

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.

Related

matplotlib subplot shrinks automatically

I am currently using matplotlib to plot my images to subplots and as there are many images (as much as 100), the subplots shrinks automatically as there are too many subplots.
subplots of 63 images
As you can see from the image, this is the issue I am currently facing.
I am wondering if there are any ways to fix the size of the subplots to prevent the automatic shrinking so they can be seen and also including a scrollable bar so that if there are over 100 images and all couldn't be fitted onto the figure, it will still maintain the size of the subplots and just allow the user to scroll and view all these images.
I am using TKagg backend.
I've tried doing:
fig = plt.figure(figsize=(8,8))
to maintain the size of the subplots but it seems that this doesn't fix the images as the images still shrunk.
I don't think matplotlib is the best module if you want to show a large number of images.
A good alternative might be Plotly, combined with dash.
pip install dash
This will make it possible to generate many images onto a webpage, which automatically enables scrolling. Also you can now add a manual slider, such that you can select the part that you want.
In order to give some reference I will display two minimal working examples.
Option 1
This is the solution closest to your specifications of generating a grid of images, through which you can scroll (when they become too big), but also requires dash_bootstrap for the formatting and pillow to convert images to base64.
It is build from two parts:
A numpy image convertor to base64, this is for serving the image as url to the html.Img component.
The dash app that creates a grid layout that will wrap around when changing the width or height value.
Part 1:
import base64
from io import BytesIO
from PIL import Image
def arr_to_b64(array, ext='jpeg'):
""" Convert an array to an image source that can be displayed."""
image = Image.fromarray(array)
if ext == 'jpg': ext = 'jpeg'
if ext == 'jpeg' and image.mode in ('RGBA', 'A'):
background = Image.new(image.mode[:-1], image.size, (255, 255, 255))
background.paste(image, image.split()[-1])
image = background
buffer = BytesIO()
image.save(buffer, format=ext)
encoded = base64.b64encode(buffer.getvalue()).decode('utf-8')
return f'data:image/{ext};base64, ' + encoded
part 2 (this is the dash server)
import numpy as np
import dash
import dash_html_components as html
import dash_bootstrap_components as dbc # Extra import besides `dash`
app = dash.Dash(__name__,
external_stylesheets=[dbc.themes.BOOTSTRAP])
images = np.random.randint(0, 255, (60, 30, 30, 3), dtype=np.uint8)
app.layout = html.Div(children=[
html.Div(id='image-output', className='d-flex flex-wrap justify-content-start m-5',
children=[html.Img(src=arr_to_b64(image), className='m-1', width=100, height=100)
for image in images])
])
if __name__ == '__main__':
app.run_server(port='8051', debug=True)
Output:
Option 2
This is a slightly simpler to create example without requiring any knowledge of bootstrap. But doesn't display a grid, but a slider. This slider makes it possible to select a specific image.
import dash
import dash_core_components as dcc
import dash_html_components as html
import plotly.graph_objs as go
from dash.dependencies import Input, Output
app = dash.Dash(__name__)
images = np.random.randint(0, 255, (60, 30, 30, 3), dtype=np.uint8)
app.layout = html.Div(children=[
dcc.Slider(id='image-number', min=0, max=60, value=0,
marks={key: str(key) for key in range(0, len(images), 5)},
tooltip=dict(always_visible=True, placement='top')),
html.Div(id='image-output'),
])
#app.callback(Output('image-output', 'children'),
[Input('image-number', 'value')])
def update_image(idx):
return dcc.Graph(figure=go.Figure(go.Image(z=images[idx])))
if __name__ == '__main__':
app.run_server(port='8051', debug=True)
It might be possible to do with matplotlib, but sometimes the alternatives are easier.
Hopefully this will help you further.

Matplotlib setting `axes` object with `imshow` causes y-axis to become variable

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

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