Responsive text in Matplotlib in Python - python-3.x

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

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.

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)

How to change the font weight of individual colorbar labels?

I would like to have different font weights for each of my colorbar labels.
I have tried to let LaTeX format the labels in the following way:
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
im = ax.imshow(np.random.rand(50, 50)/20)
cbar = ax.figure.colorbar(im, ticks=np.arange(0, 0.05, 0.01))
cbar.ax.set_yticklabels([r'{\fontsize{50pt}{3em}\selectfont{}{0}}',
r'{\fontsize{40pt}{3em}\selectfont{}{0.01}}',
r'{\fontsize{30pt}{3em}\selectfont{}{0.03}}',
r'{\fontsize{20pt}{3em}\selectfont{}{0.03}}',
r'{\fontsize{10pt}{3em}\selectfont{}{0.04}}',
r'{\fontsize{1pt}{3em}\selectfont{}{0.05}}', ])
but this only updates the text of the labels to the whole string (e.g., {\fontsize{50pt}{3em}\selectfont{}{0}}). The pyplot TeX demo works for me. Even if this solution would work it would not be ideal as I would probably need to specify everything manually.
Much more convenient would be something like in this question. There, I learned that the font size of single labels of the regular x and y axis can be updated by calling
label = axes.yaxis.get_major_ticks()[2].label
label.set_fontsize(size)
replacing set_fontsize by set_fontweight correctly updates the weight of the selected label.
Unfortunately I could not find the equivalent of axes.yaxis.get_major_ticks()[2].label for the colorbar.
Is it possible to change the font weight of individual labels of the colorbar directly? With directly I mean without using a workaround like plotting some new text above existing labels.
If this is not possible a solution plotting text above existing labels which automatically uses the position and content the previous labels and only adjusts the font weight would also be appreciated.
Thanks!
As pointed out by #ImportanceOfBingErnest , set_fontweight works for setting the weight of single colorbar labels too.
I had to try a couple of things to find which call would give me the text objects defining the colorbar labels. They are accessible in cbar.ax.get_yticklabels().
The code snippet below now properly changes the weight of the second colorbar label:
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
im = ax.imshow(np.random.rand(50, 50)/20)
cbar = ax.figure.colorbar(im, ticks=np.arange(0, 0.05, 0.01))
cbar.ax.get_yticklabels()[1].set_fontweight(1000)
plt.show()
Output of code (not enough reputation for inline images)

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

Matplotlib fade line behind transparent text box

I would like to place a text box directly over a line plot that would cause the line at that point to 'fade', for legibility. My first thought is to change alpha of the text box. However that requires me to set a background colour, which I do not want (the plot background is currently transparent, which I would like to keep). There may be other elements of various shapes/colours that would be similarly stacked under the text. Ideally, what I want is a semi-opaque text box that has no colour of its own, but which would cause elements underneath the text box to be half-hidden. Alpha seems to be not the way to go, but is there another attribute that I can modify?
import matplotlib.pyplot as plt
import pandas as pd
pd.set_option("display.mpl_style", 'default')
fig = plt.figure()
ax1 = fig.add_subplot(1,1,1)
ax1.plot((0,1),(0.5,0.5), "--",lw=2);
ax1.plot((0,0.25,0.5,0.75,1),(0,0.25,0.5,0.75,1), "go--",markersize=20)
ax1.text(0.25, 0.5, "HardToRead", fontsize=12,ha="center",va="center", bbox=dict(boxstyle="square,pad=0.1", fc='white', ec="white", lw=1,alpha=0.1))
ax1.text(0.75, 0.5, "EasyToRead", fontsize=12,ha="center",va="center", bbox=dict(boxstyle="square,pad=0.1", fc='white', ec="white", lw=1,alpha=0.9))
ax1.text(0.5, 0.5, "CrossTalk", fontsize=12,ha="center",va="center", rotation=90, bbox=dict(boxstyle="square,pad=0.1", fc='white', ec="white", lw=1,alpha=0.6))

Resources