Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grid Selections Not Being Captured #68

Open
Lucid-Will opened this issue Feb 18, 2024 · 15 comments
Open

Grid Selections Not Being Captured #68

Lucid-Will opened this issue Feb 18, 2024 · 15 comments

Comments

@Lucid-Will
Copy link

Lucid-Will commented Feb 18, 2024

Grid selections aren't being captured by event listener in a Jupyter notebook. I've tried in both Databricks and Microsoft Spark notebooks, neither of which seem to be working.

The grid itself is being populated. I've tested making selections on the grid by row as well as checkbox. I then tested the Export to CSV, Export Rows, Export Columns, and Export Range Data options. Export to CSV creates the CSV export but of the full grid, not the selections. The other export options do not appear to be functioning at all.

I've tried implementing with a button to call grid.get_selected_rows() and capture the selections into a dataframe but the dataframe is empty. I also tried creating a dataframe in a subsequent cell that calls grid.get_selected_rows() as well, also empty dataframe.

I've attached sample notebook code to recreate.

import pandas as pd
import ipywidgets as widgets
import pandas as pd
from ipyaggrid import Grid
from IPython.display import display

Sample data

data = {
    'ID': [1, 2, 3, 4, 5],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'Age': [24, 30, 35, 22, 29],
    'City': ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'],
    'Subscription': ['Yes', 'No', 'Yes', 'No', 'Yes']
}

pdf_subscription_load = pd.DataFrame(data)

Display the DataFrame

display(pdf_subscription_load)

Define the grid options with range selection enabled

grid_options = {
    'columnDefs': [{'headerName': col, 'field': col, 'checkboxSelection': (col == 'subscription_name'), 'sortable': True, 'filter': True} for col in pdf_subscription_load.columns],
    'enableSorting': True,
    'enableFilter': True,
    'enableColResize': True,
    'enableRangeSelection': True,
    'rowSelection': 'multiple',
}

Create the Grid widget with configured options

grid = Grid(grid_data=pdf_subscription_load,
            grid_options=grid_options,
            grid_options_multi=[],  # Additional grid options for multi-grid setups
            columns_fit='size_to_fit',
            index=False,
            keep_multiindex=False,
            compress_data=True,
            quick_filter=True,  # Enable or disable quick filtering
            export_csv=True,  # Enable or disable CSV export
            export_excel=True,  # Enable or disable Excel export
            show_toggle_delete=True,  # Show or hide delete toggle
            show_toggle_edit=True,  # Show or hide edit toggle
            sync_on_edit=True,  # Synchronize edits
            sync_grid=True,  # Synchronize the grid
            paste_from_excel=False,  # Allow pasting from Excel
            export_mode='buttons',  # Set export mode
            export_to_df=True,  # Export to DataFrame
            hide_grid=False,  # Hide or show the grid
            theme='ag-theme-balham')  # Theme

Define a button widget

button = widgets.Button(description="Confirm Selection")

Define an event handler for the button click event

def on_button_click(b):
    # Use the get_selected_rows method to capture selected rows
    selected_rows_df = grid.get_selected_rows()
    # Assuming you have a way to display or use the selected rows DataFrame
    print(selected_rows_df)  # For demonstration, this will print the DataFrame to the output

Attach the event handler to the button

button.on_click(on_button_click)

Display the Grid widget and the button

display(grid)
display(button)

selected_rows_df = grid.get_selected_rows()
display(selected_rows_df)`
@mariobuikhuizen
Copy link
Contributor

Thanks for reporting this issue.

"Export to CSV" and "Export to Excel" don't take selections into account.

The exported data is available in grid.grid_data_out after calling get_selected_rows() (which doesn't have a return value). There seems to be an issue in get_selected_rows(), which doesn't set the row data in grid._grid_data_up. I have to look into this.

It does work for get_selected_columns() (result ingrid.grid_data_out['range"]) and get_grid() (result in grid.grid_data_out['cols"]).

@Lucid-Will
Copy link
Author

Sure thing. Let me know if/how I can assist. I have a perfect use-case for using this functionality.

Cheers.

@cyrfar
Copy link

cyrfar commented Feb 29, 2024

@mariobuikhuizen I have a use case too :). I am trying to build a Solara app that renders an ipyaggrid that can crossfilter other Solara components/charts upon rows selection, as shown in this Dash example . But I can't get it to work and I think the issue is due to grid_data_out being one step delayed (async) relative to row selections. I would hate to have to switch to Dash so looking forward to a solution!

Cheers

@mariobuikhuizen
Copy link
Contributor

Can you provide a minimal code example?

@cyrfar
Copy link

cyrfar commented Mar 2, 2024

Sure, here is a simple Solara app where you can select rows from an ipyaggrid and display them in a Solara.Dataframe on the side.

from typing import cast
import ipyaggrid
import solara


@solara.component
def AgGridCrossFiltering(df, grid_options):

    dff = solara.use_reactive(df)
    grid_data_out = solara.use_reactive({})

    def set_grid_data_out():
        grid = cast(ipyaggrid.Grid, solara.get_widget(el))
        grid_data_out.set(grid.grid_data_out)
        print(grid.grid_data_out)

    def on_click():
        # hack to force a 'dummy' change in dff
        some_col = df.columns[0]
        all_vals = df[some_col].unique().tolist()
        dff.set(df.query(f'{some_col}.isin({all_vals})'))
    
    with solara.Columns([1,1]):
    
        with solara.Card('This is my AG Grid with the data to select from'):

            solara.Markdown('Select IDs then `Confirm Selection`. Check printed `grid_data_out` in logs')
            with solara.Row():
                solara.Button('Confirm Selection', on_click=on_click)

            el = ipyaggrid.Grid.element(
                grid_data=dff.value,
                grid_options=grid_options,
                theme='ag-theme-balham',
                export_mode='auto'
            )
    
            solara.use_effect(set_grid_data_out, [dff.value, grid_options])
    
        with solara.Card('Selected rows will appear below'):
            if 'rows' in grid_data_out.value:
                solara.DataFrame(grid_data_out.value['rows'])

AgGridCrossFiltering(df, grid_options)

If you run this in a notebook you get:

image

This works but at the expense of the the somewhat hacky on_click handler. Moreover, if your data has more than 1000 rows (like mine does) then you have to set export_mode='buttons' rather than auto, otherwise it will crash the app. But then this introduces a whole new set of buttons that we don't need and requires an extra click to Export Rows before confirming the selection. Ideally we would simply have to click on the checkmarks and see the results instantly on the right.

Hope this helps!

@mariobuikhuizen
Copy link
Contributor

You can use the on_[prop] to respond to changes on [prop]. If you add on_grid_data_out=grid_data_out.set to grid.element(..., it will work.

@Lucid-Will
Copy link
Author

My use case is a bit different. Conceptually, having a grid element like this in a notebook turns the notebook itself into a console which would make managing tables incredibly easy for end users. It also opens the door for some other really cool use cases in a framework I'm building.

@mariobuikhuizen
Copy link
Contributor

@Lucid-Will , Turns out get_selected_rows() does work, I was just doing the selection wrong. You can use grid.observe(lambda change: print(change['new']), names=["grid_data_out"]) to get the change after calling get_selected_rows()

@Lucid-Will
Copy link
Author

Lucid-Will commented Mar 7, 2024

@mariobuikhuizen, apologies if I'm doing something incorrect but I tried the above and unfortunately` having the same issue.

# Sample data
data = {
    'ID': [1, 2, 3, 4, 5],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'Age': [24, 30, 35, 22, 29],
    'City': ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'],
    'Subscription': ['Yes', 'No', 'Yes', 'No', 'Yes']
}

# Create DataFrame
pdf_subscription_load = pd.DataFrame(data)

# Preview DataFrame
# display(pdf_subscription_load)

# Set grid options
grid_options = {
    'columnDefs': [{'headerName': col, 'field': col, 'checkboxSelection': (col == 'ID'), 'sortable': True, 'filter': True} for col in pdf_subscription_load.columns],
    'enableSorting': True,
    'enableFilter': True,
    'enableColResize': True,
    'enableRangeSelection': True,
    'rowSelection': 'multiple',
}

# Define grid
grid = Grid(grid_data=pdf_subscription_load,
            grid_options=grid_options,
            grid_options_multi=[],  # Additional grid options for multi-grid setups
            columns_fit='size_to_fit',
            index=False,
            keep_multiindex=False,
            compress_data=True,
            quick_filter=True,  # Enable or disable quick filtering
            export_csv=True,  # Enable or disable CSV export
            export_excel=True,  # Enable or disable Excel export
            show_toggle_delete=True,  # Show or hide delete toggle
            show_toggle_edit=True,  # Show or hide edit toggle
            sync_on_edit=True,  # Synchronize edits
            sync_grid=True,  # Synchronize the grid
            paste_from_excel=False,  # Allow pasting from Excel
            export_mode='buttons',  # Set export mode
            export_to_df=True,  # Export to DataFrame
            hide_grid=False,  # Hide or show the grid
            theme='ag-theme-balham')  # Theme

# Assign button
button = widgets.Button(description="Confirm Selection")

# Create on_button_click method
def on_button_click(b):
    # Use the get_selected_rows method to capture selected rows
    selected_rows_df = grid.get_selected_rows()

    # Observe changes in the grid data
    grid.observe(lambda change: print(change['new']), names=["grid_data_out"])
    
    # Check if any rows are selected
    if selected_rows_df is None:
        print("No rows selected")
    else:
        # Assuming you have a way to display or use the selected rows DataFrame
        print(selected_rows_df)  # For demonstration, this will print the DataFrame to the output

# Show grid and button
display(grid)
display(button)

# Bind the callback function to the button
button.on_click(on_button_click)

@Lucid-Will
Copy link
Author

For comparison, the below works as expected but doesn't look nearly as awesome as ipyaggrid or have the intended functionality.

import pandas as pd
import ipywidgets as widgets

# Sample data
data = {
    'ID': [1, 2, 3, 4, 5],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'Age': [24, 30, 35, 22, 29],
    'City': ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'],
    'Subscription': ['Yes', 'No', 'Yes', 'No', 'Yes']
}

# Create DataFrame
pdf_subscription_load = pd.DataFrame(data)

# Create SelectMultiple widget
select = widgets.SelectMultiple(
    options=pdf_subscription_load.values.tolist(),
    rows=10,
    description='Rows',
    disabled=False
)

# Assign button
button = widgets.Button(description="Confirm Selection")

# Create on_button_click method
def on_button_click(b):
    # Get the selected rows from the SelectMultiple widget
    selected_rows = select.value

    # Check if any rows are selected
    if not selected_rows:
        print("No rows selected")
    else:
        # Create a DataFrame from the selected rows
        selected_rows_df = pd.DataFrame(selected_rows, columns=pdf_subscription_load.columns)

        # Print the selected rows DataFrame
        print(selected_rows_df)

# Show SelectMultiple widget and button
display(select)
display(button)

# Bind the callback function to the button
button.on_click(on_button_click)

@Lucid-Will
Copy link
Author

Stepping through the documentation samples yields the same results. I tried all output and all return None.

def mklbl(prefix, n):
    return ["%s%s" % (prefix, i) for i in range(n)]

miindex = pd.MultiIndex.from_product([mklbl('A', 4),
                                      mklbl('B', 2),
                                      mklbl('C', 4),
                                      mklbl('D', 2)],
                                    names=['RowIdx-1', 'RowIdx-2', 'RowIdx-3', 'RowIdx-4'])
index  =['-'.join(col).strip() for col in miindex.values]
micolumns = pd.MultiIndex.from_tuples([('a', 'foo', 'zap'),
                                       ('a', 'foo', 'zip'),
                                       ('a', 'bar', 'zap'),
                                       ('a', 'bar', 'zip'),
                                       ('b', 'foo', 'zap'),
                                       ('b', 'foo', 'zep'),
                                       ('b', 'bah', 'zep'),
                                       ('b', 'bah', 'zyp'),
                                       ('b', 'bah', 'zap'),
                                      ],
                                      names=['ColIdx-{}'.format(i) for i in range(1, 4)])
cols  =['-'.join(col).strip() for col in micolumns.values]
data = np.arange(len(miindex) * len(micolumns), dtype=np.float64).reshape((len(miindex),len(micolumns)))
data = data.tolist()
df  = pd.DataFrame(data, index=index, columns=cols).sort_index()
df  = pd.DataFrame(data, index=index, columns=cols).sort_index()
df.index.name = 'UniqueRow'
df.columns.name = 'UniqueCol'

column_defs_1 = [{'field': df.index.name}] + [{'field': c} for c in df.columns[:5]]


grid_options_1 = {
    'columnDefs' : column_defs_1,
    'defaultColDef': {'sortable': 'true', 'filter': 'true', 'resizable': 'true'},
    'enableRangeSelection': 'true',
    'rowSelection': 'multiple',
}

grid1 = Grid(grid_data=df,
            grid_options=grid_options_1,
            quick_filter=True,
            export_csv=False,
            export_excel=False,
            show_toggle_edit=True,
            export_mode='auto',
            index=True,
            keep_multiindex=False,
            theme='ag-theme-fresh')
grid1

print(grid1.grid_data_out.get('grid'))
print(grid1.grid_data_out.get('rows'))
print(grid1.grid_data_out.get('cols'))
print(grid1.grid_data_out.get('range'))
Screenshot 2024-03-07 at 12 02 52 AM

@mariobuikhuizen
Copy link
Contributor

@mariobuikhuizen, apologies if I'm doing something incorrect but I tried the above and unfortunately` having the same issue.

No worries @Lucid-Will. I've modified your example, this should work:

# Sample data
data = {
    'ID': [1, 2, 3, 4, 5],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'Age': [24, 30, 35, 22, 29],
    'City': ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'],
    'Subscription': ['Yes', 'No', 'Yes', 'No', 'Yes']
}

# Create DataFrame
pdf_subscription_load = pd.DataFrame(data)

# Preview DataFrame
# display(pdf_subscription_load)

# Set grid options
grid_options = {
    'columnDefs': [{'headerName': col, 'field': col, 'checkboxSelection': (col == 'ID'), 'sortable': True, 'filter': True} for col in pdf_subscription_load.columns],
    'enableSorting': True,
    'enableFilter': True,
    'enableColResize': True,
    'enableRangeSelection': True,
    'rowSelection': 'multiple',
}

# Define grid
grid = Grid(grid_data=pdf_subscription_load,
            grid_options=grid_options,
            grid_options_multi=[],  # Additional grid options for multi-grid setups
            columns_fit='size_to_fit',
            index=False,
            keep_multiindex=False,
            compress_data=True,
            quick_filter=True,  # Enable or disable quick filtering
            export_csv=True,  # Enable or disable CSV export
            export_excel=True,  # Enable or disable Excel export
            show_toggle_delete=True,  # Show or hide delete toggle
            show_toggle_edit=True,  # Show or hide edit toggle
            sync_on_edit=True,  # Synchronize edits
            sync_grid=True,  # Synchronize the grid
            paste_from_excel=False,  # Allow pasting from Excel
            export_mode='buttons',  # Set export mode
            export_to_df=True,  # Export to DataFrame
            hide_grid=False,  # Hide or show the grid
            theme='ag-theme-balham')  # Theme

# Assign button
button = widgets.Button(description="Confirm Selection")


def on_rows_selected(change):
    selected_rows_df = change["new"].get("rows")
    # Check if any rows are selected
    if selected_rows_df is None:
        print("No rows selected")
    else:
        # Assuming you have a way to display or use the selected rows DataFrame
        print(selected_rows_df)  # For demonstration, this will print the DataFrame to the output

# Observe changes in the grid data
grid.observe(on_rows_selected, names=["grid_data_out"])


# Create on_button_click method
def on_button_click(b):
    # Use the get_selected_rows method to capture selected rows
    grid.get_selected_rows()

# Show grid and button
display(grid)
display(button)

# Bind the callback function to the button
button.on_click(on_button_click)

@Lucid-Will
Copy link
Author

@mariobuikhuizen, I really appreciate your support on this! Unfortunately, it appears to be a limitation of the platform I'm trying to run this on (Microsoft Fabric data engineering notebook). I made a few modifications to the update you provided. I'm able to execute the following in VS Code but receive failures when trying to execute in Fabric.

I'll keep testing to see if there's a solution and report back.

Cheers, and thanks again!

#!pip install ipyaggrid -q
import pandas as pd
from ipyaggrid import Grid
import ipywidgets as widgets

# Sample data
data = {
    'ID': [1, 2, 3, 4, 5],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'Age': [24, 30, 35, 22, 29],
    'City': ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'],
    'Subscription': ['Yes', 'No', 'Yes', 'No', 'Yes']
}

# Create DataFrame
pdf_subscription_load = pd.DataFrame(data)

# Preview DataFrame
# display(pdf_subscription_load)

# Set grid options
grid_options = {
    'columnDefs': [{'headerName': col, 'field': col, 'checkboxSelection': (col == 'ID'), 'sortable': True, 'filter': True} for col in pdf_subscription_load.columns],
    'enableSorting': True,
    'enableFilter': True,
    'enableColResize': True,
    'enableRangeSelection': True,
    'rowSelection': 'multiple',
}

# Define grid
grid = Grid(grid_data=pdf_subscription_load,
            grid_options=grid_options,
            grid_options_multi=[],  # Additional grid options for multi-grid setups
            columns_fit='size_to_fit',
            index=False,
            keep_multiindex=False,
            compress_data=True,
            quick_filter=True,  # Enable or disable quick filtering
            export_csv=True,  # Enable or disable CSV export
            export_excel=True,  # Enable or disable Excel export
            show_toggle_delete=True,  # Show or hide delete toggle
            show_toggle_edit=True,  # Show or hide edit toggle
            sync_on_edit=True,  # Synchronize edits
            sync_grid=True,  # Synchronize the grid
            paste_from_excel=False,  # Allow pasting from Excel
            export_mode='auto',  # Set export mode
            export_to_df=True,  # Export to DataFrame
            hide_grid=False,  # Hide or show the grid
            theme='ag-theme-balham')  # Theme

# Assign button
button = widgets.Button(description="Confirm Selection")

def on_rows_selected(change):
    # Set global variable
    global df_selected_rows
    
    # Capture selected rows
    selected_rows = change["new"].get("rows")
    
    # Convert selected rows to DataFrame
    df_selected_rows = pd.DataFrame(selected_rows)
    
    # Check if any rows are selected
    if selected_rows is None:
        print("No rows selected")

# Observe changes in the grid data
grid.observe(on_rows_selected, names=["grid_data_out"])


# Create on_button_click method
def on_button_click(b):
    # Use the get_selected_rows method to capture selected rows
    grid.get_selected_rows()
    
    # Display selected rows
    display(df_selected_rows)
    

# Show grid and button
display(grid)
display(button)

# Bind the callback function to the button
button.on_click(on_button_click)

@mariobuikhuizen
Copy link
Contributor

One comment about calling display(df_selected_rows) after grid.get_selected_rows(): this will always be one click behind, because grid.get_selected_rows() is called later.

@Lucid-Will
Copy link
Author

One comment about calling display(df_selected_rows) after grid.get_selected_rows(): this will always be one click behind, because grid.get_selected_rows() is called later.

I've done more testing today and believe that the observer itself isn't functioning as expected in the notebook environment. I also tried in a Synapse Spark notebook. Adding back the print statements for on_row_selection yields no output.

Quite a bummer, was a pretty awesome use-case. Cheers again for your help on this. I'll keep an eye on the repo and save my testing for retrying later.

Tested code:

#!pip install ipyaggrid -q
import pandas as pd
from ipyaggrid import Grid
import ipywidgets as widgets

# Sample data
data = {
    'ID': [1, 2, 3, 4, 5],
    'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Eve'],
    'Age': [24, 30, 35, 22, 29],
    'City': ['New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix'],
    'Subscription': ['Yes', 'No', 'Yes', 'No', 'Yes']
}

# Create DataFrame
pdf_subscription_load = pd.DataFrame(data)

# Preview DataFrame
# display(pdf_subscription_load)

# Set grid options
grid_options = {
    'columnDefs': [{'headerName': col, 'field': col, 'checkboxSelection': (col == 'ID'), 'sortable': True, 'filter': True} for col in pdf_subscription_load.columns],
    'enableSorting': True,
    'enableFilter': True,
    'enableColResize': True,
    'enableRangeSelection': True,
    'rowSelection': 'multiple',
}

# Define grid
grid = Grid(grid_data=pdf_subscription_load,
            grid_options=grid_options,
            grid_options_multi=[],  # Additional grid options for multi-grid setups
            columns_fit='size_to_fit',
            index=False,
            keep_multiindex=False,
            compress_data=True,
            quick_filter=True,  # Enable or disable quick filtering
            export_csv=True,  # Enable or disable CSV export
            export_excel=True,  # Enable or disable Excel export
            show_toggle_delete=True,  # Show or hide delete toggle
            show_toggle_edit=True,  # Show or hide edit toggle
            sync_on_edit=True,  # Synchronize edits
            sync_grid=True,  # Synchronize the grid
            paste_from_excel=False,  # Allow pasting from Excel
            export_mode='auto',  # Set export mode
            export_to_df=True,  # Export to DataFrame
            hide_grid=False,  # Hide or show the grid
            theme='ag-theme-balham')  # Theme

# Initialize df_selected_rows globally
df_selected_rows = pd.DataFrame()

# Assign button
button = widgets.Button(description="Confirm Selection")

def on_rows_selected(change):
    # Set global variable
    global df_selected_rows
    
    # Capture selected rows
    selected_rows = change["new"].get("rows")
    
    # Convert selected rows to DataFrame
    df_selected_rows = pd.DataFrame(selected_rows)
    
    # Check if any rows are selected
    if selected_rows is None:
        print("No rows selected")
    else:
        print(selected_rows)

# Observe changes in the grid data
grid.observe(on_rows_selected, names=["grid_data_out"])


# Create on_button_click method
def on_button_click(b):
    # Use the get_selected_rows method to capture selected rows
    #grid.get_selected_rows()
    
    # Display selected rows
    display(df_selected_rows)
    

# Show grid and button
display(grid)
display(button)

# Bind the callback function to the button
button.on_click(on_button_click)

Results in Jupyter notebook in Visual Studio Code:

image

Results in Jupyter notebook in Synapse Spark and Fabric notebooks:

No print statements upon select:

image

Empty dataframe indicating observer not identifying selections:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants