diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 39dd754..cc20286 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -39,6 +39,7 @@ website: - get-started/index.qmd #- get-started/overview.qmd - get-started/code-structure.qmd + - get-started/display-export.qmd - section: "Controls" contents: - get-started/controls-sorting.qmd diff --git a/docs/examples/index.qmd b/docs/examples/index.qmd deleted file mode 100644 index 47070f3..0000000 --- a/docs/examples/index.qmd +++ /dev/null @@ -1,1515 +0,0 @@ ---- -title: Examples -jupyter: python3 -execute: - daemon: false -format: - html: - toc: true ---- - - -```{python} -# | label: setup -# | code-fold: true -import htmltools -from reactable import bigblock, RT, options, Reactable -from reactable.widgets import BigblockWidget, embed_css -from reactable.models import ( - process_data, - default_columns, - CellInfo, - Column, - ColFormat, - ColGroup, - Language, - Props, - Theme, - JS, - ColFormatGroupBy, -) -from reactable.data import cars_93, co2, prices, sleep, starwars, us_states, us_expenditures -from htmltools import tags -from palmerpenguins import load_penguins -import polars as pl - - -embed_css() - -penguins = load_penguins() -pl_penguins = pl.from_pandas(penguins) - -``` - -## Basic usage - -```{python} -# | label: basic-usage -Reactable( - data={"a": [1, 2], "b": ["a", "b"], "c": ["c", "d"]}, - columns=dict( - a=Column(name="colA", footer="footer", min_width=50), - b=Column(name="colB", width=120, min_width=50), - c=Column(name="colC"), - ), - # theme = asdict(Theme(color = "blue")) -) - -``` - - -### Column definitions - - -```{python} -# | label: column-definitions -# shouldn't subset columns -lil_penguins = process_data(pl_penguins.head()) -Reactable( - data=lil_penguins, - columns=[ - Column(id="species", name="Species"), - Column(id="island", name="Island"), - Column(id="bill_length_mm", align="center"), - ], -) -``` - -### Default columns - -(NOTE: Added header) - -* TODO: defaultColDef merged in on python side - -```{python} -# | label: default-columns -Reactable( - lil_penguins, - default_col_def=Column(align="center"), - columns=[Column(id="species", min_width=140)], - bordered=True, - highlight=True, -) - -``` - -## Sorting - -### Default sorted columns - -```{python} -# | label: default-sorted-columns -# defaultSorted may be.. -# * dict mapping name -> order -# * list of names -# for each col in default sorted: -# * set Column.defaultSortDesc (if not set already) -# * -Reactable( - pl_penguins, - default_sorted=["species", "island"], -) -``` - -```{python} -# | label: default-sorted-columns2 -# TODO: support defaultSorted being a dict? -BigblockWidget( - props=Props( - pl_penguins, - columns=[Column(id="species", default_sort_order="desc")], - default_sorted=["species", "island"], - ).to_props() -) -``` - -### Default sort order - - -```{python} -# | label: default-sort-order -Reactable( - pl_penguins, - default_sort_order="desc", - columns=[Column(id="species", default_sort_order="asc")], - default_sorted=["species", "island"], -) -``` - -### Sort missing values last - -TODO(jupyter): Out of range float values are not JSON compliant - -```{python} -# | label: sort-missing -import math - -df = pl.DataFrame( - { - "n": [1.0, 2.0, 3.0, -math.inf, math.inf], - "x": [2.0, 3.0, 1.0, None, math.nan], - "y": ["aa", "cc", "bb", None, None], - }, - strict=False, -) - -Reactable( - df, - default_col_def=Column(sort_na_last=True), - default_sorted=["x"], -) -``` - -### No sorting - -```{python} -# | label: sort-false -Reactable( - pl_penguins.head(), - sortable=False, - show_sortable=True, - columns=[ - Column(id="bill_length_mm", sortable=True), - Column(id="bill_depth_mm", sortable=True), - ], -) -``` - - -### Hide sort icons - - -```{python} -# | label: sort-hide -Reactable( - pl_penguins.head(), - show_sort_icon=False, -) -``` - -## Filtering - -```{python} -# | label: filter -cars = cars_93[:20, ["manufacturer", "model", "type", "air_bags", "price"]] -Reactable( - cars, - filterable=True, - min_rows=10, -) -``` - - -```{python} -# | label: filter-column -Reactable( - cars, - filterable=True, - columns=[Column(id="price", filterable=False)], - default_page_size=5, -) -``` - -### Custom filtering - -```{python} -# | label: filter-custom -titled = pl_penguins.with_columns(pl.col("species").str.to_titlecase()) - -js_filter = JS( - """ -function(rows, columnId, filterValue) { - return rows.filter(function(row) { - return row.values[columnId].indexOf(filterValue) !== -1 - }) -}""" -) - -Reactable( - data=titled.head(2), - filterable=True, - columns={ - "manufacturer": Column(filter_method=js_filter), - }, - default_page_size=5, -) -``` - -## Searching - - -```{python} -# | label: search -cars = cars_93[:20, ["manufacturer", "model", "type", "air_bags", "price"]] -Reactable(data=cars, searchable=True, min_rows=10) -``` - -### Custom searching - -## Pagination - -```{python} -# | label: pagination -Reactable(pl_penguins, default_page_size=4) -``` - - - -```{python} -# | label: pagination-min-rows -Reactable(pl_penguins, default_page_size=4, min_rows=4, searchable=True) - -``` - - -### Page size options - -```{python} -# | label: pagination-size-options -Reactable( - pl_penguins, - show_page_size_options=True, - page_size_options=[4, 8, 12], - default_page_size=4, -) -``` - -### Alternative pagination types - -#### Page jump - -```{python} -# | label: pagination-type-jump -Reactable( - pl_penguins, - pagination_type="jump", - default_page_size=4, -) -``` - -#### Simple - -```{python} -# | label: pagination-type-simple -Reactable( - pl_penguins, - pagination_type="simple", - default_page_size=4, -) -``` - -### Hide page info - - -```{python} -# | label: pagination-page-info-false -Reactable( - pl_penguins, - show_page_info=False, - default_page_size=4, -) -``` - -```{python} -# | label: pagination-page-size -Reactable( - pl_penguins, - show_page_info=False, - show_page_size_options=True, - default_page_size=4, -) -``` - -### Always show pagination - - -```{python} -# | label: pagination-show -Reactable( - pl_penguins, - show_pagination=True, -) -``` - -### No pagination - -```{python} -# | label: pagination-false -Reactable( - pl_penguins, - pagination=False, - highlight=True, - height=250, -) -``` - -## Grouping and aggregation - -```{python} -# | label: group-by -data = cars_93[10:22, ["manufacturer", "model", "type", "price", "mpg_city"]] - -Reactable( - data, - group_by="manufacturer", -) -``` - -```{python} -# | label: group-by-aggregate -data = cars_93[14:38, ["type", "price", "mpg_city", "drive_train", "man_trans_avail"]] -Reactable( - data, - group_by="type", - columns=[ - Column(id="price", aggregate="max"), - Column(id="mpg_city", aggregate="mean", format=ColFormat(digits=1)), - Column(id="drive_train", aggregate="unique"), - Column(id="man_trans_avail", aggregate="frequency"), - ], -) -``` - -```python -colDef(aggregate = "sum") # Sum of numbers -colDef(aggregate = "mean") # Mean of numbers -colDef(aggregate = "max") # Maximum of numbers -colDef(aggregate = "min") # Minimum of numbers -colDef(aggregate = "median") # Median of numbers -colDef(aggregate = "count") # Count of values -colDef(aggregate = "unique") # Comma-separated list of unique values -colDef(aggregate = "frequency") # Comma-separated counts of unique values -``` - -```python -colDef( - aggregate = JS(" - function(values, rows) { - // input: - // - values: an array of all values in the group - // - rows: an array of row data values for all rows in the group (optional) - // - // output: - // - an aggregated value, e.g. a comma-separated list - return values.join(', ') - } - ") -) -``` - - -### Multiple groups - -```{python} -# | label: group-by-multiple -Reactable( - us_states, - group_by=["Region", "Division"], - columns=[ - Column(id="Division", aggregate="unique"), - Column(id="Area", aggregate="sum", format=ColFormat(separators=True)), - ], -) - -``` - - -### Custom aggregate function - -* TODO: custom JS in colDef.aggregate - - -### Include sub rows in pagination - - -```{python} -# | label: group-by-paginate-sub-rows -data = cars_93[["manufacturer", "model", "type", "price", "mpg_city"]] - -Reactable( - data=cars_93, - group_by="type", - paginate_sub_rows=True, -) -``` - -## Column formatting - -```{python} -# | label: column-format-overview -Reactable( - data=prices, - columns=[ - Column(id="price_usd", format=ColFormat(prefix="$", separators=True, digits=2)), - Column(id="price_inr", format=ColFormat(currency="INR", separators=True, locales="hi-IN")), - Column(id="number_fr", format=ColFormat(locales="fr-FR")), - Column(id="temp", format=ColFormat(suffix="°C")), - Column(id="percent", format=ColFormat(percent=True, digits=1)), - Column(id="date", format=ColFormat(date=True, locales="en-GB")), - ], -) -``` - -### Date formatting - - -```{python} -# | label: column-format-date -ts = ["2019-01-02 03:22:15", "2019-03-15 09:15:55", "2019-09-22 14:20:00"] -ser = pl.Series(ts, dtype=pl.Date).str.to_datetime().dt.replace_time_zone("America/New_York") - -data = pl.DataFrame( - { - "datetime": ser, - "date": ser, - "time": ser, - "time_24h": ser, - "datetime_pt_BR": ser, - } -) - -Reactable( - data, - columns=[ - Column(id="datetime", format=ColFormat(datetime=True)), - Column(id="date", format=ColFormat(date=True)), - Column(id="time", format=ColFormat(time=True)), - Column(id="time_24h", format=ColFormat(time=True, hour12=False)), - Column(id="datetime_pt_BR", format=ColFormat(datetime=True, locales="pt-BR")), - ], -) -``` - - -### Currency formatting - -```{python} -# | label: column-format-currency -data = pl.DataFrame( - { - "USD": [12.12, 2141.213, 0.42, 1.55, 34414], - "EUR": [10.68, 1884.27, 0.37, 1.36, 30284.32], - "INR": [861.07, 152122.48, 29.84, 110, 2444942.63], - "JPY": [1280.0, 226144, 44.36, 164.0, 3634634.61], - "MAD": [115.78, 20453.94, 4.01, 15.0, 328739.73], - } -) - -Reactable( - data, - columns=[ - Column(id="USD", format=ColFormat(currency="USD", separators=True, locales="en-US")), - Column(id="EUR", format=ColFormat(currency="EUR", separators=True, locales="de-DE")), - Column(id="INR", format=ColFormat(currency="INR", separators=True, locales="hi-IN")), - Column(id="JPY", format=ColFormat(currency="JPY", separators=True, locales="ja-JP")), - Column(id="MAD", format=ColFormat(currency="MAD", separators=True, locales="ar-MA")), - ], -) -``` - -### Formatting aggregated cells - -```python -from reactable.models import ColFormatGroupBy - -Column( - format = ColFormatGroupBy( - cell = colFormat(...), # Standard cells - aggregated = colFormat(...) # Aggregated cells - ) -) -``` - - -```{python} -# | label: column-format-aggregate -data = us_states - -Reactable( - data, - group_by="Region", - columns=[ - Column( - id="States", - aggregate="count", - format=ColFormatGroupBy(aggregated=ColFormat(suffix=" states")), - ), - Column(id="Area", aggregate="sum", format=ColFormat(suffix=" mi²", separators=True)), - ], -) -``` - -### Displaying missing values - - - -```{python} -# | label: column-format-missing -data = pl.DataFrame( - { - "n": [1, 2, None, 4, 5], - "x": [55, 27, None, float("nan"), 19], - "y": [1, None, 0.25, 0.55, None], - }, - strict=False, -) - -Reactable( - data, - columns=[ - Column(id="x", na="-"), - Column(id="y", na="NA", format=ColFormat(percent=True)), - ], -) -``` - -### Custom data formatting - -## Custom rendering - -### Cell rendering (py) - - -```{python} -from reactable.models import CellInfo -from reactable.tags import to_hydrate_format -import htmltools - -data = cars_93[:5, ["manufacturer", "model", "type", "air_bags", "price"]] - - -def render_link(ci: CellInfo) -> htmltools.Tag: - # TODO: should use polars expressions - manufacturer = data[ci.row_index, "manufacturer"] - url = htmltools.a( - ci.value, - href=f"https://wikipedia.org/wiki/{manufacturer}_{ci.value}", - target="blank_", - ) - - return to_hydrate_format(url) - - -Reactable( - data=data, - columns=[ - Column(id="model", cell=render_link), - Column(id="air_bags", cell=lambda ci: "❌ No" if ci.value == "None" else f"✅ Yes"), - Column(id="price", cell=lambda ci: f"${int(ci.value*1000):,}"), - ], - ) -``` - -### Cell rendering (js) - -* TODO finish rendering sections - -### Grouped cell rendering (py) - -### Grouped cell rendering (js) - -### Aggregated cell rendering (py) - -### Aggregated cell rendering (js) - -### Heading rendering (py) - -### Heading rendering (js) - -### Custom metadata - -## Footers - -### Python render function - -```{python} -# | label: footer-render-py -data = cars_93[17:47, ["manufacturer", "model", "type", "price"]] - -Reactable( - data, - default_page_size=5, - columns=[ - Column(id="manufacturer", footer="Total"), - Column(id="price", footer=lambda col_info: f"{sum(col_info.values):.2f}"), - ], - default_col_def=Column(footer_style={"font-weight": "bold"}), -) - -``` - -### JS render function - -```{python} -# | label: footer-render-js -js_sum_column = JS( - """function(column, state) { - let total = 0 - state.sortedData.forEach(function(row) { - total += row[column.id] - }) - return total.toLocaleString('en-US', { style: 'currency', currency: 'USD' }) -} """ -) - -Reactable( - data, - default_page_size=5, - columns=[ - Column(id="manufacturer", footer="Total"), - Column(id="price", footer=js_sum_column), - ], - default_col_def=Column(footer_style={"font-weight": "bold"}), -) - -``` - -### Embedding HTML Widgets - -* TODO: complete original example - -```{python} -# | label: footer-render-widget -import plotly.express as px -import htmltools as h - -cars = cars_93[["manufacturer", "type", "price"]] -p = px.box(cars.to_pandas(), x="price") - -import plotly.graph_objects as go - -data = pl.DataFrame({"x": [1]}) - -Reactable( - cars, - columns={"price": Column(footer=go.FigureWidget(p))}, - # details=lambda indx: go.FigureWidget(p), -) - -``` - -## Expanding row details - - -* TODO: using python function - -```{python} -# | label: details-render-py -import htmltools - -Reactable( - pl_penguins, - details=lambda row_info: htmltools.div( - "Details for row: ", row_info.row_index, htmltools.pre("nice") - ), -) - -``` - -```{python} -# | label: details-render-js -js_details = JS( - """function(rowInfo) { - return `Details for row: ${rowInfo.index}` + - `
${JSON.stringify(rowInfo.values, null, 2)}` -}""" -) - -Reactable( - pl_penguins, - details=Column( - name="more", - html=True, - width=60, - details=js_details, - ), -) -``` - -### Nested tables - - - -```{python} -# | label: details-nested-tables -from htmltools import HTML, div - - -sub_tables = { - g: div(bigblock(Props(df, outlined=True)), style="padding: 1rem") - for g, df in pl_penguins.group_by("species", "island") -} - -df_uniq = pl_penguins.select(["species", "island"]).unique() - -Reactable( - df_uniq, - details=lambda row_info: list(sub_tables.values())[row_info.row_index], -) - -``` - -### Conditional row details - -* TODO: sub frames are centered? Maybe a quarto style thing? - -```{python} -# | label: details-conditional-none -sub_frame = pl.DataFrame({"x": [1, 2, 3], "y": ["a", "b", "c"]}) - -Reactable( - pl_penguins[:5], - details=lambda row_info: ( - bigblock(Props(sub_frame, full_width=False)) if row_info.row_index in [2, 4] else None - ), -) -``` - -### Multiple row details - -* TODO: Should be able to validate output of column object - -```{python} -# | label: details-multiple-cols-rows - -sub_frame = pl.DataFrame({"x": [1, 2, 3], "y": ["a", "b", "c"]}) -Reactable( - pl_penguins[:5], - details=lambda row_info: ( - bigblock(Props(sub_frame, full_width=False)) if row_info.row_index in [2, 4] else None - ), - columns=[ - Column( - id="bill_length_mm", - details=lambda row_info: f"bill_length_mm: {pl_penguins[row_info.row_index, 'bill_length_mm']}", - ), - Column( - id="bill_depth_mm", - format=ColFormat(digits=1), - details=JS( - """ - function(rowInfo) { - return 'bill_depth_mm: ' + rowInfo.values['bill_depth_mm'] - } - """ - ), - ), - ], -) -``` - -### Default expanded rows - - -```{python} -# | label: details-default-expanded -Reactable( - pl_penguins[:12], - default_page_size=4, - details=lambda indx: f"Details for row: {indx}", - default_expanded=True, -) -``` - - -## Conditional styling - -### Cell styling (python) - - -```{python} -# | label: style-cond-cell-py -from reactable.models import CellInfo - - -def cond_style(ci: CellInfo): - return { - "color": "#008800" if ci.value > 0 else "#e00000", - "font-weight": "bold", - } - - -Reactable( - sleep[:6, :], - columns=[ - Column( - id="extra", - style=cond_style, - ) - ], -) -``` - -### Cell styling (js) - - -```{python} -# | label: style-cond-cell-js -js_style = JS( - """function(rowInfo) { - const value = rowInfo.values['extra'] - let color - if (value > 0) { - color = '#008000' - } else if (value < 0) { - color = '#e00000' - } else { - color = '#777' - } - return { color: color, fontWeight: 'bold' } - }""" -) -Reactable( - sleep[:6, :], - columns=[ - Column( - id="extra", - style=js_style, - ) - ], -) -``` - -### Row styling (python) - -* TODO: somehow set bold css class style - -```{python} -# | label: style-cond-row-py -Reactable( - sleep[:6, :], - row_style=lambda indx: ( - {"background": "rgba(0, 0, 0, 0.05)"} if sleep[indx, "extra"] < -1 else None - ), - row_class=lambda indx: "bold" if sleep[indx, "extra"] < -1 else None, -) -``` - - -### Row styling (js) - - -```{python} -# | label: style-cond-row-js - -Reactable( - sleep[:6, :], - row_style=JS( - """function(rowInfo) { - if (rowInfo.values['extra'] < -1) { - return { background: 'rgba(0, 0, 0, 0.05)' } - } - }""" - ), - row_class=JS( - """function(rowInfo) { - if (rowInfo.values['extra'] < -1) { - return 'bold' - } - }""" - ), -) -``` - -### Custom metadata - -* TODO: expose Reactable on window - -```{python} -# | label: style-metadata - -from IPython.display import display - -cars = cars_93[:6, ["manufacturer", "model", "type", "price", "mpg_city"]] - -js_mpg_background = JS( - """function(rowInfo, column, state) { - const { showColors, mpgColor } = state.meta - if (showColors) { - return { - backgroundColor: rowInfo.values[column.id] > 20 ? mpgColor : 'transparent' - } - } - } -""" -) - -bb = Reactable( - cars, - columns=[ - Column( - id="mpg_city", - style=js_mpg_background, - ) - ], - meta={ - # yellow - "mpgColor": "#ff9f1a", - "showColors": True, - }, - element_id="cars-colors-table", -) - -import htmltools as ht - -clicker = ht.TagList( - ht.tags.label( - ht.tags.input( - type="checkbox", - checked=None, - onclick="Reactable.setMeta('cars-colors-table', function(prevMeta) { return { showColors: !prevMeta.showColors } })", - ), - "Show color scale", - ), -) - -display(bb) -clicker -``` - -## Table styling - -### Highlight rows on hover - -```{python} -# | label: style-table-highlight -Reactable( - pl_penguins[:5], - highlight=True, -) - -``` - -### Bordered - -```{python} -# | label: style-table-border -Reactable( - pl_penguins[:5], - bordered=True, -) -``` - - -### Borderless - - -```{python} -# | label: style-table-borderless -Reactable( - pl_penguins[:5], - borderless=True, -) - -``` - -### Outlined - -```{python} -# | label: style-table-outlined -Reactable( - pl_penguins[:5], - outlined=True, -) -``` - -### Striped - - -```{python} -# | label: style-table-striped -Reactable( - pl_penguins[:5], - striped=True, -) -``` - -### Bordered + striped + highlighting - - -```{python} -# | label: style-table-bordered -Reactable( - pl_penguins[:5], - bordered=True, - striped=True, - highlight=True, -) -``` - -### Outlined + borderless - -```{python} -# | label: style-table-outlined-borderless -Reactable( - pl_penguins[:5], - outlined=True, - borderless=True, -) -``` - -### Compact - - -```{python} -# | label: style-table-compact -Reactable( - pl_penguins[:5], - compact=True, -) -``` - -### No text wrapping - - -```{python} -# | label: style-table-wrap -import polars.selectors as cs - -cat_penguins = pl_penguins.group_by("species").agg(cs.ends_with("_mm").str.concat(", ")) - -Reactable( - cat_penguins, - wrap=False, - resizable=True, - bordered=True, -) -``` - -### Fixed height + sticky header/footer - - -```{python} -# | label: style-table-height-sticky-footer -Reactable( - pl_penguins, - height=270, - striped=True, - default_col_def=Column( - footer=lambda col_info: htmltools.div(col_info.name, style="font-weight: bold") - ), -) -``` - -### Column widths - - -```{python} -# | label: style-column-widths -Reactable( - cars_93[:6, ["make", "type", "weight"]], - columns=[ - Column(id="make", min_width=200), - Column(id="type", min_width=100), - Column(id="weight", min_width=100), - ], - bordered=True, -) -``` - -### No full width - - -```{python} -# | label: style-full-width -Reactable( - cars_93[:6, :5], - full_width=False, - bordered=True, - default_col_def=Column(min_width=120), -) -``` - -You can also set a maximum or fixed width on the table: - -```{python} -# | label: style-min-max-width -Reactable( - cars_93[:6, :5], - full_width=False, - bordered=True, - default_col_def=Column(min_width=120), - # Set a maximum width on the table - style={"max-width": 650}, - # or a fixed width: - width=650, -) - -``` - -### Vertical alignment - -```{python} -# | label: style-vertical-alignment -from htmltools import div - -data = starwars[:6, ["name", "height", "mass", "gender", "homeworld", "species"]] - - -def render_species(ci: CellInfo): - species = data[ci.row_index, "species"] - species_name = species if species else "Unknown" - return div( - div(ci.value, style="font-weight: 600"), - div(species_name, style="font-size: 0.75rem"), - ) - - -Reactable( - data, - columns=[ - Column( - id="name", - name="Character / Species", - cell=render_species, - ), - Column(id="species", show=False), - ], - default_col_def=Column(v_align="center", header_v_align="bottom"), - bordered=True, -) -``` - -### Custom CSS - - - -```{python} -# | label: style-custom-css -from IPython.display import display, HTML - -display( - HTML( - """ - -""" - ) -) - -Reactable( - pl_penguins[:18], - default_page_size=6, - borderless=True, - class_="my-tbl", - default_col_def=Column(header_class="my-header"), - # columns but for penguins data - columns=[ - Column(id="species", class_="my-col"), - Column(id="island", class_="my-col"), - ], - row_class="my-row", -) - -``` - -## Theming - -```{python} -# | label: theme -Reactable( - pl_penguins[:30], - searchable=True, - striped=True, - highlight=True, - bordered=True, - theme=Theme( - border_color="#dfe2e5", - striped_color="#f6f8fa", - highlight_color="#f0f5f9", - cell_padding="8px 12px", - style={ - "font-family": "-apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif" - }, - search_input_style={"width": "100%"}, - ), -) - -``` - -### Global theme - - -```{python} -# | label: theme-global -options.theme = Theme( - color="hsl(233, 9%, 87%)", - background_color="hsl(233, 9%, 19%) !important", - border_color="hsl(233, 9%, 22%) !important", - striped_color="hsl(233, 12%, 22%)", - highlight_color="hsl(233, 12%, 24%)", - input_style={"background-color": "hsl(233, 9%, 25%) !important"}, - select_style={"background-color": "hsl(233, 9%, 25%)"}, - page_button_hover_style={"background-color": "hsl(233, 9%, 25%)"}, - page_button_active_style={"background-color": "hsl(233, 9%, 28%)"}, -) - -Reactable( - pl_penguins[:30], - filterable=True, - show_page_size_options=True, - striped=True, - highlight=True, - details=lambda index: f"Details for row {index}", -) -``` - -```{python} -options.reset() -``` - -### Nested selectors - -### Dynamic theming - -## Column groups - - -```{python} -# | label: column-groups -Reactable( - pl_penguins[:5, ["species", "island", "bill_length_mm", "bill_depth_mm"]], - columns=[ - Column(id="bill_length_mm", name="Length (mm)"), - Column(id="bill_depth_mm", name="Depth (mm)"), - Column(id="species", name="Species"), - Column(id="island", name="Island"), - ], - column_groups=[ - ColGroup(name="Bill", columns=["bill_length_mm", "bill_depth_mm"]), - ], -) -``` - -## Column resizing - - -```{python} -# | label: resize-columns -Reactable( - cars_93[:5, :], - resizable=True, - wrap=False, - bordered=True, -) -``` - -### Sticky columns - -```{python} -# | label: sticky-columns -Reactable( - cars_93[:5, :], - columns=[ - Column( - id="manufacturer", - sticky="left", - style={"border-right": "1px solid #eee"}, - header_style={"border-right": "1px solid #eee"}, - ), - Column( - id="make", - sticky="right", - style={"border-left": "1px solid #eee"}, - header_style={"border-left": "1px solid #eee"}, - ), - ], - default_col_def=Column(min_width=150), -) -``` - -### Multiple sticky columns - -```{python} -# | label: sticky-columns-multiple -stick_style = {"background-color": "#f7f7f7"} - -Reactable( - cars_93[:5, :], - columns=[ - Column(id="manufacturer", sticky="left", style=stick_style, header_style=stick_style), - Column(id="make", sticky="left", style=stick_style, header_style=stick_style), - Column(id="type", sticky="left", style=stick_style, header_style=stick_style), - ], -) -``` - -### Sticky column groups - - -```{python} -# | label: sticky-column-groups -Reactable( - cars_93[:5, :], - column_groups=[ - ColGroup(name="Make", columns=["manufacturer", "model"], sticky="left"), - ColGroup(name="Price", columns=["min_price", "price", "max_price"], sticky="left"), - ], - default_col_def=Column(footer="Footer"), - resizable=True, - wrap=False, - bordered=True, -) -``` - -## Row names and row headers - -### Row names - -* TODO: how far to go for pandas? -* Display spanner index over names? - - -```{python} -# | label: rownames -Reactable( - pl_penguins[:5], - rownames=True, -) -``` - -### Row headers - - -```{python} -#| label: rownames-headers -data = cars_93[:5, ["manufacturer", "model", "type", "price", "mpg_city", "air_bags"]] - -Reactable( - data, - columns=[ - Column(id="manufacturer", row_header=True, style={"font-weight": "600"}), - ], - bordered=True, - ) -``` - -## Cell click actions - -### Expand on click - - - -```{python} -#| label: click-expand -Reactable( - pl_penguins[150:155], - group_by="species", - details=lambda index: f"Details for row: {index}", - on_click="expand", - row_style={"cursor": "pointer"}, - ) - -``` - -### Select on click - - -```{python} -# | label: click-select -Reactable( - pl_penguins[:5], - selection="multiple", - on_click="select", -) -``` - -### Custom action - -```{python} -# | label: click-custom -data = cars_93[:5, ["manufacturer", "model", "type", "price"]] -data["details"] = None - -js_on_click = JS( - """function(rowInfo, column) { - // Only handle click events on the 'details' column - if (column.id !== 'details') { - return - } - - // Display an alert dialog with details for the row - window.alert('Details for row ' + rowInfo.index + ':\\n' + JSON.stringify(rowInfo.values, null, 2)) - - // Send the click event to Shiny, which will be available in input$show_details - // Note that the row index starts at 0 in JavaScript, so we add 1 - if (window.Shiny) { - Shiny.setInputValue('show_details', { index: rowInfo.index + 1 }, { priority: 'event' }) - } - }""" -) - -Reactable( - data, - columns=[ - Column( - id="details", - name="", - sortable=False, - cell=lambda ci: htmltools.tags.button("Show details"), - ) - ], - on_click=js_on_click, -) -``` - -## Language options - -```{python} -# | label: language -from reactable.models import Language - -Reactable( - pl_penguins[:30], - searchable=True, - pagination_type="simple", - language=Language( - search_placeholder="Search...", - no_data="No entries found", - page_info="{rowStart} to {rowEnd} of {rows} entries", - page_previous="\u276e", - page_next="\u276f", - # Accessible labels for assistive technologies such as screen readers. - # These are already set by default, but don't forget to update them when - # changing visible text. - page_previous_label="Previous page", - page_next_label="Next page", - ), -) -``` - -### Global language options - - -```{python} -# | label: language-global -options.language = Language( - page_size_options="显示 {rows}", - page_info="{rowStart} 至 {rowEnd} 项结果,共 {rows} 项", - page_previous="上页", - page_next="下页", -) - -bigblock(Props(pl_penguins[:12], default_page_size=4, show_page_size_options=True)) -``` - - -```{python} -options.reset() - -``` \ No newline at end of file diff --git a/docs/get-started/code-structure.qmd b/docs/get-started/code-structure.qmd index fd17166..bc61149 100644 --- a/docs/get-started/code-structure.qmd +++ b/docs/get-started/code-structure.qmd @@ -5,7 +5,7 @@ title: Code basics This page covers the basics of making a simple reactable table: ```{python} -# | echo: false +# | code-fold: true from reactable import embed_css from reactable.models import Reactable, ColGroup, Column, ColFormat, Theme, Language from reactable.data import cars_93 diff --git a/reactable/__init__.py b/reactable/__init__.py index b2906be..929928e 100644 --- a/reactable/__init__.py +++ b/reactable/__init__.py @@ -14,7 +14,7 @@ Language, JS, ) -from .widgets import BigblockWidget, embed_css, bigblock, RT +from .widgets import ReactableWidget, embed_css, bigblock from .tags import to_widget from .options import options from .render_gt import render diff --git a/reactable/models.py b/reactable/models.py index df05b30..e4751da 100644 --- a/reactable/models.py +++ b/reactable/models.py @@ -1212,6 +1212,6 @@ def _repr_mimebundle_(self, **kwargs: dict) -> tuple[dict, dict] | None: return self.to_widget()._repr_mimebundle_() def to_widget(self): - from .widgets import BigblockWidget + from .widgets import ReactableWidget - return BigblockWidget(props=self.to_props()) + return ReactableWidget(props=self.to_props()) diff --git a/reactable/options.py b/reactable/options.py index 573451a..b719df3 100644 --- a/reactable/options.py +++ b/reactable/options.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass, field from .models import Theme, Language @@ -16,6 +18,7 @@ def __setattr__(self, name, value): def reset(self): self.theme = Theme() + self.language = Language() options = Options() diff --git a/reactable/tags.py b/reactable/tags.py index f821657..8dcc605 100644 --- a/reactable/tags.py +++ b/reactable/tags.py @@ -29,8 +29,10 @@ def to_widget(el: htmltools.Tag): from reactable import Reactable if isinstance(el, htmltools.TagList): - raise NotImplementedError() - + return ipyreact.Widget( + _type="Fragment", + children=[to_widget(child) for child in el], + ) elif isinstance(el, Reactable): return el.to_widget() elif not isinstance(el, htmltools.Tag): diff --git a/reactable/widgets.py b/reactable/widgets.py index cb175eb..ca7e277 100644 --- a/reactable/widgets.py +++ b/reactable/widgets.py @@ -12,6 +12,11 @@ from .models import Props +# This ensures that the javascript is only loaded once, rather +# than included in every widget instance. Note that +ipyreact.define_module("reactable", Path(str(STATIC_FILES / "reactable-py.esm.js"))) + + def embed_css(): # https://github.com/widgetti/ipyreact/issues/39 from IPython.display import HTML, display @@ -36,34 +41,29 @@ def embed_css(): ) -class BigblockWidget(ipyreact.Widget): - _esm = Path(str(STATIC_FILES / "reactable-py.esm.js")) +class ReactableWidget(ipyreact.Widget): + # _esm = Path(str(STATIC_FILES / "reactable-py.esm.js")) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, _module="reactable", _type="default") def tagify(self) -> str: # to appease htmltools return str(self) + def _repr_mimebundle_(self, **kwargs: dict) -> tuple[dict, dict] | None: + # import os + # from reactable import options + # + # if options.path_js is not None: + # self._esm = options.path_js + # elif os.environ.get("REACTABLE_PATH_JS"): + # self._esm = os.environ["REACTABLE_PATH_JS"] + return super()._repr_mimebundle_(**kwargs) + # def to_tag(self): # return htmltool.Tag("Reactable", ) def bigblock(props: Props): - return BigblockWidget(props=props.to_props()) - - -class RT: - props: Props - widget: "None | BigblockWidget" - - def __init__(self, props: Props): - self.props = Props - self._widget = None - - def _repr_mimebundle_(self, **kwargs: dict) -> tuple[dict, dict] | None: - # TODO: note that this means updates to props won't affect widget - if self._widget is not None: - return self._widget._repr_mimebundle_(**kwargs) - - self._widget = BigblockWidget(props=self.props.to_props()) - return self._widget._repr_mimebundle_(**kwargs) + return ReactableWidget(props=props.to_props())