Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions custom_plot_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,19 @@
import numpy as np

import graph_utils
from plot_spec import PlotSpec
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from var_list_widget import VarListWidget
from maths_widget import MathsWidget
# PlotSpec is already imported above, no need to re-import under TYPE_CHECKING unless it was causing issues


class CustomPlotItem(QLabel):
PEN_WIDTH = 2

def __init__(self, parent, plot_data_item, source, current_tick):
def __init__(self, parent, plot_data_item, source, current_tick, plot_spec_from_source: 'PlotSpec | None' = None): # Added plot_spec_from_source
QLabel.__init__(self, plot_data_item.name(), parent=parent)

''' This item should handle the following things:
Expand Down Expand Up @@ -64,6 +71,31 @@ def __init__(self, parent, plot_data_item, source, current_tick):
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.show_menu)

# Initialize self.plot_spec based on plot_spec_from_source
if plot_spec_from_source:
self.plot_spec = plot_spec_from_source
else:
# Create a default "file" PlotSpec if none is provided
name = plot_data_item.name() # Use the name from the plot_data_item
file_id = "unknown_source_widget" # Default file identifier

# Attempt to get a more specific file_id from the source
# source is typically VarListWidget or MathsWidget (or their models)
if hasattr(source, 'filename') and source.filename: # Check for direct filename (e.g. VarListWidget)
file_id = source.filename
elif hasattr(source, 'idx') and source.idx is not None: # Check for direct idx (e.g. VarListWidget)
file_id = str(source.idx)
# If source is MathsWidget, it's more complex as it doesn't have a single 'filename' or 'idx'
# However, if plot_spec_from_source is None, it implies the data isn't from a known math operation.
# This logic assumes that if data comes from MathsWidget, plot_spec_from_source should be set.

self.plot_spec = PlotSpec(
name=name,
source_type="file", # Default to "file" if not otherwise specified
original_name=name, # Assume original_name is same as current name if not specified
file_source_identifier=file_id
Comment on lines +80 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of placeholder strings like "unknown_source_widget" for file_id when a more specific identifier can't be found is a reasonable fallback. However, if these kinds of identifiers become more common or need to be managed more strictly across different parts of the application (e.g., here and in DataModel), would it be worth defining them as constants in a shared module to ensure consistency and avoid typos?

)

def update_color(self, color_str):
pen = pg.mkPen(color=color_str, width=CustomPlotItem.PEN_WIDTH)
self.trace.setPen(pen)
Expand Down Expand Up @@ -121,7 +153,8 @@ def name(self):
def get_plot_spec(self):
# For now, we'll just get the name of the trace, but this will become more complex in the
# future when we start supporting derived signals.
return self.trace.name()
# This method should now return the full PlotSpec object.
return self.plot_spec

@pyqtSlot(float)
def on_time_changed(self, time):
Expand Down
74 changes: 67 additions & 7 deletions data_file_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ def __init__(self, parent):
self.tabs.currentChanged.connect(lambda x: self.tabChanged.emit())
layout.addWidget(self.tabs)

# Make var_lists easily accessible for get_data_item_by_file_id_and_name
self.var_lists = [] # List to store VarListWidget instances

self.filter_box = FilterBoxWidget(self.tabs)
layout.addWidget(self.filter_box)

Expand All @@ -68,8 +71,12 @@ def open_file(self, filepath):
tab_name = os.path.basename(filepath)
# Create a new tab and add the varListWidget to it.
self.latest_data_file_name = filepath
self.tabs.addTab(var_list, tab_name)
self.sources[filepath] = self.tabs.widget(self.tabs.count() - 1)
tab_idx = self.tabs.addTab(var_list, tab_name) # addTab returns the index
# self.sources[filepath] = self.tabs.widget(self.tabs.count() - 1) # Old way
self.sources[filepath] = var_list # Store the instance directly
var_list.idx = tab_idx # Assign tab index as an ad-hoc idx if needed for PlotSpec compatibility
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Assigning var_list.idx = tab_idx dynamically adds an attribute to VarListWidget instances. While this works for PlotSpec compatibility, it can make the VarListWidget class contract less explicit. If idx is a consistently needed piece of information for VarListWidget, would it be clearer to formally add it as an optional parameter in its __init__ method and store it as a regular instance attribute?

self.var_lists.append(var_list) # Add to our list

self.tabs.setCurrentWidget(var_list)
self._update_range_slider()

Expand All @@ -81,11 +88,22 @@ def open_file(self, filepath):

def close_file(self, index):
# Add function for closing the tab here.
filename = self.tabs.widget(index).filename
self.tabs.widget(index).close()
self.tabs.removeTab(index)
widget_to_close = self.tabs.widget(index)
filename = widget_to_close.filename

if widget_to_close in self.var_lists:
self.var_lists.remove(widget_to_close)
if filename in self.sources:
del self.sources[filename]

widget_to_close.close() # Close the widget first
self.tabs.removeTab(index) # Then remove tab

if self.tabs.count() > 0:
self._update_range_slider()
else: # Reset slider if no files are open
self.controller.plot_manager.update_slider_limits(0, 1.e9)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The value 1.e9 is used here to reset the slider limits when no files are open. If this large default maximum is used in other places or has a specific meaning (e.g., 'effectively infinite for practical purposes'), could defining it as a named constant (e.g., DEFAULT_SLIDER_MAX_RANGE) improve readability and maintainability?



self.countChanged.emit()
self.fileClosed[str].emit(filename)
Expand All @@ -99,17 +117,59 @@ def get_latest_data_file_name(self):
def get_data_file(self, idx):
return self.tabs.widget(idx)

def get_data_file_by_name(self, name):
return self.sources[name]
def get_data_file_by_name(self, name): # name is filepath
return self.sources.get(name) # Use .get for safer access

def get_data_item_by_file_id_and_name(self, file_identifier: str, signal_name: str) -> 'DataItem | None':
"""
Retrieves a DataItem from one of the loaded files.
file_identifier can be the file's full path or its stringified tab index at time of PlotSpec creation.
signal_name is the original_name of the signal.
"""
# Try matching by full filepath first (most robust)
var_list_widget = self.sources.get(file_identifier)
if var_list_widget:
model = var_list_widget.model()
if model:
data_item = model.get_data_by_name(signal_name)
if data_item:
return data_item

# Fallback: iterate through all var_lists if not found by path (e.g., if file_identifier was an index string)
# This is less robust if tab order changed or files were closed/reopened.
for vlw in self.var_lists:
# Check against filename (if file_identifier was a filename but not full path)
if os.path.basename(vlw.filename) == file_identifier:
model = vlw.model()
if model:
data_item = model.get_data_by_name(signal_name)
if data_item: return data_item

# Check against ad-hoc idx (tab index at time of creation)
# This relies on VarListWidget having an 'idx' attribute that was set to its tab index.
if hasattr(vlw, 'idx') and str(vlw.idx) == file_identifier:
model = vlw.model()
if model:
data_item = model.get_data_by_name(signal_name)
if data_item: return data_item

print(f"Warning: Could not find DataItem for signal '{signal_name}' from file ID '{file_identifier}'")
return None

def get_sources(self):
return self.sources

def get_time(self, idx=0):
if self.tabs.count() == 0:
return None
# Ensure idx is valid before trying to access widget
if idx < 0 or idx >= self.tabs.count():
if self.tabs.count() > 0: # Default to the current tab if idx is bad but tabs exist
return self.tabs.currentWidget().time
return None # No tabs, no time
return self.get_data_file(idx).time


@pyqtSlot(QPoint)
def on_context_menu_request(self, pos):
# We only want to bring up the context menu when an actual tab is right-clicked. Check that
Expand Down
46 changes: 38 additions & 8 deletions data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@

import numpy as np

from plot_spec import PlotSpec # Added import


class DataItem(object):
"""
Data structure for storing data items in the list widget
"""

def __init__(self, var_name, data):
def __init__(self, var_name, data, plot_spec: PlotSpec | None = None): # Added plot_spec
self._var_name = var_name
self._data = data
self._plot_spec = plot_spec # Added plot_spec
self._time = None

@property
Expand All @@ -27,6 +30,14 @@ def data(self):
def time(self):
return self._time

@property
def plot_spec(self): # Added plot_spec property
return self._plot_spec

@plot_spec.setter
def plot_spec(self, value: PlotSpec | None): # Added plot_spec setter
self._plot_spec = value

def __repr__(self):
return self._var_name

Expand Down Expand Up @@ -99,10 +110,29 @@ def data(self, index, role):
def has_key(self, name):
return name in self._raw_data.index

def get_data_by_name(self, name):
data = None
try:
data = self._raw_data[name]
except KeyError:
print(f"Unknown key: {name}")
return data
def get_data_by_name(self, name) -> DataItem | None:
# Iterates self._data (which is list[DataItem]) and finds the DataItem with the matching var_name.
for item in self._data:
if item.var_name == name:
# Ensure the PlotSpec is created if it's missing for a file-loaded item
if item.plot_spec is None:
# Attempt to get a file_source_identifier
# This DataModel instance itself doesn't store the filename directly in a way
# that's easily accessible per item here. We'll assume a generic one or improve later if needed.
# For now, we know it's from this model, which is usually file-based.
file_id = "unknown_data_model_source"
# A better approach would be if data_loader.source (filename) was stored in DataModel
# and accessible here, or if DataItem was initialized with it.
# Let's assume self.filename could exist if DataModel was enhanced.
# if hasattr(self, 'filename') and self.filename:
# file_id = self.filename

item.plot_spec = PlotSpec(
name=item.var_name,
source_type="file",
original_name=item.var_name,
file_source_identifier=file_id # Placeholder, actual file ID needs better handling
Comment on lines +123 to +134
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fallback file_id = "unknown_data_model_source" is noted. The comment on lines 124-128 about potentially enhancing DataModel to store the filename (perhaps from data_loader.source during initialization) is a good point. This would make the file_source_identifier more robust here. For now, this fallback is understandable, but it's a good candidate for future refinement.

)
return item
print(f"Unknown key: {name} in DataModel")
return None
14 changes: 14 additions & 0 deletions maths/diff_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ def do_math(self, data, dt):
def default_var_name(self, vname):
return f"Diff({vname})"

def get_operation_details(self):
# Differentiation is straightforward, parameters could be added if different methods are implemented
return {'method': 'finite_difference', 'order': 1}

def get_source_type(self):
return "math_diff"


class IntegrateSpec(MathSpecBase):
def __init__(self, parent):
Expand All @@ -35,3 +42,10 @@ def do_math(self, data, dt):

def default_var_name(self, vname):
return f"Int({vname})"

def get_operation_details(self):
# Integration is also straightforward for this implementation
return {'method': 'cumulative_sum'}

def get_source_type(self):
return "math_integrate"
13 changes: 13 additions & 0 deletions maths/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,16 @@ def do_math(self, data, dt):

def default_var_name(self, vname):
return f"Filter({vname},{self._params.order},{self._params.type},{self._params.cutoff})"

def get_operation_details(self):
if hasattr(self, '_params') and self._params:
return {
'order': self._params.order,
'type': self._params.type,
'cutoff': self._params.cutoff,
'filtfilt': self._params.filtfilt
}
return None # Or raise an error if params are expected to always be there

def get_source_type(self):
return "math_filter"
36 changes: 33 additions & 3 deletions maths/maths_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import abc

from data_model import DataItem
from plot_spec import PlotSpec # Added import
from var_list_widget import VarListWidget


Expand Down Expand Up @@ -77,9 +78,38 @@ def eventFilter(self, obj, event):
vname, accept = QInputDialog.getText(self.parent(), "Enter variable name", "Variable name:",
text=self.default_var_name(selected.var_name))
if accept:
data_item = DataItem(vname, val)
data_item._time = selected._time
self.parent().add_new_var(data_item, vlist)
# PlotSpec creation for math operations
input_plot_spec = selected.plot_spec
if not input_plot_spec:
print(f"Warning: Input variable {selected.var_name} for {self.name} operation is missing PlotSpec. Creating a fallback.")
fallback_file_id = "unknown_input_source"
# vlist is the VarListWidget, vlist.model() is the DataModel
source_model = vlist.model()
if hasattr(source_model, 'filename') and source_model.filename: # Check if source_model has filename
fallback_file_id = source_model.filename
elif hasattr(source_model, 'idx') and source_model.idx is not None: # Check if source_model has idx
fallback_file_id = str(source_model.idx)

input_plot_spec = PlotSpec(
name=selected.var_name,
source_type="file_fallback",
original_name=selected.var_name,
file_source_identifier=fallback_file_id
)

operation_details = self.get_operation_details()
source_type = self.get_source_type()

output_plot_spec = PlotSpec(
name=vname,
source_type=source_type,
input_plot_specs=[input_plot_spec],
operation_details=operation_details
)

data_item = DataItem(vname, val, plot_spec=output_plot_spec)
data_item._time = selected._time # selected is a DataItem, its _time should be set
self.parent().add_new_var(data_item, vlist, output_plot_spec) # Pass output_plot_spec
else:
print("User cancelled operation!")
else:
Expand Down
12 changes: 12 additions & 0 deletions maths/running_minmax.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,15 @@ def do_math(self, data, dt):

def default_var_name(self, vname):
return f"RunningMinMax({vname},{self._params.type.name.lower()},{self._params.window_sz},{int(self._params.is_ticks)}) "

def get_operation_details(self):
if hasattr(self, '_params') and self._params:
return {
'type': self._params.type.name.lower(),
'window_sz': self._params.window_sz,
'is_ticks': self._params.is_ticks
}
return None

def get_source_type(self):
return "math_running_minmax"
12 changes: 12 additions & 0 deletions maths/running_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,15 @@ def do_math(self, data, dt):

def default_var_name(self, vname):
return f"RunningWindow({vname},{self._params.type.name.lower()},{self._params.window_sz},{int(self._params.is_ticks)})"

def get_operation_details(self):
if hasattr(self, '_params') and self._params:
return {
'type': self._params.type.name.lower(), # e.g. "mean", "median"
'window_sz': self._params.window_sz,
'is_ticks': self._params.is_ticks
}
return None

def get_source_type(self):
return "math_running_window"
Loading