├── .gitignore ├── images ├── lenna.jpg ├── test.bmp ├── test.jpg ├── test2.jpg ├── long_dog.jpg └── qt_agg.png ├── pygame_matplotlib ├── __init__.py ├── gui_window.py └── backend_pygame.py ├── examples ├── save_to_file.py ├── show_in_gameloop.py ├── basic.py ├── text.py ├── gui_window.py ├── blitting.py ├── rotation.py ├── blitting_class.py └── show.py ├── tests ├── test_show_in_gameloop.py ├── test_save_to_file.py ├── test_mpl_functionalities.py ├── test_blitting.py ├── test_gui_window.py ├── test_blitting_class.py └── test_show.py ├── pyproject.toml ├── LICENSE ├── .github └── workflows │ ├── python-package.yml │ └── python-publish.yml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | dist 4 | *.egg-info -------------------------------------------------------------------------------- /images/lenna.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionel42/pygame-matplotlib-backend/HEAD/images/lenna.jpg -------------------------------------------------------------------------------- /images/test.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionel42/pygame-matplotlib-backend/HEAD/images/test.bmp -------------------------------------------------------------------------------- /images/test.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionel42/pygame-matplotlib-backend/HEAD/images/test.jpg -------------------------------------------------------------------------------- /images/test2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionel42/pygame-matplotlib-backend/HEAD/images/test2.jpg -------------------------------------------------------------------------------- /images/long_dog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionel42/pygame-matplotlib-backend/HEAD/images/long_dog.jpg -------------------------------------------------------------------------------- /images/qt_agg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lionel42/pygame-matplotlib-backend/HEAD/images/qt_agg.png -------------------------------------------------------------------------------- /pygame_matplotlib/__init__.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | 3 | 4 | def pygame_color_to_plt(color: pygame.Color): 5 | """Convert a pygame Color to a matplot lib value.""" 6 | # Interval from 0 to 1 in matplotlib 7 | return tuple(value / 255.0 for value in [color.r, color.g, color.b, color.a]) 8 | -------------------------------------------------------------------------------- /examples/save_to_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | import matplotlib 3 | 4 | matplotlib.use("pygame") 5 | 6 | import matplotlib.pyplot as plt 7 | 8 | fig = plt.figure() 9 | 10 | print(fig.canvas.get_supported_filetypes()) 11 | 12 | plt.plot([1, 2], [1, 2], color="green") 13 | plt.text(1.5, 1.5, "2", size=50) 14 | plt.xlabel("swag") 15 | 16 | plt.savefig("images" + os.sep + "test.jpg") 17 | plt.savefig("images" + os.sep + "test.bmp") 18 | plt.savefig("images" + os.sep + "test2.jpg") 19 | -------------------------------------------------------------------------------- /tests/test_show_in_gameloop.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | 3 | matplotlib.use("pygame") 4 | import matplotlib.pyplot as plt 5 | 6 | import pygame 7 | 8 | 9 | def test_in_game_loop(): 10 | fig, axes = plt.subplots(1, 1) 11 | axes.plot([1, 2], [1, 2], color="green", label="test") 12 | 13 | fig.canvas.draw() 14 | 15 | screen = pygame.display.set_mode((800, 600)) 16 | screen.blit(fig, (100, 100)) 17 | 18 | show = True 19 | i = 0 20 | while show: 21 | if i > 2: 22 | show = False 23 | pygame.display.update() 24 | i += 1 25 | -------------------------------------------------------------------------------- /tests/test_save_to_file.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import matplotlib 4 | 5 | matplotlib.use("pygame") 6 | 7 | import matplotlib.pyplot as plt 8 | 9 | 10 | def test_save_jpg(): 11 | plt.figure() 12 | plt.plot([1, 2], [1, 2], color="green") 13 | plt.text(1.5, 1.5, "2", size=50) 14 | plt.xlabel("swag") 15 | plt.savefig("images" + os.sep + "test.jpg") 16 | 17 | 18 | def test_save_bmp(): 19 | plt.figure() 20 | plt.plot([1, 2], [1, 2], color="green") 21 | plt.text(1.5, 1.5, "2", size=50) 22 | plt.xlabel("swag") 23 | plt.savefig("images" + os.sep + "test.bmp") 24 | -------------------------------------------------------------------------------- /examples/show_in_gameloop.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | 3 | matplotlib.use("pygame") 4 | # matplotlib.use('Qt4Agg') 5 | 6 | import matplotlib.pyplot as plt 7 | 8 | import pygame 9 | import pygame.display 10 | 11 | fig, axes = plt.subplots(1, 1) 12 | axes.plot([1, 2], [1, 2], color="green", label="test") 13 | 14 | fig.canvas.draw() 15 | 16 | screen = pygame.display.set_mode((800, 600)) 17 | screen.blit(fig, (100, 100)) 18 | 19 | show = True 20 | while show: 21 | for event in pygame.event.get(): 22 | if event.type == pygame.QUIT: 23 | # Stop showing when quit 24 | show = False 25 | pygame.display.update() 26 | -------------------------------------------------------------------------------- /tests/test_mpl_functionalities.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import matplotlib 3 | 4 | matplotlib.use("pygame") 5 | 6 | 7 | def test_save_jpg(): 8 | plt.figure() 9 | plt.plot([1, 2], [1, 2], color="green") 10 | plt.text(1.5, 1.5, "2", size=50) 11 | 12 | 13 | def test_tight_layout(): 14 | plt.figure() 15 | plt.plot([1, 2], [1, 2], color="green") 16 | plt.text(1.5, 1.5, "2", size=50) 17 | plt.tight_layout() 18 | 19 | 20 | def test_ylabel(): 21 | # Issue that the ylabel was not reotated, sadly I cannot visually test this 22 | plt.figure() 23 | plt.plot([1, 2], [1, 2], color="green") 24 | plt.text(1.5, 1.5, "2", size=50) 25 | plt.ylabel("swag") 26 | plt.tight_layout() 27 | -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | """The most basic example of how to use the pygame-matplotlib backend.""" 2 | 3 | # Select pygame backend 4 | import matplotlib 5 | import logging 6 | import matplotlib.pyplot as plt 7 | 8 | logging.basicConfig() 9 | logger = logging.getLogger("pygame_matplotlib") 10 | logger.setLevel(logging.DEBUG) 11 | matplotlib.use("pygame") 12 | 13 | 14 | fig, ax = plt.subplots() # Create a figure containing a single axes. 15 | ax.plot([1, 2, 3, 4], [2, 4, 2, 9]) # Plot some data on the axes. 16 | ax.set_ylabel("some y numbers") # Add a y-label to the axes. 17 | ax.set_xlabel("some x numbers") # Add an x-label to the axes. 18 | ax.set_xticklabels( 19 | # Long rotated labels 20 | ["label1", "label2", "label3", "label4"], 21 | rotation=45, 22 | ) 23 | plt.show() 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "pygame-matplotlib" 7 | version = "VERSION_PLACEHOLDER" 8 | dependencies = [ 9 | "matplotlib", 10 | "pygame_gui", 11 | ] 12 | requires-python = ">= 3.9" 13 | authors = [ 14 | {name = "Lionel42"}, 15 | ] 16 | description = "A matplotlib backend using pygame." 17 | readme = "README.md" 18 | classifiers =[ 19 | "Development Status :: 5 - Production/Stable", 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python :: 3", 23 | ] 24 | keywords = [ 25 | "matplotlib", 26 | "pygame", 27 | "backend", 28 | "plot", 29 | ] 30 | 31 | # Self register the backend in matplotlib 32 | [project.entry-points."matplotlib.backend"] 33 | pygame = "pygame_matplotlib.backend_pygame" 34 | 35 | 36 | [project.urls] 37 | Homepage = "https://github.com/lionel42/pygame-matplotlib-backend" 38 | Repository = "https://github.com/lionel42/pygame-matplotlib-backend" 39 | 40 | 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 lionel42 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.13"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Set static version in pyproject.toml 28 | run: sed -i "s/VERSION_PLACEHOLDER/0.0.1/g" pyproject.toml 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | python -m pip install flake8 pytest ruff black 33 | python -m pip install . 34 | - name: Lint with black 35 | run: | 36 | black --check . 37 | - name: Lint with ruff 38 | run: | 39 | ruff check 40 | - name: Test with pytest 41 | run: | 42 | pytest 43 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | deploy: 20 | 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Set up Python 26 | uses: actions/setup-python@v3 27 | with: 28 | python-version: '3.x' 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install build 33 | - name: Extract tag name 34 | id: tag 35 | run: echo ::set-output name=TAG_NAME::$(echo $GITHUB_REF | cut -d / -f 3) 36 | - name: Update version in pyproject.toml 37 | run: >- 38 | sed -i "s/VERSION_PLACEHOLDER/${{ steps.tag.outputs.TAG_NAME }}/g" pyproject.toml 39 | - name: Build package 40 | run: python -m build 41 | - name: Publish package 42 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 43 | with: 44 | user: __token__ 45 | password: ${{ secrets.PYPI_API_TOKEN }} 46 | -------------------------------------------------------------------------------- /examples/text.py: -------------------------------------------------------------------------------- 1 | """Taken from the matplotlib text example.""" 2 | 3 | import matplotlib.pyplot as plt 4 | import logging 5 | import matplotlib 6 | 7 | # Set up logging 8 | logging.basicConfig() 9 | logger = logging.getLogger("pygame_matplotlib") 10 | logger.setLevel(logging.DEBUG) 11 | 12 | 13 | matplotlib.use("pygame") 14 | 15 | fig = plt.figure() 16 | ax = fig.add_subplot() 17 | fig.subplots_adjust(top=0.85) 18 | 19 | # Set titles for the figure and the subplot respectively 20 | fig.suptitle("bold figure suptitle", fontsize=14, fontweight="bold") 21 | ax.set_title("axes title") 22 | 23 | ax.set_xlabel("xlabel") 24 | ax.set_ylabel("ylabel") 25 | 26 | # Set both x- and y-axis limits to [0, 10] instead of default [0, 1] 27 | ax.axis([0, 10, 0, 10]) 28 | 29 | ax.text( 30 | 3, 31 | 8, 32 | "boxed italics text in data coords", 33 | style="italic", 34 | bbox={"facecolor": "red", "alpha": 0.5, "pad": 10}, 35 | ) 36 | 37 | ax.text(2, 6, r"an equation: $E=mc^2$", fontsize=15) 38 | 39 | ax.text(3, 2, "Unicode: Institut für Festkörperphysik") 40 | 41 | 42 | ax.text(3, 5, "aaa", rotation=90) 43 | ax.text(3, 5, "bbb", rotation=135) 44 | 45 | ax.text( 46 | 0.95, 47 | 0.01, 48 | "colored text in axes coords", 49 | verticalalignment="bottom", 50 | horizontalalignment="right", 51 | transform=ax.transAxes, 52 | color="green", 53 | fontsize=15, 54 | ) 55 | 56 | ax.plot([2], [1], "o") 57 | ax.annotate( 58 | "annotate", 59 | xy=(2, 1), 60 | xytext=(3, 4), 61 | arrowprops=dict(facecolor="black", shrink=0.05), 62 | ) 63 | 64 | plt.show() 65 | -------------------------------------------------------------------------------- /examples/gui_window.py: -------------------------------------------------------------------------------- 1 | """Simple script to test from https://pygame-gui.readthedocs.io/en/latest/quick_start.html""" 2 | 3 | import pygame 4 | import pygame_gui 5 | 6 | import matplotlib.pyplot as plt 7 | 8 | from pygame_matplotlib.gui_window import UIPlotWindow 9 | 10 | 11 | pygame.init() 12 | 13 | pygame.display.set_caption("Test") 14 | window_surface = pygame.display.set_mode((800, 600)) 15 | 16 | background = pygame.Surface((800, 600)) 17 | background.fill(pygame.Color("#000000")) 18 | 19 | manager = pygame_gui.UIManager((800, 600)) 20 | 21 | fig, axes = plt.subplots(1, 1) 22 | axes.plot([1, 2], [1, 2], color="green", label="test") 23 | fig.canvas.draw() 24 | 25 | 26 | fig2, axes2 = plt.subplots(1, 1) 27 | axes2.plot([1, 2], [1, 2], color="blue", label="test") 28 | fig2.canvas.draw() 29 | 30 | 31 | plot_window = UIPlotWindow( 32 | rect=pygame.Rect((350, 275), (300, 200)), 33 | manager=manager, 34 | figuresurface=fig, 35 | resizable=True, 36 | ) 37 | 38 | 39 | plot_window2 = UIPlotWindow( 40 | rect=pygame.Rect((350, 275), (200, 200)), 41 | manager=manager, 42 | figuresurface=fig2, 43 | resizable=False, 44 | ) 45 | 46 | 47 | clock = pygame.time.Clock() 48 | 49 | is_running = True 50 | 51 | while is_running: 52 | time_delta = clock.tick(60) / 1000.0 53 | for event in pygame.event.get(): 54 | if event.type == pygame.QUIT: 55 | is_running = False 56 | 57 | manager.process_events(event) 58 | 59 | manager.update(time_delta) 60 | 61 | # Blitts and draw 62 | window_surface.blit(background, (0, 0)) 63 | manager.draw_ui(window_surface) 64 | 65 | # print(plot_window2.get_container().get_size()) 66 | 67 | pygame.display.update() 68 | -------------------------------------------------------------------------------- /pygame_matplotlib/gui_window.py: -------------------------------------------------------------------------------- 1 | """Contain a window with a plot for pygame_gui.""" 2 | 3 | from typing import Union 4 | 5 | import pygame 6 | from pygame_gui.core.interfaces.manager_interface import IUIManagerInterface 7 | from pygame_gui.core.ui_element import ObjectID 8 | from pygame_gui.elements.ui_window import UIWindow 9 | 10 | from .backend_pygame import FigureSurface 11 | import matplotlib 12 | 13 | matplotlib.use("pygame") 14 | 15 | 16 | class UIPlotWindow(UIWindow): 17 | def __init__( 18 | self, 19 | rect: pygame.Rect, 20 | manager: IUIManagerInterface, 21 | figuresurface: FigureSurface, 22 | window_display_title: str = "", 23 | element_id: Union[str, None] = None, 24 | object_id: Union[ObjectID, str, None] = None, 25 | resizable: bool = False, 26 | visible: int = 1, 27 | ): 28 | self.figuresurf = figuresurface 29 | super().__init__( 30 | rect, 31 | manager, 32 | window_display_title=window_display_title, 33 | element_id=element_id, 34 | object_id=object_id, 35 | resizable=resizable, 36 | visible=visible, 37 | ) 38 | 39 | def set_dimensions(self, *args, **kwargs): 40 | super().set_dimensions(*args, **kwargs) 41 | print("setting dimensions") 42 | # Update the size of the figure with the new bounding rectangle 43 | self.figuresurf.set_bounding_rect(self.get_container().get_rect()) 44 | self.update_window_image() 45 | 46 | def update_window_image(self): 47 | # Update the image of the container 48 | container = self.get_container() 49 | container._set_image(self.figuresurf) 50 | -------------------------------------------------------------------------------- /examples/blitting.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | import matplotlib.pyplot as plt 3 | 4 | matplotlib.use("pygame") 5 | import numpy as np 6 | 7 | x = np.linspace(0, 2 * np.pi, 100) 8 | 9 | fig, ax = plt.subplots() 10 | 11 | # animated=True tells matplotlib to only draw the artist when we 12 | # explicitly request it 13 | (ln,) = ax.plot(x, np.sin(x), animated=True) 14 | 15 | # make sure the window is raised, but the script keeps going 16 | plt.show(block=False) 17 | 18 | # stop to admire our empty window axes and ensure it is rendered at 19 | # least once. 20 | # 21 | # We need to fully draw the figure at its final size on the screen 22 | # before we continue on so that : 23 | # a) we have the correctly sized and drawn background to grab 24 | # b) we have a cached renderer so that ``ax.draw_artist`` works 25 | # so we spin the event loop to let the backend process any pending operations 26 | plt.pause(1) 27 | 28 | # get copy of entire figure (everything inside fig.bbox) sans animated artist 29 | bg = fig.canvas.copy_from_bbox(fig.bbox) 30 | 31 | # draw the animated artist, this uses a cached renderer 32 | ax.draw_artist(ln) 33 | # show the result to the screen, this pushes the updated RGBA buffer from the 34 | # renderer to the GUI framework so you can see it 35 | 36 | for j in range(1000): 37 | # reset the background back in the canvas state, screen unchanged 38 | fig.canvas.restore_region(bg) 39 | # update the artist, neither the canvas state nor the screen have changed 40 | ln.set_ydata(np.sin(x + (j / 100) * np.pi)) 41 | # re-render the artist, updating the canvas state, but not the screen 42 | ax.draw_artist(ln) 43 | # copy the image to the GUI state, but screen might not be changed yet 44 | fig.canvas.blit(fig.bbox) 45 | # flush any pending GUI events, re-painting the screen if needed 46 | fig.canvas.flush_events() 47 | # you can put a pause in if you want to slow things down 48 | plt.pause(0.01) 49 | -------------------------------------------------------------------------------- /tests/test_blitting.py: -------------------------------------------------------------------------------- 1 | import matplotlib 2 | import matplotlib.pyplot as plt 3 | 4 | matplotlib.use("pygame") 5 | import numpy as np 6 | 7 | 8 | def test_blitting(): 9 | x = np.linspace(0, 2 * np.pi, 100) 10 | 11 | fig, ax = plt.subplots() 12 | 13 | # animated=True tells matplotlib to only draw the artist when we 14 | # explicitly request it 15 | (ln,) = ax.plot(x, np.sin(x), animated=True) 16 | 17 | # make sure the window is raised, but the script keeps going 18 | plt.show(block=False) 19 | 20 | # stop to admire our empty window axes and ensure it is rendered at 21 | # least once. 22 | # 23 | # We need to fully draw the figure at its final size on the screen 24 | # before we continue on so that : 25 | # a) we have the correctly sized and drawn background to grab 26 | # b) we have a cached renderer so that ``ax.draw_artist`` works 27 | # so we spin the event loop to let the backend process any pending operations 28 | plt.pause(0.1) 29 | 30 | # get copy of entire figure (everything inside fig.bbox) sans animated artist 31 | bg = fig.canvas.copy_from_bbox(fig.bbox) 32 | 33 | # draw the animated artist, this uses a cached renderer 34 | ax.draw_artist(ln) 35 | # show the result to the screen, this pushes the updated RGBA buffer from the 36 | # renderer to the GUI framework so you can see it 37 | 38 | for j in range(2): 39 | # reset the background back in the canvas state, screen unchanged 40 | fig.canvas.restore_region(bg) 41 | # update the artist, neither the canvas state nor the screen have changed 42 | ln.set_ydata(np.sin(x + (j / 100) * np.pi)) 43 | # re-render the artist, updating the canvas state, but not the screen 44 | ax.draw_artist(ln) 45 | # copy the image to the GUI state, but screen might not be changed yet 46 | fig.canvas.blit(fig.bbox) 47 | # flush any pending GUI events, re-painting the screen if needed 48 | fig.canvas.flush_events() 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pygame Matplotlib Backend 2 | 3 | Create plots in pygame. 4 | 5 | 6 | Note that the library is in an experimental developement stage and not 7 | all features of standard matplotlib backends are implement at the moment. 8 | 9 | ## Installation 10 | ``` 11 | pip install pygame-matplotlib 12 | ``` 13 | 14 | ## Usage 15 | 16 | First you will need to specify that you want to use pygame backend. 17 | ```python 18 | # Select pygame backend 19 | import matplotlib 20 | matplotlib.use('pygame') 21 | ``` 22 | 23 | Then you can use matplotlib as you usually do. 24 | ```python 25 | # Standard matplotlib syntax 26 | import matplotlib.pyplot as plt 27 | fig, ax = plt.subplots() # Create a figure containing a single axes. 28 | ax.plot([1, 2, 3, 4], [1, 4, 2, 3]) # Plot some data on the axes. 29 | plt.show() 30 | ``` 31 | 32 | Or you can include the plot in your game using the fact that a ```Figure``` is 33 | also a ```pygame.Surface``` with this backend. 34 | ```python 35 | import pygame 36 | import pygame.display 37 | 38 | fig, axes = plt.subplots(1, 1,) 39 | axes.plot([1,2], [1,2], color='green', label='test') 40 | 41 | fig.canvas.draw() 42 | 43 | screen = pygame.display.set_mode((800, 600)) 44 | 45 | # Use the fig as a pygame.Surface 46 | screen.blit(fig, (100, 100)) 47 | 48 | show = True 49 | while show: 50 | for event in pygame.event.get(): 51 | if event.type == pygame.QUIT: 52 | # Stop showing when quit 53 | show = False 54 | pygame.display.update() 55 | ``` 56 | 57 | Note that if you want to update the plot during the game, you might 58 | need to call ```fig.canvas.draw()``` and ```screen.blit(fig)``` during 59 | the game loop. 60 | 61 | See examples in test.py or test_show.py 62 | 63 | 64 | ## How it works in the back 65 | 66 | The matplotlib ```Figure``` object is replaced by a ```FigureSurface``` object 67 | which inherits from both ```matplotlib.figure.Figure``` and 68 | ```pygame.Surface```. 69 | 70 | 71 | ## Current implementation 72 | 73 | Support mainly the basic plotting capabilities. 74 | 75 | -------------------------------------------------------------------------------- /tests/test_gui_window.py: -------------------------------------------------------------------------------- 1 | """Simple script to test from https://pygame-gui.readthedocs.io/en/latest/quick_start.html""" 2 | 3 | import pygame 4 | import pygame_gui 5 | 6 | import matplotlib.pyplot as plt 7 | 8 | from pygame_matplotlib.gui_window import UIPlotWindow 9 | 10 | 11 | def test_gui_window(): 12 | pygame.init() 13 | 14 | pygame.display.set_caption("Test") 15 | window_surface = pygame.display.set_mode((800, 600)) 16 | 17 | background = pygame.Surface((800, 600)) 18 | background.fill(pygame.Color("#000000")) 19 | 20 | manager = pygame_gui.UIManager((800, 600)) 21 | 22 | fig, axes = plt.subplots(1, 1) 23 | axes.plot([1, 2], [1, 2], color="green", label="test") 24 | fig.canvas.draw() 25 | 26 | fig2, axes2 = plt.subplots(1, 1) 27 | axes2.plot([1, 2], [1, 2], color="blue", label="test") 28 | fig2.canvas.draw() 29 | 30 | plot_window = UIPlotWindow( 31 | rect=pygame.Rect((350, 275), (300, 200)), 32 | manager=manager, 33 | figuresurface=fig, 34 | resizable=True, 35 | ) 36 | 37 | plot_window2 = UIPlotWindow( 38 | rect=pygame.Rect((350, 275), (200, 200)), 39 | manager=manager, 40 | figuresurface=fig2, 41 | resizable=False, 42 | ) 43 | 44 | assert isinstance(plot_window, UIPlotWindow) 45 | assert isinstance(plot_window2, UIPlotWindow) 46 | 47 | clock = pygame.time.Clock() 48 | 49 | is_running = True 50 | i = 0 51 | 52 | while is_running: 53 | if i > 2: 54 | is_running = False 55 | time_delta = clock.tick(60) / 1000.0 56 | for event in pygame.event.get(): 57 | if event.type == pygame.QUIT: 58 | is_running = False 59 | 60 | manager.process_events(event) 61 | 62 | manager.update(time_delta) 63 | 64 | # Blitts and draw 65 | window_surface.blit(background, (0, 0)) 66 | manager.draw_ui(window_surface) 67 | 68 | # print(plot_window2.get_container().get_size()) 69 | 70 | pygame.display.update() 71 | i += 1 72 | -------------------------------------------------------------------------------- /examples/rotation.py: -------------------------------------------------------------------------------- 1 | """Script taken from matplotlib examples to test rotation modes in text rendering.""" 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | import matplotlib 6 | import logging 7 | 8 | logging.basicConfig() 9 | logger = logging.getLogger("pygame_matplotlib") 10 | # logger.setLevel(logging.DEBUG) 11 | matplotlib.use("pygame") 12 | 13 | 14 | def test_rotation_mode(fig, mode): 15 | ha_list = ["left", "center", "right"] 16 | va_list = ["top", "center", "baseline", "bottom"] 17 | axs = fig.subplots( 18 | len(va_list), 19 | len(ha_list), 20 | sharex=True, 21 | sharey=True, 22 | subplot_kw=dict(aspect=1), 23 | gridspec_kw=dict(hspace=0, wspace=0), 24 | ) 25 | 26 | # labels and title 27 | for ha, ax in zip(ha_list, axs[-1, :]): 28 | ax.set_xlabel(ha) 29 | for va, ax in zip(va_list, axs[:, 0]): 30 | ax.set_ylabel(va) 31 | axs[0, 1].set_title(f"rotation_mode='{mode}'", size="large") 32 | 33 | kw = ( 34 | {} 35 | if mode == "default" 36 | else {"bbox": dict(boxstyle="square,pad=0.", ec="none", fc="C1", alpha=0.3)} 37 | ) 38 | 39 | texts = {} 40 | 41 | # use a different text alignment in each Axes 42 | for i, va in enumerate(va_list): 43 | for j, ha in enumerate(ha_list): 44 | ax = axs[i, j] 45 | # prepare Axes layout 46 | ax.set(xticks=[], yticks=[]) 47 | ax.axvline(0.5, color="skyblue", zorder=0) 48 | ax.axhline(0.5, color="skyblue", zorder=0) 49 | ax.plot(0.5, 0.5, color="C0", marker="o", zorder=1) 50 | # add text with rotation and alignment settings 51 | tx = ax.text( 52 | 0.5, 53 | 0.5, 54 | "Tpg", 55 | size="x-large", 56 | rotation=90, 57 | horizontalalignment=ha, 58 | verticalalignment=va, 59 | rotation_mode=mode, 60 | **kw, 61 | ) 62 | texts[ax] = tx 63 | 64 | if mode == "default": 65 | # highlight bbox 66 | fig.canvas.draw() 67 | for ax, text in texts.items(): 68 | bb = text.get_window_extent().transformed(ax.transData.inverted()) 69 | rect = plt.Rectangle( 70 | (bb.x0, bb.y0), bb.width, bb.height, facecolor="C1", alpha=0.3, zorder=2 71 | ) 72 | ax.add_patch(rect) 73 | 74 | 75 | fig = plt.figure(figsize=(8, 5)) 76 | subfigs = fig.subfigures(1, 2) 77 | test_rotation_mode(subfigs[0], "default") 78 | test_rotation_mode(subfigs[1], "anchor") 79 | plt.show() 80 | -------------------------------------------------------------------------------- /examples/blitting_class.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | import matplotlib 4 | 5 | matplotlib.use("pygame") 6 | 7 | 8 | class BlitManager: 9 | def __init__(self, canvas, animated_artists=()): 10 | """ 11 | Parameters 12 | ---------- 13 | canvas : FigureCanvasAgg 14 | The canvas to work with, this only works for sub-classes of the Agg 15 | canvas which have the `~FigureCanvasAgg.copy_from_bbox` and 16 | `~FigureCanvasAgg.restore_region` methods. 17 | 18 | animated_artists : Iterable[Artist] 19 | List of the artists to manage 20 | """ 21 | self.canvas = canvas 22 | self._bg = None 23 | self._artists = [] 24 | 25 | for a in animated_artists: 26 | self.add_artist(a) 27 | # grab the background on every draw 28 | self.cid = canvas.mpl_connect("draw_event", self.on_draw) 29 | 30 | def on_draw(self, event): 31 | """Callback to register with 'draw_event'.""" 32 | cv = self.canvas 33 | if event is not None: 34 | if event.canvas != cv: 35 | raise RuntimeError 36 | self._bg = cv.copy_from_bbox(cv.figure.bbox) 37 | self._draw_animated() 38 | 39 | def add_artist(self, art): 40 | """ 41 | Add an artist to be managed. 42 | 43 | Parameters 44 | ---------- 45 | art : Artist 46 | 47 | The artist to be added. Will be set to 'animated' (just 48 | to be safe). *art* must be in the figure associated with 49 | the canvas this class is managing. 50 | 51 | """ 52 | if art.figure != self.canvas.figure: 53 | raise RuntimeError 54 | art.set_animated(True) 55 | self._artists.append(art) 56 | 57 | def _draw_animated(self): 58 | """Draw all of the animated artists.""" 59 | fig = self.canvas.figure 60 | for a in self._artists: 61 | fig.draw_artist(a) 62 | 63 | def update(self): 64 | """Update the screen with animated artists.""" 65 | cv = self.canvas 66 | fig = cv.figure 67 | # paranoia in case we missed the draw event, 68 | if self._bg is None: 69 | self.on_draw(None) 70 | else: 71 | # restore the background 72 | cv.restore_region(self._bg) 73 | # draw all of the animated artists 74 | self._draw_animated() 75 | # update the GUI state 76 | cv.blit(fig.bbox) 77 | # let the GUI event loop process anything it has to do 78 | cv.flush_events() 79 | 80 | 81 | x = np.linspace(0, 2 * np.pi, 100) 82 | # make a new figure 83 | fig, ax = plt.subplots() 84 | # add a line 85 | (ln,) = ax.plot(x, np.sin(x), animated=True) 86 | # add a frame number 87 | fr_number = ax.annotate( 88 | "0", 89 | (0, 1), 90 | xycoords="axes fraction", 91 | xytext=(10, -10), 92 | textcoords="offset points", 93 | ha="left", 94 | va="top", 95 | animated=True, 96 | ) 97 | bm = BlitManager(fig.canvas, [ln, fr_number]) 98 | # make sure our window is on the screen and drawn 99 | plt.show(block=False) 100 | plt.pause(1) 101 | 102 | for j in range(1000): 103 | # update the artists 104 | ln.set_ydata(np.sin(x + (j / 100) * np.pi)) 105 | fr_number.set_text("frame: {j}".format(j=j)) 106 | # tell the blitting manager to do its thing 107 | bm.update() 108 | # plt.pause(1) 109 | -------------------------------------------------------------------------------- /tests/test_blitting_class.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | import matplotlib 4 | 5 | matplotlib.use("pygame") 6 | 7 | 8 | class BlitManager: 9 | def __init__(self, canvas, animated_artists=()): 10 | """ 11 | Parameters 12 | ---------- 13 | canvas : FigureCanvasAgg 14 | The canvas to work with, this only works for sub-classes of the Agg 15 | canvas which have the `~FigureCanvasAgg.copy_from_bbox` and 16 | `~FigureCanvasAgg.restore_region` methods. 17 | 18 | animated_artists : Iterable[Artist] 19 | List of the artists to manage 20 | """ 21 | self.canvas = canvas 22 | self._bg = None 23 | self._artists = [] 24 | 25 | for a in animated_artists: 26 | self.add_artist(a) 27 | # grab the background on every draw 28 | self.cid = canvas.mpl_connect("draw_event", self.on_draw) 29 | 30 | def on_draw(self, event): 31 | """Callback to register with 'draw_event'.""" 32 | cv = self.canvas 33 | if event is not None: 34 | if event.canvas != cv: 35 | raise RuntimeError 36 | self._bg = cv.copy_from_bbox(cv.figure.bbox) 37 | self._draw_animated() 38 | 39 | def add_artist(self, art): 40 | """ 41 | Add an artist to be managed. 42 | 43 | Parameters 44 | ---------- 45 | art : Artist 46 | 47 | The artist to be added. Will be set to 'animated' (just 48 | to be safe). *art* must be in the figure associated with 49 | the canvas this class is managing. 50 | 51 | """ 52 | if art.figure != self.canvas.figure: 53 | raise RuntimeError 54 | art.set_animated(True) 55 | self._artists.append(art) 56 | 57 | def _draw_animated(self): 58 | """Draw all of the animated artists.""" 59 | fig = self.canvas.figure 60 | for a in self._artists: 61 | fig.draw_artist(a) 62 | 63 | def update(self): 64 | """Update the screen with animated artists.""" 65 | cv = self.canvas 66 | fig = cv.figure 67 | # paranoia in case we missed the draw event, 68 | if self._bg is None: 69 | self.on_draw(None) 70 | else: 71 | # restore the background 72 | cv.restore_region(self._bg) 73 | # draw all of the animated artists 74 | self._draw_animated() 75 | # update the GUI state 76 | cv.blit(fig.bbox) 77 | # let the GUI event loop process anything it has to do 78 | cv.flush_events() 79 | 80 | 81 | def test_blitting_class(): 82 | """ 83 | Test the BlitManager class by creating a simple animated plot. 84 | """ 85 | # create some data 86 | x = np.linspace(0, 2 * np.pi, 100) 87 | # make a new figure 88 | fig, ax = plt.subplots() 89 | # add a line 90 | (ln,) = ax.plot(x, np.sin(x), animated=True) 91 | # add a frame number 92 | fr_number = ax.annotate( 93 | "0", 94 | (0, 1), 95 | xycoords="axes fraction", 96 | xytext=(10, -10), 97 | textcoords="offset points", 98 | ha="left", 99 | va="top", 100 | animated=True, 101 | ) 102 | bm = BlitManager(fig.canvas, [ln, fr_number]) 103 | # make sure our window is on the screen and drawn 104 | plt.show(block=False) 105 | plt.pause(0.1) 106 | 107 | for j in range(3): 108 | # update the artists 109 | ln.set_ydata(np.sin(x + (j / 100) * np.pi)) 110 | fr_number.set_text("frame: {j}".format(j=j)) 111 | # tell the blitting manager to do its thing 112 | bm.update() 113 | # plt.pause(1) 114 | plt.close(fig) # Close the figure after testing 115 | -------------------------------------------------------------------------------- /examples/show.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | import matplotlib 5 | 6 | import matplotlib.pyplot as plt 7 | import matplotlib.image as mpimg 8 | 9 | 10 | matplotlib.use("pygame") 11 | 12 | 13 | def plot_error_bars_ex(ax): 14 | # example data 15 | x = np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]) 16 | y = np.exp(-x) 17 | xerr = 0.1 18 | yerr = 0.2 19 | 20 | # lower & upper limits of the error 21 | lolims = np.array([0, 0, 1, 0, 1, 0, 0, 0, 1, 0], dtype=bool) 22 | uplims = np.array([0, 1, 0, 0, 0, 1, 0, 0, 0, 1], dtype=bool) 23 | ls = "dotted" 24 | 25 | # standard error bars 26 | ax.errorbar(x, y, xerr=xerr, yerr=yerr, linestyle=ls) 27 | 28 | # including upper limits 29 | ax.errorbar(x, y + 0.5, xerr=xerr, yerr=yerr, uplims=uplims, linestyle=ls) 30 | 31 | # including lower limits 32 | ax.errorbar(x, y + 1.0, xerr=xerr, yerr=yerr, lolims=lolims, linestyle=ls) 33 | 34 | # including upper and lower limits 35 | ax.errorbar( 36 | x, 37 | y + 1.5, 38 | xerr=xerr, 39 | yerr=yerr, 40 | lolims=lolims, 41 | uplims=uplims, 42 | marker="o", 43 | markersize=8, 44 | linestyle=ls, 45 | ) 46 | 47 | # Plot a series with lower and upper limits in both x & y 48 | # constant x-error with varying y-error 49 | xerr = 0.2 50 | yerr = np.full_like(x, 0.2) 51 | yerr[[3, 6]] = 0.3 52 | 53 | # mock up some limits by modifying previous data 54 | xlolims = lolims 55 | xuplims = uplims 56 | lolims = np.zeros_like(x) 57 | uplims = np.zeros_like(x) 58 | lolims[[6]] = True # only limited at this index 59 | uplims[[3]] = True # only limited at this index 60 | 61 | # do the plotting 62 | ax.errorbar( 63 | x, 64 | y + 2.1, 65 | xerr=xerr, 66 | yerr=yerr, 67 | xlolims=xlolims, 68 | xuplims=xuplims, 69 | uplims=uplims, 70 | lolims=lolims, 71 | marker="o", 72 | markersize=8, 73 | linestyle="none", 74 | ) 75 | 76 | # tidy up the figure 77 | ax.set_xlim((0, 5.5)) 78 | ax.set_title("Errorbar upper and lower limits") 79 | 80 | 81 | def plot_violin(ax): 82 | def adjacent_values(vals, q1, q3): 83 | upper_adjacent_value = q3 + (q3 - q1) * 1.5 84 | upper_adjacent_value = np.clip(upper_adjacent_value, q3, vals[-1]) 85 | 86 | lower_adjacent_value = q1 - (q3 - q1) * 1.5 87 | lower_adjacent_value = np.clip(lower_adjacent_value, vals[0], q1) 88 | return lower_adjacent_value, upper_adjacent_value 89 | 90 | def set_axis_style(ax, labels): 91 | ax.xaxis.set_tick_params(direction="out") 92 | ax.xaxis.set_ticks_position("bottom") 93 | ax.set_xticks(np.arange(1, len(labels) + 1)) 94 | ax.set_xticklabels(labels) 95 | ax.set_xlim(0.25, len(labels) + 0.75) 96 | ax.set_xlabel("Sample name") 97 | 98 | # create test data 99 | np.random.seed(19680801) 100 | data = [sorted(np.random.normal(0, std, 100)) for std in range(1, 5)] 101 | 102 | ax.set_title("Customized violin plot") 103 | parts = ax.violinplot(data, showmeans=False, showmedians=False, showextrema=False) 104 | 105 | for pc in parts["bodies"]: 106 | pc.set_facecolor("#D43F3A") 107 | pc.set_edgecolor("black") 108 | pc.set_alpha(1) 109 | 110 | quartile1, medians, quartile3 = np.percentile(data, [25, 50, 75], axis=1) 111 | whiskers = np.array( 112 | [ 113 | adjacent_values(sorted_array, q1, q3) 114 | for sorted_array, q1, q3 in zip(data, quartile1, quartile3) 115 | ] 116 | ) 117 | whiskers_min, whiskers_max = whiskers[:, 0], whiskers[:, 1] 118 | 119 | inds = np.arange(1, len(medians) + 1) 120 | ax.scatter(inds, medians, marker="o", color="white", s=30, zorder=3) 121 | ax.vlines(inds, quartile1, quartile3, color="k", linestyle="-", lw=5) 122 | ax.vlines(inds, whiskers_min, whiskers_max, color="k", linestyle="-", lw=1) 123 | 124 | # set style for the axes 125 | labels = ["A", "B", "C", "D"] 126 | set_axis_style(ax, labels) 127 | 128 | 129 | fig, axes = plt.subplots(3, 2, figsize=(16, 12)) 130 | print(type(fig)) 131 | 132 | axes[0, 0].plot([1, 2], [1, 2], color="green", label="test") 133 | axes[0, 0].plot([1, 2], [1, 1], color="orange", lw=5, label="test other larger") 134 | # axes[0, 0].legend() 135 | axes[0, 1].text(0.5, 0.5, "2", size=50) 136 | axes[1, 0].set_xlabel("swag") 137 | axes[1, 0].fill_between([0, 1, 2], [1, 2, 3], [3, 4, 5]) 138 | axes[1, 0].scatter([0, 1, 2], [2, 3, 4], s=50) 139 | axes[1, 1].imshow(mpimg.imread("images" + os.sep + "long_dog.jpg")) 140 | plot_error_bars_ex(axes[2, 1]) 141 | 142 | plot_violin(axes[2, 0]) 143 | 144 | plt.show() 145 | -------------------------------------------------------------------------------- /tests/test_show.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | import matplotlib 5 | 6 | # matplotlib.use('pygame') 7 | matplotlib.use("pygame") 8 | 9 | import matplotlib.pyplot as plt 10 | 11 | import matplotlib.image as mpimg 12 | 13 | 14 | def plot_error_bars_ex(ax): 15 | # example data 16 | x = np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]) 17 | y = np.exp(-x) 18 | xerr = 0.1 19 | yerr = 0.2 20 | 21 | # lower & upper limits of the error 22 | lolims = np.array([0, 0, 1, 0, 1, 0, 0, 0, 1, 0], dtype=bool) 23 | uplims = np.array([0, 1, 0, 0, 0, 1, 0, 0, 0, 1], dtype=bool) 24 | ls = "dotted" 25 | 26 | # standard error bars 27 | ax.errorbar(x, y, xerr=xerr, yerr=yerr, linestyle=ls) 28 | 29 | # including upper limits 30 | ax.errorbar(x, y + 0.5, xerr=xerr, yerr=yerr, uplims=uplims, linestyle=ls) 31 | 32 | # including lower limits 33 | ax.errorbar(x, y + 1.0, xerr=xerr, yerr=yerr, lolims=lolims, linestyle=ls) 34 | 35 | # including upper and lower limits 36 | ax.errorbar( 37 | x, 38 | y + 1.5, 39 | xerr=xerr, 40 | yerr=yerr, 41 | lolims=lolims, 42 | uplims=uplims, 43 | marker="o", 44 | markersize=8, 45 | linestyle=ls, 46 | ) 47 | 48 | # Plot a series with lower and upper limits in both x & y 49 | # constant x-error with varying y-error 50 | xerr = 0.2 51 | yerr = np.full_like(x, 0.2) 52 | yerr[[3, 6]] = 0.3 53 | 54 | # mock up some limits by modifying previous data 55 | xlolims = lolims 56 | xuplims = uplims 57 | lolims = np.zeros_like(x) 58 | uplims = np.zeros_like(x) 59 | lolims[[6]] = True # only limited at this index 60 | uplims[[3]] = True # only limited at this index 61 | 62 | # do the plotting 63 | ax.errorbar( 64 | x, 65 | y + 2.1, 66 | xerr=xerr, 67 | yerr=yerr, 68 | xlolims=xlolims, 69 | xuplims=xuplims, 70 | uplims=uplims, 71 | lolims=lolims, 72 | marker="o", 73 | markersize=8, 74 | linestyle="none", 75 | ) 76 | 77 | # tidy up the figure 78 | ax.set_xlim((0, 5.5)) 79 | ax.set_title("Errorbar upper and lower limits") 80 | 81 | 82 | def plot_violin(ax): 83 | def adjacent_values(vals, q1, q3): 84 | upper_adjacent_value = q3 + (q3 - q1) * 1.5 85 | upper_adjacent_value = np.clip(upper_adjacent_value, q3, vals[-1]) 86 | 87 | lower_adjacent_value = q1 - (q3 - q1) * 1.5 88 | lower_adjacent_value = np.clip(lower_adjacent_value, vals[0], q1) 89 | return lower_adjacent_value, upper_adjacent_value 90 | 91 | def set_axis_style(ax, labels): 92 | ax.xaxis.set_tick_params(direction="out") 93 | ax.xaxis.set_ticks_position("bottom") 94 | ax.set_xticks(np.arange(1, len(labels) + 1)) 95 | ax.set_xticklabels(labels) 96 | ax.set_xlim(0.25, len(labels) + 0.75) 97 | ax.set_xlabel("Sample name") 98 | 99 | # create test data 100 | np.random.seed(19680801) 101 | data = [sorted(np.random.normal(0, std, 100)) for std in range(1, 5)] 102 | 103 | ax.set_title("Customized violin plot") 104 | parts = ax.violinplot(data, showmeans=False, showmedians=False, showextrema=False) 105 | 106 | for pc in parts["bodies"]: 107 | pc.set_facecolor("#D43F3A") 108 | pc.set_edgecolor("black") 109 | pc.set_alpha(1) 110 | 111 | quartile1, medians, quartile3 = np.percentile(data, [25, 50, 75], axis=1) 112 | whiskers = np.array( 113 | [ 114 | adjacent_values(sorted_array, q1, q3) 115 | for sorted_array, q1, q3 in zip(data, quartile1, quartile3) 116 | ] 117 | ) 118 | whiskers_min, whiskers_max = whiskers[:, 0], whiskers[:, 1] 119 | 120 | inds = np.arange(1, len(medians) + 1) 121 | ax.scatter(inds, medians, marker="o", color="white", s=30, zorder=3) 122 | ax.vlines(inds, quartile1, quartile3, color="k", linestyle="-", lw=5) 123 | ax.vlines(inds, whiskers_min, whiskers_max, color="k", linestyle="-", lw=1) 124 | 125 | # set style for the axes 126 | labels = ["A", "B", "C", "D"] 127 | set_axis_style(ax, labels) 128 | 129 | 130 | def test_multiplot(): 131 | fig, axes = plt.subplots(3, 2, figsize=(16, 12)) 132 | 133 | axes[0, 0].plot([1, 2], [1, 2], color="green", label="test") 134 | axes[0, 0].plot([1, 2], [1, 1], color="orange", lw=5, label="test other larger") 135 | # axes[0, 0].legend() 136 | axes[0, 1].text(0.5, 0.5, "2", size=50) 137 | axes[1, 0].set_xlabel("swag") 138 | axes[1, 0].fill_between([0, 1, 2], [1, 2, 3], [3, 4, 5]) 139 | axes[1, 0].scatter([0, 1, 2], [2, 3, 4], s=50) 140 | axes[1, 1].imshow(mpimg.imread("images" + os.sep + "long_dog.jpg")) 141 | plot_error_bars_ex(axes[2, 1]) 142 | 143 | plot_violin(axes[2, 0]) 144 | 145 | plt.show(block=False) 146 | plt.close(fig) 147 | -------------------------------------------------------------------------------- /pygame_matplotlib/backend_pygame.py: -------------------------------------------------------------------------------- 1 | """Pygame Matplotlib Backend. 2 | 3 | A functional backend for using matplotlib in pygame display. 4 | You can select it as a backend using 5 | with :: 6 | 7 | import matplotlib 8 | matplotlib.use("pygame") 9 | """ 10 | 11 | from __future__ import annotations 12 | 13 | import logging 14 | from typing import List 15 | from matplotlib.artist import Artist 16 | from matplotlib.transforms import IdentityTransform 17 | import numpy as np 18 | from matplotlib.transforms import Affine2D 19 | import pygame 20 | import pygame.image 21 | import pygame.freetype 22 | from pygame import gfxdraw 23 | from matplotlib._pylab_helpers import Gcf 24 | from matplotlib.backend_bases import ( 25 | FigureCanvasBase, 26 | FigureManagerBase, 27 | GraphicsContextBase, 28 | RendererBase, 29 | ) 30 | from matplotlib.figure import Figure 31 | from matplotlib.path import Path 32 | from matplotlib.transforms import Bbox 33 | 34 | 35 | logger = logging.getLogger(__name__) 36 | 37 | 38 | class FigureSurface(pygame.Surface, Figure): 39 | """Hybrid object mixing pygame.Surface and matplotlib.Figure. 40 | 41 | The functionality of both objects is kept with some special features 42 | that help handling the figures in pygame. 43 | """ 44 | 45 | canvas: FigureCanvasPygame 46 | animated_artists: List[Artist] 47 | 48 | def __init__(self, *args, **kwargs): 49 | """Create a FigureSurface object. 50 | 51 | Signature is the same as matplotlib Figure object. 52 | """ 53 | Figure.__init__(self, *args, **kwargs) 54 | pygame.Surface.__init__(self, self.bbox.size) 55 | # self.fill("white") 56 | 57 | def set_bounding_rect(self, rect: pygame.Rect): 58 | """Set a bounding rectangle around the figure.""" 59 | # First convert inches to pixels for matplotlib 60 | DPI = self.get_dpi() 61 | self.set_size_inches(rect.width / float(DPI), rect.height / float(DPI)) 62 | # Initialize a new surface for the plot 63 | pygame.Surface.__init__(self, (rect.width, rect.height)) 64 | # Redraw the figure 65 | self.canvas.draw() 66 | 67 | def draw(self, renderer): 68 | return super().draw(renderer) 69 | 70 | def set_alpha(self): 71 | pass 72 | 73 | def set_at(self): 74 | pass 75 | 76 | def set_clip(self): 77 | pass 78 | 79 | def set_colorkey(self): 80 | pass 81 | 82 | def set_masks(self): 83 | pass 84 | 85 | def set_palette(self): 86 | pass 87 | 88 | def set_palette_at(self): 89 | pass 90 | 91 | def set_shifts(self): 92 | pass 93 | 94 | def get_interactive_artists(self, renderer): 95 | """Get the interactive artists. 96 | 97 | Custom method used for the blitting. 98 | Modification of _get_draw_artists 99 | Also runs apply_aspect. 100 | """ 101 | artists = self.get_children() 102 | 103 | for sfig in self.subfigs: 104 | artists.remove(sfig) 105 | childa = sfig.get_children() 106 | for child in childa: 107 | if child in artists: 108 | artists.remove(child) 109 | 110 | artists.remove(self.patch) 111 | artists = sorted( 112 | (artist for artist in artists if artist.get_animated()), 113 | key=lambda artist: artist.get_zorder(), 114 | ) 115 | 116 | for ax in self._localaxes.as_list(): 117 | locator = ax.get_axes_locator() 118 | 119 | if locator: 120 | pos = locator(ax, renderer) 121 | ax.apply_aspect(pos) 122 | else: 123 | ax.apply_aspect() 124 | 125 | for child in ax.get_children(): 126 | if child.get_animated(): 127 | artists.append(child) 128 | if hasattr(child, "apply_aspect"): 129 | locator = child.get_axes_locator() 130 | if locator: 131 | pos = locator(child, renderer) 132 | child.apply_aspect(pos) 133 | else: 134 | child.apply_aspect() 135 | 136 | return artists 137 | 138 | 139 | class RendererPygame(RendererBase): 140 | """The renderer handles drawing/rendering operations. 141 | 142 | The draw methods convert maptplotlib into pygame.draw . 143 | """ 144 | 145 | surface: pygame.Surface 146 | 147 | def __init__(self, dpi): 148 | super().__init__() 149 | self.dpi = dpi 150 | 151 | def rect_from_bbox(self, bbox: Bbox) -> pygame.Rect: 152 | """Convert a matplotlib bbox to the equivalent pygame Rect.""" 153 | raise NotImplementedError() 154 | 155 | def draw_path(self, gc, path, transform, rgbFace=None): 156 | """Draw a path using pygame functions.""" 157 | if rgbFace is not None: 158 | color = tuple([int(val * 255) for i, val in enumerate(rgbFace) if i < 3]) 159 | else: 160 | color = tuple( 161 | [int(val * 255) for i, val in enumerate(gc.get_rgb()) if i < 3] 162 | ) 163 | 164 | linewidth = int(gc.get_linewidth()) 165 | 166 | transfrom_to_pygame_axis = Affine2D() 167 | transfrom_to_pygame_axis.set_matrix( 168 | [[1, 0, 0], [0, -1, self.surface.get_height()], [0, 0, 1]] 169 | ) 170 | if not isinstance(transform, IdentityTransform): 171 | transform += transfrom_to_pygame_axis 172 | 173 | draw_func = ( 174 | # Select whether antialiased will be used in pygame 175 | # Antialiased cannot hanlde linewidth > 1 176 | pygame.draw.aaline 177 | if gc.get_antialiased() and linewidth <= 1 178 | else lambda *args: pygame.draw.line(*args, width=linewidth) 179 | ) 180 | 181 | previous_point = (0, 0) 182 | poly_points = [] 183 | 184 | for point, code in path.iter_segments(transform): 185 | logger.debug(f"{point=}, {code=}") 186 | if code == Path.LINETO: 187 | draw_func(self.surface, color, previous_point, point) 188 | previous_point = point 189 | poly_points.append(point) 190 | elif code == Path.CURVE3 or code == Path.CURVE4: 191 | end_point = point[2:] 192 | points_curve = np.concatenate((previous_point, point)).reshape((-1, 2)) 193 | gfxdraw.bezier(self.surface, points_curve, len(points_curve), color) 194 | previous_point = end_point 195 | poly_points.append(end_point) 196 | elif code == Path.CLOSEPOLY: 197 | if len(poly_points) > 2: 198 | gfxdraw.filled_polygon(self.surface, poly_points, color) 199 | elif code == Path.MOVETO: 200 | poly_points.append(point) 201 | previous_point = point 202 | else: # STOP 203 | previous_point = point 204 | 205 | # draw_markers is optional, and we get more correct relative 206 | # timings by leaving it out. backend implementers concerned with 207 | # performance will probably want to implement it 208 | # def draw_markers(self, gc, marker_path, marker_trans, path, trans, 209 | # rgbFace=None): 210 | # pass 211 | 212 | # draw_path_collection is optional, and we get more correct 213 | # relative timings by leaving it out. backend implementers concerned with 214 | # performance will probably want to implement it 215 | # def draw_path_collection(self, gc, master_transform, paths, 216 | # all_transforms, offsets, offsetTrans, 217 | # facecolors, edgecolors, linewidths, linestyles, 218 | # antialiaseds): 219 | # pass 220 | 221 | # draw_quad_mesh is optional, and we get more correct 222 | # relative timings by leaving it out. backend implementers concerned with 223 | # performance will probably want to implement it 224 | # def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, 225 | # coordinates, offsets, offsetTrans, facecolors, 226 | # antialiased, edgecolors): 227 | # pass 228 | 229 | def draw_image(self, gc, x, y, im): 230 | img_surf = pygame.image.frombuffer( 231 | # Need to flip the image as pygame starts top left 232 | np.ascontiguousarray(np.flip(im, axis=0)), 233 | (im.shape[1], im.shape[0]), 234 | "RGBA", 235 | ) 236 | self.surface.blit( 237 | img_surf, 238 | # Image starts top left 239 | (x, self.surface.get_height() - y - im.shape[0]), 240 | ) 241 | 242 | def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): 243 | logger.debug( 244 | f"Drawing text: {s=} at ({x=}, {y=}) with {angle=} and ismath={ismath} " 245 | f"{mtext=} {prop=} {gc=}" 246 | ) 247 | 248 | # make sure font module is initialized 249 | if not pygame.freetype.get_init(): 250 | pygame.freetype.init() 251 | 252 | # prop is the font properties 253 | # Size must be adjusted 254 | font_size = prop.get_size() * 1.42 255 | font_file = prop.get_file() 256 | logger.debug(f"Font file: {font_file}, size: {font_size}") 257 | 258 | # Set bold 259 | # 'light', 'normal', 'regular', 'book', 260 | #' medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 261 | # 'heavy', 'extra bold', 'black' 262 | font_weight = prop.get_weight() 263 | if isinstance(font_weight, int): 264 | # If the weight is an int, we assume it is a font weight 265 | # in the range 0-1000 266 | bold = font_weight >= 600 267 | else: 268 | bold = font_weight in ["bold", "heavy", "black", "extra bold"] 269 | logger.debug(f"Font weight: {font_weight}, bold: {bold}") 270 | 271 | # Set italic 272 | # style can be 'normal', 'italic' or 'oblique' 273 | font_style = prop.get_style() 274 | italic = font_style in ["italic", "oblique"] 275 | 276 | if font_file is None: 277 | # Use default matplotlib font 278 | font_file = "DejaVuSans" 279 | 280 | pgfont = pygame.freetype.SysFont( 281 | font_file, int(font_size), bold=bold, italic=italic 282 | ) 283 | 284 | logger.debug(f"Font: {pgfont}") 285 | 286 | # apply it to text on a label 287 | fg_color = [val * 255 for val in gc.get_rgb()] 288 | 289 | # Use freetype to render the text 290 | font_surface, rotated_rect = pgfont.render( 291 | s, 292 | fg_color, 293 | size=font_size, 294 | rotation=int(angle), 295 | ) 296 | no_rotation_rect = pgfont.get_rect(s, size=font_size) 297 | 298 | # Get the expected size of the font 299 | # width, height = (pgfont.get_rect(s).size if USE_FREETYPE else pgfont.size(s)) 300 | width, height = rotated_rect.size 301 | 302 | # Tuple for the position of the font 303 | font_surf_position = (x, self.surface.get_height() - y) 304 | if mtext is not None: 305 | # Use the alignement from mtext or default 306 | h_alignment = mtext.get_horizontalalignment() 307 | v_alignment = mtext.get_verticalalignment() 308 | rotation_mode = mtext.get_rotation_mode() 309 | else: 310 | h_alignment = "center" 311 | v_alignment = "center" 312 | 313 | logger.debug( 314 | f"{h_alignment=}, {v_alignment=}, {rotation_mode=}, {width=}, {height=}" 315 | ) 316 | # Use the alignement to know where the font should go 317 | if rotation_mode == "anchor": 318 | # Anchor the text to the position 319 | sin_a = np.sin(np.radians(angle)) 320 | cos_a = np.cos(np.radians(angle)) 321 | 322 | sub_width, sub_height = no_rotation_rect.size 323 | 324 | if h_alignment == "left": 325 | if v_alignment == "top": 326 | h_offset = 0 327 | v_offset = sub_width * sin_a 328 | elif v_alignment == "center": 329 | h_offset = sub_height * sin_a / 2 330 | v_offset = height - sub_height * cos_a / 2 331 | else: # "bottom" or "baseline" 332 | h_offset = sub_height * sin_a 333 | v_offset = height 334 | elif h_alignment == "center": 335 | if v_alignment == "top": 336 | h_offset = sub_width / 2 * cos_a 337 | v_offset = sub_width / 2 * sin_a 338 | elif v_alignment == "center": 339 | h_offset = width / 2 340 | v_offset = height / 2 341 | else: 342 | h_offset = width - (sub_width / 2 * cos_a) 343 | v_offset = height - sub_width / 2 * sin_a 344 | elif h_alignment == "right": 345 | if v_alignment == "top": 346 | h_offset = width - sub_height * sin_a 347 | v_offset = 0 348 | elif v_alignment == "center": 349 | h_offset = width - (sub_height / 2 * sin_a) 350 | v_offset = sub_height / 2 * cos_a 351 | else: 352 | h_offset = width 353 | v_offset = cos_a * sub_height 354 | else: 355 | raise ValueError(f"Unknown {h_alignment=}") 356 | h_offset, v_offset = -h_offset, -v_offset 357 | 358 | else: 359 | # The text box is aligned to the position 360 | 361 | if h_alignment == "left": 362 | h_offset = 0 363 | elif h_alignment == "center": 364 | h_offset = -width / 2 365 | elif h_alignment == "right": 366 | h_offset = -width 367 | else: 368 | h_offset = 0 369 | 370 | if v_alignment == "top": 371 | v_offset = 0 372 | elif v_alignment == "center" or v_alignment == "center_baseline": 373 | v_offset = -height / 2 374 | elif v_alignment == "bottom" or v_alignment == "baseline": 375 | v_offset = -height 376 | else: 377 | v_offset = 0 378 | 379 | # Tuple for the position of the font 380 | font_surf_position = ( 381 | x + h_offset, 382 | self.surface.get_height() - y + v_offset, 383 | ) 384 | self.surface.blit(font_surface, font_surf_position) 385 | 386 | def flipy(self): 387 | # docstring inherited 388 | return False 389 | 390 | def get_canvas_width_height(self): 391 | # docstring inherited 392 | return 100, 100 393 | 394 | def get_text_width_height_descent(self, s, prop, ismath): 395 | return 1, 1, 1 396 | 397 | def new_gc(self): 398 | # docstring inherited 399 | return GraphicsContextPygame() 400 | 401 | def points_to_pixels(self, points): 402 | # points are pixels in pygame 403 | return points 404 | # elif backend assumes a value for pixels_per_inch 405 | # return points/72.0 * self.dpi.get() * pixels_per_inch/72.0 406 | # else 407 | # return points/72.0 * self.dpi.get() 408 | 409 | def clear(self): 410 | if not hasattr(self, "surface"): 411 | return 412 | self.surface.fill("white") 413 | 414 | def copy_from_bbox(self, bbox): 415 | copy = self.surface.copy() 416 | copy.bbox = bbox 417 | return copy 418 | 419 | 420 | class GraphicsContextPygame(GraphicsContextBase): 421 | """ 422 | The graphics context provides the color, line styles, etc... See the cairo 423 | and postscript backends for examples of mapping the graphics context 424 | attributes (cap styles, join styles, line widths, colors) to a particular 425 | backend. In cairo this is done by wrapping a cairo.Context object and 426 | forwarding the appropriate calls to it using a dictionary mapping styles 427 | to gdk constants. In Postscript, all the work is done by the renderer, 428 | mapping line styles to postscript calls. 429 | 430 | If it's more appropriate to do the mapping at the renderer level (as in 431 | the postscript backend), you don't need to override any of the GC methods. 432 | If it's more appropriate to wrap an instance (as in the cairo backend) and 433 | do the mapping here, you'll need to override several of the setter 434 | methods. 435 | 436 | The base GraphicsContext stores colors as a RGB tuple on the unit 437 | interval, e.g., (0.5, 0.0, 1.0). You may need to map this to colors 438 | appropriate for your backend. 439 | """ 440 | 441 | 442 | ######################################################################## 443 | # 444 | # The following functions and classes are for pyplot and implement 445 | # window/figure managers, etc... 446 | # 447 | ######################################################################## 448 | 449 | 450 | def draw_if_interactive(): 451 | """ 452 | For image backends - is not required. 453 | For GUI backends - this should be overridden if drawing should be done in 454 | interactive python mode. 455 | """ 456 | 457 | 458 | def show(*, block=None): 459 | """ 460 | For image backends - is not required. 461 | For GUI backends - show() is usually the last line of a pyplot script and 462 | tells the backend that it is time to draw. In interactive mode, this 463 | should do nothing. 464 | """ 465 | for manager in Gcf.get_all_fig_managers(): 466 | manager.show(block) 467 | 468 | 469 | def new_figure_manager(num, *args, FigureClass=FigureSurface, **kwargs): 470 | """Create a new figure manager instance.""" 471 | # If a main-level app must be created, this (and 472 | # new_figure_manager_given_figure) is the usual place to do it -- see 473 | # backend_wx, backend_wxagg and backend_tkagg for examples. Not all GUIs 474 | # require explicit instantiation of a main-level app (e.g., backend_gtk3) 475 | # for pylab. 476 | 477 | # Pygame surfaces require surface objects 478 | thisFig = FigureSurface(*args, **kwargs) 479 | return new_figure_manager_given_figure(num, thisFig) 480 | 481 | 482 | def new_figure_manager_given_figure(num, figure): 483 | """Create a new figure manager instance for the given figure.""" 484 | canvas = FigureCanvasPygame(figure) 485 | manager = FigureManagerPygame(canvas, num) 486 | return manager 487 | 488 | 489 | class FigureCanvasPygame(FigureCanvasBase): 490 | """ 491 | The canvas the figure renders into. Calls the draw and print fig 492 | methods, creates the renderers, etc. 493 | 494 | Note: GUI templates will want to connect events for button presses, 495 | mouse movements and key presses to functions that call the base 496 | class methods button_press_event, button_release_event, 497 | motion_notify_event, key_press_event, and key_release_event. See the 498 | implementations of the interactive backends for examples. 499 | 500 | Attributes 501 | ---------- 502 | figure : `matplotlib.figure.Figure` 503 | A high-level Figure instance 504 | """ 505 | 506 | blitting: bool = False 507 | 508 | # File types allowed for saving 509 | filetypes = { 510 | "jpeg": "Joint Photographic Experts Group", 511 | "jpg": "Joint Photographic Experts Group", 512 | "png": "Portable Network Graphics", 513 | "bmp": "Bitmap Image File", 514 | "tga": "Truevision Graphics Adapter", 515 | } 516 | figure: FigureSurface 517 | renderer: RendererPygame 518 | main_display: pygame.Surface 519 | 520 | def __init__(self, figure=None): 521 | super().__init__(figure) 522 | 523 | # You should provide a print_xxx function for every file format 524 | # you can write. 525 | for file_extension in self.filetypes.keys(): 526 | setattr(self, "print_{}".format(file_extension), self._print_any) 527 | 528 | def copy_from_bbox(self, bbox): 529 | renderer = self.get_renderer() 530 | return renderer.copy_from_bbox(bbox) 531 | 532 | def restore_region(self, region, bbox=None, xy=None): 533 | rect = ( 534 | pygame.Rect( 535 | *(0, 0 if xy is None else xy), 536 | *self.get_renderer().surface.get_size(), 537 | ) 538 | if bbox is None 539 | else self.rect_from_bbox(bbox) 540 | ) 541 | self.figure.blit(region, rect) 542 | 543 | def draw(self): 544 | """ 545 | Draw the figure using the renderer. 546 | 547 | It is important that this method actually walk the artist tree 548 | even if not output is produced because this will trigger 549 | deferred work (like computing limits auto-limits and tick 550 | values) that users may want access to before saving to disk. 551 | """ 552 | self.renderer = self.get_renderer(cleared=True) 553 | self.renderer.surface = self.figure 554 | if self.blitting: 555 | # Draw only interactive artists 556 | artists = self.figure.get_interactive_artists(self.renderer) 557 | for a in artists: 558 | a.draw(self.renderer) 559 | else: 560 | # Full redraw of the figure 561 | self.figure.draw(self.renderer) 562 | 563 | def blit(self, bbox=None): 564 | self.renderer = self.get_renderer(cleared=False) 565 | self.renderer.surface = self.figure 566 | self.blitting = True 567 | 568 | def get_renderer(self, cleared=False) -> RendererPygame: 569 | fig = self.figure 570 | reuse_renderer = ( 571 | hasattr(self, "renderer") and getattr(self, "_last_fig", None) == fig 572 | ) 573 | if not reuse_renderer: 574 | self.renderer = RendererPygame(self.figure.dpi) 575 | self._last_fig = fig 576 | elif cleared: 577 | self.renderer.clear() 578 | return self.renderer 579 | 580 | def _print_any(self, filename, *args, **kwargs): 581 | """Call the pygame image saving method for the correct extenstion.""" 582 | self.draw() 583 | pygame.image.save(self.figure, filename) 584 | 585 | def get_default_filetype(self): 586 | return "jpg" 587 | 588 | def flush_events(self): 589 | self.main_display.blit(self.figure, (0, 0)) 590 | pygame.display.update() 591 | self.pygame_quit_event_check() 592 | 593 | def pygame_quit_event_check(self): 594 | events = pygame.event.get() 595 | for event in events: 596 | if event.type == pygame.QUIT: 597 | pygame.quit() 598 | 599 | def start_event_loop(self, interval: float): 600 | FPS = 60 601 | FramePerSec = pygame.time.Clock() 602 | time_elapsed = 0 603 | while time_elapsed < interval: 604 | events = pygame.event.get() 605 | time_elapsed += FramePerSec.tick(FPS) / 1000.0 # Convert ms 606 | for event in events: 607 | if event.type == pygame.QUIT: 608 | pygame.quit() 609 | 610 | 611 | class FigureManagerPygame(FigureManagerBase): 612 | """ 613 | Helper class for pyplot mode, wraps everything up into a neat bundle. 614 | 615 | For non-interactive backends, the base class is sufficient. 616 | """ 617 | 618 | canvas: FigureCanvasPygame 619 | 620 | def get_main_display(self): 621 | if hasattr(self.canvas, "main_display"): 622 | if self.canvas.figure.get_size() == self.canvas.main_display.get_size(): 623 | # If the main display exist and its size has not changed 624 | return self.canvas.main_display 625 | main_display = pygame.display.set_mode( 626 | self.canvas.figure.get_size(), # Size matches figure size 627 | ) 628 | main_display.fill("white") 629 | self.canvas.main_display = main_display 630 | return main_display 631 | 632 | def show(self, block=True): 633 | # do something to display the GUI 634 | pygame.init() 635 | main_display = self.get_main_display() 636 | 637 | FPS = 60 638 | FramePerSec = pygame.time.Clock() 639 | 640 | if not self.canvas.blitting: 641 | # Draw if we are not blitting 642 | self.canvas.draw() 643 | main_display.blit(self.canvas.figure, (0, 0)) 644 | pygame.display.update() 645 | 646 | show_fig = True if block is None else False 647 | while show_fig: 648 | events = pygame.event.get() 649 | pygame.display.update() 650 | FramePerSec.tick(FPS) 651 | 652 | for event in events: 653 | if event.type == pygame.QUIT: 654 | pygame.quit() 655 | show_fig = False 656 | 657 | 658 | ######################################################################## 659 | # 660 | # Now just provide the standard names that backend.__init__ is expecting 661 | # 662 | ######################################################################## 663 | 664 | FigureCanvas = FigureCanvasPygame 665 | FigureManager = FigureManagerPygame 666 | --------------------------------------------------------------------------------