From 78e2d8d424e739ffef7a3a386674d2e5db772b5e Mon Sep 17 00:00:00 2001
From: detlefarend <detlef@arend-privat.net>
Date: Wed, 30 Oct 2024 17:30:23 +0100
Subject: [PATCH] Bug: Missing plot refresh of tasks sharing the Matplotlib
 figure with the prior task #1076

---
 src/mlpro/bf/plot.py                          | 133 +++++++++++++-----
 src/mlpro/bf/streams/basics.py                |  12 +-
 .../bf/streams/tasks/windows/ringbuffer.py    |   2 +-
 3 files changed, 106 insertions(+), 41 deletions(-)

diff --git a/src/mlpro/bf/plot.py b/src/mlpro/bf/plot.py
index 0d83466a9..e648a982c 100644
--- a/src/mlpro/bf/plot.py
+++ b/src/mlpro/bf/plot.py
@@ -53,10 +53,14 @@
 ## --                                - New property Plottable.color
 ## --                                - Class PlotSettings: removed parameter p_plot_depth
 ## -- 2024-07-08  2.16.1    SY       Add MinVal for undefined range in DataPlotting
+## -- 2024-10-30  2.17.0    DA       Extensions on classes PlotSettings, Plottable:
+## --                                - Class PlotSettings: new methods register(), unregister(),
+## --                                  is_last_registered()
+## --                                - Class Plottable: extensions on init_plot(), update_plot() 
 ## -------------------------------------------------------------------------------------------------
 
 """
-Ver. 2.16.1 (2024-07-08)
+Ver. 2.17.0 (2024-10-30)
 
 This module provides various classes related to data plotting.
 
@@ -168,6 +172,7 @@ def __init__( self,
         self.id              = p_id
         self.view_autoselect = p_view_autoselect
         self.kwargs          = p_kwargs.copy()
+        self._registered_obj = []
 
         if ( p_plot_horizon > 0 ) and ( p_data_horizon > 0 ):
             self.plot_horizon = min(p_plot_horizon, p_data_horizon)
@@ -176,23 +181,80 @@ def __init__( self,
             self.plot_horizon    = p_plot_horizon
             self.data_horizon    = p_data_horizon
 
+        
+## -------------------------------------------------------------------------------------------------
+    def register( self, p_plot_obj : type):
+        """
+        Registers the specified plotting object. Internally used in class Plottable.
+
+        Parameters
+        ----------
+        p_plot_obj : type
+            Plotting object to be registered
+        """
+
+        self._registered_obj.append(p_plot_obj)
+
+
+## -------------------------------------------------------------------------------------------------
+    def unregister( self, p_plot_obj : type):
+        """
+        Unregisters the specified plotting object. Internally used in class Plottable.
+
+        Parameters
+        ----------
+        p_plot_obj : type
+            Plotting object to be registered
+        """
+
+        try:
+            self._registered_obj.remove(p_plot_obj)
+        except:
+            pass
+
+
+## -------------------------------------------------------------------------------------------------
+    def is_last_registered( self, p_plot_obj : type ) -> bool:
+        """
+        Checks whether the specified plot object was the last one registered. Internally used in
+        class Plottable.
+
+        Parameters
+        ----------
+        p_plot_obj : type
+            Plotting object to be registered
+
+        Returns
+        -------
+        bool
+            True, if the specified plotting object was the last one registering. False otherwise.     
+        """
+        
+        try:
+            return p_plot_obj == self._registered_obj[-1]
+        except:
+            return False
+
 
 ## -------------------------------------------------------------------------------------------------
     def copy(self):
-        return self.__class__( p_view = self.view,
-                               p_axes = self.axes,
-                               p_pos_x = self.pos_x,
-                               p_pos_y = self.pos_y,
-                               p_size_x = self.size_x,
-                               p_size_y = self.size_y,
-                               p_step_rate = self.step_rate,
-                               p_plot_horizon = self.plot_horizon,
-                               p_data_horizon = self.data_horizon,
-                               p_detail_level = self.detail_level,
-                               p_force_fg = self.force_fg,
-                               p_id = self.id,
-                               p_view_autoselect = self.view_autoselect,
-                               p_kwargs = self.kwargs )
+        duplicate = self.__class__( p_view = self.view,
+                                    p_axes = self.axes,
+                                    p_pos_x = self.pos_x,
+                                    p_pos_y = self.pos_y,
+                                    p_size_x = self.size_x,
+                                    p_size_y = self.size_y,
+                                    p_step_rate = self.step_rate,
+                                    p_plot_horizon = self.plot_horizon,
+                                    p_data_horizon = self.data_horizon,
+                                    p_detail_level = self.detail_level,
+                                    p_force_fg = self.force_fg,
+                                    p_id = self.id,
+                                    p_view_autoselect = self.view_autoselect,
+                                    p_kwargs = self.kwargs )
+
+        #duplicate._registered_obj = self._registered_obj.copy()
+        return duplicate
     
 
 
@@ -322,26 +384,27 @@ def init_plot( self,
         plt.ioff()
 
 
-         # 2 Prepare internal data structures
-
-        # 2.1 Plot settings per view
-        self.set_plot_settings( p_plot_settings=p_plot_settings )
-
-        # 2.2 Dictionary with methods for initialization and update of a plot per view 
-        self._plot_methods = { PlotSettings.C_VIEW_2D : [ self._init_plot_2d, self._update_plot_2d, self._remove_plot_2d ], 
-                               PlotSettings.C_VIEW_3D : [ self._init_plot_3d, self._update_plot_3d, self._remove_plot_3d ], 
-                               PlotSettings.C_VIEW_ND : [ self._init_plot_nd, self._update_plot_nd, self._remove_plot_nd ] }
+        # 2 Prepare internal data structures
 
+        # 2.1 Dictionary with methods for initialization and update of a plot per view 
+        self._plot_methods = { PlotSettings.C_VIEW_2D : ( self._init_plot_2d, self._update_plot_2d, self._remove_plot_2d ), 
+                               PlotSettings.C_VIEW_3D : ( self._init_plot_3d, self._update_plot_3d, self._remove_plot_3d ), 
+                               PlotSettings.C_VIEW_ND : ( self._init_plot_nd, self._update_plot_nd, self._remove_plot_nd ) }
 
-        # 3 Setup the Matplotlib host figure if no one is provided as parameter
+        # 2.2 Plot settings per view
+        self.set_plot_settings( p_plot_settings=p_plot_settings )
+        
+        # 2.3 Setup the Matplotlib host figure if no one is provided as parameter
         if p_figure is None:
             self._figure : Figure   = self._init_figure()
             self._plot_own_figure   = True
         else:
             self._figure : Figure   = p_figure
+            
+        self._plot_settings.register( p_plot_obj = self )
 
             
-        # 4 Call of all initialization methods of the required views
+        # 3 Call of all initialization methods of the required views
         view = self._plot_settings.view
         try:
             plot_method = self._plot_methods[view][0]
@@ -354,13 +417,13 @@ def init_plot( self,
             raise ImplementationError('Please set attribute "axes" in your custom _init_plot_' + view + ' method')
                 
 
-        # # 5 In standalone mode: refresh figure
+        # 4 In standalone mode: refresh figure
         if self._plot_own_figure:
             self._figure.canvas.draw()
             self._figure.canvas.flush_events()
 
 
-        # 6 Marker to ensure that initialization runs only once
+        # 5 Marker to ensure that initialization runs only once
         self._plot_initialized = True
         self._plot_first_time  = True
 
@@ -471,10 +534,10 @@ def refresh_plot(self, p_force:bool=False):
         
 
          # 1 Object has own figure or refresh is forced by caller?
-        if not self._plot_own_figure and not p_force: return
+#        if not self._plot_own_figure and not p_force: return
             
-        if self._plot_own_figure:
-            self._plot_step_counter = mod(self._plot_step_counter+1, self._plot_settings.step_rate)
+#        if self._plot_own_figure:
+        self._plot_step_counter = mod(self._plot_step_counter+1, self._plot_settings.step_rate)
         
 
         # 2 Refresh plot
@@ -589,8 +652,9 @@ def update_plot(self, **p_kwargs):
         self._plot_methods[view][1](p_settings=self._plot_settings, **p_kwargs)
 
         
-        # 4 Update content of own(!) figure after self._plot_step_rate calls
-        self.refresh_plot(p_force=False)
+        # 4 The last plotting object for the figure refreshs the plot
+        if self._plot_settings.is_last_registered( p_plot_obj = self ):
+            self.refresh_plot(p_force=True)
 
 
 ## -------------------------------------------------------------------------------------------------
@@ -665,6 +729,8 @@ def remove_plot(self, p_refresh:bool = True):
         # 3 Optionally refresh
         if p_refresh: self.refresh_plot(p_force=False)
 
+        # 4 Clear internal plot parameters
+        self._plot_settings.unregister( p_plot_obj = self )
         self._plot_first_time = True
    
 
@@ -695,6 +761,7 @@ def _remove_plot_nd(self):
         pass
 
 
+## -------------------------------------------------------------------------------------------------
     color             = property( fget = get_plot_color, fset = set_plot_color )
     plot_detail_level = property( fget = get_plot_detail_level, fset = assign_plot_detail_level )
 
diff --git a/src/mlpro/bf/streams/basics.py b/src/mlpro/bf/streams/basics.py
index 5a0572c34..272cc3c29 100644
--- a/src/mlpro/bf/streams/basics.py
+++ b/src/mlpro/bf/streams/basics.py
@@ -70,10 +70,11 @@
 ## --                                visualization 2D,3D,ND
 ## -- 2024-09-11  2.1.0     DA       Class Instance: new parent KWArgs
 ## -- 2024-10-29  2.2.0     DA       Changed definiton of InstType, InstTypeNew, InstTypeDel
+## -- 2024-10-30  2.3.0     DA       Refactoring of StreamTask.update_plot()
 ## -------------------------------------------------------------------------------------------------
 
 """
-Ver. 2.2.0 (2024-10-29)
+Ver. 2.3.0 (2024-10-30)
 
 This module provides classes for standardized data stream processing. 
 
@@ -1096,21 +1097,18 @@ def update_plot( self,
         else:
             inst = p_inst
 
-        if len(inst) == 0: return
-
         try:
             self._plot_view_finalized
         except:
-            if self._plot_settings.view_autoselect:
+            if self._plot_settings.view_autoselect and ( len(inst) > 0 ):
                 self._finalize_plot_view(p_inst_ref=next(iter(inst.values()))[1])
-
-            self._plot_view_finalized = True
+                self._plot_view_finalized = True
 
         Task.update_plot(self, p_inst=inst, **p_kwargs)
 
         self._plot_num_inst += len(inst)
 
-
+            
 ## -------------------------------------------------------------------------------------------------
     def _update_plot_2d( self, 
                          p_settings : PlotSettings, 
diff --git a/src/mlpro/bf/streams/tasks/windows/ringbuffer.py b/src/mlpro/bf/streams/tasks/windows/ringbuffer.py
index 1524ef9fc..580bbf014 100644
--- a/src/mlpro/bf/streams/tasks/windows/ringbuffer.py
+++ b/src/mlpro/bf/streams/tasks/windows/ringbuffer.py
@@ -124,7 +124,7 @@ def _run(self, p_inst : InstDict ):
 
 
         # 1 Main processing loop
-        for inst_id, (inst_type, inst)  in sorted(inst.items()):
+        for inst_id, (inst_type, inst) in sorted(inst.items()):
 
             if inst_type != InstTypeNew: 
                 # Obsolete instances need to be removed from the buffer (not yet implemented)