Improved matplotlib graphic debug panel implementation
authorLaurent Bessard
Tue, 19 Feb 2013 00:06:59 +0100
changeset 930 4be515ac635e
parent 929 c562031146e4
child 931 da4970f6c46c
Improved matplotlib graphic debug panel implementation
IDEFrame.py
controls/DebugVariablePanel.py
controls/PouInstanceVariablesPanel.py
images/maximize_graph.png
images/middle_graph.png
images/plcopen_icons.svg
--- a/IDEFrame.py	Sun Feb 17 23:47:41 2013 +0100
+++ b/IDEFrame.py	Tue Feb 19 00:06:59 2013 +0100
@@ -6,6 +6,13 @@
 import wx, wx.grid
 import wx.aui
 
+try:
+    import matplotlib
+    matplotlib.use('WX')
+    USE_MPL = True
+except:
+    USE_MPL = False
+
 from editors.EditorPanel import EditorPanel
 from editors.SFCViewer import SFC_Viewer
 from editors.LDViewer import LD_Viewer
@@ -2014,6 +2021,7 @@
 
     def OpenDebugViewer(self, instance_category, instance_path, instance_type):
         openedidx = self.IsOpened(instance_path)
+        new_window = None
         if openedidx is not None:
             old_selected = self.TabsOpened.GetSelection()
             if old_selected != openedidx:
@@ -2023,12 +2031,14 @@
         
         elif instance_category in ITEMS_VARIABLE:
             if self.Controler.IsNumType(instance_type, True):
-                new_window = GraphicViewer(self.TabsOpened, self, self.Controler, instance_path)
-                icon = GetBitmap("GRAPH")
+                if USE_MPL:
+                    self.AddDebugVariable(instance_path, True)
+                else:
+                    new_window = GraphicViewer(self.TabsOpened, self, self.Controler, instance_path)
+                    icon = GetBitmap("GRAPH")
         
         else:
             bodytype = self.Controler.GetEditedElementBodyType(instance_type, True)
-            new_window = None
             if bodytype == "FBD":
                 new_window = Viewer(self.TabsOpened, instance_type, self, self.Controler, True, instance_path)
                 new_window.RefreshScaling(False)
--- a/controls/DebugVariablePanel.py	Sun Feb 17 23:47:41 2013 +0100
+++ b/controls/DebugVariablePanel.py	Tue Feb 19 00:06:59 2013 +0100
@@ -39,7 +39,8 @@
     from matplotlib.backends.backend_wxagg import _convert_agg_to_wx_bitmap
     from matplotlib.backends.backend_agg import FigureCanvasAgg
     from mpl_toolkits.mplot3d import Axes3D
-    color_cycle = matplotlib.rcParams['axes.color_cycle']
+    color_cycle = ['r', 'b', 'g', 'm', 'y', 'k']
+    cursor_color = '#800080'
     USE_MPL = True
 except:
     USE_MPL = False
@@ -618,61 +619,53 @@
     HIGHLIGHT_PEN = wx.Pen(wx.Colour(0, 128, 255))
     HIGHLIGHT_BRUSH = wx.Brush(wx.Colour(0, 128, 255, 128))
     
-    if wx.Platform == '__WXMSW__':
-        popupclass = wx.PopupTransientWindow
-    else:
-        popupclass = wx.PopupWindow
-    
-    class PopupWithButtons(popupclass):
-    
-        def __init__(self, parent, window, item, style=wx.HORIZONTAL):
-            popupclass.__init__(self, parent, wx.NO_BORDER)
-            self.SetBackgroundColour(wx.WHITE)
-            
-            self.ParentWindow = window
-            self.Item = item
-            
-            main_sizer = wx.BoxSizer(style)
-            
-            if self.Item.IsForced():
-                buttons = [("ReleaseButton", "release", _("Release value"))]
-            
-            else:
-                buttons = [("ForceButton", "force", _("Force value"))]
-            buttons.append(("DeleteButton", "delete_graph", _("Remove debug variable")))
-            
-            for name, bitmap, help in buttons:
-                button = wx.lib.buttons.GenBitmapButton(self, bitmap=GetBitmap(bitmap), 
-                      size=wx.Size(20, 20), style=wx.NO_BORDER)
-                button.SetToolTipString(help)
-                setattr(self, name, button)
-                self.Bind(wx.EVT_BUTTON, getattr(self, "On" + name), button)
-                main_sizer.AddWindow(button)
-            main_sizer.Layout()
-            
-            self.SetSizer(main_sizer)
-            main_sizer.Fit(self)
-        
-        def GetItem(self):
-            return self.Item
-        
-        def OnForceButton(self, event):
-            wx.CallAfter(self.Parent.DismissButtons)
-            wx.CallAfter(self.Parent.ForceValue, self.Item)
-            event.Skip()
-        
-        def OnReleaseButton(self, event):
-            wx.CallAfter(self.Parent.DismissButtons)
-            wx.CallAfter(self.Parent.ReleaseValue, self.Item)
-            event.Skip()
-        
-        def OnDeleteButton(self, event):            
-            wx.CallAfter(self.Parent.DismissButtons)
-            wx.CallAfter(self.ParentWindow.DeleteValue, self.Parent, self.Item)
-            event.Skip()
-    
-        def OnDismiss(self):
-            wx.CallAfter(self.Parent.DismissButtons)
+    #CANVAS_SIZE_TYPES
+    [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI] = range(3)
+    
+    class GraphButton():
+        
+        def __init__(self, x, y, bitmap, callback):
+            self.Position = wx.Point(x, y)
+            self.Bitmap = bitmap
+            self.Shown = True
+            self.Callback = callback
+        
+        def __del__(self):
+            self.callback = None
+        
+        def GetSize(self):
+            return self.Bitmap.GetSize()
+        
+        def SetPosition(self, x, y):
+            self.Position = wx.Point(x, y)
+        
+        def SetBitmap(self, bitmap):
+            self.Bitmap = bitmap
+            
+        def SetCallback(self, callback):
+            self.Callback = callback
+        
+        def Show(self):
+            self.Shown = True
+            
+        def Hide(self):
+            self.Shown = False
+        
+        def HitTest(self, x, y):
+            if self.Shown:
+                w, h = self.Bitmap.GetSize()
+                rect = wx.Rect(self.Position.x, self.Position.y, w, h)
+                if rect.InsideXY(x, y):
+                    return True
+            return False
+        
+        def ProcessCallback(self):
+            if self.Callback is not None:
+                wx.CallAfter(self.Callback)
+                
+        def Draw(self, dc):
+            if self.Shown:
+                dc.DrawBitmap(self.Bitmap, self.Position.x, self.Position.y, True)
     
     class DraggingFigureCanvas(FigureCanvas):
         
@@ -685,17 +678,16 @@
             
             self.ParentWindow = window
             self.Highlight = HIGHLIGHT_NONE
-            
-            self.ChangeSizeButton = wx.lib.buttons.GenBitmapToggleButton(self, 
-                    bitmap=GetBitmap("minimize_graph"), 
-                    size=wx.Size(20, 20), style=wx.NO_BORDER)
-            self.ChangeSizeButton.SetBitmapSelected(GetBitmap("maximize_graph"))
-            self.Bind(wx.EVT_BUTTON, self.OnChangeSizeButton, self.ChangeSizeButton)
-            
-            self.CloseButton = wx.lib.buttons.GenBitmapButton(self, 
-                    bitmap=GetBitmap("delete_graph"), 
-                    size=wx.Size(20, 20), style=wx.NO_BORDER)
-            self.Bind(wx.EVT_BUTTON, self.OnCloseButton, self.CloseButton)
+            self.CanvasSize = SIZE_MAXI
+            
+            self.Buttons = []
+            self.ContextualButtons = []
+            self.ContextualButtonsItem = None
+            
+            self.Buttons.append(
+                GraphButton(0, 0, GetBitmap("minimize_graph"), self.OnChangeSizeButton))
+            self.Buttons.append(
+                GraphButton(0, 0, GetBitmap("delete_graph"), self.OnCloseButton))
             
             self.ShowButtons(False)
             
@@ -723,7 +715,7 @@
             destDC.SelectObject(self.bitmap)
             
             destGC = wx.GCDC(destDC)
-                    
+            
             destGC.BeginDrawing()
             destGC.SetPen(HIGHLIGHT_PEN)
             destGC.SetBrush(HIGHLIGHT_BRUSH)
@@ -738,6 +730,9 @@
                 destGC.DrawRectangle(bbox.x + bbox.width / 2, bbox.y, 
                                      bbox.width / 2, bbox.height)
             
+            for button in self.Buttons + self.ContextualButtons:
+                button.Draw(destGC)
+            
             if self.ParentWindow.IsDragging():
                 destBBox = self.ParentWindow.GetDraggingAxesClippingRegion(self.Parent)
                 if destBBox.width > 0 and destBBox.height > 0:
@@ -766,13 +761,71 @@
             self._isDrawn = True
             self.gui_repaint(drawDC=drawDC)
         
+        def HandleButtons(self, x, y):
+            for button in self.Buttons + self.ContextualButtons:
+                if button.HitTest(x, y):
+                    button.ProcessCallback()
+                    return True
+            return False
+        
+        def PopupContextualButtons(self, item, rect, style=wx.HORIZONTAL):
+            if self.ContextualButtonsItem is not None and item != self.ContextualButtonsItem:
+                self.DismissContextualButtons()
+            
+            if self.ContextualButtonsItem is None:
+                self.ContextualButtonsItem = item
+                
+                if self.ContextualButtonsItem.IsForced():
+                    self.ContextualButtons.append(
+                        GraphButton(0, 0, GetBitmap("release"), self.OnReleaseButton))
+                else:
+                    self.ContextualButtons.append(
+                        GraphButton(0, 0, GetBitmap("force"), self.OnForceButton))
+                self.ContextualButtons.append(
+                    GraphButton(0, 0, GetBitmap("delete_graph"), self.OnRemoveItemButton))
+                
+                offset = 0
+                buttons = self.ContextualButtons[:]
+                if style == wx.VERTICAL:
+                     buttons.reverse()
+                for button in buttons:
+                    w, h = button.GetSize()
+                    if style == wx.HORIZONTAL:
+                        x = rect.x + rect.width + offset
+                        y = rect.y + (rect.height - h) / 2
+                        offset += w
+                    else:
+                        x = rect.x + (rect.width - w ) / 2
+                        y = rect.y - h - offset
+                        offset += h
+                    button.SetPosition(x, y)
+            self.ParentWindow.ForceRefresh()
+        
+        def DismissContextualButtons(self):
+            if self.ContextualButtonsItem is not None:
+                self.ContextualButtonsItem = None
+                self.ContextualButtons = []
+            self.ParentWindow.ForceRefresh()
+        
+        def IsOverButton(self, x, y):
+            for button in self.Buttons + self.ContextualButtons:
+                if button.HitTest(x, y):
+                    return True
+            return False
+        
+        def IsOverContextualButton(self, x, y):
+            for button in self.ContextualButtons:
+                if button.HitTest(x, y):
+                    return True
+            return False
+        
         def ShowButtons(self, show):
-            if show:
-                self.ChangeSizeButton.Show()
-                self.CloseButton.Show()
-            else:
-                self.ChangeSizeButton.Hide()
-                self.CloseButton.Hide()
+            for button in self.Buttons:
+                if show:
+                    button.Show()
+                else:
+                    button.Hide()
+            self.ParentWindow.ForceRefresh()
         
         def OnEnterWindow(self, event):
             self.ShowButtons(True)
@@ -786,21 +839,41 @@
                 self.ShowButtons(False)
             event.Skip()
         
-        def OnChangeSizeButton(self, event):
-            if self.ChangeSizeButton.GetToggle():
+        def OnChangeSizeButton(self):
+            if self.CanvasSize == SIZE_MAXI:
+                self.CanvasSize = SIZE_MIDDLE
                 self.Parent.Minimize()
             else:
+                self.CanvasSize = SIZE_MAXI
                 self.Parent.Maximize()
-            event.Skip()
-        
-        def OnCloseButton(self, event):
-            wx.CallAfter(self.ParentWindow.DeleteValue, self.Parent)
-            event.Skip()
-    
+        
+        def OnCloseButton(self):
+            self.ParentWindow.DeleteValue(self.Parent)
+        
+        def OnForceButton(self):
+            wx.CallAfter(self.Parent.ForceValue, 
+                         self.ContextualButtonsItem)
+            self.DismissContextualButtons()
+            
+        def OnReleaseButton(self):
+            wx.CallAfter(self.Parent.ReleaseValue, 
+                         self.ContextualButtonsItem)
+            self.DismissContextualButtons()
+            
+        def OnRemoveItemButton(self):            
+            wx.CallAfter(self.ParentWindow.DeleteValue, self.Parent, 
+                         self.ContextualButtonsItem)
+            self.DismissContextualButtons()
+            
         def OnResizeWindow(self, event):
             width, height = self.GetSize()
-            self.ChangeSizeButton.SetPosition(wx.Point(width - 50, 5))
-            self.CloseButton.SetPosition(wx.Point(width - 25, 5))
+            offset = 0
+            buttons = self.Buttons[:]
+            buttons.reverse()
+            for button in buttons:
+                w, h = button.GetSize()
+                button.SetPosition(width - 5 - w - offset, 5)
+                offset += w
             event.Skip()
     
     class DebugVariableGraphic(DebugVariableViewer):
@@ -817,6 +890,7 @@
             main_sizer = wx.BoxSizer(wx.VERTICAL)
             
             self.Figure = matplotlib.figure.Figure(facecolor='w')
+            self.Figure.subplotpars.update(top=0.95, left=0.1, bottom=0.1, right=0.95)
             
             self.Canvas = DraggingFigureCanvas(self, self.ParentWindow, -1, self.Figure)
             self.Canvas.SetMinSize(wx.Size(200, 200))
@@ -832,16 +906,34 @@
             
             self.ResetGraphics()
         
+        def RefreshLabelsPosition(self, ratio):
+            self.MaskLabel.set_position((0.05, 1.0 - 0.1 * ratio))
+            if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas():
+                num_item = len(self.Items)
+                for idx in xrange(num_item):
+                    if not self.Is3DCanvas():
+                        self.AxesLabels[idx].set_position((0.05, 1.0 - (0.1 + 0.075 * (idx + 1)) * ratio))
+                    self.Labels[idx].set_position((0.95, 0.1 + (num_item - idx - 1) * 0.1 * ratio))
+            else:
+                self.AxesLabels[0].set_position((0.1, 0.05 * ratio))
+                self.Labels[0].set_position((0.95, 0.05 * ratio))
+                self.AxesLabels[1].set_position((0.05, 0.1 * ratio))
+                self.Labels[1].set_position((0.05, 1.0 - 0.05 * ratio))
+        
         def Minimize(self):
             self.Canvas.SetMinSize(wx.Size(200, 100))
-            self.Figure.subplotpars.update(bottom=0.20)
+            self.Figure.subplotpars.update(top=0.9, bottom=0.2)
+            self.RefreshLabelsPosition(2)
+            self.Figure.subplots_adjust()
             self.ParentWindow.RefreshGraphicsSizer()
-        
+            
         def Maximize(self):
             self.Canvas.SetMinSize(wx.Size(200, 200))
-            self.Figure.subplotpars.update(bottom=0.1)
+            self.Figure.subplotpars.update(top=0.95, bottom=0.1)
+            self.RefreshLabelsPosition(1)
+            self.Figure.subplots_adjust()
             self.ParentWindow.RefreshGraphicsSizer()
-        
+            
         def GetAxesBoundingBox(self, absolute=False):
             bbox = self.Canvas.GetAxesBoundingBox()
             if absolute:
@@ -851,20 +943,14 @@
             return bbox
         
         def OnCanvasButtonPressed(self, event):
-            if not self.Is3DCanvas():
-                width, height = self.Canvas.GetSize()
-                x, y = event.x, height - event.y
+            width, height = self.Canvas.GetSize()
+            x, y = event.x, height - event.y
+            if not self.Canvas.IsOverButton(x, y) and not self.Is3DCanvas():
                 rect = self.GetAxesBoundingBox()
                 if rect.InsideXY(x, y):
                     self.MouseStartPos = wx.Point(x, y)
-                if self.Legend is not None:
-                    texts = self.Legend.get_texts()
-                elif len(self.AxesLabels) > 0:
-                    texts = self.AxesLabels
-                else:
-                    texts = []
                 item_idx = None
-                for i, t in enumerate(texts):
+                for i, t in enumerate(self.AxesLabels):
                     (x0, y0), (x1, y1) = t.get_window_extent().get_points()
                     rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0)
                     if rect.InsideXY(x, y):
@@ -872,16 +958,16 @@
                         break
                 if item_idx is not None:
                     self.Canvas.ShowButtons(False)
-                    self.DismissButtons()
+                    self.Canvas.DismissContextualButtons()
                     xw, yw = self.GetPosition()
                     self.ParentWindow.StartDragNDrop(self, 
                         self.Items[item_idx], x + xw, y + yw, x + xw, y + yw)
-                elif event.button == 1:
+                elif event.button == 1 and event.inaxes == self.Axes:
                     self.HandleCursorMove(event)
                 elif event.button == 2 and self.GraphType == GRAPH_PARALLEL:
                     width, height = self.Canvas.GetSize()
                     start_tick, end_tick = self.ParentWindow.GetRange()
-                    self.MouseStartPos = wx.Point(event.x, height - event.y)
+                    self.MouseStartPos = wx.Point(x, y)
                     self.StartCursorTick = start_tick
         
         def OnCanvasButtonReleased(self, event):
@@ -895,6 +981,8 @@
             else:
                 self.MouseStartPos = None
                 self.StartCursorTick = None
+                width, height = self.Canvas.GetSize()
+                self.Canvas.HandleButtons(event.x, height - event.y)
         
         def OnCanvasMotion(self, event):
             width, height = self.Canvas.GetSize()
@@ -921,16 +1009,13 @@
                         self.StartCursorTick + (self.MouseStartPos.x - event.x) *
                         (end_tick - start_tick) / rect.width)
                 elif event.button is None:
-                    if self.Legend is not None:
-                        labels = self.Legend.get_texts()
-                        texts = zip(labels, [wx.HORIZONTAL] * len(labels))
+                    if self.GraphType == GRAPH_PARALLEL:
+                        orientation = [wx.HORIZONTAL] * len(self.AxesLabels)
                     elif len(self.AxesLabels) > 0:
-                        texts = zip(self.AxesLabels, [wx.HORIZONTAL, wx.VERTICAL])
-                    else:
-                        texts = []
+                        orientation = [wx.HORIZONTAL, wx.VERTICAL]
                     item_idx = None
                     item_style = None
-                    for i, (t, style) in enumerate(texts):
+                    for i, (t, style) in enumerate(zip(self.AxesLabels, orientation)):
                         (x0, y0), (x1, y1) = t.get_window_extent().get_points()
                         rect = wx.Rect(x0, height - y1, x1 - x0, y1 - y0)
                         if rect.InsideXY(event.x, height - event.y):
@@ -938,10 +1023,10 @@
                             item_style = style
                             break
                     if item_idx is not None:
-                        self.PopupButtons(item_idx, rect, item_style)
+                        self.Canvas.PopupContextualButtons(self.Items[item_idx], rect, item_style)
                         return 
-                    if self.ItemButtons is not None:
-                        self.DismissButtons()
+                    if not self.Canvas.IsOverContextualButton(event.x, height - event.y):
+                        self.Canvas.DismissContextualButtons()
         
         def OnCanvasDragging(self, x, y, refresh=True):
             width, height = self.Canvas.GetSize()
@@ -996,44 +1081,12 @@
         
         def DoDragDrop(self, item_idx):
             self.Canvas.ShowButtons(False)
-            self.DismissButtons()
+            self.Canvas.DismissContextualButtons()
             data = wx.TextDataObject(str((self.Items[item_idx].GetVariable(), "debug", "move")))
             dragSource = wx.DropSource(self.Canvas)
             dragSource.SetData(data)
             dragSource.DoDragDrop()
         
-        def PopupButtons(self, item_idx, rect, style=wx.HORIZONTAL):
-            item = self.Items[item_idx]
-            if self.ItemButtons is not None and item != self.ItemButtons.GetItem():
-                self.DismissButtons()
-            if self.ItemButtons is None:
-                
-                self.ItemButtons = PopupWithButtons(self, self.ParentWindow, item, style)
-                
-                # Show the popup right below or above the button
-                # depending on available screen space...
-                w, h = self.ItemButtons.GetSize()
-                if style == wx.HORIZONTAL:
-                    x = rect.x + rect.width
-                    y = rect.y + (rect.height - h) / 2
-                else:
-                    x = rect.x + (rect.width - w ) / 2
-                    y = rect.y - h
-                self.ItemButtons.SetPosition(self.ClientToScreen((x, y)))
-                
-                if wx.Platform == '__WXMSW__':
-                    self.ItemButtons.Popup()
-                else:
-                    self.ItemButtons.Show()
-        
-        def DismissButtons(self):
-            if self.ItemButtons:
-                if wx.Platform == '__WXMSW__':
-                    self.ItemButtons.Dismiss()
-                else:
-                    self.ItemButtons.Destroy()
-                self.ItemButtons = None
-        
         def OnAxesMotion(self, event):
             if self.Is3DCanvas():
                 current_time = gettime()
@@ -1049,46 +1102,62 @@
                 self.LastMotionTime = gettime()
                 setattr(self.Axes, "_on_move", self.OnAxesMotion)
                 self.Axes.mouse_init()
+                self.Axes.tick_params(axis='z', labelsize='small')
             else:
                 self.Axes = self.Figure.gca()
-                self.Figure.subplotpars.update(top=0.95, right=0.95)
+                self.Axes.set_color_cycle(color_cycle)
+            self.Axes.tick_params(axis='x', labelsize='small')
+            self.Axes.tick_params(axis='y', labelsize='small')
             self.Plots = []
             self.VLine = None
             self.HLine = None
-            self.Legend = None
             self.Labels = []
             self.AxesLabels = []
+            if not self.Is3DCanvas():
+                text_func = self.Axes.text
+            else:
+                text_func = self.Axes.text2D
+            self.MaskLabel = text_func(0, 0, "", size='small', 
+                                       transform=self.Axes.transAxes)
             if self.GraphType == GRAPH_PARALLEL or self.Is3DCanvas():
                 num_item = len(self.Items)
-                if not self.Is3DCanvas():
-                    text_func = self.Axes.text
-                else:
-                    text_func = self.Axes.text2D
                 for idx in xrange(num_item):
+                    if num_item == 1:
+                        color = 'k'
+                    else:
+                        color = color_cycle[idx % len(color_cycle)]
+                    if not self.Is3DCanvas():
+                        self.AxesLabels.append(
+                            text_func(0, 0, "", size='small', 
+                                      color=color,
+                                      transform=self.Axes.transAxes))
                     self.Labels.append(
-                        text_func(0.95, 0.05 + (num_item - idx - 1) * 0.1, 
-                                  "", size='large', 
+                        text_func(0, 0, "", size='large', 
                                   horizontalalignment='right',
-                                  color=color_cycle[idx % len(color_cycle)],
+                                  color=color,
                                   transform=self.Axes.transAxes))
             else:
                 self.AxesLabels.append(
-                    self.Axes.text(0.1, 0.05, "", size='small',
+                    self.Axes.text(0, 0, "", size='small',
                                    transform=self.Axes.transAxes))
                 self.Labels.append(
-                    self.Axes.text(0.95, 0.05, "", size='large',
+                    self.Axes.text(0, 0, "", size='large',
                                    horizontalalignment='right',
                                    transform=self.Axes.transAxes))
                 self.AxesLabels.append(
-                    self.Axes.text(0.05, 0.1, "", size='small',
+                    self.Axes.text(0, 0, "", size='small',
                                    rotation='vertical',
                                    verticalalignment='bottom',
                                    transform=self.Axes.transAxes))
                 self.Labels.append(
-                    self.Axes.text(0.05, 0.95, "", size='large',
+                    self.Axes.text(0, 0, "", size='large',
                                    rotation='vertical',
                                    verticalalignment='top',
                                    transform=self.Axes.transAxes))
+            if self.Canvas.CanvasSize == SIZE_MAXI:
+                self.RefreshLabelsPosition(1)
+            else:
+                self.RefreshLabelsPosition(2)
             
         def AddItem(self, item):
             DebugVariableViewer.AddItem(self, item)
@@ -1150,7 +1219,7 @@
                     
                     if self.CursorTick is not None and start_tick <= self.CursorTick <= end_tick:
                         if self.VLine is None:
-                            self.VLine = self.Axes.axvline(self.CursorTick, color='r')
+                            self.VLine = self.Axes.axvline(self.CursorTick, color=cursor_color)
                         else:
                             self.VLine.set_xdata((self.CursorTick, self.CursorTick))
                         self.VLine.set_visible(True)
@@ -1184,11 +1253,11 @@
                         
                         if self.CursorTick is not None and start_tick <= self.CursorTick <= end_tick:
                             if self.VLine is None:
-                                self.VLine = self.Axes.axvline(x_cursor, color='r')
+                                self.VLine = self.Axes.axvline(x_cursor, color=cursor_color)
                             else:
                                 self.VLine.set_xdata((x_cursor, x_cursor))
                             if self.HLine is None:
-                                self.HLine = self.Axes.axhline(y_cursor, color='r')
+                                self.HLine = self.Axes.axhline(y_cursor, color=cursor_color)
                             else:
                                 self.HLine.set_ydata((y_cursor, y_cursor))
                             self.VLine.set_visible(True)
@@ -1218,7 +1287,7 @@
                                                      ("ys", numpy.array([y_cursor, y_cursor])),
                                                      ("zs", numpy.array([z_cursor, z_cursor]))]:
                                     kwargs.setdefault(param, value)
-                                kwargs["color"] = 'r'
+                                kwargs["color"] = cursor_color
                                 self.Axes.plot(**kwargs)
                     
                 self.Axes.set_xlim(x_min, x_max)
@@ -1230,27 +1299,19 @@
             else:
                 values, forced = apply(zip, [(item.GetValue(), item.IsForced()) for item in self.Items])
             labels = [item.GetVariable(variable_name_mask) for item in self.Items]
-            colors = map(lambda x: {True: 'b', False: 'k'}[x], forced)
-            if self.GraphType == GRAPH_PARALLEL:
-                if self.Legend is None:
-                    self.Legend = self.Axes.legend(self.Plots, labels, 
-                        loc="upper left", frameon=False, prop={'size':'small'}, 
-                        title = '.'.join(variable_name_mask))
-                    self.Legend.get_title().set_fontsize('small')
-                for t, color in zip(self.Legend.get_texts(), colors):
-                    t.set_color(color)
-            else:
-                self.Legend = None
-                if self.Is3DCanvas():
-                    self.Axes.set_xlabel(labels[0], fontdict={'size':'small','color':colors[0]})
-                    self.Axes.set_ylabel(labels[1], fontdict={'size':'small','color':colors[1]})
-                    self.Axes.set_zlabel(labels[2], fontdict={'size':'small','color':colors[2]})
-                else:
-                    for label, text, color in zip(self.AxesLabels, labels, colors):
-                        label.set_text(text)
-                        label.set_color(color)
-            for label, value in zip(self.Labels, values):
+            styles = map(lambda x: {True: 'italic', False: 'normal'}[x], forced)
+            self.MaskLabel.set_text('.'.join(variable_name_mask))
+            if self.Is3DCanvas():
+                for idx, label_func in enumerate([self.Axes.set_xlabel, 
+                                                  self.Axes.set_ylabel,
+                                                  self.Axes.set_zlabel]):
+                    label_func(labels[idx], fontdict={'size': 'small','color': color_cycle[idx]})
+            else:
+                for label, text in zip(self.AxesLabels, labels):
+                    label.set_text(text)
+            for label, value, style in zip(self.Labels, values, styles):
                 label.set_text(value)
+                label.set_style(style)
                         
             self.Canvas.draw()
     
--- a/controls/PouInstanceVariablesPanel.py	Sun Feb 17 23:47:41 2013 +0100
+++ b/controls/PouInstanceVariablesPanel.py	Tue Feb 19 00:06:59 2013 +0100
@@ -26,6 +26,13 @@
 import wx.lib.buttons
 import wx.lib.agw.customtreectrl as CT
 
+try:
+    import matplotlib
+    matplotlib.use('WX')
+    USE_MPL = True
+except:
+    USE_MPL = False
+
 from PLCControler import ITEMS_VARIABLE, ITEM_CONFIGURATION, ITEM_RESOURCE, ITEM_POU, ITEM_TRANSITION, ITEM_ACTION
 from util.BitmapLibrary import GetBitmap
 
@@ -146,7 +153,7 @@
                     
                 buttons = []
                 if var_infos["class"] in ITEMS_VARIABLE:
-                    if (var_infos["debug"] and self.Debug and
+                    if (not USE_MPL and var_infos["debug"] and self.Debug and
                         (self.Controller.IsOfType(var_infos["type"], "ANY_NUM", True) or
                          self.Controller.IsOfType(var_infos["type"], "ANY_BIT", True))):
                         graph_button = wx.lib.buttons.GenBitmapButton(panel, 
Binary file images/maximize_graph.png has changed
Binary file images/middle_graph.png has changed
--- a/images/plcopen_icons.svg	Sun Feb 17 23:47:41 2013 +0100
+++ b/images/plcopen_icons.svg	Tue Feb 19 00:06:59 2013 +0100
@@ -10381,7 +10381,7 @@
        x2="16.330999"
        y1="13.023"
        x1="36.011002"
-       gradientTransform="matrix(0.44717834,-0.00406845,-0.01961605,0.44676642,-78.185829,253.58647)"
+       gradientTransform="matrix(0.44717834,-0.00406845,-0.01961605,0.44676642,-30.185829,253.58647)"
        gradientUnits="userSpaceOnUse"
        id="linearGradient6801-3"
        xlink:href="#linearGradient7916-9-1"
@@ -10406,7 +10406,7 @@
        x2="81.401299"
        y2="234.28734"
        gradientUnits="userSpaceOnUse"
-       gradientTransform="matrix(0.96236471,0,0,5.1478585,-150.36967,-920.8619)" />
+       gradientTransform="matrix(0.96236471,0,0,5.1478585,-102.36967,-920.8619)" />
     <linearGradient
        id="linearGradient3234-0-5"
        y2="22"
@@ -10815,6 +10815,109 @@
          id="feGaussianBlur3409-6"
          stdDeviation="0.82668559" />
     </filter>
+    <linearGradient
+       y2="32.702"
+       x2="16.330999"
+       y1="13.023"
+       x1="36.011002"
+       gradientTransform="matrix(0.44717834,-0.00406845,-0.01961605,0.44676642,-81.135865,253.52592)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient6801-3-1"
+       xlink:href="#linearGradient7916-9-1-2"
+       inkscape:collect="always" />
+    <linearGradient
+       id="linearGradient7916-9-1-2">
+      <stop
+         id="stop7918-2-7-1"
+         style="stop-color:#fff"
+         offset="0" />
+      <stop
+         id="stop7920-4-4-9"
+         style="stop-color:#fff;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3234-0-5-8"
+       id="linearGradient6750-4-2"
+       x1="89.006996"
+       y1="228.31628"
+       x2="81.401299"
+       y2="234.28734"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.96236471,0,0,5.1478585,-153.3197,-920.92248)" />
+    <linearGradient
+       id="linearGradient3234-0-5-8"
+       y2="22"
+       gradientUnits="userSpaceOnUse"
+       x2="11.192"
+       gradientTransform="matrix(0.57895,0,0,0.61111,1.0526,0.36108)"
+       y1="3"
+       x1="11.192">
+      <stop
+         id="stop3205-5-5-8"
+         style="stop-color:#777976"
+         offset="0" />
+      <stop
+         id="stop3207-15-6-0"
+         style="stop-color:#565755"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5528-8-9"
+       y2="39.924"
+       gradientUnits="userSpaceOnUse"
+       x2="21.780001"
+       gradientTransform="matrix(0.45455,0,0,0.45902,-3.3637,-2.6312)"
+       y1="8.5762997"
+       x1="21.865999">
+      <stop
+         id="stop2783-3-7-9"
+         style="stop-color:#505050"
+         offset="0" />
+      <stop
+         id="stop6301-8-3-3"
+         style="stop-color:#6e6e6e"
+         offset=".13216" />
+      <stop
+         id="stop2785-2-9-6"
+         style="stop-color:#8c8c8c"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5525-3-1"
+       y2="15.044"
+       gradientUnits="userSpaceOnUse"
+       x2="16.075001"
+       gradientTransform="matrix(0.41935,0,0,0.41379,-2.4838,-1.431)"
+       y1="9.0734997"
+       x1="16.034">
+      <stop
+         id="stop3692-4-8-3"
+         style="stop-color:#fff"
+         offset="0" />
+      <stop
+         id="stop3694-1-8-1"
+         style="stop-color:#fff;stop-opacity:.46875"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5522-9-3"
+       y2="40"
+       gradientUnits="userSpaceOnUse"
+       x2="24"
+       gradientTransform="matrix(0.36842,0,0,0.48002992,-0.8421,-4.2013409)"
+       y1="13"
+       x1="24">
+      <stop
+         id="stop6459-0-4-8"
+         style="stop-color:#fff;stop-opacity:.94118"
+         offset="0" />
+      <stop
+         id="stop6461-6-4-9"
+         style="stop-color:#fff;stop-opacity:.70588"
+         offset="1" />
+    </linearGradient>
   </defs>
   <sodipodi:namedview
      id="base"
@@ -10824,8 +10927,8 @@
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
      inkscape:zoom="1.9999999"
-     inkscape:cx="-254.99706"
-     inkscape:cy="-254.41678"
+     inkscape:cx="104.95693"
+     inkscape:cy="-309.97229"
      inkscape:document-units="px"
      inkscape:current-layer="layer1"
      width="16px"
@@ -15568,24 +15671,24 @@
     <text
        xml:space="preserve"
        style="font-size:4.49727678px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans"
-       x="-93.511536"
+       x="-45.511536"
        y="255.83347"
        id="text3638-3-3-2-0-9-84-1-0-7-8-0"><tspan
          sodipodi:role="line"
-         x="-93.511536"
+         x="-45.511536"
          y="255.83347"
          id="tspan5713-0">%%maximize_graph%%</tspan></text>
     <rect
        inkscape:label="#rect3636"
        y="259.08347"
-       x="-75.261528"
+       x="-27.261528"
        height="16"
        width="16"
        id="maximize_graph"
        style="opacity:0;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
     <g
        style="display:inline"
-       transform="translate(-75.199039,258.45845)"
+       transform="translate(-27.199039,258.45845)"
        id="g5498-6">
       <rect
          id="rect1887-0-5"
@@ -15613,14 +15716,14 @@
        style="color:#000000;fill:url(#linearGradient6750-4);fill-opacity:1;fill-rule:nonzero;stroke:#565853;stroke-width:0.99638373;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
        id="rect5849-1"
        width="11.037974"
-       height="8.9308977"
-       x="-72.785027"
-       y="263.14575" />
+       height="9.2590332"
+       x="-24.785027"
+       y="262.81763" />
     <path
        inkscape:connector-curvature="0"
        id="path7076-0-1"
        style="opacity:0.3;fill:none;stroke:url(#linearGradient6801-3);stroke-width:1;stroke-linecap:square;display:inline"
-       d="m -62.761059,271.04244 0.004,-6.90262 -9.03453,0.0166"
+       d="m -14.761059,271.04244 0.004,-7.23075 -9.03453,0.0166"
        sodipodi:nodetypes="ccc" />
     <g
        transform="translate(-261.99974,259.1098)"
@@ -15717,5 +15820,62 @@
          d="m 31.844,17.125 c -0.003,-2.113 0.006,-4.2261 -0.0047,-6.339 -0.034,-1.6107 -0.543,-3.2452 -1.612,-4.4726 -1.355,-1.5843 -3.379,-2.4626 -5.414,-2.7423 -0.487,-0.0673 -0.978,-0.1013 -1.469,-0.1023 -2.075,0.00558 -4.1949,0.54216 -5.9005,1.7543 -1.1825,0.83615 -2.1108,2.0716 -2.4313,3.4977 -0.22328,0.90342 -0.15731,1.8388 -0.19188,2.7603 -0.03376,1.798 -0.06753,3.5959 -0.10129,5.3939"
          transform="matrix(0.29233,0,0,0.31489,6.45176,0.31291)" />
     </g>
+    <text
+       xml:space="preserve"
+       style="font-size:4.49727678px;font-style:normal;font-weight:normal;fill:#000000;fill-opacity:1;stroke:none;display:inline;font-family:Bitstream Vera Sans"
+       x="-94.52224"
+       y="255.6499"
+       id="text3638-3-3-2-0-9-84-1-0-7-8-0-7"><tspan
+         sodipodi:role="line"
+         x="-94.52224"
+         y="255.6499"
+         id="tspan5713-0-6">%%middle_graph%%</tspan></text>
+    <rect
+       inkscape:label="#rect3636"
+       y="259.02292"
+       x="-78.211563"
+       height="16"
+       width="16"
+       id="middle_graph"
+       style="opacity:0;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" />
+    <g
+       style="display:inline"
+       transform="translate(-78.149075,258.3979)"
+       id="g5498-6-6">
+      <rect
+         id="rect1887-0-5-9"
+         style="fill:url(#linearGradient5528-8-9);stroke:#565853;stroke-width:0.99993002;stroke-linejoin:round"
+         height="14"
+         width="15"
+         y="1.5"
+         x="0.49996999" />
+      <rect
+         id="rect2779-3-7-6"
+         style="opacity:0.2;fill:none;stroke:url(#linearGradient5525-3-1);stroke-width:1.00010002"
+         height="12"
+         width="13"
+         y="2.5000999"
+         x="1.5001" />
+      <rect
+         id="rect6287-4-9-3"
+         style="fill:url(#linearGradient5522-9-3)"
+         height="12.960938"
+         width="14"
+         y="2.0390625"
+         x="1" />
+    </g>
+    <rect
+       style="color:#000000;fill:url(#linearGradient6750-4-2);fill-opacity:1;fill-rule:nonzero;stroke:#565853;stroke-width:0.99638373;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+       id="rect5849-1-0"
+       width="11.037971"
+       height="6.2522917"
+       x="-75.735062"
+       y="265.76382" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path7076-0-1-1"
+       style="opacity:0.3;fill:none;stroke:url(#linearGradient6801-3-1);stroke-width:1;stroke-linecap:square;display:inline"
+       d="m -65.711095,270.98189 0.004,-4.20727 -9.03453,0.0166"
+       sodipodi:nodetypes="ccc" />
   </g>
 </svg>