feat: enhance DataFrame Operations component with contains filter and modern UI (#8838)
* feat: enhance DataFrame Operations component with contains filter and modern UI - Add "contains" filter operator for partial string matching in DataFrame filters - Update UI to use SortableListInput with icons for consistent modern design - Add 7 filter operators: equals, not equals, contains, starts with, ends with, greater than, less than - Fix deselection handling to prevent "unhashable type: list" errors - Improve dynamic field visibility when operations are deselected - Add comprehensive test suite with 25 tests covering all operations and edge cases - Update placeholder text from "Select DataFrame Operation" to "Select Operation" 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * [autofix.ci] apply automated fixes * Update dataframe_operations.py --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
This commit is contained in:
parent
f6a9e8fad4
commit
e0400f29eb
2 changed files with 504 additions and 79 deletions
|
|
@ -1,4 +1,7 @@
|
|||
import pandas as pd
|
||||
|
||||
from langflow.custom.custom_component.component import Component
|
||||
from langflow.inputs import SortableListInput
|
||||
from langflow.io import (
|
||||
BoolInput,
|
||||
DataFrameInput,
|
||||
|
|
@ -39,12 +42,25 @@ class DataFrameOperationsComponent(Component):
|
|||
info="The input DataFrame to operate on.",
|
||||
required=True,
|
||||
),
|
||||
DropdownInput(
|
||||
SortableListInput(
|
||||
name="operation",
|
||||
display_name="Operation",
|
||||
options=OPERATION_CHOICES,
|
||||
placeholder="Select Operation",
|
||||
info="Select the DataFrame operation to perform.",
|
||||
options=[
|
||||
{"name": "Add Column", "icon": "plus"},
|
||||
{"name": "Drop Column", "icon": "minus"},
|
||||
{"name": "Filter", "icon": "filter"},
|
||||
{"name": "Head", "icon": "arrow-up"},
|
||||
{"name": "Rename Column", "icon": "pencil"},
|
||||
{"name": "Replace Value", "icon": "replace"},
|
||||
{"name": "Select Columns", "icon": "columns"},
|
||||
{"name": "Sort", "icon": "arrow-up-down"},
|
||||
{"name": "Tail", "icon": "arrow-down"},
|
||||
{"name": "Drop Duplicates", "icon": "copy-x"},
|
||||
],
|
||||
real_time_refresh=True,
|
||||
limit=1,
|
||||
),
|
||||
StrInput(
|
||||
name="column_name",
|
||||
|
|
@ -60,6 +76,16 @@ class DataFrameOperationsComponent(Component):
|
|||
dynamic=True,
|
||||
show=False,
|
||||
),
|
||||
DropdownInput(
|
||||
name="filter_operator",
|
||||
display_name="Filter Operator",
|
||||
options=["equals", "not equals", "contains", "starts with", "ends with", "greater than", "less than"],
|
||||
value="equals",
|
||||
info="The operator to apply for filtering rows.",
|
||||
advanced=False,
|
||||
dynamic=True,
|
||||
show=False,
|
||||
),
|
||||
BoolInput(
|
||||
name="ascending",
|
||||
display_name="Sort Ascending",
|
||||
|
|
@ -126,6 +152,7 @@ class DataFrameOperationsComponent(Component):
|
|||
dynamic_fields = [
|
||||
"column_name",
|
||||
"filter_value",
|
||||
"filter_operator",
|
||||
"ascending",
|
||||
"new_column_name",
|
||||
"new_column_value",
|
||||
|
|
@ -138,36 +165,57 @@ class DataFrameOperationsComponent(Component):
|
|||
build_config[field]["show"] = False
|
||||
|
||||
if field_name == "operation":
|
||||
if field_value == "Filter":
|
||||
# Handle SortableListInput format
|
||||
if isinstance(field_value, list):
|
||||
operation_name = field_value[0].get("name", "") if field_value else ""
|
||||
else:
|
||||
operation_name = field_value or ""
|
||||
|
||||
# If no operation selected, all dynamic fields stay hidden (already set to False above)
|
||||
if not operation_name:
|
||||
return build_config
|
||||
|
||||
if operation_name == "Filter":
|
||||
build_config["column_name"]["show"] = True
|
||||
build_config["filter_value"]["show"] = True
|
||||
elif field_value == "Sort":
|
||||
build_config["filter_operator"]["show"] = True
|
||||
elif operation_name == "Sort":
|
||||
build_config["column_name"]["show"] = True
|
||||
build_config["ascending"]["show"] = True
|
||||
elif field_value == "Drop Column":
|
||||
elif operation_name == "Drop Column":
|
||||
build_config["column_name"]["show"] = True
|
||||
elif field_value == "Rename Column":
|
||||
elif operation_name == "Rename Column":
|
||||
build_config["column_name"]["show"] = True
|
||||
build_config["new_column_name"]["show"] = True
|
||||
elif field_value == "Add Column":
|
||||
elif operation_name == "Add Column":
|
||||
build_config["new_column_name"]["show"] = True
|
||||
build_config["new_column_value"]["show"] = True
|
||||
elif field_value == "Select Columns":
|
||||
elif operation_name == "Select Columns":
|
||||
build_config["columns_to_select"]["show"] = True
|
||||
elif field_value in {"Head", "Tail"}:
|
||||
elif operation_name in {"Head", "Tail"}:
|
||||
build_config["num_rows"]["show"] = True
|
||||
elif field_value == "Replace Value":
|
||||
elif operation_name == "Replace Value":
|
||||
build_config["column_name"]["show"] = True
|
||||
build_config["replace_value"]["show"] = True
|
||||
build_config["replacement_value"]["show"] = True
|
||||
elif field_value == "Drop Duplicates":
|
||||
elif operation_name == "Drop Duplicates":
|
||||
build_config["column_name"]["show"] = True
|
||||
|
||||
return build_config
|
||||
|
||||
def perform_operation(self) -> DataFrame:
|
||||
df_copy = self.df.copy()
|
||||
op = self.operation
|
||||
|
||||
# Handle SortableListInput format for operation
|
||||
operation_input = getattr(self, "operation", [])
|
||||
if isinstance(operation_input, list) and len(operation_input) > 0:
|
||||
op = operation_input[0].get("name", "")
|
||||
else:
|
||||
op = ""
|
||||
|
||||
# If no operation selected, return original DataFrame
|
||||
if not op:
|
||||
return df_copy
|
||||
|
||||
if op == "Filter":
|
||||
return self.filter_rows_by_value(df_copy)
|
||||
|
|
@ -194,7 +242,42 @@ class DataFrameOperationsComponent(Component):
|
|||
raise ValueError(msg)
|
||||
|
||||
def filter_rows_by_value(self, df: DataFrame) -> DataFrame:
|
||||
return DataFrame(df[df[self.column_name] == self.filter_value])
|
||||
column = df[self.column_name]
|
||||
filter_value = self.filter_value
|
||||
|
||||
# Handle regular DropdownInput format (just a string value)
|
||||
operator = getattr(self, "filter_operator", "equals") # Default to equals for backward compatibility
|
||||
|
||||
if operator == "equals":
|
||||
mask = column == filter_value
|
||||
elif operator == "not equals":
|
||||
mask = column != filter_value
|
||||
elif operator == "contains":
|
||||
mask = column.astype(str).str.contains(str(filter_value), na=False)
|
||||
elif operator == "starts with":
|
||||
mask = column.astype(str).str.startswith(str(filter_value), na=False)
|
||||
elif operator == "ends with":
|
||||
mask = column.astype(str).str.endswith(str(filter_value), na=False)
|
||||
elif operator == "greater than":
|
||||
try:
|
||||
# Try to convert filter_value to numeric for comparison
|
||||
numeric_value = pd.to_numeric(filter_value)
|
||||
mask = column > numeric_value
|
||||
except (ValueError, TypeError):
|
||||
# If conversion fails, compare as strings
|
||||
mask = column.astype(str) > str(filter_value)
|
||||
elif operator == "less than":
|
||||
try:
|
||||
# Try to convert filter_value to numeric for comparison
|
||||
numeric_value = pd.to_numeric(filter_value)
|
||||
mask = column < numeric_value
|
||||
except (ValueError, TypeError):
|
||||
# If conversion fails, compare as strings
|
||||
mask = column.astype(str) < str(filter_value)
|
||||
else:
|
||||
mask = column == filter_value # Fallback to equals
|
||||
|
||||
return DataFrame(df[mask])
|
||||
|
||||
def sort_by_column(self, df: DataFrame) -> DataFrame:
|
||||
return DataFrame(df.sort_values(by=self.column_name, ascending=self.ascending))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue