PySimpleGUI / PySimpleGUI

Python GUIs for Humans! PySimpleGUI is the top-rated Python application development environment. Launched in 2018 and actively developed, maintained, and supported in 2024. Transforms tkinter, Qt, WxPython, and Remi into a simple, intuitive, and fun experience for both hobbyists and expert users.

Home Page:https://www.PySimpleGUI.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[Question] Is there a way to blur the rectangle that contains a Matplotlib plot?

Minoslo opened this issue · comments

Type of Issue (Enhancement, Error, Bug, Question)

Question


Operating System

Windows 11

PySimpleGUI Port (tkinter, Qt, Wx, Web)

tkinter


Versions

Version information can be obtained by calling sg.main_get_debug_data()
Or you can print each version shown in ()

Python version (sg.sys.version)

3.12.0 (tags/v3.12.0:0fb18b0, Oct 2 2023, 13:03:39) [MSC v.1935 64 bit (AMD64)]

PySimpleGUI Version (sg.__version__)

4.60.5

GUI Version (tkinter (sg.tclversion_detailed), PySide2, WxPython, Remi)

8.6.13


Your Experience In Months or Years (optional)

Have used another Python GUI Framework? (tkinter, Qt, etc) (yes/no is fine)
No

Anything else you think would be helpful?
I'm pretty much a Python begginer


Troubleshooting

These items may solve your problem. Please check those you've done by changing - [ ] to - [X]

  • Searched main docs for your problem PySimpleGUI Documenation
  • Looked for Demo Programs that are similar to your goal. It is recommend you use the Demo Browser! Demo Programs
  • None of your GUI code was generated by an AI algorithm like GPT
  • If not tkinter - looked for Demo Programs for specific port
  • For non tkinter - Looked at readme for your specific port if not PySimpleGUI (Qt, WX, Remi)
  • Run your program outside of your debugger (from a command line)
  • Searched through Issues (open and closed) to see if already reported Issues.PySimpleGUI.com
  • Have upgraded to the latest release of PySimpleGUI on PyPI (lastest official version)
  • Tried running the Development Build. Your problem may have already been fixed but not released. Check Home Window for release notes and upgrading capability
  • For licensing questions please email license@PySimpleGUI.com

Detailed Description

The graphic rectangle containing the chart is too noticeable. I'd like it to be more integrated into the design, for example, blurring the edges or something like that.
I'm not sure if there's a solution and I'm not sure if the solution is about PySimpleGUI or Matplotlib, so I'm sorry in advance if this is not the correct place to ask my question.

I hope someone can help me to keep learning about PySimpleGUI.

Thanks.

Edited: Include a short video rather than an image

Code To Duplicate

A short program that isolates and demonstrates the problem (Do not paste your massive program, but instead 10-20 lines that clearly show the problem)

This pre-formatted code block is all set for you to paste in your bit of code:

# Paste your code here
# ----- IMPORT OF PACKAGES -----
import PySimpleGUI as sg
import threading
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

matplotlib.use('TkAgg')


# ----- DEFINITION OF CLIENTS, DATATIME ELEMENTS, THREAD EVENTS AND CONSOLE COMMANDS -----
def draw_figure(canvas, figure, loc=(0, 0)):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas_agg


def delete_figure_agg(figure_agg):
    figure_agg.get_tk_widget().forget()
    plt.close('all')


# ----- DEFINITION OF FUNCTIONS TO BE EXECUTED AS THREADS -----
graphics1_done = threading.Event()
graphics1_done.set()


def generate_data1():
    global xSolar, ySolar
    while True:
        graphics1_done.wait()
        new_y_value = np.random.normal(0.01, 1)
        ySolar = abs(np.append(ySolar, ySolar[-1] + new_y_value))
        new_x = xSolar[-1] + (xSolar[1] - xSolar[0])
        xSolar = np.append(xSolar, new_x)

        if len(xSolar) > 200:
            xSolar = xSolar[-200:]
            ySolar = ySolar[-200:]

        # Update plot
        axSolar.cla()
        axSolar.axis('off')
        axSolar.plot(xSolar, ySolar, color='black', linewidth=0.5)
        # Draw y_mean line
        axSolar.plot([xSolar[0], xSolar[-1]], [ySolar.mean(), ySolar.mean()], linestyle='--', color='white',
                     linewidth=1)
        # Add text
        text_offset = 0.05 * (axSolar.get_ylim()[1] - axSolar.get_ylim()[0])
        axSolar.text(xSolar[-1], ySolar.mean() - text_offset, f'mean: {ySolar.mean():.2f}', color='white',
                     fontsize=10, verticalalignment='top', horizontalalignment='right')

        # Gradients
        grad1 = axSolar.imshow(np.linspace(0, 1, 256).reshape(-1, 1), cmap='Blues', vmin=-0.5, aspect='auto',
                               extent=[xSolar.min(), xSolar.max(), ySolar.mean(), ySolar.max()], origin='lower')
        poly_pos = axSolar.fill_between(xSolar, ySolar.min(), ySolar, alpha=0.1)
        grad1.set_clip_path(poly_pos.get_paths()[0], transform=axSolar.transData)
        poly_pos.remove()

        grad2 = axSolar.imshow(np.linspace(0, 1, 256).reshape(-1, 1), cmap='Oranges', vmin=-0.5, aspect='auto',
                               extent=[xSolar.min(), xSolar.max(), ySolar.min(), ySolar.mean()], origin='upper')
        poly_neg = axSolar.fill_between(xSolar, ySolar.min(), ySolar, alpha=0.1)
        grad2.set_clip_path(poly_neg.get_paths()[0], transform=axSolar.transData)
        poly_neg.remove()

        axSolar.set_ylim(ySolar.min(), ySolar.max())

        # Ajusta el tiempo de espera según sea necesario
        graphics1_done.clear()


# ----- GUI SET UP -----
theme_dict = {'BACKGROUND': '#2B475D',
              'TEXT': '#FFFFFF',
              'INPUT': '#F2EFE8',
              'TEXT_INPUT': '#000000',
              'SCROLL': '#F2EFE8',
              'BUTTON': ('#000000', '#C2D4D8'),
              'PROGRESS': ('#FFFFFF', '#C7D5E0'),
              'BORDER': 1, 'SLIDER_DEPTH': 0, 'PROGRESS_DEPTH': 10}

sg.LOOK_AND_FEEL_TABLE['Dashboard'] = theme_dict
sg.theme('Dashboard')

BORDER_COLOR = '#C7D5E0'
DARK_HEADER_COLOR = '#163C6B'
LAYOUT_COLOR = '#C88200'

layout0o0 = [[sg.HSep()],
             [sg.VPush(background_color='black')],
             [sg.Push(background_color='black'),
              sg.Column([[sg.Graph((328, 164), (-328, -164), (-328, -164),
                                   background_color='black', pad=(0, 0),
                                   key='-SolarGraph-')],
                         [sg.Graph((328, 164), (-328, -164), (-328, -164),
                                   background_color='black', pad=(0, 0),
                                   key='-LoadGraph-')]],
                        background_color='black'),
              sg.Push(background_color='black')],
             [sg.VPush(background_color='black')],
             [sg.HSep()]]

layout = [[sg.Column(layout0o0, expand_x=True, expand_y=True, pad=(0, 0), background_color='black',
                     element_justification='c', key='-LAYOUT0o0-')]]

window = sg.Window("INTERFAZ 0.1", layout, margins=(0, 0), size=(1024, 600), background_color=BORDER_COLOR,
                   no_titlebar=False, grab_anywhere_using_control=True, location=(0, 0), finalize=True)

# Graphs from main window
np.random.seed(123)
xSolar = np.linspace(0, 10, 200)
ySolar = abs(np.random.normal(0.01, 1, 200).cumsum())
figSolar = plt.figure(num=1, figsize=(3.4166666667, 1.7083333333), facecolor='black', dpi=96, clear=True)
axSolar = figSolar.add_axes([0, 0, 1, 1])  # La gráfica ocupa la figura completa
axSolar.axis('off')  # Elimina los ejes
axSolar.plot(xSolar, ySolar, color='black', linewidth=0.5)
ylimSolar = axSolar.get_ylim()
grad1Solar = axSolar.imshow(np.linspace(0, 1, 256).reshape(-1, 1), cmap='Blues', vmin=-0.5, aspect='auto',
                            extent=[xSolar.min(), xSolar.max(), 0, ySolar.max()], origin='lower')
poly_posSolar = axSolar.fill_between(xSolar, ySolar.min(), ySolar, alpha=0.1)
grad1Solar.set_clip_path(poly_posSolar.get_paths()[0], transform=axSolar.transData)
poly_posSolar.remove()
grad2 = axSolar.imshow(np.linspace(0, 1, 256).reshape(-1, 1), cmap='Oranges', vmin=-0.5, aspect='auto',
                       extent=[xSolar.min(), xSolar.max(), ySolar.min(), 0], origin='upper')
poly_negSolar = axSolar.fill_between(xSolar, ySolar, ySolar.max(), alpha=0.1)
grad2.set_clip_path(poly_negSolar.get_paths()[0], transform=axSolar.transData)
poly_negSolar.remove()
axSolar.set_ylim(ylimSolar)

figSolar = plt.gcf()
fig_canvas_aggSolar = draw_figure(window['-SolarGraph-'].TKCanvas, figSolar)

data1_thread = threading.Thread(target=generate_data1)
data1_thread.daemon = True
data1_thread.start()

# Bunch of variables needed for GUI
shown_layout = '0o0'  # Current layout

# ----- MAIN LOOP -----
# Create an event loop
while True:
    event, values = window.read(timeout=16, timeout_key="-refresh-")  # timeout = 16 --> 62,5 Hz
    # print(event, values)
    if event == "-refresh-":
        window.refresh()
        if shown_layout == '0o0':
            if not graphics1_done.is_set():
                fig_canvas_aggSolar.draw()
                graphics1_done.set()
    if event == sg.WINDOW_CLOSED:
        break

window.close()

Screenshot, Sketch, or Drawing

Grabacion.2024-03-22.141951.mp4

Watcha Makin?

Hi everyone 😊,

I've been learning about PySimpleGUI because I wanted to make an interface for myself since I have solar installation in my house.
PySimpleGUI was my best choice to get started with Python GUIs and so I did.
I was able to render all the data I wanted in multiple layouts in the same window thanks to all the demos that exist on the web. My way forward was to copy the code from the demos, try to understand how everything works by changing things and, once understood, try to modify it to be the way I wanted it to be.
Now I was trying to include a graph in the GUI using Matplotlib and I think I did a really good job (and I say THINK, don't judge me too much). This graph is going to represent how much solar energy is being produced overall. Gradients are also great because at the end (now that I know how it works), it will represent in blue how much energy my load is consuming out of the total and in orange how much energy is free to go to my batteries or the grid.

Not sure what the requirement you want, if possible, can you give me an exact image for your target.

Hi Jason,
Thank you so much for being so fast. What I'm trying to accomplish is something I saw in modern solar software. I'm attaching an image.
image
I think I can get the top-down blur effect by researching more on how to make my own color map in Matplotlib. Instead of starting with the orange color and blur it into white, I can blur into black and maybe that solves half the problem.
But blurring the horizontal edges, I don't know how to do it, really. I tried to investigate to see if I could modify the alpha channel of the sg object Graph(), but it's not possible or I haven't been able to do it, and it may not even be the solution.
Greetings and thank you in advance whether or not there is a solution to the problem.

Pd: Thank you so much for your posts, I saw the circular progress bar one and I included it in my project after spending so many hours trying to realize how the code works. For me it was fascinating how did you use the pillow package for solving that.

Not sure how it can be done or not by using matplotlib, but it cannot be done by using PySimpleGUI/tkinter.

For my knowledge, if the target is correct for you, I will do it by using Pillow library and PySimpleGUI Image element.

import io
import time
import random
import threading
from PIL import Image, ImageDraw, ImageFilter
import PySimpleGUI as sg

class GUI():

    def __init__(self, size=(400, 300)):
        self.w, self.h = self.size = size
        sg.theme('DarkBlue3')
        sg.set_options(font=("Courier New", 16))
        layout = [
            [sg.Image(size=self.size, background_color='black', key='IMAGE')],
            [sg.Push(), sg.Button("BLUR"), sg.Push()]
        ]
        self.window = sg.Window(
            'Title', layout=layout, enable_close_attempted_event=True,
            use_default_focus=False, margins=(0, 0), finalize=True)
        self.window["BLUR"].block_focus()
        self.blur = False
        self.running = True
        threading.Thread(target=self.get_data, daemon=True).start()
        self.start()

    def get_data(self, fg=(255, 255, 255, 255), bg=(0, 0, 0, 255)):
        y = self.h//2
        while self.running:
            self.data = [(i, random.randint(-y, y) + y) for i in range(self.w)]
            im = Image.new("RGBA", self.size, color=bg)
            draw = ImageDraw.Draw(im, mode="RGBA")
            draw.line(self.data, fill=fg, width=1)
            draw.line([(0, y), (self.w, y)], fill=fg, width=1)
            if self.blur:
                im = im.filter(ImageFilter.BLUR)
            data = self.image_to_data(im)
            self.window.write_event_value("Update", data)
            time.sleep(0.1)             # time delay to reduce the CPU loading

    def image_to_data(self, im):
        with io.BytesIO() as output:
            im.save(output, format="PNG")
            data = output.getvalue()
        return data

    def start(self):
        while True:
            event, values = self.window.read()
            if event == sg.WINDOW_CLOSE_ATTEMPTED_EVENT:
                break
            elif event == 'Update':
                data = values[event]
                self.window['IMAGE'].update(data)
            elif event == 'BLUR':
                self.blur =  not self.blur
        self.running = False
        time.sleep(0.2)                                     # wait thread stop
        self.window.close()

GUI()

image
image

Hi Jason,

First of all, I'm sorry I didn't answer in these two days. It's been Holy Week in my city and I've been a bit absent.
The solution you show me is great man! But I'd like to know if it can be fine-tuned it a little bit more and just blur the edges.

Best regards and thank you very much!

just blur the edges.

What the edges are ? No exact definition for the edges may get nothing returned.

That's nice, I want to learn that library so this is a good chance to do it.
Just to clarify what I trying to mean by edges:
image
That blurred effect that occurs in this image and that blends the graphic with the background.

Firstly, I created an edge-spur mask by PIL.Image

tmptx9w_5nn

Then using Image.alpha_composite with both images, main image and the mask.

import io
import time
import random
import threading
from PIL import Image, ImageDraw, ImageFilter
import PySimpleGUI as sg

class GUI():

    def __init__(self, size):
        self.w, self.h = self.size = size
        sg.theme('DarkBlue3')
        sg.set_options(font=("Courier New", 16))
        layout = [
            [sg.Image(size=self.size, background_color='black', key='IMAGE')],
            [sg.Push(), sg.Button("BLUR"), sg.Push()]
        ]
        self.window = sg.Window(
            'Title', layout=layout, enable_close_attempted_event=True,
            use_default_focus=False, margins=(0, 0), finalize=True)
        self.window["BLUR"].block_focus()
        self.blur = False
        self.running = True
        threading.Thread(target=self.get_data, daemon=True).start()
        self.start()

    def get_data(self, fg=(255, 255, 255, 255), bg=(0, 0, 0, 255)):
        y = self.h//2
        while self.running:
            self.data = [(i, random.randint(-y, y) + y) for i in range(self.w)]
            im = Image.new("RGBA", self.size, color=bg)
            draw = ImageDraw.Draw(im, mode="RGBA")
            draw.line(self.data, fill=fg, width=1)
            draw.line([(0, y), (self.w, y)], fill=fg, width=1)
            if self.blur:
                im = Image.alpha_composite(im, mask)
            data = self.image_to_data(im)
            self.window.write_event_value("Update", data)
            time.sleep(0.1)             # time delay to reduce the CPU loading

    def image_to_data(self, im):
        with io.BytesIO() as output:
            im.save(output, format="PNG")
            data = output.getvalue()
        return data

    def start(self):
        while True:
            event, values = self.window.read()
            if event == sg.WINDOW_CLOSE_ATTEMPTED_EVENT:
                break
            elif event == 'Update':
                data = values[event]
                self.window['IMAGE'].update(data)
            elif event == 'BLUR':
                self.blur =  not self.blur
        self.running = False
        time.sleep(0.2)                                     # wait thread stop
        self.window.close()

# Create an edge-spur mask

size = w, h = (400, 300)
mask = Image.new("RGBA", size, color=(0, 0, 0, 0))  # Black
width = 100
for x in range(w):
    for y in range(h):
        r, g, b, a = mask.getpixel((x, y))
        alpha1, alpha2 = 0, 0
        if x < width:
            alpha1 = int((width - x)/width * 255)
        elif x > w - width:
            alpha1 = int((x - w + width)/width * 255)
        if y < width:
            alpha2 = int((width - y)/width * 255)
        elif y > h - width:
            alpha2 = int((y - h + width)/width * 255)
        alpha = max(alpha1, alpha2)
        mask.putpixel((x, y), (r, g, g, alpha))

GUI(size)

image
image

THAT'S EXACTLY what I wanted man! You are awesome Jason, thank you very much.
I'll try to apply this one, after be able to know how did you do that hehe, to my graph.
Thank you man.

Should I close this thread, Jason?