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.
https://www.PySimpleGUI.com
Other
13.33k stars 1.84k forks source link

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

Closed Minoslo closed 5 months ago

Minoslo commented 5 months ago

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]

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

https://github.com/PySimpleGUI/PySimpleGUI/assets/157976718/264f0404-5c85-46c4-b303-f9db029924b6


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.

jason990420 commented 5 months ago

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

Minoslo commented 5 months ago

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.

jason990420 commented 5 months ago

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

Minoslo commented 5 months ago

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!

jason990420 commented 5 months ago

just blur the edges.

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

Minoslo commented 5 months ago

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.

jason990420 commented 5 months ago

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

Minoslo commented 5 months ago

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.

Minoslo commented 5 months ago

Should I close this thread, Jason?