1 from CurveBrowserView import CurveBrowserView
2 from PlotManager import PlotManager
3 from CurveTabsView import CurveTabsView
4 from CurveModel import CurveModel
5 from TableModel import TableModel
6 from utils import Logger
9 class PlotController(object):
10 """ Controller for 2D curve plotting functionalities.
12 __UNIQUE_INSTANCE = None # my poor impl. of a singleton
14 ## For testing purposes:
15 WITH_CURVE_BROWSER = True
16 WITH_CURVE_TABS = True
18 def __init__(self, sgPyQt=None):
19 if self.__UNIQUE_INSTANCE is None:
20 self.__trueInit(sgPyQt)
22 raise Exception("The PlotController must be a singleton - use GetInstance()")
24 def __trueInit(self, sgPyQt=None):
27 sgPyQt = SalomePyQt.SalomePyQt()
30 self._browserContextualMenu = None
31 self._blockNotifications = False
32 self._blockViewClosing = False
35 self._plotManager = PlotManager(self)
37 if self.WITH_CURVE_BROWSER:
38 self._curveBrowserView = CurveBrowserView(self)
39 self.associate(self._plotManager, self._curveBrowserView)
41 self._curveBrowserView = None
42 if self.WITH_CURVE_TABS:
43 self._curveTabsView = CurveTabsView(self)
44 self.associate(self._plotManager, self._curveTabsView)
46 self._curveTabsView = None
47 PlotController.__UNIQUE_INSTANCE = self
50 def GetInstance(cls, sgPyQt=None):
51 if cls.__UNIQUE_INSTANCE is None:
52 # First instanciation:
53 PlotController(sgPyQt)
54 return cls.__UNIQUE_INSTANCE
58 cls.__UNIQUE_INSTANCE = None
60 def setFixedSizeWidget(self):
61 """ For testing purposes - ensure visible Qt widgets have a fixed size.
63 if self.WITH_CURVE_BROWSER:
64 self._curveBrowserView.treeWidget.resize(100,200)
65 if self.WITH_CURVE_TABS:
66 self._sgPyQt._tabWidget.resize(600,600)
68 def associate(self, model, view):
70 Associates a model to a view, and sets the view to listen to this model
73 :param model: Model -- The model to be associated to the view.
74 :param view: View -- The view.
77 if model is None or view is None:
81 self.setModelListener(model, view)
83 def setModelListener(self, model, view):
85 Sets a view to listen to all changes of the given model
87 l = self._modelViews.setdefault(model, [])
88 if not view in l and view is not None:
91 def removeModelListeners(self, model):
93 Removes the given model from the list of listeners. All views previously connected to this model
94 won't receive its update notification anymore.
96 self._modelViews.pop(model)
98 def notify(self, model, what=""):
100 Notifies the view when model changes.
102 :param model: Model -- The updated model.
104 if model is None or self._blockNotifications:
107 if model not in self._modelViews:
110 for view in self._modelViews[model]:
111 method = "on%s" % what
112 if what != "" and what is not None and hasattr(view, method):
113 exec("view.%s()" % method)
114 elif hasattr(view, "update"):
118 def setBrowserContextualMenu(self, menu):
119 """ Provide a menu to be contextually shown in the curve browser """
120 self._browserContextualMenu = menu
122 def setCurvePlotRequestingClose(self, bool):
123 self._blockViewClosing = bool
125 def onCurrentCurveChange(self):
126 ps = self._plotManager.getCurrentPlotSet()
128 crv = ps.getCurrentCurve()
131 for c in self._callbacks:
135 ##### Public static API
139 def AddCurve(cls, x, y, curve_label="", x_label="", y_label="", append=True):
140 """ Add a new curve and make the plot set where it is drawn the active one.
141 If no plot set exists, or none is active, a new plot set will be created, even if append is True.
144 @param curve_label label of the curve being ploted (optional, default to empty string). This is what is
146 @param x_label label for the X axis
147 @param y_label label for the Y axis
148 @param append whether to add the curve to the active plot set (default) or into a new one.
149 @return the id of the created curve, and the id of the corresponding plot set.
151 from XYView import XYView
152 control = cls.GetInstance()
153 pm = control._plotManager
154 t = TableModel(control)
155 data = np.transpose(np.vstack([x, y]))
157 # ensure a single Matplotlib repaint for all operations to come in AddCurve
158 prevLock = pm.isRepaintLocked()
161 curveID, plotSetID = control.plotCurveFromTable(t, x_col_index=0, y_col_index=1,
162 curve_label=curve_label, append=append)
163 ps = pm._plotSets[plotSetID]
165 ps.setXLabel(x_label)
167 ps.setYLabel(y_label)
170 return curveID, plotSetID
173 def ExtendCurve(cls, crv_id, x, y):
174 """ Add new points to an already created curve
175 @raise if invalid plot set ID is given
177 control = cls.GetInstance()
178 ps = control._plotManager.getPlotSetContainingCurve(crv_id)
180 raise ValueError("Curve ID (%d) not found for extension!" % crv_id)
181 crv_mod = ps._curves[crv_id]
182 data = np.transpose(np.vstack([x, y]))
183 crv_mod.extendData(data)
186 def ResetCurve(cls, crv_id):
187 """ Reset a given curve: all data are cleared, but the curve is still
188 alive with all its attributes (color, etc ...). Mostly used in conjunction
189 with ExtendCurve above
190 @raise if invalid plot set ID is given
192 control = cls.GetInstance()
193 ps = control._plotManager.getPlotSetContainingCurve(crv_id)
195 raise ValueError("Curve ID (%d) not found for reset!" % crv_id)
196 crv_mod = ps._curves[crv_id]
200 def AddPlotSet(cls, title=""):
201 """ Creates a new plot set (a tab with several curves) and returns its ID. A title can be passed,
202 otherwise a default one will be created.
203 By default this new plot set becomes the active one.
205 control = cls.GetInstance()
206 ps = control._plotManager.createXYPlotSet()
207 control.setModelListener(ps, control._curveBrowserView)
208 # Controller itself must be notified for curve picking:
209 control.setModelListener(ps, control)
215 def CopyCurve(cls, curve_id, plot_set_id):
216 """ Copy a given curve to a given plot set ID
217 @return ID of the newly created curve
219 control = cls.GetInstance()
220 psID = cls.GetPlotSetID(curve_id)
222 raise ValueError("Curve ID (%d) not found for duplication!" % curve_id)
223 plot_set_src = control._plotManager._plotSets[psID]
224 plot_set_tgt = control._plotManager._plotSets.get(plot_set_id, None)
225 if plot_set_tgt is None:
226 raise ValueError("Plot set ID (%d) invalid for duplication!" % plot_set_id)
227 crv = plot_set_src._curves[curve_id]
228 new_crv = crv.clone()
229 control.setModelListener(new_crv, control._curveBrowserView)
230 plot_set_tgt.addCurve(new_crv)
231 return new_crv.getID()
234 def DeleteCurve(cls, curve_id=-1):
235 """ By default, delete the current curve, if any. Otherwise do nothing.
236 @return the id of the deleted curve or -1
238 Logger.Debug("Delete curve")
239 control = cls.GetInstance()
240 # Find the right plot set:
242 curve_id = cls.GetCurrentCurveID()
244 # No current curve, do nothing
247 psID = cls.GetPlotSetID(curve_id)
249 raise ValueError("Curve ID (%d) not found for deletion!" % curve_id)
250 crv = control._plotManager._plotSets[psID]._curves[curve_id]
251 control._plotManager._plotSets[psID].removeCurve(curve_id)
252 control.removeModelListeners(crv)
256 def DeletePlotSet(cls, plot_set_id=-1):
257 """ By default, delete the current plot set, if any. Otherwise do nothing.
258 This will automatically make the last added plot set the current one.
259 @return the id of the deleted plot set or -1
261 Logger.Debug("PlotController::DeletePlotSet %d" % plot_set_id)
262 control = cls.GetInstance()
263 # Find the right plot set:
264 if plot_set_id == -1:
265 plot_set_id = cls.GetCurrentPlotSetID()
266 if plot_set_id == -1:
267 # No current, do nothing
270 ps = control._plotManager.removeXYPlotSet(plot_set_id)
271 for _, crv in list(ps._curves.items()):
272 control.removeModelListeners(crv)
273 control.removeModelListeners(ps)
274 psets = control._plotManager._plotSets
276 control._plotManager.setCurrentPlotSet(list(psets.keys())[-1])
284 m = resource.getrusage(resource.RUSAGE_SELF)[2]*resource.getpagesize()/1e6
285 print("** Used memory: %.2f Mb" % m)
288 def DeleteCurrentItem(cls):
289 """ Delete currently active item, be it a plot set or a curve.
290 @return (True, plot_sed_id) if a plot set was deleted or (False, curve_id) if a curve was deleted, or (True, -1)
291 if nothing was deleted.
293 c_id = cls.GetCurrentCurveID()
294 ps_id = cls.GetCurrentPlotSetID()
297 Logger.Info("PlotController.DeleteCurrentItem(): nothing selected, nothing to delete!")
299 # Do we delete a curve or a full plot set
301 cls.DeletePlotSet(ps_id)
304 cls.DeleteCurve(c_id)
309 def ClearPlotSet(cls, ps_id=-1):
310 """ Clear all curves in a given plot set. By default clear the current plot set without deleting it,
311 if no default plot set is currently active, do nothing.
312 @return id of the cleared plot set
313 @raise if invalid plot set ID is given
315 pm = cls.GetInstance()._plotManager
317 ps_id = cls.GetCurrentPlotSetID()
320 ps = pm._plotSets.get(ps_id, None)
322 raise ValueError("Invalid plot set ID (%d)!" % ps_id)
329 # pm = cls.GetInstance()._plotManager
330 # ids = pm._plotSets.keys()
332 # cls.DeletePlotSet(i)
335 def SetXLabel(cls, x_label, plot_set_id=-1):
336 """ By default set the X axis label for the current plot set, if any. Otherwise do nothing.
337 @return True if the label was set
339 pm = cls.GetInstance()._plotManager
340 if plot_set_id == -1:
341 plot_set_id = cls.GetCurrentPlotSetID()
342 if plot_set_id == -1:
345 ps = pm._plotSets.get(plot_set_id, None)
347 raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
348 ps.setXLabel(x_label)
352 def SetYLabel(cls, y_label, plot_set_id=-1):
353 """ By default set the Y axis label for the current plot set, if any. Otherwise do nothing.
354 @return True if the label was set
356 pm = cls.GetInstance()._plotManager
357 if plot_set_id == -1:
358 plot_set_id = cls.GetCurrentPlotSetID()
359 if plot_set_id == -1:
362 ps = pm._plotSets.get(plot_set_id, None)
364 raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
365 ps.setYLabel(y_label)
369 def SetPlotSetTitle(cls, title, plot_set_id=-1):
370 """ By default set the title for the current plot set, if any. Otherwise do nothing.
371 @return True if the title was set
373 pm = cls.GetInstance()._plotManager
374 if plot_set_id == -1:
375 plot_set_id = cls.GetCurrentPlotSetID()
376 if plot_set_id == -1:
379 ps = pm._plotSets.get(plot_set_id, None)
381 raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
386 def GetPlotSetID(cls, curve_id):
387 """ @return plot set id for a given curve or -1 if invalid curve ID
389 control = cls.GetInstance()
390 cps = control._plotManager.getPlotSetContainingCurve(curve_id)
396 def GetPlotSetIDByName(cls, name):
397 """ @return the first plot set whose name matches the provided name. Otherwise returns -1
399 pm = cls.GetInstance()._plotManager
400 for _, ps in list(pm._plotSets.items()):
401 if ps._title == name:
406 def GetAllPlotSets(cls):
407 """ @return two lists: plot set names, and corresponding plot set IDs
409 pm = cls.GetInstance()._plotManager
410 it = list(pm._plotSets.items())
411 ids, inst, titles = [], [], []
413 ids, inst = list(zip(*it))
415 titles = [i.getTitle() for i in inst]
416 return list(ids), titles
419 def GetCurrentCurveID(cls):
420 """ @return current curve ID or -1 if no curve is currently active
422 control = cls.GetInstance()
423 crv = control._plotManager.getCurrentCurve()
429 def GetCurrentPlotSetID(cls):
430 """ @return current plot set ID or -1 if no plot set is currently active
432 control = cls.GetInstance()
433 cps = control._plotManager.getCurrentPlotSet()
439 def SetCurrentPlotSet(cls, ps_id):
440 """ Set the current active plot set. Use -1 to unset any current plot set.
441 @throw if invalid ps_id
443 control = cls.GetInstance()
444 control._plotManager.setCurrentPlotSet(ps_id)
447 def SetCurrentCurve(cls, crv_id):
448 """ Set the current active curve.
449 @return corresponding plot set ID
450 @throw if invalid crv_id
452 control = cls.GetInstance()
453 ps_id = control._plotManager.setCurrentCurve(crv_id)
457 def ActiveViewChanged(cls, viewID):
458 """ This method is to be plugged direclty in the activeViewChanged() slot of a standard
459 Python SALOME module so that the curve browser stays in sync with the selected SALOME view
461 control = cls.GetInstance()
462 # Get XYView from SALOME view ID
463 xyview = control._curveTabsView._XYViews.get(viewID, None)
464 if not xyview is None:
465 plotSetID = xyview.getModel().getID()
466 control._plotManager.setCurrentPlotSet(plotSetID)
469 def ToggleCurveBrowser(cls, active):
470 if cls.__UNIQUE_INSTANCE is not None:
471 raise Exception("ToggleCurveBrowser() must be invoked before doing anything in plot2D!")
472 cls.WITH_CURVE_BROWSER = active
475 def IsValidPlotSetID(cls, plot_set_id):
477 @return True if plot_set_id is the identifier of a valid and existing plot set.
479 control = cls.GetInstance()
480 return plot_set_id in control._plotManager._plotSets
483 def GetSalomeViewID(cls, plot_set_id):
485 @return the salome view ID associated to a given plot set. -1 if invalid plot_set_id
487 control = cls.GetInstance()
488 d = control._curveTabsView.mapModId2ViewId()
489 return d.get(plot_set_id, -1)
492 def OnSalomeViewTryClose(cls, salome_view_id):
493 control = cls.GetInstance()
494 if not control._blockViewClosing:
495 Logger.Debug("PlotController::OnSalomeViewTryClose %d" % salome_view_id)
496 # control._sgPyQt.setViewClosable(salome_view_id, False)
497 # Get XYView from SALOME view ID
498 xyview = control._curveTabsView._XYViews.get(salome_view_id, None)
499 if not xyview is None:
500 plotSetID = xyview.getModel().getID()
501 Logger.Debug("PlotController::OnSalomeViewTryClose internal CurvePlot view ID is %d" % plotSetID)
502 control._plotManager.removeXYPlotSet(plotSetID)
504 Logger.Warning("Internal error - could not match SALOME view ID %d with CurvePlot view!" % salome_view_id)
507 def SetCurveMarker(cls, crv_id, marker):
508 """ Change curve marker. Available markers are:
509 CURVE_MARKERS = [ "o" ,# circle
520 ">", # triangle_right
530 @raise if invalid curve ID or marker
532 from XYView import XYView
533 from CurveView import CurveView
534 if not marker in XYView.CURVE_MARKERS:
535 raise ValueError("Invalid marker: '%s'" % marker)
537 cont = cls.GetInstance()
538 for mod, views in list(cont._modelViews.items()):
539 if isinstance(mod, CurveModel) and mod.getID() == crv_id:
541 if isinstance(v, CurveView):
543 # Update curve display and legend:
544 v._parentXYView.repaint()
545 v._parentXYView.showHideLegend()
549 raise Exception("Invalid curve ID or curve currently not displayed (curve_id=%d)!" % crv_id)
552 def SetCurveLabel(cls, crv_id, label):
553 """ Change curve label
554 @raise if invalid curve id
556 cont = cls.GetInstance()
557 cps = cont._plotManager.getPlotSetContainingCurve(crv_id)
559 raise ValueError("Invalid curve ID: %d" % crv_id)
560 cps._curves[crv_id].setTitle(label)
563 def __XYViewOperation(cls, func, ps_id, args, kwargs):
564 """ Private. To factorize methods accessing the XYView to change a display element. """
565 from XYPlotSetModel import XYPlotSetModel
566 from XYView import XYView
568 cont = cls.GetInstance()
569 for mod, views in list(cont._modelViews.items()):
570 if isinstance(mod, XYPlotSetModel) and mod.getID() == ps_id:
572 if isinstance(v, XYView):
573 exec("v.%s(*args, **kwargs)" % func)
576 raise Exception("Invalid plot set ID or plot set currently not displayed (ps_id=%d)!" % ps_id)
580 def SetXLog(cls, ps_id, log=True):
581 """ Toggle the X axis into logarithmic scale.
582 @param ps_id plot set ID
583 @param log if set to True, log scale is used, otherwise linear scale is used
584 @raise if invalid plot set ID
586 args, kwargs = [log], {}
587 cls.__XYViewOperation("setXLog", ps_id, args, kwargs)
590 def SetYLog(cls, ps_id, log=True):
591 """ Toggle the Y axis into logarithmic scale.
592 @param ps_id plot set ID
593 @param log if set to True, log scale is used, otherwise linear scale is used
594 @raise if invalid plot set ID
596 args, kwargs = [log], {}
597 cls.__XYViewOperation("setYLog", ps_id, args, kwargs)
600 def SetXSciNotation(cls, ps_id, sciNotation=False):
601 """ Change the format (scientific notation or not) of the X axis.
602 @param ps_id plot set ID
603 @param sciNotation if set to True, scientific notation is used, otherwise plain notation is used
604 @raise if invalid plot set ID
606 args, kwargs = [sciNotation], {}
607 cls.__XYViewOperation("setXSciNotation", ps_id, args, kwargs)
610 def SetYSciNotation(cls, ps_id, sciNotation=False):
611 """ Change the format (scientific notation or not) of the Y axis.
612 @param ps_id plot set ID
613 @param sciNotation if set to True, scientific notation is used, otherwise plain notation is used
614 @raise if invalid plot set ID
616 args, kwargs = [sciNotation], {}
617 cls.__XYViewOperation("setYSciNotation", ps_id, args, kwargs)
620 def SetLegendVisible(cls, ps_id, visible=True):
621 """ Change the visibility of the legend.
622 @param ps_id plot set ID
623 @param visible if set to True, show legend, otherwise hide it.
624 @raise if invalid plot set ID
626 args, kwargs = [visible], {}
627 cls.__XYViewOperation("setLegendVisible", ps_id, args, kwargs)
631 ### More advanced functions
634 def RegisterCallback(cls, callback):
635 cont = cls.GetInstance()
636 cont._callbacks.append(callback)
639 def ClearCallbacks(cls):
640 cont = cls.GetInstance()
644 def LockRepaint(cls):
645 control = cls.GetInstance()
646 control._plotManager.lockRepaint()
649 def UnlockRepaint(cls):
650 control = cls.GetInstance()
651 control._plotManager.unlockRepaint()
653 def createTable(self, data, table_name="table"):
656 t.setTitle(table_name)
659 def plotCurveFromTable(self, table, x_col_index=0, y_col_index=1, curve_label="", append=True):
661 :returns: a tuple containing the unique curve ID and the plot set ID
663 # Regardless of 'append', we must create a view if none there:
664 if self._plotManager.getCurrentPlotSet() is None or not append:
665 ps = self._plotManager.createXYPlotSet()
666 self.setModelListener(ps, self._curveBrowserView)
667 # For curve picking, controller must listen:
668 self.setModelListener(ps, self)
669 cps_title = table.getTitle()
673 cps = self._plotManager.getCurrentPlotSet()
675 cm = CurveModel(self, table, y_col_index)
676 cm.setXAxisIndex(x_col_index)
679 tix = table.getColumnTitle(x_col_index)
684 if curve_label != "":
685 cm.setTitle(curve_label)
687 ti = table.getColumnTitle(y_col_index)
692 if cps_title != "" and cps_title is not None:
693 Logger.Debug("about to set title to: " + cps_title)
694 cps.setTitle(cps_title)
697 mp = self._curveTabsView.mapModId2ViewId()
698 xyview_id = mp[cps.getID()]
699 xyview = self._curveTabsView._XYViews[xyview_id]
701 if cps_title is None: # no plot set was created above
702 self._plotManager.setCurrentPlotSet(cps.getID())
704 # Make CurveBrowser and CurveView depend on changes in the curve itself:
705 self.setModelListener(cm, self._curveBrowserView)
706 self.setModelListener(cm, xyview._curveViews[cm.getID()])
707 # Upon change on the curve also update the full plot, notably for the auto-fit and the legend:
708 self.setModelListener(cm, xyview)
710 return cm.getID(),cps.getID()