# HG changeset patch
# User Laurent Bessard
# Date 1371672824 -7200
# Node ID fae0809eae985313e1153619b42e3c5ff55c2546
# Parent 4282f62c1cf06110b8dcb95902cba7d4e61aa833
Added support for zooming graph so that it fits canvas size in Debug Variable Panel
diff -r 4282f62c1cf0 -r fae0809eae98 controls/DebugVariablePanel/DebugVariableGraphicViewer.py
--- 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):
diff -r 4282f62c1cf0 -r fae0809eae98 controls/DebugVariablePanel/DebugVariableItem.py
--- 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):
"""
diff -r 4282f62c1cf0 -r fae0809eae98 controls/DebugVariablePanel/GraphButton.py
--- 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
diff -r 4282f62c1cf0 -r fae0809eae98 images/fit_graph.png
Binary file images/fit_graph.png has changed
diff -r 4282f62c1cf0 -r fae0809eae98 images/full_graph.png
Binary file images/full_graph.png has changed
diff -r 4282f62c1cf0 -r fae0809eae98 images/plcopen_icons.svg
--- 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" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ %%fit_graph%%
+
+ %%full_graph%%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+