From 8650ad4bb5bf90bbb073dcb7ab3b9c1dd199ab94 Mon Sep 17 00:00:00 2001 From: michaellans Date: Tue, 25 Nov 2025 13:20:14 -0800 Subject: [PATCH 1/6] move add_curve to ControlPanel for testing --- trace/widgets/control_panel.py | 135 ++++++++++++++++++++++++--------- 1 file changed, 100 insertions(+), 35 deletions(-) diff --git a/trace/widgets/control_panel.py b/trace/widgets/control_panel.py index 45e9742..9c7f6dc 100644 --- a/trace/widgets/control_panel.py +++ b/trace/widgets/control_panel.py @@ -324,7 +324,7 @@ def curve_item_dict(self): return plot_curves - @QtCore.Slot() + """@QtCore.Slot() def add_curve(self, pv: str = None) -> "CurveItem": if pv is None and self.sender(): pv = self.sender().text() @@ -336,7 +336,103 @@ def add_curve(self, pv: str = None) -> "CurveItem": if pv.startswith("f://"): return last_axis.add_formula_curve(pv) else: - return last_axis.add_curve(pv) + return last_axis.add_curve(pv)""" + + @QtCore.Slot() + def add_curve(self, pv: str = None, channel_args: dict = None) -> "CurveItem": + """Create a new ArchivePlotCurveItem for the given PV and add it + to this AxisItem. Also creates a CurveItem widget for it. + + Parameters + ---------- + pv : str + The process variable name to create the curve for. + channel_args : dict, optional + The arguments to pass to the ArchivePlotCurveItem constructor, + by default None + + Returns + ------- + CurveItem + The created CurveItem widget. + """ + print("Add curve") + + # pv name from line edit probably? + if pv is None and self.sender(): + pv = self.sender().text() + + last_axis = self.get_last_axis_item() + if not last_axis: + last_axis = self.add_empty_axis() + print(f"New last axis: {last_axis.source.name}") + + # formula curve break here idk how that works + if pv.startswith("f://"): + return last_axis.add_formula_curve(pv) + + + # make curve w/ args + palette = self.curve_palette + color = ColorButton.index_color(len(self.plot._curves), palette=palette) + # default args values + args = { + "y_channel": pv, + "name": pv, + "color": color, + "useArchiveData": True, + } + # if channel_args given as parameter update with channel_args + if channel_args is not None: + args.update(channel_args) + + # This is probably where the axis name should get changed if auto-assigning but + # you don't actually have a PV object yet + # why is this method in axisItem + + # or get the right axis if it exits, or create it if not, then add curve to that axis! + + # add curve to plot + plot_curve_item = self.plot.addYChannel(**args) + curve_item = last_axis.make_curve_widget(plot_curve_item) + + # add curve to desired axis + if "yAxisName" in args: + print("yAxisName") + axis_name = args.get("yAxisName") + else: + print("no yAxisName") + # # set based on unit + units = curve_item.source.units + print(f"Units: {units}") + if units: + axis_name = units + else: + axis_name = None + + if axis_name: + print(f"Axis name: {axis_name}") + self.move_curve_to_axis(curve_item, axis_name) + else: + print("none") + # + # # Move to axis automatically based on unit + # # curve_item.source.unitSignal.connect(lambda unit: self.move_curve_to_axis(self, unit)) + + + # now try to get the right axis or create a new one + # try: + # # axis_name = args.get("yAxisName", "Y-Axis 0") + # axis_item = self.get_axis_item(axis_name) + # except KeyError: + # axis_item = self.get_last_axis_item() + + # if axis_item is None: + # axis_item = self.add_empty_axis(axis_name) + + # self.move_curve_to_axis(self, axis_name) + return curve_item + # return axis_item.make_curve_widget(plot_curve_item) def clear_all(self) -> None: """Clear all axes and curves from the plot and control panel.""" @@ -589,38 +685,7 @@ def make_curve_widget(self, plot_curve_item: ArchivePlotCurveItem | FormulaCurve return curve_item - def add_curve(self, pv: str, channel_args: dict = None) -> "CurveItem": - """Create a new ArchivePlotCurveItem for the given PV and add it - to this AxisItem. Also creates a CurveItem widget for it. - - Parameters - ---------- - pv : str - The process variable name to create the curve for. - channel_args : dict, optional - The arguments to pass to the ArchivePlotCurveItem constructor, - by default None - - Returns - ------- - CurveItem - The created CurveItem widget. - """ - palette = self.control_panel.curve_palette - color = ColorButton.index_color(len(self.plot._curves), palette=palette) - args = { - "y_channel": pv, - "name": pv, - "color": color, - "useArchiveData": True, - "yAxisName": self.source.name, - } - if channel_args is not None: - args.update(channel_args) - - plot_curve_item = self.plot.addYChannel(**args) - return self.make_curve_widget(plot_curve_item) def add_formula_curve(self, formula: str) -> "CurveItem": """Create a new FormulaCurveItem for the given formula and add it @@ -950,8 +1015,8 @@ def __init__(self, axis_item: AxisItem, source: ArchivePlotCurveItem | FormulaCu self.variable_name = self.control_panel.key_gen.send(self.source) self.control_panel.curve_dict[self.variable_name] = self.source - if not self.is_formula_curve(): - self.source.unitSignal.connect(lambda unit: self.control_panel.move_curve_to_axis(self, unit)) + # if not self.is_formula_curve(): + # self.source.unitSignal.connect(lambda unit: self.control_panel.move_curve_to_axis(self, unit)) self.setup_layout() From 071872c22dfe490a526976051e5983dd8ac06668 Mon Sep 17 00:00:00 2001 From: michaellans Date: Tue, 25 Nov 2025 15:54:56 -0800 Subject: [PATCH 2/6] moved auto-set axis from CurveItem init to separate function --- trace/widgets/control_panel.py | 144 ++++++++++----------------------- 1 file changed, 44 insertions(+), 100 deletions(-) diff --git a/trace/widgets/control_panel.py b/trace/widgets/control_panel.py index 9c7f6dc..4152380 100644 --- a/trace/widgets/control_panel.py +++ b/trace/widgets/control_panel.py @@ -324,7 +324,7 @@ def curve_item_dict(self): return plot_curves - """@QtCore.Slot() + @QtCore.Slot() def add_curve(self, pv: str = None) -> "CurveItem": if pv is None and self.sender(): pv = self.sender().text() @@ -334,105 +334,12 @@ def add_curve(self, pv: str = None) -> "CurveItem": last_axis = self.add_empty_axis() if pv.startswith("f://"): - return last_axis.add_formula_curve(pv) + curve = last_axis.add_formula_curve(pv) else: - return last_axis.add_curve(pv)""" - - @QtCore.Slot() - def add_curve(self, pv: str = None, channel_args: dict = None) -> "CurveItem": - """Create a new ArchivePlotCurveItem for the given PV and add it - to this AxisItem. Also creates a CurveItem widget for it. - - Parameters - ---------- - pv : str - The process variable name to create the curve for. - channel_args : dict, optional - The arguments to pass to the ArchivePlotCurveItem constructor, - by default None - - Returns - ------- - CurveItem - The created CurveItem widget. - """ - print("Add curve") + curve = last_axis.add_curve(pv) + curve.move_to_axis_from_unit() - # pv name from line edit probably? - if pv is None and self.sender(): - pv = self.sender().text() - - last_axis = self.get_last_axis_item() - if not last_axis: - last_axis = self.add_empty_axis() - print(f"New last axis: {last_axis.source.name}") - - # formula curve break here idk how that works - if pv.startswith("f://"): - return last_axis.add_formula_curve(pv) - - - # make curve w/ args - palette = self.curve_palette - color = ColorButton.index_color(len(self.plot._curves), palette=palette) - # default args values - args = { - "y_channel": pv, - "name": pv, - "color": color, - "useArchiveData": True, - } - # if channel_args given as parameter update with channel_args - if channel_args is not None: - args.update(channel_args) - - # This is probably where the axis name should get changed if auto-assigning but - # you don't actually have a PV object yet - # why is this method in axisItem - - # or get the right axis if it exits, or create it if not, then add curve to that axis! - - # add curve to plot - plot_curve_item = self.plot.addYChannel(**args) - curve_item = last_axis.make_curve_widget(plot_curve_item) - - # add curve to desired axis - if "yAxisName" in args: - print("yAxisName") - axis_name = args.get("yAxisName") - else: - print("no yAxisName") - # # set based on unit - units = curve_item.source.units - print(f"Units: {units}") - if units: - axis_name = units - else: - axis_name = None - - if axis_name: - print(f"Axis name: {axis_name}") - self.move_curve_to_axis(curve_item, axis_name) - else: - print("none") - # - # # Move to axis automatically based on unit - # # curve_item.source.unitSignal.connect(lambda unit: self.move_curve_to_axis(self, unit)) - - - # now try to get the right axis or create a new one - # try: - # # axis_name = args.get("yAxisName", "Y-Axis 0") - # axis_item = self.get_axis_item(axis_name) - # except KeyError: - # axis_item = self.get_last_axis_item() - - # if axis_item is None: - # axis_item = self.add_empty_axis(axis_name) - - # self.move_curve_to_axis(self, axis_name) - return curve_item - # return axis_item.make_curve_widget(plot_curve_item) + return curve def clear_all(self) -> None: """Clear all axes and curves from the plot and control panel.""" @@ -685,7 +592,38 @@ def make_curve_widget(self, plot_curve_item: ArchivePlotCurveItem | FormulaCurve return curve_item + def add_curve(self, pv: str, channel_args: dict = None) -> "CurveItem": + """Create a new ArchivePlotCurveItem for the given PV and add it + to this AxisItem. Also creates a CurveItem widget for it. + + Parameters + ---------- + pv : str + The process variable name to create the curve for. + channel_args : dict, optional + The arguments to pass to the ArchivePlotCurveItem constructor, + by default None + + Returns + ------- + CurveItem + The created CurveItem widget. + """ + palette = self.control_panel.curve_palette + color = ColorButton.index_color(len(self.plot._curves), palette=palette) + args = { + "y_channel": pv, + "name": pv, + "color": color, + "useArchiveData": True, + "yAxisName": self.source.name, + } + if channel_args is not None: + args.update(channel_args) + plot_curve_item = self.plot.addYChannel(**args) + + return self.make_curve_widget(plot_curve_item) def add_formula_curve(self, formula: str) -> "CurveItem": """Create a new FormulaCurveItem for the given formula and add it @@ -1015,11 +953,17 @@ def __init__(self, axis_item: AxisItem, source: ArchivePlotCurveItem | FormulaCu self.variable_name = self.control_panel.key_gen.send(self.source) self.control_panel.curve_dict[self.variable_name] = self.source - # if not self.is_formula_curve(): - # self.source.unitSignal.connect(lambda unit: self.control_panel.move_curve_to_axis(self, unit)) self.setup_layout() + def move_to_axis_from_unit(self): + if not self.is_formula_curve(): + unit = self.source.units + if unit: + self.control_panel.move_curve_to_axis(self, unit) + else: + self.source.unitSignal.connect(lambda unit: self.control_panel.move_curve_to_axis(self, unit)) + @property def plot(self): """Get the PlotWidget that this CurveItem belongs to.""" From 0268025685ba3a67286ae41a5ffff51eb99b0658 Mon Sep 17 00:00:00 2001 From: michaellans Date: Tue, 25 Nov 2025 16:41:46 -0800 Subject: [PATCH 3/6] updated comments --- trace/widgets/control_panel.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/trace/widgets/control_panel.py b/trace/widgets/control_panel.py index 4152380..4513d5f 100644 --- a/trace/widgets/control_panel.py +++ b/trace/widgets/control_panel.py @@ -334,12 +334,12 @@ def add_curve(self, pv: str = None) -> "CurveItem": last_axis = self.add_empty_axis() if pv.startswith("f://"): - curve = last_axis.add_formula_curve(pv) + return last_axis.add_formula_curve(pv) else: curve = last_axis.add_curve(pv) - curve.move_to_axis_from_unit() + curve.move_to_axis_from_unit() # Move to axis based on unit - return curve + return curve def clear_all(self) -> None: """Clear all axes and curves from the plot and control panel.""" @@ -957,6 +957,7 @@ def __init__(self, axis_item: AxisItem, source: ArchivePlotCurveItem | FormulaCu self.setup_layout() def move_to_axis_from_unit(self): + """Automatically move curve to axis based on unit""" if not self.is_formula_curve(): unit = self.source.units if unit: From 6ed381b11d8b674245fea8929f2baecd9209cb0f Mon Sep 17 00:00:00 2001 From: michaellans Date: Mon, 12 Jan 2026 12:40:20 -0800 Subject: [PATCH 4/6] prevent creation of duplicate curves on an axis --- trace/widgets/control_panel.py | 47 ++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/trace/widgets/control_panel.py b/trace/widgets/control_panel.py index 4513d5f..b3f314a 100644 --- a/trace/widgets/control_panel.py +++ b/trace/widgets/control_panel.py @@ -337,10 +337,27 @@ def add_curve(self, pv: str = None) -> "CurveItem": return last_axis.add_formula_curve(pv) else: curve = last_axis.add_curve(pv) - curve.move_to_axis_from_unit() # Move to axis based on unit + self.move_to_axis_from_unit(curve) # Move to axis based on unit return curve + def move_to_axis_from_unit(self, curve: "CurveItem") -> None: + """ + Detect curve units and automatically move curve to an axis matching its unit + + Parameters + ---------- + curve : CurveItem + """ + if not isinstance(curve, CurveItem): + return + if not curve.is_formula_curve(): + unit = curve.source.units + if unit: + self.move_curve_to_axis(curve, unit) + else: + curve.source.unitSignal.connect(lambda unit: self.move_curve_to_axis(curve, unit)) + def clear_all(self) -> None: """Clear all axes and curves from the plot and control panel.""" logger.debug("Clearing all axes and curves from the plot") @@ -609,6 +626,11 @@ def add_curve(self, pv: str, channel_args: dict = None) -> "CurveItem": CurveItem The created CurveItem widget. """ + + # Avoid adding duplicate PVs + if pv in self.get_curve_names(): + return None + palette = self.control_panel.curve_palette color = ColorButton.index_color(len(self.plot._curves), palette=palette) args = { @@ -625,6 +647,20 @@ def add_curve(self, pv: str, channel_args: dict = None) -> "CurveItem": return self.make_curve_widget(plot_curve_item) + def get_curves(self) -> list["CurveItem"]: + """Returns a list of CurveItems on this axis""" + curves = [] + for i in range(self.layout().count() - 1, -1, -1): + item = self.layout().itemAt(i).widget() + if isinstance(item, CurveItem): + curves.append(item) + + return curves + + def get_curve_names(self) -> list[str]: + """Returns a list of curve names on this axis""" + return [curve.source.name() for curve in self.get_curves()] + def add_formula_curve(self, formula: str) -> "CurveItem": """Create a new FormulaCurveItem for the given formula and add it to this AxisItem. Also creates a CurveItem widget for it. @@ -956,15 +992,6 @@ def __init__(self, axis_item: AxisItem, source: ArchivePlotCurveItem | FormulaCu self.setup_layout() - def move_to_axis_from_unit(self): - """Automatically move curve to axis based on unit""" - if not self.is_formula_curve(): - unit = self.source.units - if unit: - self.control_panel.move_curve_to_axis(self, unit) - else: - self.source.unitSignal.connect(lambda unit: self.control_panel.move_curve_to_axis(self, unit)) - @property def plot(self): """Get the PlotWidget that this CurveItem belongs to.""" From 44860160e4d68479eba26df6431221933734d645 Mon Sep 17 00:00:00 2001 From: michaellans Date: Wed, 14 Jan 2026 12:43:42 -0800 Subject: [PATCH 5/6] Duplicate PVs get added to new axis, or prevented if added from list --- trace/widgets/control_panel.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/trace/widgets/control_panel.py b/trace/widgets/control_panel.py index b3f314a..d7102de 100644 --- a/trace/widgets/control_panel.py +++ b/trace/widgets/control_panel.py @@ -235,7 +235,7 @@ def add_curves(self, pvs: list[str]) -> None: List of PV names to add as curves """ for pv in pvs: - self.add_curve(pv) + self.add_curve(pv, duplicate=False) def add_empty_axis(self, name: str = "") -> "AxisItem": logger.debug("Adding new empty axis to the plot") @@ -325,7 +325,7 @@ def curve_item_dict(self): return plot_curves @QtCore.Slot() - def add_curve(self, pv: str = None) -> "CurveItem": + def add_curve(self, pv: str = None, duplicate: bool = True) -> "CurveItem": if pv is None and self.sender(): pv = self.sender().text() @@ -336,11 +336,30 @@ def add_curve(self, pv: str = None) -> "CurveItem": if pv.startswith("f://"): return last_axis.add_formula_curve(pv) else: + curves = self.curve_item_dict + if pv in [curves[curve]["name"] for curve in curves]: + logger.debug(f"Tried adding {pv} but curve for PV '{pv}' already exists.") + if duplicate: + logger.debug(f"Adding duplicate curve to new axis for PV '{pv}'.") + return self.add_curve_to_new_axis(pv) + else: + logger.debug(f"Not adding duplicate curve for PV '{pv}'.") + return None + curve = last_axis.add_curve(pv) self.move_to_axis_from_unit(curve) # Move to axis based on unit return curve + def add_curve_to_new_axis(self, pv: str) -> "CurveItem": + """ + Add curve to a new, empty axis. Used to avoid adding duplicate curves to the same axis. + """ + if not pv: + return None + axis_item = self.add_empty_axis() + return axis_item.add_curve(pv) + def move_to_axis_from_unit(self, curve: "CurveItem") -> None: """ Detect curve units and automatically move curve to an axis matching its unit @@ -627,10 +646,6 @@ def add_curve(self, pv: str, channel_args: dict = None) -> "CurveItem": The created CurveItem widget. """ - # Avoid adding duplicate PVs - if pv in self.get_curve_names(): - return None - palette = self.control_panel.curve_palette color = ColorButton.index_color(len(self.plot._curves), palette=palette) args = { From 710f6bdd1c76514ceee7e2e4336cfb148d683836 Mon Sep 17 00:00:00 2001 From: michaellans Date: Thu, 15 Jan 2026 09:47:52 -0800 Subject: [PATCH 6/6] add comments and docstring --- trace/widgets/control_panel.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/trace/widgets/control_panel.py b/trace/widgets/control_panel.py index d7102de..e94e9ca 100644 --- a/trace/widgets/control_panel.py +++ b/trace/widgets/control_panel.py @@ -326,6 +326,13 @@ def curve_item_dict(self): @QtCore.Slot() def add_curve(self, pv: str = None, duplicate: bool = True) -> "CurveItem": + """ + Add curve to the plot. New curves are added to the last axis by default, then sorted by unit. + + Parameters: + pv (str): EPICS PV or formula string + duplicate (bool): If True, duplicate PVs will be added on a new axis. If False, skip duplicates. + """ if pv is None and self.sender(): pv = self.sender().text() @@ -336,14 +343,15 @@ def add_curve(self, pv: str = None, duplicate: bool = True) -> "CurveItem": if pv.startswith("f://"): return last_axis.add_formula_curve(pv) else: + # Check for duplicate PVs. If pv already exists, either add to new axis or skip + # depending on 'duplicate' flag curves = self.curve_item_dict if pv in [curves[curve]["name"] for curve in curves]: - logger.debug(f"Tried adding {pv} but curve for PV '{pv}' already exists.") if duplicate: logger.debug(f"Adding duplicate curve to new axis for PV '{pv}'.") return self.add_curve_to_new_axis(pv) else: - logger.debug(f"Not adding duplicate curve for PV '{pv}'.") + logger.debug(f"Skipping duplicate curve for PV '{pv}'.") return None curve = last_axis.add_curve(pv) @@ -354,6 +362,10 @@ def add_curve(self, pv: str = None, duplicate: bool = True) -> "CurveItem": def add_curve_to_new_axis(self, pv: str) -> "CurveItem": """ Add curve to a new, empty axis. Used to avoid adding duplicate curves to the same axis. + + Parameters + ---------- + pv (str): EPICS PV name """ if not pv: return None