Skip to content

Commit c1ed749

Browse files
committed
First upload
1 parent 2b79f95 commit c1ed749

File tree

8 files changed

+455
-0
lines changed

8 files changed

+455
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Pygame Matplotlib Backend
2+
3+
This is an attempt to create a dedicated backend for matplotlib
4+
in pygame.
5+
6+
See examples in test.py or test_show.py
7+
8+
Still in progress ...

pygame_matplotlib/__init__.py

Whitespace-only changes.
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
"""
2+
A fully functional, do-nothing backend intended as a template for backend
3+
writers. It is fully functional in that you can select it as a backend e.g.
4+
with ::
5+
6+
import matplotlib
7+
matplotlib.use("template")
8+
9+
and your program will (should!) run without error, though no output is
10+
produced. This provides a starting point for backend writers; you can
11+
selectively implement drawing methods (`~.RendererTemplate.draw_path`,
12+
`~.RendererTemplate.draw_image`, etc.) and slowly see your figure come to life
13+
instead having to have a full blown implementation before getting any results.
14+
15+
Copy this file to a directory outside of the Matplotlib source tree, somewhere
16+
where Python can import it (by adding the directory to your ``sys.path`` or by
17+
packaging it as a normal Python package); if the backend is importable as
18+
``import my.backend`` you can then select it using ::
19+
20+
import matplotlib
21+
matplotlib.use("module://my.backend")
22+
23+
If your backend implements support for saving figures (i.e. has a `print_xyz`
24+
method), you can register it as the default handler for a given file type::
25+
26+
from matplotlib.backend_bases import register_backend
27+
register_backend('xyz', 'my_backend', 'XYZ File Format')
28+
...
29+
plt.savefig("figure.xyz")
30+
"""
31+
from matplotlib.transforms import Affine2D
32+
import pygame
33+
from matplotlib._pylab_helpers import Gcf
34+
from matplotlib.backend_bases import (
35+
FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase)
36+
from matplotlib.figure import Figure
37+
from matplotlib.path import Path
38+
import math
39+
40+
class FigureSurface(pygame.Surface, Figure):
41+
def __init__(self, *args, **kwargs):
42+
print("initialized")
43+
Figure.__init__(self, *args, **kwargs)
44+
pygame.Surface.__init__(self, self.bbox.size)
45+
self.fill('white')
46+
47+
class RendererTemplate(RendererBase):
48+
"""
49+
The renderer handles drawing/rendering operations.
50+
51+
This is a minimal do-nothing class that can be used to get started when
52+
writing a new backend. Refer to `backend_bases.RendererBase` for
53+
documentation of the methods.
54+
"""
55+
56+
def __init__(self, dpi):
57+
super().__init__()
58+
self.dpi = dpi
59+
60+
61+
def draw_path(self, gc, path, transform, rgbFace=None):
62+
63+
if rgbFace is None:
64+
rgbFace = gc.get_rgb()
65+
color = tuple([int(val*255) for i, val in enumerate(rgbFace) if i < 3])
66+
67+
linewidth = int(gc.get_linewidth())
68+
69+
transfrom_to_pygame_axis = Affine2D()
70+
transfrom_to_pygame_axis.set_matrix([
71+
[1, 0, 0], [0, -1, self.surface.get_height()], [0, 0, 1]
72+
])
73+
74+
transform += transfrom_to_pygame_axis
75+
76+
draw_func = pygame.draw.aaline if gc.get_antialiased() else pygame.draw.line
77+
78+
previous_point = (0, 0)
79+
for point, code in path.iter_segments(transform):
80+
# previous_point = point
81+
# print(point, code)
82+
if code == Path.LINETO:
83+
draw_func(
84+
self.surface, color, previous_point, point, linewidth
85+
)
86+
previous_point = point
87+
elif code == Path.CURVE3:
88+
end_point = point[2:]
89+
control_point = point[:2]
90+
# Creates the bounding rectangle for the arc
91+
# rect = pygame.Rect(
92+
# x:=min(previous_point[0], end_point[0]),
93+
# y:=min(previous_point[1], end_point[1]),
94+
# w:=max(previous_point[0], end_point[0]) - x,
95+
# h:=max(previous_point[1], end_point[1]) - y,
96+
# )
97+
# start_angle = math.atan(abs(
98+
# (x - control_point[0])
99+
# / (y- control_point[1])
100+
# ))
101+
# stop_angle = math.atan(abs(
102+
# (x + w - control_point[0])
103+
# / (y + h - control_point[1])
104+
# ))
105+
# Find the vectors to get the angle of the arc
106+
#c_p = (control_point[0] - previous_point[0], control_point[1] - previous_point[1])
107+
#c_e = (control_point[0] - end_point[0], control_point[1] - end_point[1])
108+
#arc_angle = math.acos(
109+
# ((c_p[0] * c_e[0]) + (c_p[1] * c_e[1]))
110+
# / (math.hypot(*c_p) * math.hypot(*c_e))
111+
#)
112+
# The focals are at the intersection
113+
draw_func(
114+
self.surface, color, previous_point, end_point, linewidth
115+
)
116+
previous_point = end_point
117+
else:
118+
previous_point = point
119+
pass
120+
121+
# draw_markers is optional, and we get more correct relative
122+
# timings by leaving it out. backend implementers concerned with
123+
# performance will probably want to implement it
124+
# def draw_markers(self, gc, marker_path, marker_trans, path, trans,
125+
# rgbFace=None):
126+
# pass
127+
128+
# draw_path_collection is optional, and we get more correct
129+
# relative timings by leaving it out. backend implementers concerned with
130+
# performance will probably want to implement it
131+
# def draw_path_collection(self, gc, master_transform, paths,
132+
# all_transforms, offsets, offsetTrans,
133+
# facecolors, edgecolors, linewidths, linestyles,
134+
# antialiaseds):
135+
# pass
136+
137+
# draw_quad_mesh is optional, and we get more correct
138+
# relative timings by leaving it out. backend implementers concerned with
139+
# performance will probably want to implement it
140+
# def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
141+
# coordinates, offsets, offsetTrans, facecolors,
142+
# antialiased, edgecolors):
143+
# pass
144+
145+
def draw_image(self, gc, x, y, im):
146+
pass
147+
148+
def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
149+
# make sure font module is initialized
150+
if not pygame.font.get_init():
151+
pygame.font.init()
152+
# prop is the fondt properties
153+
font_size = prop.get_size() * self.dpi / 57
154+
myfont = pygame.font.Font(prop.get_file(), int(font_size))
155+
# apply it to text on a label
156+
font_surface = myfont.render(
157+
s, gc.get_antialiased(), [val*255 for val in gc.get_rgb()]
158+
)
159+
if mtext is not None:
160+
mtext.set_verticalalignment('bottom')
161+
mtext.set_horizontalalignment('right')
162+
x, y, _, _ = mtext.get_window_extent().bounds
163+
# pygame starts from bottom left instead of middle
164+
mtext.set_verticalalignment('center')
165+
mtext.set_horizontalalignment('center')
166+
new_x, new_y, _, _ = mtext.get_window_extent().bounds
167+
print(x, y, new_x, new_y)
168+
y += (new_y - y) * 2
169+
x -= (new_x - x) * 2
170+
self.surface.blit(font_surface, (x, self.surface.get_height() - y))
171+
172+
def flipy(self):
173+
# docstring inherited
174+
return True
175+
176+
def get_canvas_width_height(self):
177+
# docstring inherited
178+
return 100, 100
179+
180+
def get_text_width_height_descent(self, s, prop, ismath):
181+
return 1, 1, 1
182+
183+
def new_gc(self):
184+
# docstring inherited
185+
return GraphicsContextTemplate()
186+
187+
def points_to_pixels(self, points):
188+
# if backend doesn't have dpi, e.g., postscript or svg
189+
return points
190+
# elif backend assumes a value for pixels_per_inch
191+
#return points/72.0 * self.dpi.get() * pixels_per_inch/72.0
192+
# else
193+
#return points/72.0 * self.dpi.get()
194+
195+
196+
class GraphicsContextTemplate(GraphicsContextBase):
197+
"""
198+
The graphics context provides the color, line styles, etc... See the cairo
199+
and postscript backends for examples of mapping the graphics context
200+
attributes (cap styles, join styles, line widths, colors) to a particular
201+
backend. In cairo this is done by wrapping a cairo.Context object and
202+
forwarding the appropriate calls to it using a dictionary mapping styles
203+
to gdk constants. In Postscript, all the work is done by the renderer,
204+
mapping line styles to postscript calls.
205+
206+
If it's more appropriate to do the mapping at the renderer level (as in
207+
the postscript backend), you don't need to override any of the GC methods.
208+
If it's more appropriate to wrap an instance (as in the cairo backend) and
209+
do the mapping here, you'll need to override several of the setter
210+
methods.
211+
212+
The base GraphicsContext stores colors as a RGB tuple on the unit
213+
interval, e.g., (0.5, 0.0, 1.0). You may need to map this to colors
214+
appropriate for your backend.
215+
"""
216+
217+
218+
########################################################################
219+
#
220+
# The following functions and classes are for pyplot and implement
221+
# window/figure managers, etc...
222+
#
223+
########################################################################
224+
225+
226+
def draw_if_interactive():
227+
"""
228+
For image backends - is not required.
229+
For GUI backends - this should be overridden if drawing should be done in
230+
interactive python mode.
231+
"""
232+
233+
234+
def show(*, block=None):
235+
"""
236+
For image backends - is not required.
237+
For GUI backends - show() is usually the last line of a pyplot script and
238+
tells the backend that it is time to draw. In interactive mode, this
239+
should do nothing.
240+
"""
241+
for manager in Gcf.get_all_fig_managers():
242+
manager.show()
243+
244+
245+
246+
def new_figure_manager(num, *args, FigureClass=FigureSurface, **kwargs):
247+
"""Create a new figure manager instance."""
248+
# If a main-level app must be created, this (and
249+
# new_figure_manager_given_figure) is the usual place to do it -- see
250+
# backend_wx, backend_wxagg and backend_tkagg for examples. Not all GUIs
251+
# require explicit instantiation of a main-level app (e.g., backend_gtk3)
252+
# for pylab.
253+
254+
# Pygame surfaces require surface objects
255+
thisFig = FigureSurface(*args, **kwargs)
256+
return new_figure_manager_given_figure(num, thisFig)
257+
258+
259+
def new_figure_manager_given_figure(num, figure):
260+
"""Create a new figure manager instance for the given figure."""
261+
canvas = FigureCanvasTemplate(figure)
262+
manager = FigureManagerTemplate(canvas, num)
263+
return manager
264+
265+
266+
267+
268+
class FigureCanvasTemplate(FigureCanvasBase):
269+
"""
270+
The canvas the figure renders into. Calls the draw and print fig
271+
methods, creates the renderers, etc.
272+
273+
Note: GUI templates will want to connect events for button presses,
274+
mouse movements and key presses to functions that call the base
275+
class methods button_press_event, button_release_event,
276+
motion_notify_event, key_press_event, and key_release_event. See the
277+
implementations of the interactive backends for examples.
278+
279+
Attributes
280+
----------
281+
figure : `matplotlib.figure.Figure`
282+
A high-level Figure instance
283+
"""
284+
285+
def __init__(self, figure=None):
286+
FigureCanvasBase.__init__(self, figure)
287+
288+
def draw(self):
289+
"""
290+
Draw the figure using the renderer.
291+
292+
It is important that this method actually walk the artist tree
293+
even if not output is produced because this will trigger
294+
deferred work (like computing limits auto-limits and tick
295+
values) that users may want access to before saving to disk.
296+
"""
297+
renderer = RendererTemplate(self.figure.dpi)
298+
renderer.surface = self.figure
299+
self.figure.draw(renderer)
300+
301+
# You should provide a print_xxx function for every file format
302+
# you can write.
303+
304+
# If the file type is not in the base set of filetypes,
305+
# you should add it to the class-scope filetypes dictionary as follows:
306+
filetypes = {**FigureCanvasBase.filetypes, 'foo': 'My magic Foo format'}
307+
308+
def print_foo(self, filename, *args, **kwargs):
309+
"""
310+
Write out format foo.
311+
312+
This method is normally called via `.Figure.savefig` and
313+
`.FigureCanvasBase.print_figure`, which take care of setting the figure
314+
facecolor, edgecolor, and dpi to the desired output values, and will
315+
restore them to the original values. Therefore, `print_foo` does not
316+
need to handle these settings.
317+
"""
318+
self.draw()
319+
320+
def get_default_filetype(self):
321+
return 'foo'
322+
323+
324+
class FigureManagerTemplate(FigureManagerBase):
325+
"""
326+
Helper class for pyplot mode, wraps everything up into a neat bundle.
327+
328+
For non-interactive backends, the base class is sufficient.
329+
"""
330+
331+
def show(self):
332+
# do something to display the GUI
333+
pygame.init()
334+
main_display = pygame.display.set_mode(self.canvas.figure.get_size())
335+
336+
FPS = 144
337+
FramePerSec = pygame.time.Clock()
338+
339+
self.canvas.figure.canvas.draw()
340+
main_display.blit(self.canvas.figure, (0, 0))
341+
342+
show_fig = True
343+
while show_fig:
344+
events = pygame.event.get()
345+
pygame.display.update()
346+
FramePerSec.tick(FPS)
347+
348+
for event in events:
349+
if event.type == pygame.QUIT:
350+
pygame.quit()
351+
show_fig = False
352+
353+
354+
########################################################################
355+
#
356+
# Now just provide the standard names that backend.__init__ is expecting
357+
#
358+
########################################################################
359+
360+
FigureCanvas = FigureCanvasTemplate
361+
FigureManager = FigureManagerTemplate

pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build-system]
2+
requires = [
3+
"setuptools>=42",
4+
"wheel"
5+
]
6+
build-backend = "setuptools.build_meta"

0 commit comments

Comments
 (0)