PlotCanvas.py 10 KB

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