Added support for zooming graph so that it fits canvas size in Debug Variable Panel
authorLaurent Bessard
Wed, 19 Jun 2013 22:13:44 +0200
changeset 1267 fae0809eae98
parent 1266 4282f62c1cf0
child 1268 f049c901c85b
Added support for zooming graph so that it fits canvas size in Debug Variable Panel
controls/DebugVariablePanel/DebugVariableGraphicViewer.py
controls/DebugVariablePanel/DebugVariableItem.py
controls/DebugVariablePanel/GraphButton.py
images/fit_graph.png
images/full_graph.png
images/plcopen_icons.svg
--- a/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Tue Jun 18 09:55:45 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableGraphicViewer.py	Wed Jun 19 22:13:44 2013 +0200
@@ -59,6 +59,46 @@
 CURSOR_COLOR = '#800080'
 
 #-------------------------------------------------------------------------------
+#                      Debug Variable Graphic Viewer Helpers
+#-------------------------------------------------------------------------------
+
+def merge_ranges(ranges):
+    """
+    Merge variables data range in a list to return a range of minimal min range
+    value and maximal max range value extended of 10% for keeping a padding
+    around graph in canvas
+    @param ranges: [(range_min_value, range_max_value),...]
+    @return: merged_range_min_value, merged_range_max_value
+    """
+    # Get minimal and maximal range value
+    min_value = max_value = None
+    for range_min, range_max in ranges:
+        # Update minimal range value
+        if min_value is None:
+            min_value = range_min
+        elif range_min is not None:
+            min_value = min(min_value, range_min)
+        
+        # Update maximal range value
+        if max_value is None:
+            max_value = range_max
+        elif range_min is not None:
+            max_value = max(max_value, range_max)
+    
+    # Calculate range center and width if at least one valid range is defined
+    if min_value is not None and max_value is not None:
+        center = (min_value + max_value) / 2.
+        range_size = max(1.0, max_value - min_value)
+    
+    # Set default center and with if no valid range is defined
+    else:
+        center = 0.5
+        range_size = 1.0
+    
+    # Return range expended from 10 %
+    return center - range_size * 0.55, center + range_size * 0.55
+
+#-------------------------------------------------------------------------------
 #                   Debug Variable Graphic Viewer Drop Target
 #-------------------------------------------------------------------------------
 
@@ -224,6 +264,10 @@
         # Reference to item for which contextual buttons was displayed
         self.ContextualButtonsItem = None
         
+        # Flag indicating that zoom fit current displayed data range or whole
+        # data range if False
+        self.ZoomFit = False
+        
         # Create figure for drawing graphs
         self.Figure = matplotlib.figure.Figure(facecolor='w')
         # Defined border around figure in canvas
@@ -255,6 +299,10 @@
         self.mpl_connect('button_release_event', self.OnCanvasButtonReleased)
         self.mpl_connect('scroll_event', self.OnCanvasScroll)
         
+        # Add buttons for zooming on current displayed data range
+        self.Buttons.append(
+                GraphButton(0, 0, "fit_graph", self.OnZoomFitButton))
+        
         # Add buttons for changing canvas size with predefined height
         for size, bitmap in zip(
                 [SIZE_MINI, SIZE_MIDDLE, SIZE_MAXI],
@@ -303,13 +351,34 @@
         """
         self.CursorTick = cursor_tick
     
+    def SetZoomFit(self, zoom_fit):
+        """
+        Set flag indicating that zoom fit current displayed data range
+        @param zoom_fit: Flag for zoom fit (False: zoom fit whole data range)
+        """
+        # Flag is different from the actual one 
+        if zoom_fit != self.ZoomFit:
+            # Save new flag value
+            self.ZoomFit = zoom_fit
+            
+            # Update button for zoom fit bitmap
+            self.Buttons[0].SetBitmap("full_graph" if zoom_fit else "fit_graph")
+            
+            # Refresh canvas
+            self.RefreshViewer()
+        
     def SubscribeAllDataConsumers(self):
         """
         Function that unsubscribe and remove every item that store values of
         a variable that doesn't exist in PLC anymore
         """
         DebugVariableViewer.SubscribeAllDataConsumers(self)
+        
+        # Graph still have data to display
         if not self.ItemsIsEmpty():
+            # Reset flag indicating that zoom fit current displayed data range
+            self.SetZoomFit(False)
+            
             self.ResetGraphics()
     
     def Is3DCanvas(self):
@@ -427,6 +496,13 @@
                           if item is None 
                           else [item])])
     
+    def OnZoomFitButton(self):
+        """
+        Function called when Viewer Zoom Fit button is pressed
+        """
+        # Toggle zoom fit flag value
+        self.SetZoomFit(not self.ZoomFit)
+        
     def GetOnChangeSizeButton(self, height):
         """
         Function that generate callback function for change Viewer height to
@@ -494,9 +570,8 @@
         # Graph is orthogonal
         if self.GraphType == GRAPH_ORTHOGONAL:
             # Extract items data displayed in canvas figure
-            min_start_tick = max(start_tick, self.GetItemsMinCommonTick())
-            start_tick = max(start_tick, min_start_tick)
-            end_tick = max(end_tick, min_start_tick)
+            start_tick = max(start_tick, self.GetItemsMinCommonTick())
+            end_tick = max(end_tick, start_tick)
             x_data = items[0].GetData(start_tick, end_tick)
             y_data = items[1].GetData(start_tick, end_tick)
             
@@ -769,8 +844,8 @@
     KEY_CURSOR_INCREMENT = {
         wx.WXK_LEFT: -1,
         wx.WXK_RIGHT: 1,
-        wx.WXK_UP: -10,
-        wx.WXK_DOWN: 10}
+        wx.WXK_UP: 10,
+        wx.WXK_DOWN: -10}
     
     def OnKeyDown(self, event):
         """
@@ -1043,135 +1118,241 @@
         self.Figure.subplots_adjust()
     
     def RefreshViewer(self, refresh_graphics=True):
-        
+        """
+        Function called to refresh displayed by matplotlib canvas
+        @param refresh_graphics: Flag indicating that graphs have to be
+        refreshed (False: only label values have to be refreshed)
+        """
+        # Refresh graphs if needed
         if refresh_graphics:
+            # Get tick range of values to display
             start_tick, end_tick = self.ParentWindow.GetRange()
             
+            # Graph is parallel
             if self.GraphType == GRAPH_PARALLEL:    
-                min_value = max_value = None
-                
+                # Init list of data range for each variable displayed
+                ranges = []
+                
+                # Get data and range for each variable displayed
                 for idx, item in enumerate(self.Items):
-                    data = item.GetData(start_tick, end_tick)
+                    data, min_value, max_value = item.GetDataAndValueRange(
+                                start_tick, end_tick, not self.ZoomFit)
+                    
+                    # Check that data is not empty
                     if data is not None:
-                        item_min_value, item_max_value = item.GetValueRange()
-                        if min_value is None:
-                            min_value = item_min_value
-                        elif item_min_value is not None:
-                            min_value = min(min_value, item_min_value)
-                        if max_value is None:
-                            max_value = item_max_value
-                        elif item_max_value is not None:
-                            max_value = max(max_value, item_max_value)
+                        # Add variable range to list of variable data range
+                        ranges.append((min_value, max_value))
                         
+                        # Add plot to canvas if not yet created
                         if len(self.Plots) <= idx:
                             self.Plots.append(
                                 self.Axes.plot(data[:, 0], data[:, 1])[0])
+                        
+                        # Set data to already created plot in canvas
                         else:
                             self.Plots[idx].set_data(data[:, 0], data[:, 1])
+                
+                # Get X and Y axis ranges
+                x_min, x_max = start_tick, end_tick
+                y_min, y_max = merge_ranges(ranges)
+                
+                # Display cursor in canvas if a cursor tick is defined and it is
+                # include in values tick range
+                if (self.CursorTick is not None and 
+                    start_tick <= self.CursorTick <= end_tick):
                     
-                if min_value is not None and max_value is not None:
-                    y_center = (min_value + max_value) / 2.
-                    y_range = max(1.0, max_value - min_value)
-                else:
-                    y_center = 0.5
-                    y_range = 1.0
-                x_min, x_max = start_tick, end_tick
-                y_min, y_max = y_center - y_range * 0.55, y_center + y_range * 0.55
-                
-                if self.CursorTick is not None and start_tick <= self.CursorTick <= end_tick:
+                    # Define a vertical line to display cursor position if no
+                    # line is already defined
                     if self.VLine is None:
-                        self.VLine = self.Axes.axvline(self.CursorTick, color=CURSOR_COLOR)
+                        self.VLine = self.Axes.axvline(self.CursorTick, 
+                                                       color=CURSOR_COLOR)
+                    
+                    # Set value of vertical line if already defined
                     else:
                         self.VLine.set_xdata((self.CursorTick, self.CursorTick))
                     self.VLine.set_visible(True)
-                else:
-                    if self.VLine is not None:
-                        self.VLine.set_visible(False)
+                
+                # Hide vertical line if cursor tick is not defined or reset
+                elif self.VLine is not None:
+                    self.VLine.set_visible(False)
+            
+            # Graph is orthogonal
             else:
-                min_start_tick = max(start_tick, self.GetItemsMinCommonTick())
-                start_tick = max(start_tick, min_start_tick)
-                end_tick = max(end_tick, min_start_tick)
+                # Update tick range, removing ticks that don't have a value for
+                # each variable
+                start_tick = max(start_tick, self.GetItemsMinCommonTick())
+                end_tick = max(end_tick, start_tick)
                 items = self.ItemsDict.values()
-                x_data, x_min, x_max = items[0].OrthogonalDataAndRange(start_tick, end_tick)
-                y_data, y_min, y_max = items[1].OrthogonalDataAndRange(start_tick, end_tick)
+                
+                # Get data and range for first variable (X coordinate)
+                x_data, x_min, x_max = items[0].GetDataAndValueRange(
+                                        start_tick, end_tick, not self.ZoomFit)
+                # Get data and range for second variable (Y coordinate)
+                y_data, y_min, y_max = items[1].GetDataAndValueRange(
+                                        start_tick, end_tick, not self.ZoomFit)
+                
+                # Normalize X and Y coordinates value range
+                x_min, x_max = merge_ranges([(x_min, x_max)])
+                y_min, y_max = merge_ranges([(y_min, y_max)])
+                
+                # Get X and Y coordinates for cursor if cursor tick is defined 
                 if self.CursorTick is not None:
-                    x_cursor, x_forced = items[0].GetValue(self.CursorTick, raw=True)
-                    y_cursor, y_forced = items[1].GetValue(self.CursorTick, raw=True)
-                length = 0
-                if x_data is not None and y_data is not None:  
-                    length = min(len(x_data), len(y_data))
+                    x_cursor, x_forced = items[0].GetValue(
+                                            self.CursorTick, raw=True)
+                    y_cursor, y_forced = items[1].GetValue(
+                                            self.CursorTick, raw=True)
+                
+                # Get common data length so that each value has an x and y
+                # coordinate
+                length = (min(len(x_data), len(y_data))
+                          if x_data is not None and y_data is not None
+                          else 0)
+                
+                # Graph is orthogonal 2D 
                 if len(self.Items) < 3:
+                    
+                    # Check that x and y data are not empty
                     if x_data is not None and y_data is not None:
+                        
+                        # Add plot to canvas if not yet created
                         if len(self.Plots) == 0:
                             self.Plots.append(
                                 self.Axes.plot(x_data[:, 1][:length], 
                                                y_data[:, 1][:length])[0])
+                        
+                        # Set data to already created plot in canvas
                         else:
                             self.Plots[0].set_data(
                                 x_data[:, 1][:length], 
                                 y_data[:, 1][:length])
                     
-                    if self.CursorTick is not None and start_tick <= self.CursorTick <= end_tick:
+                    # Display cursor in canvas if a cursor tick is defined and it is
+                    # include in values tick range
+                    if (self.CursorTick is not None and 
+                        start_tick <= self.CursorTick <= end_tick):
+                        
+                        # Define a vertical line to display cursor x coordinate
+                        # if no line is already defined
                         if self.VLine is None:
-                            self.VLine = self.Axes.axvline(x_cursor, color=CURSOR_COLOR)
+                            self.VLine = self.Axes.axvline(x_cursor, 
+                                                           color=CURSOR_COLOR)
+                        # Set value of vertical line if already defined
                         else:
                             self.VLine.set_xdata((x_cursor, x_cursor))
+                        
+                        
+                        # Define a horizontal line to display cursor y
+                        # coordinate if no line is already defined
                         if self.HLine is None:
-                            self.HLine = self.Axes.axhline(y_cursor, color=CURSOR_COLOR)
+                            self.HLine = self.Axes.axhline(y_cursor, 
+                                                           color=CURSOR_COLOR)
+                        # Set value of horizontal line if already defined
                         else:
                             self.HLine.set_ydata((y_cursor, y_cursor))
+                        
                         self.VLine.set_visible(True)
                         self.HLine.set_visible(True)
+                    
+                    # Hide vertical and horizontal line if cursor tick is not
+                    # defined or reset
                     else:
                         if self.VLine is not None:
                             self.VLine.set_visible(False)
                         if self.HLine is not None:
                             self.HLine.set_visible(False)
+                
+                # Graph is orthogonal 3D
                 else:
+                    # Remove all plots already defined in 3D canvas
                     while len(self.Axes.lines) > 0:
                         self.Axes.lines.pop()
-                    z_data, z_min, z_max = items[2].OrthogonalDataAndRange(start_tick, end_tick)
-                    if self.CursorTick is not None:
-                        z_cursor, z_forced = items[2].GetValue(self.CursorTick, raw=True)
-                    if x_data is not None and y_data is not None and z_data is not None:
+                    
+                    # Get data and range for third variable (Z coordinate)
+                    z_data, z_min, z_max = items[2].GetDataAndValueRange(
+                                    start_tick, end_tick, not self.ZoomFit)
+                    
+                    # Normalize Z coordinate value range
+                    z_min, z_max = merge_ranges([(z_min, z_max)])
+                    
+                    # Check that x, y and z data are not empty
+                    if (x_data is not None and y_data is not None and 
+                        z_data is not None):
+                        
+                        # Get common data length so that each value has an x, y
+                        # and z coordinate
                         length = min(length, len(z_data))
+                        
+                        # Add plot to canvas
                         self.Axes.plot(x_data[:, 1][:length],
                                        y_data[:, 1][:length],
                                        zs = z_data[:, 1][:length])
-                    self.Axes.set_zlim(z_min, z_max)
-                    if self.CursorTick is not None and start_tick <= self.CursorTick <= end_tick:
+                    
+                    # Display cursor in canvas if a cursor tick is defined and
+                    # it is include in values tick range
+                    if (self.CursorTick is not None and 
+                        start_tick <= self.CursorTick <= end_tick):
+                        
+                        # Get Z coordinate for cursor
+                        z_cursor, z_forced = items[2].GetValue(
+                                                self.CursorTick, raw=True)
+                        
+                        # Add 3 lines parallel to x, y and z axis to display
+                        # cursor position in 3D
                         for kwargs in [{"xs": numpy.array([x_min, x_max])},
                                        {"ys": numpy.array([y_min, y_max])},
                                        {"zs": numpy.array([z_min, z_max])}]:
-                            for param, value in [("xs", numpy.array([x_cursor, x_cursor])),
-                                                 ("ys", numpy.array([y_cursor, y_cursor])),
-                                                 ("zs", numpy.array([z_cursor, z_cursor]))]:
+                            for param, value in [
+                                    ("xs", numpy.array([x_cursor, x_cursor])),
+                                    ("ys", numpy.array([y_cursor, y_cursor])),
+                                    ("zs", numpy.array([z_cursor, z_cursor]))]:
                                 kwargs.setdefault(param, value)
                             kwargs["color"] = CURSOR_COLOR
                             self.Axes.plot(**kwargs)
-                
+                    
+                    # Set Z axis limits
+                    self.Axes.set_zlim(z_min, z_max)
+            
+            # Set X and Y axis limits
             self.Axes.set_xlim(x_min, x_max)
             self.Axes.set_ylim(y_min, y_max)
         
-        variable_name_mask = self.ParentWindow.GetVariableNameMask()
-        if self.CursorTick is not None:
-            values, forced = apply(zip, [item.GetValue(self.CursorTick) for item in self.Items])
-        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]
+        # Get value and forced flag for each variable displayed in graph
+        # If cursor tick is not defined get value and flag of last received
+        # or get value and flag of variable at cursor tick
+        values, forced = apply(zip, [
+                (item.GetValue(self.CursorTick)
+                 if self.CursorTick is not None
+                 else (item.GetValue(), item.IsForced()))
+                for item in self.Items])
+        
+        # Get path of each variable displayed simplified using panel variable
+        # name mask
+        labels = [item.GetVariable(self.ParentWindow.GetVariableNameMask()) 
+                  for item in self.Items]
+        
+        # Get style for each variable according to 
         styles = map(lambda x: {True: 'italic', False: 'normal'}[x], forced)
+        
+        # Graph is orthogonal 3D, set variables path as 3D axis label
         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]})
+                label_func(labels[idx], fontdict={'size': 'small',
+                                                  'color': COLOR_CYCLE[idx]})
+        
+        # Graph is not orthogonal 3D, set variables path in axes labels
         else:
             for label, text in zip(self.AxesLabels, labels):
                 label.set_text(text)
+        
+        # Set value label text and style according to value and forced flag for
+        # each variable displayed
         for label, value, style in zip(self.Labels, values, styles):
             label.set_text(value)
             label.set_style(style)
         
+        # Refresh figure
         self.draw()
 
     def draw(self, drawDC=None):
--- a/controls/DebugVariablePanel/DebugVariableItem.py	Tue Jun 18 09:55:45 2013 +0200
+++ b/controls/DebugVariablePanel/DebugVariableItem.py	Wed Jun 19 22:13:44 2013 +0200
@@ -172,24 +172,33 @@
         """
         return self.MinValue, self.MaxValue
     
-    def OrthogonalDataAndRange(self, start_tick, end_tick):
-        """
-        Return variable value range
+    def GetDataAndValueRange(self, start_tick, end_tick, full_range=True):
+        """
+        Return variable data and value range for a given tick range
         @param start_tick: Start tick of given range (default None, first data)
         @param end_tick: end tick of given range (default None, last data)
+        @param full_range: Value range is calculated on whole data (False: only
+        calculated on data in given range)
         @return: (numpy.array([(tick, value, forced),...]), 
                   min_value, max_value)
         """
-        # Calculate min_value and max_value so that range size is greater
-        # than 1.0
-        if self.MinValue is not None and self.MaxValue is not None:
-            center = (self.MinValue + self.MaxValue) / 2.
-            range = max(1.0, self.MaxValue - self.MinValue)
-        else:
-            center = 0.5
-            range = 1.0
-        return (self.GetData(start_tick, end_tick), 
-                center - range * 0.55, center + range * 0.55)
+        # Get data in given tick range
+        data = self.GetData(start_tick, end_tick)
+        
+        # Value range is calculated on whole data
+        if full_range:
+            return data, self.MinValue, self.MaxValue
+        
+        # Check that data in given range is not empty
+        values = data[:, 1]
+        if len(values) > 0:
+            # Return value range for data in given tick range
+            return (data,
+                    data[numpy.argmin(values), 1],
+                    data[numpy.argmax(values), 1])
+        
+        # Return default values
+        return data, None, None
     
     def ResetData(self):
         """
--- a/controls/DebugVariablePanel/GraphButton.py	Tue Jun 18 09:55:45 2013 +0200
+++ b/controls/DebugVariablePanel/GraphButton.py	Wed Jun 19 22:13:44 2013 +0200
@@ -46,8 +46,8 @@
         """
         # Save button position
         self.SetPosition(x, y)
-        # Get wx.Bitmap object corresponding to bitmap
-        self.Bitmap = GetBitmap(bitmap)
+        # Set button bitmap
+        self.SetBitmap(bitmap)
         
         # By default button is hide and enabled
         self.Shown = False
@@ -63,6 +63,14 @@
         # Remove reference to callback function
         self.callback = None
     
+    def SetBitmap(self, bitmap):
+        """
+        Set bitmap to use for button
+        @param bitmap: Name of bitmap to use for button
+        """
+        # Get wx.Bitmap object corresponding to bitmap
+        self.Bitmap = GetBitmap(bitmap)
+    
     def GetSize(self):
         """
         Return size of button
Binary file images/fit_graph.png has changed
Binary file images/full_graph.png has changed
--- a/images/plcopen_icons.svg	Tue Jun 18 09:55:45 2013 +0200
+++ b/images/plcopen_icons.svg	Wed Jun 19 22:13:44 2013 +0200
@@ -11114,6 +11114,318 @@
        id="radialGradient5955"
        xlink:href="#radialGradient4208-2"
        inkscape:collect="always" />
+    <linearGradient
+       id="linearGradient5533-1"
+       y2="609.51001"
+       gradientUnits="userSpaceOnUse"
+       x2="302.85999"
+       gradientTransform="matrix(0.031048,0,0,0.013668,0.77854,15.669)"
+       y1="366.64999"
+       x1="302.85999">
+      <stop
+         id="stop5050-7-1"
+         style="stop-opacity:0"
+         offset="0" />
+      <stop
+         id="stop5056-19-2"
+         offset=".5" />
+      <stop
+         id="stop5052-4-7"
+         style="stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       id="radialGradient5535-3"
+       xlink:href="#linearGradient5060-1-4"
+       gradientUnits="userSpaceOnUse"
+       cy="486.64999"
+       cx="605.71002"
+       gradientTransform="matrix(0.031048,0,0,0.013668,0.78465,15.669)"
+       r="117.14" />
+    <linearGradient
+       id="linearGradient5060-1-4">
+      <stop
+         id="stop5062-7-0"
+         offset="0" />
+      <stop
+         id="stop5064-9-6"
+         style="stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <radialGradient
+       id="radialGradient5537-8"
+       xlink:href="#linearGradient5060-1-4"
+       gradientUnits="userSpaceOnUse"
+       cy="486.64999"
+       cx="605.71002"
+       gradientTransform="matrix(-0.031048,0,0,0.013668,23.215,15.669)"
+       r="117.14" />
+    <linearGradient
+       id="linearGradient5891">
+      <stop
+         id="stop5893"
+         offset="0" />
+      <stop
+         id="stop5895-3"
+         style="stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5565-0"
+       y2="39.924"
+       gradientUnits="userSpaceOnUse"
+       x2="21.780001"
+       gradientTransform="matrix(0.63636,0,0,0.62295,-3.9091,-3.1066)"
+       y1="8.5762997"
+       x1="21.865999">
+      <stop
+         id="stop2783-4"
+         style="stop-color:#505050"
+         offset="0" />
+      <stop
+         id="stop6301-2"
+         style="stop-color:#6e6e6e"
+         offset=".13216" />
+      <stop
+         id="stop2785-4"
+         style="stop-color:#8c8c8c"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5562-5-4"
+       y2="15.044"
+       gradientUnits="userSpaceOnUse"
+       x2="16.075001"
+       gradientTransform="matrix(0.61291,0,0,0.58621,-3.3226,-2.069)"
+       y1="9.0734997"
+       x1="16.034">
+      <stop
+         id="stop3692-0"
+         style="stop-color:#fff"
+         offset="0" />
+      <stop
+         id="stop3694-8"
+         style="stop-color:#fff;stop-opacity:.46875"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5559-2"
+       y2="40"
+       gradientUnits="userSpaceOnUse"
+       x2="24"
+       gradientTransform="matrix(0.52632,0,0,0.48148,-0.63158,1.7407)"
+       y1="13"
+       x1="24">
+      <stop
+         id="stop6459-5"
+         style="stop-color:#fff;stop-opacity:.94118"
+         offset="0" />
+      <stop
+         id="stop6461-67"
+         style="stop-color:#fff;stop-opacity:.70588"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient6639-4"
+       y2="22.839001"
+       xlink:href="#linearGradient6388-8"
+       gradientUnits="userSpaceOnUse"
+       x2="33.25"
+       gradientTransform="matrix(0,1,-1,0,25.121,-26.636)"
+       y1="16.121"
+       x1="39.879002" />
+    <linearGradient
+       id="linearGradient6388-8">
+      <stop
+         id="stop6390-9"
+         style="stop-color:#73a300"
+         offset="0" />
+      <stop
+         id="stop6392-8"
+         style="stop-color:#428300;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient6643-1"
+       y2="22.839001"
+       xlink:href="#linearGradient6388-8"
+       gradientUnits="userSpaceOnUse"
+       x2="33.25"
+       gradientTransform="matrix(0,-1,-1,0,25.121,55.879)"
+       y1="16.121"
+       x1="39.879002" />
+    <linearGradient
+       id="linearGradient5912">
+      <stop
+         id="stop5914"
+         style="stop-color:#73a300"
+         offset="0" />
+      <stop
+         id="stop5916"
+         style="stop-color:#428300;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient6647-7"
+       y2="22.839001"
+       xlink:href="#linearGradient6388-8"
+       gradientUnits="userSpaceOnUse"
+       x2="33.25"
+       gradientTransform="matrix(0,1,1,0,-1.1213,-26.879)"
+       y1="16.121"
+       x1="39.879002" />
+    <linearGradient
+       id="linearGradient5919">
+      <stop
+         id="stop5921"
+         style="stop-color:#73a300"
+         offset="0" />
+      <stop
+         id="stop5923"
+         style="stop-color:#428300;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       y2="22.839001"
+       x2="33.25"
+       y1="16.121"
+       x1="39.879002"
+       gradientTransform="matrix(0,-1,1,0,-1.1213,55.879)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient3099-5-7"
+       xlink:href="#linearGradient6388-8"
+       inkscape:collect="always" />
+    <linearGradient
+       id="linearGradient5926">
+      <stop
+         id="stop5928"
+         style="stop-color:#73a300"
+         offset="0" />
+      <stop
+         id="stop5930-6"
+         style="stop-color:#428300;stop-opacity:0"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       y2="22.839001"
+       x2="33.25"
+       y1="16.121"
+       x1="39.879002"
+       gradientTransform="matrix(0,-1,1,0,-1.1213,55.879)"
+       gradientUnits="userSpaceOnUse"
+       id="linearGradient5947"
+       xlink:href="#linearGradient6388-8"
+       inkscape:collect="always" />
+    <linearGradient
+       id="linearGradient5528-2"
+       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-0"
+         style="stop-color:#505050"
+         offset="0" />
+      <stop
+         id="stop6301-1"
+         style="stop-color:#6e6e6e"
+         offset=".13216" />
+      <stop
+         id="stop2785-8"
+         style="stop-color:#8c8c8c"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5525-2"
+       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-5"
+         style="stop-color:#fff"
+         offset="0" />
+      <stop
+         id="stop3694-7"
+         style="stop-color:#fff;stop-opacity:.46875"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5522-2"
+       y2="40"
+       gradientUnits="userSpaceOnUse"
+       x2="24"
+       gradientTransform="matrix(0.36842,0,0,0.33333,-0.8421,1.6667)"
+       y1="13"
+       x1="24">
+      <stop
+         id="stop6459-3"
+         style="stop-color:#fff;stop-opacity:.94118"
+         offset="0" />
+      <stop
+         id="stop6461-4"
+         style="stop-color:#fff;stop-opacity:.70588"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5528-5"
+       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-8"
+         style="stop-color:#505050"
+         offset="0" />
+      <stop
+         id="stop6301-3"
+         style="stop-color:#6e6e6e"
+         offset=".13216" />
+      <stop
+         id="stop2785-3"
+         style="stop-color:#8c8c8c"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5525-31"
+       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-48"
+         style="stop-color:#fff"
+         offset="0" />
+      <stop
+         id="stop3694-10"
+         style="stop-color:#fff;stop-opacity:.46875"
+         offset="1" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient5522-7"
+       y2="40"
+       gradientUnits="userSpaceOnUse"
+       x2="24"
+       gradientTransform="matrix(0.36842,0,0,0.33333,-0.8421,1.6667)"
+       y1="13"
+       x1="24">
+      <stop
+         id="stop6459-7"
+         style="stop-color:#fff;stop-opacity:.94118"
+         offset="0" />
+      <stop
+         id="stop6461-7"
+         style="stop-color:#fff;stop-opacity:.70588"
+         offset="1" />
+    </linearGradient>
   </defs>
   <sodipodi:namedview
      id="base"
@@ -11122,9 +11434,9 @@
      borderopacity="1.0"
      inkscape:pageopacity="0"
      inkscape:pageshadow="2"
-     inkscape:zoom="2.828427"
-     inkscape:cx="60.957367"
-     inkscape:cy="-299.13955"
+     inkscape:zoom="7.9999995"
+     inkscape:cx="103.60893"
+     inkscape:cy="-243.21864"
      inkscape:document-units="px"
      inkscape:current-layer="layer1"
      width="16px"
@@ -16155,5 +16467,161 @@
          d="M 18.531,8.7812 V 10 A 0.51754,0.51754 0 0 1 18,10.531 H 9.4375 l 0.03125,2.9375 h 8.5312 a 0.51754,0.51754 0 0 1 0.531,0.532 v 1.1562 l 3.469,-3.281 -3.469,-3.0938 z"
          transform="translate(0,0.99987)" />
     </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="70.198586"
+       y="255.80313"
+       id="text3638-3-3-2-0-9-8-5-5-2-6"><tspan
+         sodipodi:role="line"
+         id="tspan3640-1-8-0-6-8-4-3-9-5-4"
+         x="70.198586"
+         y="255.80313">%%fit_graph%%</tspan></text>
+    <rect
+       inkscape:label="#rect3636"
+       y="259.125"
+       x="80.25"
+       height="16"
+       width="16"
+       id="fit_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" />
+    <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="108.19859"
+       y="255.80313"
+       id="text3638-3-3-2-0-9-8-5-5-2-6-9"><tspan
+         sodipodi:role="line"
+         id="tspan3640-1-8-0-6-8-4-3-9-5-4-0"
+         x="108.19859"
+         y="255.80313">%%full_graph%%</tspan></text>
+    <rect
+       inkscape:label="#rect3636"
+       y="259.125"
+       x="119.84098"
+       height="16"
+       width="16"
+       id="full_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
+       transform="translate(119.73268,258.52291)"
+       id="g5512">
+      <rect
+         id="rect1887-08"
+         style="fill:url(#linearGradient5528-2);stroke:#565853;stroke-width:0.99993002;stroke-linejoin:round"
+         height="14"
+         width="15"
+         y="1.5"
+         x="0.49996999" />
+      <rect
+         id="rect2779-5"
+         style="opacity:0.2;fill:none;stroke:url(#linearGradient5525-2);stroke-width:1.00010002"
+         height="12"
+         width="13"
+         y="2.5000999"
+         x="1.5001" />
+      <rect
+         id="rect6287-2"
+         style="fill:url(#linearGradient5522-2)"
+         height="9"
+         width="14"
+         y="6"
+         x="1" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6293-3"
+         style="fill:#ffc24c"
+         d="M 14,4.25 C 14,4.6642 13.664,5 13.25,5 12.836,5 12.5,4.6642 12.5,4.25 c 0,-0.4142 0.336,-0.75 0.75,-0.75 0.41434,0 0.75018,0.33584 0.75,0.75 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="rect5590-5"
+         style="opacity:0.7;fill:#aa0000"
+         d="M 5.5355,13.293 2.7071,10.464 2,14 5.5355,13.293 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6302"
+         style="fill:#ffc24c"
+         d="M 12,4.25 C 12,4.6642 11.664,5 11.25,5 10.836,5 10.5,4.6642 10.5,4.25 c 0,-0.4142 0.336,-0.75 0.75,-0.75 0.41434,0 0.75018,0.33584 0.75,0.75 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6304"
+         style="fill:#ffc24c"
+         d="M 10,4.25 C 10,4.6642 9.6643,5 9.25,5 8.8357,5 8.4998,4.6642 8.5,4.25 8.4998,3.8358 8.8357,3.5 9.25,3.5 9.6643,3.5 10,3.8358 10,4.25 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6312"
+         style="opacity:0.7;fill:#aa0000"
+         d="M 5.5355,7.7071 2.7071,10.536 2,7 5.5355,7.7071 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6316"
+         style="opacity:0.7;fill:#aa0000"
+         d="M 10.464,13.293 13.293,10.464 14,14 10.464,13.293 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6318"
+         style="opacity:0.7;fill:#aa0000"
+         d="M 10.464,7.7071 13.293,10.536 14,7 10.464,7.7071 z" />
+    </g>
+    <g
+       transform="translate(80.18127,258.41201)"
+       id="g5498-7">
+      <rect
+         id="rect1887-3"
+         style="fill:url(#linearGradient5528-5);stroke:#565853;stroke-width:0.99993002;stroke-linejoin:round"
+         height="14"
+         width="15"
+         y="1.5"
+         x="0.49996999" />
+      <rect
+         id="rect2779-33"
+         style="opacity:0.2;fill:none;stroke:url(#linearGradient5525-31);stroke-width:1.00010002"
+         height="12"
+         width="13"
+         y="2.5000999"
+         x="1.5001" />
+      <rect
+         id="rect6287-0"
+         style="fill:url(#linearGradient5522-7)"
+         height="9"
+         width="14"
+         y="6"
+         x="1" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6293-8"
+         style="fill:#ffc24c"
+         d="M 14,4.25 C 14,4.6642 13.664,5 13.25,5 12.836,5 12.5,4.6642 12.5,4.25 c 0,-0.4142 0.336,-0.75 0.75,-0.75 0.41434,0 0.75018,0.33584 0.75,0.75 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="rect5590-8"
+         style="fill:#73a300"
+         d="M 2.4645,11.707 5.2929,14.536 6,11 2.4645,11.707 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6302-0"
+         style="fill:#ffc24c"
+         d="M 12,4.25 C 12,4.6642 11.664,5 11.25,5 10.836,5 10.5,4.6642 10.5,4.25 c 0,-0.4142 0.336,-0.75 0.75,-0.75 0.41434,0 0.75018,0.33584 0.75,0.75 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6304-7"
+         style="fill:#ffc24c"
+         d="M 10,4.25 C 10,4.6642 9.6643,5 9.25,5 8.8357,5 8.4998,4.6642 8.5,4.25 8.4998,3.8358 8.8357,3.5 9.25,3.5 9.6643,3.5 10,3.8358 10,4.25 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6312-0"
+         style="fill:#73a300"
+         d="M 2.4645,9.2929 5.2929,6.4645 6,10 2.4645,9.2929 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6316-1"
+         style="fill:#73a300"
+         d="M 13.536,11.707 10.707,14.536 10,11 l 3.5355,0.70711 z" />
+      <path
+         inkscape:connector-curvature="0"
+         id="path6318-5"
+         style="fill:#73a300"
+         d="M 13.536,9.2929 10.707,6.4645 10,10 13.536,9.2929 z" />
+    </g>
   </g>
 </svg>