PlotCanvas.py 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. from gi.repository import Gdk
  2. from matplotlib.figure import Figure
  3. from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas
  4. #from FlatCAMApp import *
  5. import FlatCAMApp
  6. class PlotCanvas:
  7. """
  8. Class handling the plotting area in the application.
  9. """
  10. def __init__(self, container):
  11. """
  12. The constructor configures the Matplotlib figure that
  13. will contain all plots, creates the base axes and connects
  14. events to the plotting area.
  15. :param container: The parent container in which to draw plots.
  16. :rtype: PlotCanvas
  17. """
  18. # Options
  19. self.x_margin = 15 # pixels
  20. self.y_margin = 25 # Pixels
  21. # Parent container
  22. self.container = container
  23. # Plots go onto a single matplotlib.figure
  24. self.figure = Figure(dpi=50) # TODO: dpi needed?
  25. self.figure.patch.set_visible(False)
  26. # These axes show the ticks and grid. No plotting done here.
  27. # New axes must have a label, otherwise mpl returns an existing one.
  28. self.axes = self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label="base", alpha=0.0)
  29. self.axes.set_aspect(1)
  30. self.axes.grid(True)
  31. # The canvas is the top level container (Gtk.DrawingArea)
  32. self.canvas = FigureCanvas(self.figure)
  33. self.canvas.set_hexpand(1)
  34. self.canvas.set_vexpand(1)
  35. self.canvas.set_can_focus(True) # For key press
  36. # Attach to parent
  37. self.container.attach(self.canvas, 0, 0, 600, 400) # TODO: Height and width are num. columns??
  38. # Events
  39. self.canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
  40. self.canvas.connect('configure-event', self.auto_adjust_axes)
  41. self.canvas.add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK)
  42. self.canvas.connect("scroll-event", self.on_scroll)
  43. self.canvas.mpl_connect('key_press_event', self.on_key_down)
  44. self.canvas.mpl_connect('key_release_event', self.on_key_up)
  45. self.mouse = [0, 0]
  46. self.key = None
  47. def on_key_down(self, event):
  48. """
  49. :param event:
  50. :return:
  51. """
  52. self.key = event.key
  53. def on_key_up(self, event):
  54. """
  55. :param event:
  56. :return:
  57. """
  58. self.key = None
  59. def mpl_connect(self, event_name, callback):
  60. """
  61. Attach an event handler to the canvas through the Matplotlib interface.
  62. :param event_name: Name of the event
  63. :type event_name: str
  64. :param callback: Function to call
  65. :type callback: func
  66. :return: Connection id
  67. :rtype: int
  68. """
  69. return self.canvas.mpl_connect(event_name, callback)
  70. def mpl_disconnect(self, cid):
  71. """
  72. Disconnect callback with the give id.
  73. :param cid: Callback id.
  74. :return: None
  75. """
  76. self.canvas.mpl_disconnect(cid)
  77. def connect(self, event_name, callback):
  78. """
  79. Attach an event handler to the canvas through the native GTK interface.
  80. :param event_name: Name of the event
  81. :type event_name: str
  82. :param callback: Function to call
  83. :type callback: function
  84. :return: Nothing
  85. """
  86. self.canvas.connect(event_name, callback)
  87. def clear(self):
  88. """
  89. Clears axes and figure.
  90. :return: None
  91. """
  92. # Clear
  93. self.axes.cla()
  94. try:
  95. self.figure.clf()
  96. except KeyError:
  97. FlatCAMApp.App.log.warning("KeyError in MPL figure.clf()")
  98. # Re-build
  99. self.figure.add_axes(self.axes)
  100. self.axes.set_aspect(1)
  101. self.axes.grid(True)
  102. # Re-draw
  103. self.canvas.queue_draw()
  104. def adjust_axes(self, xmin, ymin, xmax, ymax):
  105. """
  106. Adjusts all axes while maintaining the use of the whole canvas
  107. and an aspect ratio to 1:1 between x and y axes. The parameters are an original
  108. request that will be modified to fit these restrictions.
  109. :param xmin: Requested minimum value for the X axis.
  110. :type xmin: float
  111. :param ymin: Requested minimum value for the Y axis.
  112. :type ymin: float
  113. :param xmax: Requested maximum value for the X axis.
  114. :type xmax: float
  115. :param ymax: Requested maximum value for the Y axis.
  116. :type ymax: float
  117. :return: None
  118. """
  119. FlatCAMApp.App.log.debug("PC.adjust_axes()")
  120. width = xmax - xmin
  121. height = ymax - ymin
  122. try:
  123. r = width / height
  124. except ZeroDivisionError:
  125. FlatCAMApp.App.log.error("Height is %f" % height)
  126. return
  127. canvas_w, canvas_h = self.canvas.get_width_height()
  128. canvas_r = float(canvas_w) / canvas_h
  129. x_ratio = float(self.x_margin) / canvas_w
  130. y_ratio = float(self.y_margin) / canvas_h
  131. if r > canvas_r:
  132. ycenter = (ymin + ymax) / 2.0
  133. newheight = height * r / canvas_r
  134. ymin = ycenter - newheight / 2.0
  135. ymax = ycenter + newheight / 2.0
  136. else:
  137. xcenter = (xmax + xmin) / 2.0
  138. newwidth = width * canvas_r / r
  139. xmin = xcenter - newwidth / 2.0
  140. xmax = xcenter + newwidth / 2.0
  141. # Adjust axes
  142. for ax in self.figure.get_axes():
  143. if ax._label != 'base':
  144. ax.set_frame_on(False) # No frame
  145. ax.set_xticks([]) # No tick
  146. ax.set_yticks([]) # No ticks
  147. ax.patch.set_visible(False) # No background
  148. ax.set_aspect(1)
  149. ax.set_xlim((xmin, xmax))
  150. ax.set_ylim((ymin, ymax))
  151. ax.set_position([x_ratio, y_ratio, 1 - 2 * x_ratio, 1 - 2 * y_ratio])
  152. # Re-draw
  153. self.canvas.queue_draw()
  154. def auto_adjust_axes(self, *args):
  155. """
  156. Calls ``adjust_axes()`` using the extents of the base axes.
  157. :rtype : None
  158. :return: None
  159. """
  160. xmin, xmax = self.axes.get_xlim()
  161. ymin, ymax = self.axes.get_ylim()
  162. self.adjust_axes(xmin, ymin, xmax, ymax)
  163. def zoom(self, factor, center=None):
  164. """
  165. Zooms the plot by factor around a given
  166. center point. Takes care of re-drawing.
  167. :param factor: Number by which to scale the plot.
  168. :type factor: float
  169. :param center: Coordinates [x, y] of the point around which to scale the plot.
  170. :type center: list
  171. :return: None
  172. """
  173. xmin, xmax = self.axes.get_xlim()
  174. ymin, ymax = self.axes.get_ylim()
  175. width = xmax - xmin
  176. height = ymax - ymin
  177. if center is None or center == [None, None]:
  178. center = [(xmin + xmax) / 2.0, (ymin + ymax) / 2.0]
  179. # For keeping the point at the pointer location
  180. relx = (xmax - center[0]) / width
  181. rely = (ymax - center[1]) / height
  182. new_width = width / factor
  183. new_height = height / factor
  184. xmin = center[0] - new_width * (1 - relx)
  185. xmax = center[0] + new_width * relx
  186. ymin = center[1] - new_height * (1 - rely)
  187. ymax = center[1] + new_height * rely
  188. # Adjust axes
  189. for ax in self.figure.get_axes():
  190. ax.set_xlim((xmin, xmax))
  191. ax.set_ylim((ymin, ymax))
  192. # Re-draw
  193. self.canvas.queue_draw()
  194. def pan(self, x, y):
  195. xmin, xmax = self.axes.get_xlim()
  196. ymin, ymax = self.axes.get_ylim()
  197. width = xmax - xmin
  198. height = ymax - ymin
  199. # Adjust axes
  200. for ax in self.figure.get_axes():
  201. ax.set_xlim((xmin + x*width, xmax + x*width))
  202. ax.set_ylim((ymin + y*height, ymax + y*height))
  203. # Re-draw
  204. self.canvas.queue_draw()
  205. def new_axes(self, name):
  206. """
  207. Creates and returns an Axes object attached to this object's Figure.
  208. :param name: Unique label for the axes.
  209. :return: Axes attached to the figure.
  210. :rtype: Axes
  211. """
  212. return self.figure.add_axes([0.05, 0.05, 0.9, 0.9], label=name)
  213. def on_scroll(self, canvas, event):
  214. """
  215. Scroll event handler.
  216. :param canvas: The widget generating the event. Ignored.
  217. :param event: Event object containing the event information.
  218. :return: None
  219. """
  220. # So it can receive key presses
  221. self.canvas.grab_focus()
  222. # Event info
  223. z, direction = event.get_scroll_direction()
  224. if self.key is None:
  225. if direction is Gdk.ScrollDirection.UP:
  226. self.zoom(1.5, self.mouse)
  227. else:
  228. self.zoom(1/1.5, self.mouse)
  229. return
  230. if self.key == 'shift':
  231. if direction is Gdk.ScrollDirection.UP:
  232. self.pan(0.3, 0)
  233. else:
  234. self.pan(-0.3, 0)
  235. return
  236. if self.key == 'ctrl+control':
  237. if direction is Gdk.ScrollDirection.UP:
  238. self.pan(0, 0.3)
  239. else:
  240. self.pan(0, -0.3)
  241. return
  242. def on_mouse_move(self, event):
  243. """
  244. Mouse movement event hadler. Stores the coordinates.
  245. :param event: Contains information about the event.
  246. :return: None
  247. """
  248. self.mouse = [event.xdata, event.ydata]