Salome HOME
updated copyright message
[modules/gui.git] / tools / CurvePlot / src / python / controller / PlotController.py
1 # Copyright (C) 2016-2023  CEA, EDF
2 #
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.
7 #
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.
12 #
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
16 #
17 # See http://www.salome-platform.org/ or email : webmaster.salome@opencascade.com
18 #
19
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
26 import numpy as np
27
28 class PlotController(object):
29   """ Controller for 2D curve plotting functionalities.
30   """
31   __UNIQUE_INSTANCE = None  # my poor impl. of a singleton
32
33   ## For testing purposes:
34   WITH_CURVE_BROWSER = True
35   WITH_CURVE_TABS = True
36
37   def __init__(self, sgPyQt=None):
38     if self.__UNIQUE_INSTANCE is None:
39       self.__trueInit(sgPyQt)
40     else:
41       raise Exception("The PlotController must be a singleton - use GetInstance()")
42
43   def __trueInit(self, sgPyQt=None):
44     if sgPyQt is None:
45       import SalomePyQt
46       sgPyQt = SalomePyQt.SalomePyQt()
47     self._sgPyQt = sgPyQt
48     self._modelViews = {}
49     self._browserContextualMenu = None
50     self._blockNotifications = False
51     self._blockViewClosing = False
52     self._callbacks = []
53
54     self._plotManager = PlotManager(self)
55
56     if self.WITH_CURVE_BROWSER:
57       self._curveBrowserView = CurveBrowserView(self)
58       self.associate(self._plotManager, self._curveBrowserView)
59     else:
60       self._curveBrowserView = None
61     if self.WITH_CURVE_TABS:
62       self._curveTabsView = CurveTabsView(self)
63       self.associate(self._plotManager, self._curveTabsView)
64     else:
65       self._curveTabsView = None
66     PlotController.__UNIQUE_INSTANCE = self
67
68   @classmethod
69   def GetInstance(cls, sgPyQt=None):
70     if cls.__UNIQUE_INSTANCE is None:
71       # First instanciation:
72       PlotController(sgPyQt)
73     return cls.__UNIQUE_INSTANCE
74
75   @classmethod
76   def Destroy(cls):
77     cls.__UNIQUE_INSTANCE = None
78
79   def setFixedSizeWidget(self):
80     """ For testing purposes - ensure visible Qt widgets have a fixed size.
81     """
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)
86
87   def associate(self, model, view):
88     """
89     Associates a model to a view, and sets the view to listen to this model 
90     changes.
91     
92     :param model: Model -- The model to be associated to the view.
93     :param view: View -- The view.
94     
95     """
96     if model is None or view is None:
97         return
98
99     view.setModel(model)
100     self.setModelListener(model, view)
101
102   def setModelListener(self, model, view):
103     """
104     Sets a view to listen to all changes of the given model
105     """
106     l = self._modelViews.setdefault(model, [])
107     if not view in l and view is not None:
108       l.append(view)
109
110   def removeModelListeners(self, model):
111     """ 
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.
114     """
115     self._modelViews.pop(model)
116
117   def notify(self, model, what=""):
118     """
119     Notifies the view when model changes.
120     
121     :param model: Model -- The updated model.
122     """
123     if model is None or self._blockNotifications:
124       return
125
126     if model not in self._modelViews:
127       return
128
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"):
134         # Generic update:
135         view.update()
136
137   def setBrowserContextualMenu(self, menu):
138     """ Provide a menu to be contextually shown in the curve browser """
139     self._browserContextualMenu = menu
140
141   def setCurvePlotRequestingClose(self, bool):
142     self._blockViewClosing = bool
143
144   def onCurrentCurveChange(self):
145     ps = self._plotManager.getCurrentPlotSet()
146     if not ps is None:
147       crv = ps.getCurrentCurve()
148       if crv is not None:
149         crv_id = crv.getID()
150         for c in self._callbacks:
151           c(crv_id)
152
153   #####
154   ##### Public static API
155   #####
156
157   @classmethod
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.
161         @param x x data
162         @param y y data
163         @param curve_label label of the curve being ploted (optional, default to empty string). This is what is
164         shown in the legend.
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.
169     """
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]))
175     t.setData(data)
176     # ensure a single Matplotlib repaint for all operations to come in AddCurve
177     prevLock = pm.isRepaintLocked()
178     if not prevLock:
179       pm.lockRepaint()
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]
183     if x_label != "":
184       ps.setXLabel(x_label)
185     if y_label != "":
186       ps.setYLabel(y_label)
187     if not prevLock:
188       pm.unlockRepaint()
189     return curveID, plotSetID
190
191   @classmethod
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
195     """
196     control = cls.GetInstance()
197     ps = control._plotManager.getPlotSetContainingCurve(crv_id)
198     if ps is None:
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)
203
204   @classmethod
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
210     """
211     control = cls.GetInstance()
212     ps = control._plotManager.getPlotSetContainingCurve(crv_id)
213     if ps is None:
214       raise ValueError("Curve ID (%d) not found for reset!" % crv_id)
215     crv_mod = ps._curves[crv_id]
216     crv_mod.resetData()
217
218   @classmethod
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.
223     """
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)
229     if title != "":
230       ps.setTitle(title)
231     return ps.getID()
232
233   @classmethod
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
237     """
238     control = cls.GetInstance()
239     psID = cls.GetPlotSetID(curve_id)
240     if psID == -1:
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()
251
252   @classmethod
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
256     """
257     Logger.Debug("Delete curve")
258     control = cls.GetInstance()
259     # Find the right plot set:
260     if curve_id == -1:
261       curve_id = cls.GetCurrentCurveID()
262       if curve_id == -1:
263         # No current curve, do nothing
264         return -1
265
266     psID = cls.GetPlotSetID(curve_id)
267     if psID == -1:
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)
272     return curve_id
273
274   @classmethod
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
279     """
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
287         return -1
288
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
294     if len(psets):
295       control._plotManager.setCurrentPlotSet(list(psets.keys())[-1])
296     return plot_set_id
297
298   @classmethod
299   def usedMem(cls):
300       import gc
301       gc.collect()
302       import resource
303       m = resource.getrusage(resource.RUSAGE_SELF)[2]*resource.getpagesize()/1e6
304       print("** Used memory: %.2f Mb" % m)
305
306   @classmethod
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.
311     """
312     c_id = cls.GetCurrentCurveID()
313     ps_id = cls.GetCurrentPlotSetID()
314     ret = True, -1
315     if ps_id == -1:
316       Logger.Info("PlotController.DeleteCurrentItem(): nothing selected, nothing to delete!")
317       return True,-1
318     # Do we delete a curve or a full plot set
319     if c_id == -1:
320       cls.DeletePlotSet(ps_id)
321       ret = True, ps_id
322     else:
323       cls.DeleteCurve(c_id)
324       ret = False, c_id
325     return ret
326
327   @classmethod
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
333     """
334     pm = cls.GetInstance()._plotManager
335     if ps_id == -1:
336       ps_id = cls.GetCurrentPlotSetID()
337       if ps_id == -1:
338         return ps_id
339     ps = pm._plotSets.get(ps_id, None)
340     if ps is None:
341       raise ValueError("Invalid plot set ID (%d)!" % ps_id)
342     ps.eraseAll()
343     return ps_id
344
345 #   @classmethod
346 #   def ClearAll(cls):
347 #     # TODO: optimize
348 #     pm = cls.GetInstance()._plotManager
349 #     ids = pm._plotSets.keys()
350 #     for i in ids:
351 #       cls.DeletePlotSet(i)
352
353   @classmethod
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
357     """
358     pm = cls.GetInstance()._plotManager
359     if plot_set_id == -1:
360       plot_set_id = cls.GetCurrentPlotSetID()
361       if plot_set_id == -1:
362         # Do nothing
363         return False
364     ps = pm._plotSets.get(plot_set_id, None)
365     if ps is None:
366       raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
367     ps.setXLabel(x_label)
368     return True
369
370   @classmethod
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
374     """
375     pm = cls.GetInstance()._plotManager
376     if plot_set_id == -1:
377       plot_set_id = cls.GetCurrentPlotSetID()
378       if plot_set_id == -1:
379         # Do nothing
380         return False
381     ps = pm._plotSets.get(plot_set_id, None)
382     if ps is None:
383       raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
384     ps.setYLabel(y_label)
385     return True
386
387   @classmethod
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
391     """
392     pm = cls.GetInstance()._plotManager
393     if plot_set_id == -1:
394       plot_set_id = cls.GetCurrentPlotSetID()
395       if plot_set_id == -1:
396         # Do nothing
397         return False
398     ps = pm._plotSets.get(plot_set_id, None)
399     if ps is None:
400       raise Exception("Invalid plot set ID (%d)!" % plot_set_id)
401     ps.setTitle(title)
402     return True
403
404   @classmethod
405   def GetPlotSetID(cls, curve_id):
406     """ @return plot set id for a given curve or -1 if invalid curve ID
407     """
408     control = cls.GetInstance()
409     cps = control._plotManager.getPlotSetContainingCurve(curve_id)
410     if cps is None:
411       return -1
412     return cps.getID()
413
414   @classmethod
415   def GetPlotSetIDByName(cls, name):
416     """ @return the first plot set whose name matches the provided name. Otherwise returns -1
417     """
418     pm = cls.GetInstance()._plotManager
419     for _, ps in list(pm._plotSets.items()):
420       if ps._title == name:
421         return ps.getID()
422     return -1
423
424   @classmethod
425   def GetAllPlotSets(cls):
426     """ @return two lists: plot set names, and corresponding plot set IDs
427     """
428     pm = cls.GetInstance()._plotManager
429     it = list(pm._plotSets.items())
430     ids, inst, titles = [], [], []
431     if len(it):
432       ids, inst = list(zip(*it))
433     if len(inst):
434       titles = [i.getTitle() for i in inst]
435     return list(ids), titles
436
437   @classmethod
438   def GetCurrentCurveID(cls):
439     """ @return current curve ID or -1 if no curve is currently active
440     """
441     control = cls.GetInstance()
442     crv = control._plotManager.getCurrentCurve()
443     if crv is None:
444       return -1
445     return crv.getID()
446
447   @classmethod
448   def GetCurrentPlotSetID(cls):
449     """ @return current plot set ID or -1 if no plot set is currently active
450     """
451     control = cls.GetInstance()
452     cps = control._plotManager.getCurrentPlotSet()
453     if cps is None:
454       return -1
455     return cps.getID()
456
457   @classmethod
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
461     """
462     control = cls.GetInstance()
463     control._plotManager.setCurrentPlotSet(ps_id)
464
465   @classmethod
466   def SetCurrentCurve(cls, crv_id):
467     """ Set the current active curve.
468     @return corresponding plot set ID
469     @throw if invalid crv_id
470     """
471     control = cls.GetInstance()
472     ps_id = control._plotManager.setCurrentCurve(crv_id)
473     return ps_id
474
475   @classmethod
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
479     """
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)
486
487   @classmethod
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
492
493   @classmethod
494   def IsValidPlotSetID(cls, plot_set_id):
495     """ 
496     @return True if plot_set_id is the identifier of a valid and existing plot set.
497     """
498     control = cls.GetInstance()
499     return plot_set_id in control._plotManager._plotSets
500
501   @classmethod
502   def GetSalomeViewID(cls, plot_set_id):
503     """
504     @return the salome view ID associated to a given plot set. -1 if invalid plot_set_id
505     """
506     control = cls.GetInstance()
507     d = control._curveTabsView.mapModId2ViewId()
508     return d.get(plot_set_id, -1)
509
510   @classmethod
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)
522       else:
523         Logger.Warning("Internal error - could not match SALOME view ID %d with CurvePlot view!" % salome_view_id)
524
525   @classmethod
526   def SetCurveMarker(cls, crv_id, marker):
527     """ Change curve marker. Available markers are:
528     CURVE_MARKERS = [ "o" ,#  circle
529                     "*",  # star
530                     "+",  # plus
531                     "x",  # x
532                     "s",  # square
533                     "p",  # pentagon
534                     "h",  # hexagon1
535                     "8",  # octagon
536                     "D",  # diamond
537                     "^",  # triangle_up
538                     "<",  # triangle_left
539                     ">",  # triangle_right
540                     "1",  # tri_down
541                     "2",  # tri_up
542                     "3",  # tri_left
543                     "4",  # tri_right
544                     "v",  # triangle_down
545                     "H",  # hexagon2
546                     "d",  # thin diamond
547                     "",   # NO MARKER
548                    ]
549     @raise if invalid curve ID or marker
550     """
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)
555
556     cont = cls.GetInstance()
557     for mod, views in list(cont._modelViews.items()):
558       if isinstance(mod, CurveModel) and mod.getID() == crv_id:
559         for v in views:
560           if isinstance(v, CurveView):
561             v.setMarker(marker)
562             # Update curve display and legend:
563             v._parentXYView.repaint()
564             v._parentXYView.showHideLegend()
565             found = True
566
567     if not found:
568       raise Exception("Invalid curve ID or curve currently not displayed (curve_id=%d)!" % crv_id)
569
570   @classmethod
571   def SetCurveLabel(cls, crv_id, label):
572     """ Change curve label
573     @raise if invalid curve id
574     """
575     cont = cls.GetInstance()
576     cps = cont._plotManager.getPlotSetContainingCurve(crv_id)
577     if cps is None:
578       raise ValueError("Invalid curve ID: %d" % crv_id)
579     cps._curves[crv_id].setTitle(label)
580
581   @classmethod
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
586
587     cont = cls.GetInstance()
588     for mod, views in list(cont._modelViews.items()):
589       if isinstance(mod, XYPlotSetModel) and mod.getID() == ps_id:
590         for v in views:
591           if isinstance(v, XYView):
592             exec("v.%s(*args, **kwargs)" % func)
593             found = True
594     if not found:
595       raise Exception("Invalid plot set ID or plot set currently not displayed (ps_id=%d)!" % ps_id)
596
597
598   @classmethod
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
604     """
605     args, kwargs = [log], {}
606     cls.__XYViewOperation("setXLog", ps_id, args, kwargs)
607
608   @classmethod
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
614     """
615     args, kwargs = [log], {}
616     cls.__XYViewOperation("setYLog", ps_id, args, kwargs)
617
618   @classmethod
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
624     """
625     args, kwargs = [sciNotation], {}
626     cls.__XYViewOperation("setXSciNotation", ps_id, args, kwargs)
627
628   @classmethod
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
634     """
635     args, kwargs = [sciNotation], {}
636     cls.__XYViewOperation("setYSciNotation", ps_id, args, kwargs)
637
638   @classmethod
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
644     """
645     args, kwargs = [visible], {}
646     cls.__XYViewOperation("setLegendVisible", ps_id, args, kwargs)
647
648
649   ###
650   ### More advanced functions
651   ###
652   @classmethod
653   def RegisterCallback(cls, callback):
654     cont = cls.GetInstance()
655     cont._callbacks.append(callback)
656
657   @classmethod
658   def ClearCallbacks(cls):
659     cont = cls.GetInstance()
660     cont._callbacks = []
661
662   @classmethod
663   def LockRepaint(cls):
664     control = cls.GetInstance()
665     control._plotManager.lockRepaint()
666
667   @classmethod
668   def UnlockRepaint(cls):
669     control = cls.GetInstance()
670     control._plotManager.unlockRepaint()
671
672   def createTable(self, data, table_name="table"):
673     t = TableModel(self)
674     t.setData(data)
675     t.setTitle(table_name)
676     return t
677
678   def plotCurveFromTable(self, table, x_col_index=0, y_col_index=1, curve_label="", append=True):
679     """
680     :returns: a tuple containing the unique curve ID and the plot set ID 
681     """
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()
689     else:
690       cps_title = None
691
692     cps = self._plotManager.getCurrentPlotSet()
693
694     cm = CurveModel(self, table, y_col_index)
695     cm.setXAxisIndex(x_col_index)
696
697     # X axis label
698     tix = table.getColumnTitle(x_col_index)
699     if tix != "":
700       cps.setXLabel(tix)
701
702     # Curve label
703     if curve_label != "":
704       cm.setTitle(curve_label)
705     else:
706       ti = table.getColumnTitle(y_col_index)
707       if ti != "":
708         cm.setTitle(ti)
709
710     # Plot set title
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)
714
715     cps.addCurve(cm)
716     mp = self._curveTabsView.mapModId2ViewId()
717     xyview_id = mp[cps.getID()]
718     xyview = self._curveTabsView._XYViews[xyview_id]
719
720     if cps_title is None:  # no plot set was created above
721       self._plotManager.setCurrentPlotSet(cps.getID())
722
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)
728
729     return cm.getID(),cps.getID()