|
| 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 |
0 commit comments