--- 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 @@
#-------------------------------------------------------------------------------
+# 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 + @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 + elif range_min is not None: + min_value = min(min_value, range_min) + # Update maximal range value + 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 + # 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 # 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 + GraphButton(0, 0, "fit_graph", self.OnZoomFitButton)) # Add buttons for changing canvas size with predefined height
[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: + self.ZoomFit = zoom_fit + # Update button for zoom fit bitmap + self.Buttons[0].SetBitmap("full_graph" if zoom_fit else "fit_graph") 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 @@ -427,6 +496,13 @@
+ 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 @@
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 @@
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 + # Get tick range of values to display start_tick, end_tick = self.ParentWindow.GetRange()
if self.GraphType == GRAPH_PARALLEL:
- min_value = max_value = None
+ # Init list of data range for each variable displayed + # 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 - item_min_value, item_max_value = item.GetValueRange()
- min_value = item_min_value
- elif item_min_value is not None:
- min_value = min(min_value, item_min_value)
- 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.Axes.plot(data[:, 0], data[:, 1])[0])
+ # Set data to already created plot in canvas 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)
- 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 - self.VLine = self.Axes.axvline(self.CursorTick, color=CURSOR_COLOR)
+ self.VLine = self.Axes.axvline(self.CursorTick, + # Set value of vertical line if already defined self.VLine.set_xdata((self.CursorTick, self.CursorTick))
self.VLine.set_visible(True)
- 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) - 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 + 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)
- 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 + length = (min(len(x_data), len(y_data)) + if x_data is not None and y_data is not None + # Graph is orthogonal 2D + # 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 self.Axes.plot(x_data[:, 1][:length],
y_data[:, 1][:length])[0])
+ # Set data to already created plot in canvas - 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 - self.VLine = self.Axes.axvline(x_cursor, color=CURSOR_COLOR)
+ self.VLine = self.Axes.axvline(x_cursor, + # Set value of vertical line if already defined self.VLine.set_xdata((x_cursor, x_cursor))
+ # Define a horizontal line to display cursor y + # coordinate if no line is already defined - self.HLine = self.Axes.axhline(y_cursor, color=CURSOR_COLOR)
+ self.HLine = self.Axes.axhline(y_cursor, + # Set value of horizontal line if already defined 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 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 + # Remove all plots already defined in 3D canvas while len(self.Axes.lines) > 0:
- 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 + # Get common data length so that each value has an x, y length = min(length, len(z_data))
self.Axes.plot(x_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]))]:
+ ("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.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])
- 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 + 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 for idx, label_func in enumerate([self.Axes.set_xlabel,
- 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 for label, text in zip(self.AxesLabels, labels):
+ # 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):
def draw(self, drawDC=None):
--- 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 @@
xlink:href="#radialGradient4208-2"
inkscape:collect="always" />
+ id="linearGradient5533-1" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.031048,0,0,0.013668,0.77854,15.669)" + id="radialGradient5535-3" + xlink:href="#linearGradient5060-1-4" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.031048,0,0,0.013668,0.78465,15.669)" + id="linearGradient5060-1-4"> + id="radialGradient5537-8" + xlink:href="#linearGradient5060-1-4" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(-0.031048,0,0,0.013668,23.215,15.669)" + id="linearGradient5891"> + id="linearGradient5565-0" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.63636,0,0,0.62295,-3.9091,-3.1066)" + style="stop-color:#505050" + style="stop-color:#6e6e6e" + style="stop-color:#8c8c8c" + id="linearGradient5562-5-4" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.61291,0,0,0.58621,-3.3226,-2.069)" + style="stop-color:#fff" + style="stop-color:#fff;stop-opacity:.46875" + id="linearGradient5559-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.52632,0,0,0.48148,-0.63158,1.7407)" + style="stop-color:#fff;stop-opacity:.94118" + style="stop-color:#fff;stop-opacity:.70588" + id="linearGradient6639-4" + xlink:href="#linearGradient6388-8" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,1,-1,0,25.121,-26.636)" + id="linearGradient6388-8"> + style="stop-color:#73a300" + style="stop-color:#428300;stop-opacity:0" + id="linearGradient6643-1" + xlink:href="#linearGradient6388-8" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,-1,-1,0,25.121,55.879)" + id="linearGradient5912"> + style="stop-color:#73a300" + style="stop-color:#428300;stop-opacity:0" + id="linearGradient6647-7" + xlink:href="#linearGradient6388-8" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0,1,1,0,-1.1213,-26.879)" + id="linearGradient5919"> + style="stop-color:#73a300" + style="stop-color:#428300;stop-opacity:0" + gradientTransform="matrix(0,-1,1,0,-1.1213,55.879)" + gradientUnits="userSpaceOnUse" + id="linearGradient3099-5-7" + xlink:href="#linearGradient6388-8" + inkscape:collect="always" /> + id="linearGradient5926"> + style="stop-color:#73a300" + style="stop-color:#428300;stop-opacity:0" + gradientTransform="matrix(0,-1,1,0,-1.1213,55.879)" + gradientUnits="userSpaceOnUse" + id="linearGradient5947" + xlink:href="#linearGradient6388-8" + inkscape:collect="always" /> + id="linearGradient5528-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.45455,0,0,0.45902,-3.3637,-2.6312)" + style="stop-color:#505050" + style="stop-color:#6e6e6e" + style="stop-color:#8c8c8c" + id="linearGradient5525-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.41935,0,0,0.41379,-2.4838,-1.431)" + style="stop-color:#fff" + style="stop-color:#fff;stop-opacity:.46875" + id="linearGradient5522-2" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.36842,0,0,0.33333,-0.8421,1.6667)" + style="stop-color:#fff;stop-opacity:.94118" + style="stop-color:#fff;stop-opacity:.70588" + id="linearGradient5528-5" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.45455,0,0,0.45902,-3.3637,-2.6312)" + style="stop-color:#505050" + style="stop-color:#6e6e6e" + style="stop-color:#8c8c8c" + id="linearGradient5525-31" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.41935,0,0,0.41379,-2.4838,-1.431)" + style="stop-color:#fff" + style="stop-color:#fff;stop-opacity:.46875" + id="linearGradient5522-7" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.36842,0,0,0.33333,-0.8421,1.6667)" + style="stop-color:#fff;stop-opacity:.94118" + style="stop-color:#fff;stop-opacity:.70588" @@ -11122,9 +11434,9 @@
- 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"
@@ -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)" />
+ 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" + id="text3638-3-3-2-0-9-8-5-5-2-6"><tspan + id="tspan3640-1-8-0-6-8-4-3-9-5-4" + y="255.80313">%%fit_graph%%</tspan></text> + inkscape:label="#rect3636" + 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" /> + 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" + id="text3638-3-3-2-0-9-8-5-5-2-6-9"><tspan + id="tspan3640-1-8-0-6-8-4-3-9-5-4-0" + y="255.80313">%%full_graph%%</tspan></text> + inkscape:label="#rect3636" + 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" /> + transform="translate(119.73268,258.52291)" + style="fill:url(#linearGradient5528-2);stroke:#565853;stroke-width:0.99993002;stroke-linejoin:round" + style="opacity:0.2;fill:none;stroke:url(#linearGradient5525-2);stroke-width:1.00010002" + style="fill:url(#linearGradient5522-2)" + inkscape:connector-curvature="0" + 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" /> + inkscape:connector-curvature="0" + style="opacity:0.7;fill:#aa0000" + d="M 5.5355,13.293 2.7071,10.464 2,14 5.5355,13.293 z" /> + inkscape:connector-curvature="0" + 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" /> + inkscape:connector-curvature="0" + 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" /> + inkscape:connector-curvature="0" + style="opacity:0.7;fill:#aa0000" + d="M 5.5355,7.7071 2.7071,10.536 2,7 5.5355,7.7071 z" /> + inkscape:connector-curvature="0" + style="opacity:0.7;fill:#aa0000" + d="M 10.464,13.293 13.293,10.464 14,14 10.464,13.293 z" /> + inkscape:connector-curvature="0" + style="opacity:0.7;fill:#aa0000" + d="M 10.464,7.7071 13.293,10.536 14,7 10.464,7.7071 z" /> + transform="translate(80.18127,258.41201)" + style="fill:url(#linearGradient5528-5);stroke:#565853;stroke-width:0.99993002;stroke-linejoin:round" + style="opacity:0.2;fill:none;stroke:url(#linearGradient5525-31);stroke-width:1.00010002" + style="fill:url(#linearGradient5522-7)" + inkscape:connector-curvature="0" + 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" /> + inkscape:connector-curvature="0" + d="M 2.4645,11.707 5.2929,14.536 6,11 2.4645,11.707 z" /> + inkscape:connector-curvature="0" + 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" /> + inkscape:connector-curvature="0" + 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" /> + inkscape:connector-curvature="0" + d="M 2.4645,9.2929 5.2929,6.4645 6,10 2.4645,9.2929 z" /> + inkscape:connector-curvature="0" + d="M 13.536,11.707 10.707,14.536 10,11 l 3.5355,0.70711 z" /> + inkscape:connector-curvature="0" + d="M 13.536,9.2929 10.707,6.4645 10,10 13.536,9.2929 z" />