itables icon indicating copy to clipboard operation
itables copied to clipboard

shiny widget and itable problem (table not extending to show all rows)

Open danieltomasz opened this issue 1 year ago • 7 comments

I have my MVE code here

import pandas as pd
import numpy as np
from shiny import App, ui, render, reactive
from shinywidgets import output_widget, render_widget, reactive_read
from itables.widget import ITable

# Generate sample data with 10 rows
np.random.seed(42)  # For reproducibility
descriptions = [
    "This is a very long description that needs to wrap across multiple lines to be readable in the table.",
    "Another long text that demonstrates the need for proper column width settings and text wrapping capabilities.",
    "The third description with enough content to show how text wrapping works in DataTables with proper configuration.",
    "When setting up tables with long text content, it's important to configure the column widths appropriately.",
    "Text wrapping is essential for maintaining readability while efficiently using the available screen space.",
]

# Create a DataFrame with 10 rows
data = []
for i in range(1, 11):
    data.append(
        {
            "ID": i,
            "Title": f"Item {i}",
            "Category": np.random.choice(["Product", "Service", "Resource", "Tool"]),
            "Price": round(np.random.uniform(10, 1000), 2),
            "Description": descriptions[i % len(descriptions)],
        }
    )

df = pd.DataFrame(data)

# Define UI
app_ui = ui.page_fluid(
    ui.h1("ITables Widget Example"),
    ui.p("This example shows the use of ITable widget with Shiny output_widget"),
    # Add custom CSS for styling the table
    ui.HTML("""
    <style>
        /* Control text wrapping */
        .dataTable td {
            white-space: normal !important;
            word-wrap: break-word;
            max-width: 0;  /* Important for text wrapping */
        }
        
        /* Improve table appearance */
        .dataTable {
            border-collapse: collapse;
            width: 100%;
        }
        
        .dataTable th, .dataTable td {
            padding: 8px;
            border: 1px solid #ddd;
        }
        
        /* Optional: Add some hover effect */
        .dataTable tr:hover {
            background-color: #f5f5f5;
        }
    </style>"""),
    # Add a dropdown to select rows
    ui.input_select(
        "row_selection",
        "Select rows:",
        {"none": "None", "even": "Even rows", "odd": "Odd rows", "all": "All rows"},
    ),
    # Output widget placeholder
    output_widget("my_table"),
    # Display selected rows
    ui.output_text("selected_rows_info"),
)


# Define server
def server(input, output, session):
    @render_widget
    def my_table():
        return ITable(
            df,
            caption="Sample Data with ITable Widget",
            select=True,
            columnDefs=[
                {"width": "5%", "targets": 0},
                {"width": "15%", "targets": 1},
                {"width": "10%", "targets": 2},
                {"width": "10%", "targets": 3},
                {"width": "60%", "targets": 4},
            ],
            style="table-layout:fixed; width:100%;",
        )

    @reactive.effect
    def _():
        # Update the widget based on selection
        selection = input.row_selection()

        if selection == "none":
            selected_rows = []
        elif selection == "even":
            selected_rows = [i for i in range(len(df)) if (i + 1) % 2 == 0]
        elif selection == "odd":
            selected_rows = [i for i in range(len(df)) if (i + 1) % 2 == 1]
        else:  # all
            selected_rows = list(range(len(df)))

        my_table.widget.update(selected_rows=selected_rows)

    @render.text
    def selected_rows_info():
        rows = reactive_read(my_table.widget, "selected_rows")
        if not rows:
            return "No rows selected"
        return f"Selected rows: {rows}"


# Create the Shiny app
app = App(app_ui, server)

the rows at the bottom are cut Image

my itables are 2.2.5, shiny is 1.3 and shinywigets 0.5.1

danieltomasz avatar Mar 20 '25 12:03 danieltomasz

The difference between my example and docs is that I am using shiny core with render widget vs shiny express https://mwouts.github.io/itables/shiny.html

danieltomasz avatar Mar 20 '25 13:03 danieltomasz

Thank you @danieltomasz for testing and reporting! Very nice example, and I confirm that I can reproduce the issue in the Shiny app.

Since that version actually uses the Jupyter widget I also tried to provide the same inputs in Jupyter but there the table seemed to work fine.

Now when I look at the HTML generated by the Shiny app it seems that there is a wrapper around the table that has "only 400px height": Image

while the table itself has a height of 502px: Image

At the moment I am not sure of where the 400px height comes from, nor how to investigate this. We might ask for help on the datatables forum as this seems to happen on a div that belongs to datatables, but for that we would need to deploy an example app for them to give a look.

mwouts avatar Mar 20 '25 21:03 mwouts

Does itables supports Pyodide? Edit: I will try to rewrite my example as quarto html, maybe it will be enough

danieltomasz avatar Mar 25 '25 16:03 danieltomasz

hi @mwouts, a session of assertive debuging with Claude (until I pasted the html of the generated webpage, it was giving me halucinated advices) solved this problem for me:

Looking at the HTML source, I can see that your application is properly set up with the necessary widget infrastructure, including the ipywidget-output-binding that Shiny uses to display the ITable widget. This confirms my suspicion that we're dealing with an iframe sizing issue. When AnyWidget (which ITable uses) renders in Shiny, it creates an iframe that needs explicit height management. Root Cause The issue is that the iframe containing the ITable widget isn't properly adjusting its height to the content. Even though we're styling the containers, the iframe itself is constraining the content. Solution: Target the iframe directly Add this CSS to directly target the iframe:

ui.HTML("""
<style>
    /* Target the iframe that contains the widget */
    .shiny-ipywidget-output iframe {
        min-height: 800px !important;
        height: auto !important;
    }
    
    /* Add !important to override any inline styles */
    #my_table, #my_table iframe {
        min-height: 800px !important;
    }
</style>
"""),

danieltomasz avatar Mar 25 '25 17:03 danieltomasz

Might be related to https://github.com/mwouts/itables/issues/275 as well?

danieltomasz avatar Mar 25 '25 18:03 danieltomasz

I can reproduce the issue but I don't see what causes it exactly. I have reached out to the Shiny developers at https://github.com/posit-dev/py-shinywidgets/issues/199, hopefully they will be able to point us in the right direction.

mwouts avatar Jun 01 '25 21:06 mwouts

@cpsievert very kindly came with a workaround, which is to create the output widget with a fillable=False argument, like this: output_widget("my_table", fillable=False). That works on my MRE and seemingly on your example too - can you give it a try? Thank you!

mwouts avatar Jun 04 '25 06:06 mwouts

Closing as the workaround is now in the docs, but I will follow-up with #406 later on

mwouts avatar Jun 10 '25 20:06 mwouts

thanks for working on this, I didnt have time to follow up but everything seems to work! :)

danieltomasz avatar Jun 10 '25 20:06 danieltomasz

thanks for working on this, I didnt have time to follow up but everything seems to work! :)

You're very welcome, thanks for reporting the issue too!

mwouts avatar Jun 10 '25 20:06 mwouts