1 # Copyright (C) 2016-2019 CEA/DEN, EDF R&D
3 # This library is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU Lesser General Public
5 # License as published by the Free Software Foundation; either
6 # version 2.1 of the License, or (at your option) any later version.
8 # This library is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 # Lesser General Public License for more details.
13 # You should have received a copy of the GNU Lesser General Public
14 # License along with this library; if not, write to the Free Software
15 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
20 from .CurveBrowserView import CurveBrowserView
21 from .PlotManager import PlotManager
22 from .CurveTabsView import CurveTabsView
23 from .CurveModel import CurveModel
24 from .TableModel import TableModel
25 from .utils import Logger
28 class PlotController(object):
29 """ Controller for 2D curve plotting functionalities.
31 __UNIQUE_INSTANCE = None # my poor impl. of a singleton
33 ## For testing purposes:
34 WITH_CURVE_BROWSER = True
35 WITH_CURVE_TABS = True
37 def __init__(self, sgPyQt=None):
38 if self.__UNIQUE_INSTANCE is None:
39 self.__trueInit(sgPyQt)
41 raise Exception("The PlotController must be a singleton - use GetInstance()")
43 def __trueInit(self, sgPyQt=None):
46 sgPyQt = SalomePyQt.SalomePyQt()
49 self._browserContextualMenu = None
50 self._blockNotifications = False
51 self._blockViewClosing = False
54 self._plotManager = PlotManager(self)
56 if self.WITH_CURVE_BROWSER:
57 self._curveBrowserView = CurveBrowserView(self)
58 self.associate(self._plotManager, self._curveBrowserView)
60 self._curveBrowserView = None
61 if self.WITH_CURVE_TABS:
62 self._curveTabsView = CurveTabsView(self)
63 self.associate(self._plotManager, self._curveTabsView)
65 self._curveTabsView = None
66 PlotController.__UNIQUE_INSTANCE = self
69 def GetInstance(cls, sgPyQt=None):
70 if cls.__UNIQUE_INSTANCE is None:
71 # First instanciation:
72 PlotController(sgPyQt)
73 return cls.__UNIQUE_INSTANCE
77 cls.__UNIQUE_INSTANCE = None
79 def setFixedSizeWidget(self):
80 """ For testing purposes - ensure visible Qt widgets have a fixed size.
82 if self.WITH_CURVE_BROWSER:
83 self._curveBrowserView.treeWidget.resize(100,200)
84 if self.WITH_CURVE_TABS:
85 self._sgPyQt._tabWidget.resize(600,600)
87 def associate(self, model, view):
89 Associates a model to a view, and sets the view to listen to this model
92 :param model: Model -- The model to be associated to the view.
93 :param view: View -- The view.
96 if model is None or view is None:
100 self.setModelListener(model, view)
102 def setModelListener(self, model, view):
104 Sets a view to listen to all changes of the given model
106 l = self._modelViews.setdefault(model, [])
107 if not view in l and view is not None:
110 def removeModelListeners(self, model):
112 Removes the given model from the list of listeners. All views previously connected to this model
113 won't receive its update notification anymore.
115 self._modelViews.pop(model)
117 def notify(self, model, what=""):
119 Notifies the view when model changes.
121 :param model: Model -- The updated model.
123 if model is None or self._blockNotifications:
126 if model not in self._modelViews:
129 for view in self._modelViews[model]:
130 method = "on%s" % what
131 if what != "" and what is not None and hasattr(view, method):
132 exec("view.%s()" % method)
133 elif hasattr(view, "update"):
137 def setBrowserContextualMenu(self, menu):
138 """ Provide a menu to be contextually shown in the curve browser """
139 self._browserContextualMenu = menu
141 def setCurvePlotRequestingClose(self, bool):
142 self._blockViewClosing = bool
144 def onCurrentCurveChange(self):
145 ps = self._plotManager.getCurrentPlotSet()
147 crv = ps.getCurrentCurve()
150 for c in self._callbacks:
154 ##### Public static API
158 def AddCurve(cls, x, y, curve_label="", x_label="", y_label="", append=True):
159 """ Add a new curve and make the plot set where it is drawn the active one.
160 If no plot set exists, or none is active, a new plot set will be created, even if append is True.
163 @param curve_label label of the curve being ploted (optional, default to empty string). This is what is
165 @param x_label label for the X axis
166 @param y_label label for the Y axis
167 @param append whether to add the curve to the active plot set (default) or into a new one.
168 @return the id of the created curve, and the id of the corresponding plot set.
170 from .XYView import XYView
171 control = cls.GetInstance()
172 pm = control._plotManager
173 t = TableModel(control)
174 data = np.transpose(np.vstack([x, y]))
176 # ensure a single Matplotlib repaint for all operations to come in AddCurve
177 prevLock = pm.isRepaintLocked()
180 curveID, plotSetID = control.plotCurveFromTable(t, x_col_index=0, y_col_index=1,
181 curve_label=curve_label, append=append)
182 ps = pm._plotSets[plotSetID]
184 ps.setXLabel(x_label)
186 ps.setYLabel(y_label)
189 return curveID, plotSetID
192 def ExtendCurve(cls, crv_id, x, y):
193 """ Add new points to an already created curve
194 @raise if invalid plot set ID is given
196 control = cls.GetInstance()
197 ps = control._plotManager.getPlotSetContainingCurve(crv_id)
199 raise ValueError("Curve ID (%d) not found for extension!" % crv_id)
200 crv_mod = ps._curves[crv_id]
201 data = np.transpose(np.vstack([x, y]))
202 crv_mod.extendData(data)
205 def ResetCurve(cls, crv_id):
206 """ Reset a given curve: all data are cleared, but the curve is still
207 alive with all its attributes (color, etc ...). Mostly used in conjunction
208 with ExtendCurve above
209 @raise if invalid plot set ID is given
211 control = cls.GetInstance()
212 ps = control._plotManager.getPlotSetContainingCurve(crv_id)
214 raise ValueError("Curve ID (%d) not found for reset!" % crv_id)
215 crv_mod = ps._curves[crv_id]
219 def AddPlotSet(cls, title=""):
220 """ Creates a new plot set (a tab with several curves) and returns its ID. A title can be passed,
221 otherwise a default one will be created.
222 By default this new plot set becomes the active one.
224 control = cls.GetInstance()
225 ps = control._plotManager.createXYPlotSet()
226 control.setModelListener(ps, control._curveBrowserView)
227 # Controller itself must be notified for curve picking:
228 control.setModelListener(ps, control)
234 def CopyCurve(cls, curve_id, plot_set_id):
235 """ Copy a given curve to a given plot set ID
236 @return ID of the newly created curve
238 control = cls.GetInstance()
239 psID = cls.GetPlotSetID(curve_id)
241 raise ValueError("Curve ID (%d) not found for duplication!" % curve_id)
242 plot_set_src = control._plotManager._plotSets[psID]
243 plot_set_tgt = control._plotManager._plotSets.get(plot_set_id, None)
244 if plot_set_tgt is None:
245 raise ValueError("Plot set ID (%d) invalid for duplication!" % plot_set_id)
246 crv = plot_set_src._curves[curve_id]
247 new_crv = crv.clone()
248 control.setModelListener(new_crv, control._curveBrowserView)
249 plot_set_tgt.addCurve(new_crv)
250 return new_crv.getID()
253 def DeleteCurve(cls, curve_id=-1):
254 """ By default, delete the current curve, if any. Otherwise do nothing.
255 @return the id of the deleted curve or -1
257 Logger.Debug("Delete curve")
258 control = cls.GetInstance()
259 # Find the right plot set:
261 curve_id = cls.GetCurrentCurveID()
263 # No current curve, do nothing
266 psID = cls.GetPlotSetID(curve_id)
268 raise ValueError("Curve ID (%d) not found for deletion!" % curve_id)
269 crv = control._plotManager._plotSets[psID]._curves[curve_id]
270 control._plotManager._plotSets[psID].removeCurve(curve_id)
271 control.removeModelListeners(crv)
275 def DeletePlotSet(cls, plot_set_id=-1):
276 """ By default, delete the current plot set, if any. Otherwise do nothing.
277 This will automatically make the last added plot set the current one.
278 @return the id of the deleted plot set or -1
280 Logger.Debug("PlotController::DeletePlotSet %d" % plot_set_id)
281 control = cls.GetInstance()
282 # Find the right plot set:
283 if plot_set_id == -1:
284 plot_set_id = cls.GetCurrentPlotSetID()
285 if plot_set_id == -1:
286 # No current, do nothing
289 ps = control._plotManager.removeXYPlotSet(plot_set_id)
290 for _, crv in list(ps._curves.items()):
291 control.removeModelListeners(crv)
292 control.removeModelListeners(ps)
293 psets = control._plotManager._plotSets
295 control._plotManager.setCurrentPlotSet(list(psets.keys())[-1])
303 m = resource.getrusage(resource.RUSAGE_SELF)[2]*resource.getpagesize()/1e6
304 print("** Used memory: %.2f Mb" % m)
307 def DeleteCurrentItem(cls):
308 """ Delete currently active item, be it a plot set or a curve.
309 @return (True, plot_sed_id) if a plot set was deleted or (False, curve_id) if a curve was deleted, or (True, -1)
310 if nothing was deleted.
312 c_id = cls.GetCurrentCurveID()
313 ps_id = cls.GetCurrentPlotSetID()
316 Logger.Info("PlotController.DeleteCurrentItem(): nothing selected, nothing to delete!")
318 # Do we delete a curve or a full plot set
320 cls.DeletePlotSet(ps_id)
323 cls.DeleteCurve(c_id)
328 def ClearPlotSet(cls, ps_id=-1):
329 """ Clear all curves in a given plot set. By default clear the current plot set without deleting it,
330 if no default plot set is currently active, do nothing.
331 @return id of the cleared plot set
332 @raise if invalid plot set ID is given
334 pm = cls.GetInstance()._plotManager
336 ps_id = cls.GetCurrentPlotSetID()
339 ps = pm._plotSets.get(ps_id, None)
341 raise ValueError("Invalid plot set ID (%d)!" % ps_id)
348 # pm = cls.GetInstance()._plotManager
349 # ids = pm._plotSets.keys()
351 # cls.DeletePlotSet(i)
354 def SetXLabel(cls, x_label, plot_set_id=-1):
355 """ By default set the X axis label for the current plot set, if any. Otherwise do nothing.
356 @return True if the label was set
358 pm = cls.GetInstance()._plotManager
359 if plot_set_id == -1:
360 plot_set_id = cls.GetCurrentPlotSetID()
361 if plot_set_id == -1:
364 ps = pm._plotSets.get(plot_set_id, None)
366 raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
367 ps.setXLabel(x_label)
371 def SetYLabel(cls, y_label, plot_set_id=-1):
372 """ By default set the Y axis label for the current plot set, if any. Otherwise do nothing.
373 @return True if the label was set
375 pm = cls.GetInstance()._plotManager
376 if plot_set_id == -1:
377 plot_set_id = cls.GetCurrentPlotSetID()
378 if plot_set_id == -1:
381 ps = pm._plotSets.get(plot_set_id, None)
383 raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
384 ps.setYLabel(y_label)
388 def SetPlotSetTitle(cls, title, plot_set_id=-1):
389 """ By default set the title for the current plot set, if any. Otherwise do nothing.
390 @return True if the title was set
392 pm = cls.GetInstance()._plotManager
393 if plot_set_id == -1:
394 plot_set_id = cls.GetCurrentPlotSetID()
395 if plot_set_id == -1:
398 ps = pm._plotSets.get(plot_set_id, None)
400 raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
405 def GetPlotSetID(cls, curve_id):
406 """ @return plot set id for a given curve or -1 if invalid curve ID
408 control = cls.GetInstance()
409 cps = control._plotManager.getPlotSetContainingCurve(curve_id)
415 def GetPlotSetIDByName(cls, name):
416 """ @return the first plot set whose name matches the provided name. Otherwise returns -1
418 pm = cls.GetInstance()._plotManager
419 for _, ps in list(pm._plotSets.items()):
420 if ps._title == name:
425 def GetAllPlotSets(cls):
426 """ @return two lists: plot set names, and corresponding plot set IDs
428 pm = cls.GetInstance()._plotManager
429 it = list(pm._plotSets.items())
430 ids, inst, titles = [], [], []
432 ids, inst = list(zip(*it))
434 titles = [i.getTitle() for i in inst]
435 return list(ids), titles
438 def GetCurrentCurveID(cls):
439 """ @return current curve ID or -1 if no curve is currently active
441 control = cls.GetInstance()
442 crv = control._plotManager.getCurrentCurve()
448 def GetCurrentPlotSetID(cls):
449 """ @return current plot set ID or -1 if no plot set is currently active
451 control = cls.GetInstance()
452 cps = control._plotManager.getCurrentPlotSet()
458 def SetCurrentPlotSet(cls, ps_id):
459 """ Set the current active plot set. Use -1 to unset any current plot set.
460 @throw if invalid ps_id
462 control = cls.GetInstance()
463 control._plotManager.setCurrentPlotSet(ps_id)
466 def SetCurrentCurve(cls, crv_id):
467 """ Set the current active curve.
468 @return corresponding plot set ID
469 @throw if invalid crv_id
471 control = cls.GetInstance()
472 ps_id = control._plotManager.setCurrentCurve(crv_id)
476 def ActiveViewChanged(cls, viewID):
477 """ This method is to be plugged direclty in the activeViewChanged() slot of a standard
478 Python SALOME module so that the curve browser stays in sync with the selected SALOME view
480 control = cls.GetInstance()
481 # Get XYView from SALOME view ID
482 xyview = control._curveTabsView._XYViews.get(viewID, None)
483 if not xyview is None:
484 plotSetID = xyview.getModel().getID()
485 control._plotManager.setCurrentPlotSet(plotSetID)
488 def ToggleCurveBrowser(cls, active):
489 if cls.__UNIQUE_INSTANCE is not None:
490 raise Exception("ToggleCurveBrowser() must be invoked before doing anything in plot2D!")
491 cls.WITH_CURVE_BROWSER = active
494 def IsValidPlotSetID(cls, plot_set_id):
496 @return True if plot_set_id is the identifier of a valid and existing plot set.
498 control = cls.GetInstance()
499 return plot_set_id in control._plotManager._plotSets
502 def GetSalomeViewID(cls, plot_set_id):
504 @return the salome view ID associated to a given plot set. -1 if invalid plot_set_id
506 control = cls.GetInstance()
507 d = control._curveTabsView.mapModId2ViewId()
508 return d.get(plot_set_id, -1)
511 def OnSalomeViewTryClose(cls, salome_view_id):
512 control = cls.GetInstance()
513 if not control._blockViewClosing:
514 Logger.Debug("PlotController::OnSalomeViewTryClose %d" % salome_view_id)
515 # control._sgPyQt.setViewClosable(salome_view_id, False)
516 # Get XYView from SALOME view ID
517 xyview = control._curveTabsView._XYViews.get(salome_view_id, None)
518 if not xyview is None:
519 plotSetID = xyview.getModel().getID()
520 Logger.Debug("PlotController::OnSalomeViewTryClose internal CurvePlot view ID is %d" % plotSetID)
521 control._plotManager.removeXYPlotSet(plotSetID)
523 Logger.Warning("Internal error - could not match SALOME view ID %d with CurvePlot view!" % salome_view_id)
526 def SetCurveMarker(cls, crv_id, marker):
527 """ Change curve marker. Available markers are:
528 CURVE_MARKERS = [ "o" ,# circle
539 ">", # triangle_right
549 @raise if invalid curve ID or marker
551 from .XYView import XYView
552 from .CurveView import CurveView
553 if not marker in XYView.CURVE_MARKERS:
554 raise ValueError("Invalid marker: '%s'" % marker)
556 cont = cls.GetInstance()
557 for mod, views in list(cont._modelViews.items()):
558 if isinstance(mod, CurveModel) and mod.getID() == crv_id:
560 if isinstance(v, CurveView):
562 # Update curve display and legend:
563 v._parentXYView.repaint()
564 v._parentXYView.showHideLegend()
568 raise Exception("Invalid curve ID or curve currently not displayed (curve_id=%d)!" % crv_id)
571 def SetCurveLabel(cls, crv_id, label):
572 """ Change curve label
573 @raise if invalid curve id
575 cont = cls.GetInstance()
576 cps = cont._plotManager.getPlotSetContainingCurve(crv_id)
578 raise ValueError("Invalid curve ID: %d" % crv_id)
579 cps._curves[crv_id].setTitle(label)
582 def __XYViewOperation(cls, func, ps_id, args, kwargs):
583 """ Private. To factorize methods accessing the XYView to change a display element. """
584 from .XYPlotSetModel import XYPlotSetModel
585 from .XYView import XYView
587 cont = cls.GetInstance()
588 for mod, views in list(cont._modelViews.items()):
589 if isinstance(mod, XYPlotSetModel) and mod.getID() == ps_id:
591 if isinstance(v, XYView):
592 exec("v.%s(*args, **kwargs)" % func)
595 raise Exception("Invalid plot set ID or plot set currently not displayed (ps_id=%d)!" % ps_id)
599 def SetXLog(cls, ps_id, log=True):
600 """ Toggle the X axis into logarithmic scale.
601 @param ps_id plot set ID
602 @param log if set to True, log scale is used, otherwise linear scale is used
603 @raise if invalid plot set ID
605 args, kwargs = [log], {}
606 cls.__XYViewOperation("setXLog", ps_id, args, kwargs)
609 def SetYLog(cls, ps_id, log=True):
610 """ Toggle the Y axis into logarithmic scale.
611 @param ps_id plot set ID
612 @param log if set to True, log scale is used, otherwise linear scale is used
613 @raise if invalid plot set ID
615 args, kwargs = [log], {}
616 cls.__XYViewOperation("setYLog", ps_id, args, kwargs)
619 def SetXSciNotation(cls, ps_id, sciNotation=False):
620 """ Change the format (scientific notation or not) of the X axis.
621 @param ps_id plot set ID
622 @param sciNotation if set to True, scientific notation is used, otherwise plain notation is used
623 @raise if invalid plot set ID
625 args, kwargs = [sciNotation], {}
626 cls.__XYViewOperation("setXSciNotation", ps_id, args, kwargs)
629 def SetYSciNotation(cls, ps_id, sciNotation=False):
630 """ Change the format (scientific notation or not) of the Y axis.
631 @param ps_id plot set ID
632 @param sciNotation if set to True, scientific notation is used, otherwise plain notation is used
633 @raise if invalid plot set ID
635 args, kwargs = [sciNotation], {}
636 cls.__XYViewOperation("setYSciNotation", ps_id, args, kwargs)
639 def SetLegendVisible(cls, ps_id, visible=True):
640 """ Change the visibility of the legend.
641 @param ps_id plot set ID
642 @param visible if set to True, show legend, otherwise hide it.
643 @raise if invalid plot set ID
645 args, kwargs = [visible], {}
646 cls.__XYViewOperation("setLegendVisible", ps_id, args, kwargs)
650 ### More advanced functions
653 def RegisterCallback(cls, callback):
654 cont = cls.GetInstance()
655 cont._callbacks.append(callback)
658 def ClearCallbacks(cls):
659 cont = cls.GetInstance()
663 def LockRepaint(cls):
664 control = cls.GetInstance()
665 control._plotManager.lockRepaint()
668 def UnlockRepaint(cls):
669 control = cls.GetInstance()
670 control._plotManager.unlockRepaint()
672 def createTable(self, data, table_name="table"):
675 t.setTitle(table_name)
678 def plotCurveFromTable(self, table, x_col_index=0, y_col_index=1, curve_label="", append=True):
680 :returns: a tuple containing the unique curve ID and the plot set ID
682 # Regardless of 'append', we must create a view if none there:
683 if self._plotManager.getCurrentPlotSet() is None or not append:
684 ps = self._plotManager.createXYPlotSet()
685 self.setModelListener(ps, self._curveBrowserView)
686 # For curve picking, controller must listen:
687 self.setModelListener(ps, self)
688 cps_title = table.getTitle()
692 cps = self._plotManager.getCurrentPlotSet()
694 cm = CurveModel(self, table, y_col_index)
695 cm.setXAxisIndex(x_col_index)
698 tix = table.getColumnTitle(x_col_index)
703 if curve_label != "":
704 cm.setTitle(curve_label)
706 ti = table.getColumnTitle(y_col_index)
711 if cps_title != "" and cps_title is not None:
712 Logger.Debug("about to set title to: " + cps_title)
713 cps.setTitle(cps_title)
716 mp = self._curveTabsView.mapModId2ViewId()
717 xyview_id = mp[cps.getID()]
718 xyview = self._curveTabsView._XYViews[xyview_id]
720 if cps_title is None: # no plot set was created above
721 self._plotManager.setCurrentPlotSet(cps.getID())
723 # Make CurveBrowser and CurveView depend on changes in the curve itself:
724 self.setModelListener(cm, self._curveBrowserView)
725 self.setModelListener(cm, xyview._curveViews[cm.getID()])
726 # Upon change on the curve also update the full plot, notably for the auto-fit and the legend:
727 self.setModelListener(cm, xyview)
729 return cm.getID(),cps.getID()