From 2400084f58263c272fe3bb2f8cface4cd172075f Mon Sep 17 00:00:00 2001 From: Michael Chow Date: Wed, 31 Jul 2024 12:27:41 -0400 Subject: [PATCH] mostly finished get-started --- Makefile | 3 + docs/_quarto.yml | 36 +- docs/_sidebar.yml | 16 +- docs/examples/index.qmd | 22 +- docs/get-started/code-structure.qmd | 123 ++++++- docs/get-started/extra-advanced-filters.qmd | 7 + docs/get-started/format-aggregated.qmd | 66 ++++ docs/get-started/format-cell.qmd | 26 ++ docs/get-started/format-columns.qmd | 21 +- .../format-custom-rendering-js.qmd | 314 +++++++++++++++++ docs/get-started/format-custom-rendering.qmd | 319 +----------------- docs/get-started/format-details.qmd | 33 ++ docs/get-started/format-header-footer.qmd | 45 +++ docs/get-started/style-conditional-js.qmd | 27 ++ docs/get-started/style-conditional.qmd | 134 +++++++- .../style-custom-sort-indicators.qmd | 15 +- docs/get-started/style-theming.qmd | 10 + docs/reference/index.qmd | 31 +- react_tables/__init__.py | 20 +- react_tables/models.py | 14 +- 20 files changed, 917 insertions(+), 365 deletions(-) create mode 100644 docs/get-started/extra-advanced-filters.qmd create mode 100644 docs/get-started/format-aggregated.qmd create mode 100644 docs/get-started/format-cell.qmd create mode 100644 docs/get-started/format-custom-rendering-js.qmd create mode 100644 docs/get-started/format-details.qmd create mode 100644 docs/get-started/format-header-footer.qmd create mode 100644 docs/get-started/style-conditional-js.qmd diff --git a/Makefile b/Makefile index 77b76d2..a41eae5 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,9 @@ setup: docs-build: cd docs && quarto render +docs-reference: + quartodoc build --config docs/_quarto.yml + react_tables/static/reactable-py.esm.%: cd tmp/reactable npx esbuild \ diff --git a/docs/_quarto.yml b/docs/_quarto.yml index 8f5c07a..21ec47b 100644 --- a/docs/_quarto.yml +++ b/docs/_quarto.yml @@ -33,7 +33,7 @@ website: id: get-started contents: - get-started/index.qmd - - get-started/overview.qmd + #- get-started/overview.qmd - get-started/code-structure.qmd - section: "Controls" contents: @@ -53,15 +53,21 @@ website: - section: "Format" contents: - get-started/format-columns.qmd - - get-started/format-custom-rendering.qmd + - get-started/format-aggregated.qmd + - get-started/format-cell.qmd + - get-started/format-header-footer.qmd + - get-started/format-details.qmd - section: "Style" contents: - - get-started/style-conditional.qmd - get-started/style-table.qmd - - get-started/style-theming.qmd + - get-started/style-conditional.qmd - get-started/style-custom-sort-indicators.qmd + - get-started/style-theming.qmd - section: "Extra" contents: + - get-started/format-custom-rendering-js.qmd + - get-started/style-conditional-js.qmd + - get-started/extra-advanced-filters.qmd - examples/index.qmd # tell quarto to read the generated sidebar @@ -76,9 +82,25 @@ quartodoc: sidebar: _sidebar.yml sections: - - title: Some functions - desc: Functions to inspect docstrings. + - title: Create tables + desc: Classes to build reactable tables. contents: # the functions being documented in the package. # you can refer to anything: class methods, modules, etc.. - - Reactable \ No newline at end of file + - Reactable + - title: Customize columns + contents: + - Column + - ColGroup + - ColFormat + - ColFormatGroupBy + - title: Customize tables + contents: + - Theme + - Language + + - title: Rendering + desc: Classes used in custom rendering functions. + contents: + - CellInfo + - RowInfo \ No newline at end of file diff --git a/docs/_sidebar.yml b/docs/_sidebar.yml index ff6890b..dadfe0d 100644 --- a/docs/_sidebar.yml +++ b/docs/_sidebar.yml @@ -4,6 +4,20 @@ website: - reference/index.qmd - contents: - reference/Reactable.qmd - section: Some functions + section: Create tables + - contents: + - reference/Column.qmd + - reference/ColGroup.qmd + - reference/ColFormat.qmd + - reference/ColFormatGroupBy.qmd + section: Customize columns + - contents: + - reference/Theme.qmd + - reference/Language.qmd + section: Customize tables + - contents: + - reference/CellInfo.qmd + - reference/RowInfo.qmd + section: Rendering id: reference - id: dummy-sidebar diff --git a/docs/examples/index.qmd b/docs/examples/index.qmd index 96c53f1..f9ebaee 100644 --- a/docs/examples/index.qmd +++ b/docs/examples/index.qmd @@ -819,15 +819,22 @@ Reactable( ```{python} # | label: style-cond-cell-py +from react_tables.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=lambda val: { - "color": "#008800" if val > 0 else "#e00000", - "font-weight": "bold", - }, + style=cond_style, ) ], ) @@ -1367,11 +1374,6 @@ Reactable( * TODO: how far to go for pandas? * Display spanner index over names? -```{python} -# pd_expenditures = us_expenditures.to_pandas().set_index("index") -# bigblock(Props(, )) -``` - ```{python} # | label: rownames @@ -1469,6 +1471,7 @@ Reactable( ## Language options ```{python} +#| label: language from react_tables.models import Language Reactable( @@ -1494,6 +1497,7 @@ Reactable( ```{python} +#| label: language-global options.language = Language( pageSizeOptions="显示 {rows}", pageInfo="{rowStart} 至 {rowEnd} 项结果,共 {rows} 项", diff --git a/docs/get-started/code-structure.qmd b/docs/get-started/code-structure.qmd index 80d0471..d3ba29f 100644 --- a/docs/get-started/code-structure.qmd +++ b/docs/get-started/code-structure.qmd @@ -1,8 +1,11 @@ --- -title: Code structure +title: Code basics --- +This page covers the basics of making a simple reactable table: + ```{python} +# | echo: false from react_tables import embed_css from react_tables.models import Reactable, ColGroup, Column, ColFormat, Theme, Language from react_tables.data import cars_93 @@ -10,20 +13,104 @@ from react_tables.data import cars_93 embed_css() cars = cars_93[:3, ["manufacturer", "model", "type", "price", "min_price", "max_price"]] + +fmt = ColFormat(prefix="$", digits=2) + +Reactable( + cars, + columns={ + "manufacturer": Column(name="Manufacturer"), + "model": Column(name="Model"), + "type": Column(name="Type"), + "max_price": Column(name="Max", format=fmt), + "min_price": Column(name="Min", format=fmt), + "price": Column(name="Amount", format=fmt), + }, + column_groups=[ + ColGroup( + name="Price", + columns=["price", "min_price", "max_price"], + ), + ], + bordered=True, + theme=Theme( + backgroundColor="#ad85ff", + ), + language=Language( + pageNext="SLAM THE NEXT PAGE", + pagePrevious="GO BACK", + ), + filterable=True, + default_page_size=2, +) + ``` +* **Reactable controls**: filters for each column. +* **Columns**: custom names, price columns formatted for currency. +* **Column groups**: `"Price"` label above price related columns. +* **Theme**: custom background color. +* **Language**: customized text for next and previous page. + +## Setup and data + +In order to create the table, we'll import classes from reactable, along with an example dataset. + +```{python} +from react_tables import embed_css +from react_tables.models import Reactable, ColGroup, Column, ColFormat, Theme, Language +from react_tables.data import cars_93 + +embed_css() + +cars = cars_93[:3, ["manufacturer", "model", "type", "price", "min_price", "max_price"]] +``` + +Note two important pieces: + +* `embed_css()` is currently required once, in order to add the necessary CSS. +* `cars_93` is a tiny built-in DataFrame implementation called SimpleFrame. + +In this walkthrough, we'll turn the `cars` data directly into a reactable table. If you want to explore the data, use methods like `.to_polars()` or `.to_pandas()` to convert it to a Polars or Pandas DataFrame, respectively. + +```{python} +cars.to_polars() +``` + + + + ## Reactable +The `Reactable()` class is responsible for building the table: + ```{python} Reactable( cars, filterable=True, + default_page_size=2, ) - ``` +The code above used `filterable=True` argument added filters to the top of each column, and `default_page_size=2` to limit each page to 2 rows. `Reactable()` has many optional parameters, designed for quick customization of pieces like sorting, filtering, searching, and pagination. + +It also has four parameters which combine with other classes for configuration: + +| name | description | +| ---- | ----------- | +| `columns=` | use `Column()` to customize column names, format, and more. | +| `column_groups=` | use `ColGroup()` to group columns together, with a label. | +| `theme=` | use `Theme()` to customize table styling. | +| `language=` | use `Language()` to customize prompts like "Next page". | + +The following sections walk through these four parameters in depth. + ## Column definitions +The `columns=` argument uses the `Column()` class to customize pieces like column name and value formatting (e.g. as a date or currency). + +Below, we configure the name and format of the `"max_price"` column: + ```{python} Reactable( cars, @@ -36,10 +123,13 @@ Reactable( ) ``` -* data column name on left -* relabel using `name=` -* `format=` takes a ColFormat() object +Notice these three pieces above: +* `columns=` maps columns of data to `Column()` configurations. +* `Column(name=...)` cleans up the name displayed to "Max Price" +* `Column(format=...)` uses `ColFormat()` to specify how to format column values. + +The code above handled a single price column, but there are three related to price that need formatting. To avoid too much duplication, we can assign `ColFormat()` to a variable, and re-use that for each column definition. ```{python} fmt = ColFormat(prefix="$", digits=2) @@ -79,6 +169,8 @@ Notice that the `"max_price"` column is not filterable. ## Column groups (ColGroup) +The `column_groups=` argument uses the `ColGroup()` class to create groupings of columns. This allows you to put a custom label over related columns. + ```{python} Reactable( cars, @@ -93,6 +185,8 @@ Reactable( ## Theme +The `theme=` argument uses the `Theme()` class to customize the overall style of the table. + ```{python} Reactable( cars, @@ -105,6 +199,8 @@ Reactable( ## Language +The `language=` argument uses the `Language()` class to customize text prompts on the table like "Next Page". + ```{python} Reactable( cars, @@ -117,21 +213,22 @@ Reactable( ``` -## Renderers - -TODO - ## Putting it all together +In the sections above, we customized the columns, column groupings, theme, and language individually. Now we'll put it all together to make the complete table. ```{python} +fmt = ColFormat(prefix="$", digits=2) + Reactable( cars, columns={ - "max_price": Column( - name="Max Price", - format=ColFormat(prefix="$", digits=2), - ), + "manufacturer": Column(name="Manufacturer"), + "model": Column(name="Model"), + "type": Column(name="Type"), + "max_price": Column(name="Max", format=fmt), + "min_price": Column(name="Min", format=fmt), + "price": Column(name="Amount", format=fmt), }, column_groups=[ ColGroup( diff --git a/docs/get-started/extra-advanced-filters.qmd b/docs/get-started/extra-advanced-filters.qmd new file mode 100644 index 0000000..7aa1702 --- /dev/null +++ b/docs/get-started/extra-advanced-filters.qmd @@ -0,0 +1,7 @@ +--- +title: Javascript filters +--- + +reactable supports advanced customization of filters and searchbars. +See the R documentation on [Custom filtering](https://glin.github.io/reactable/articles/custom-filtering.html?q=filtermethod#column-filter-methods) for a tutorial with many examples. + diff --git a/docs/get-started/format-aggregated.qmd b/docs/get-started/format-aggregated.qmd new file mode 100644 index 0000000..4193f41 --- /dev/null +++ b/docs/get-started/format-aggregated.qmd @@ -0,0 +1,66 @@ +--- +title: Column aggregate cells +--- + +```{python} +from react_tables import embed_css, Reactable, Column, ColFormat, ColFormatGroupBy +from react_tables.data import us_states + +embed_css() +``` + +Column formatters can be applied to aggregated cells, produced by [grouping data](./structure-grouping.qmd). + + +By default, formatters apply to both standard cells and aggregate cells. + +```{python} +data = us_states + +col_format = ColFormat(suffix=" mi²", separators=True) + +Reactable( + data, + group_by="Region", + columns=[ + Column(id="Area", aggregate="sum", format=col_format), + ], +) +``` + +Note that the data is collapsed, with aggregate cells displaying the total area per group. The formatter has applied the suffix ` mi²` to the aggregates. + + +## Formatting aggregated cells + +If you want to format aggregated cells separately, provide a named list of `cell` and `aggregated` options: + +```python +from react_tables.models import ColFormatGroupBy + +Column( + format = ColFormatGroupBy( + cell = colFormat(...), # Standard cells + aggregated = colFormat(...) # Aggregated cells + ) +) +``` + +For example, only the aggregated `States` are formatted here: + +```{python} +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)), + ], +) +``` \ No newline at end of file diff --git a/docs/get-started/format-cell.qmd b/docs/get-started/format-cell.qmd new file mode 100644 index 0000000..ec92e1d --- /dev/null +++ b/docs/get-started/format-cell.qmd @@ -0,0 +1,26 @@ +--- +title: Rendering cells +--- + +```{python} +import polars as pl +import htmltools as html +from react_tables.data import cars_93 +from react_tables import Reactable, reactable, Props, embed_css +from react_tables.models import Column, ColInfo, CellInfo, HeaderCellInfo, RowInfo, JS + +embed_css() + +data = cars_93[20:25, ["manufacturer", "model", "type", "price"]] +``` + +```{python} +def fmt_cell_red(ci: CellInfo): + return html.div(ci.value.upper(), style="color: red") + + +Reactable( + data, + columns={"manufacturer": Column(cell=fmt_cell_red)}, +) +``` \ No newline at end of file diff --git a/docs/get-started/format-columns.qmd b/docs/get-started/format-columns.qmd index 45f22dc..ceb8b4d 100644 --- a/docs/get-started/format-columns.qmd +++ b/docs/get-started/format-columns.qmd @@ -2,9 +2,28 @@ title: Column formatting --- +You can format data in a column by providing `ColFormat()` options to the format argument in `Column`. + +The formatters for numbers, dates, times, and currencies are locale-sensitive and automatically adapt to language preferences of the user’s browser. This means, for example, that users will see dates formatted in their own timezone or numbers formatted in their own locale. + +To use a specific locale for data formatting, provide a vector of BCP 47 language tags in the locales argument. See a list of [common BCP 47 language tags](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a) for reference. + +:::{.callout-note} +Column formatters change how data is displayed without affecting the underlying data. Sorting, filtering, and grouping will still work on the original data. +::: + {{< embed ../examples/index.qmd#setup echo=true >}} + {{< embed ../examples/index.qmd#column-format-overview echo=true >}} + +## Date formatting + {{< embed ../examples/index.qmd#column-format-date echo=true >}} + +## Currency formatting + {{< embed ../examples/index.qmd#column-format-currency echo=true >}} -{{< embed ../examples/index.qmd#column-format-aggregate echo=true >}} + +## Displaying missing values + {{< embed ../examples/index.qmd#column-format-missing echo=true >}} \ No newline at end of file diff --git a/docs/get-started/format-custom-rendering-js.qmd b/docs/get-started/format-custom-rendering-js.qmd new file mode 100644 index 0000000..6298212 --- /dev/null +++ b/docs/get-started/format-custom-rendering-js.qmd @@ -0,0 +1,314 @@ +--- +title: Javascript formatters +--- + +```{python} +import polars as pl +import htmltools as html +from react_tables.data import cars_93 +from react_tables import Reactable, reactable, Props, embed_css +from react_tables.models import Column, ColInfo, CellInfo, HeaderCellInfo, RowInfo, JS + +embed_css() + +data = cars_93[20:25, ["manufacturer", "model", "type", "price"]] +``` + +## Cell + +```{python} +column = Column( + cell=JS( + """ + function(cellInfo, state) { + // input: + // - cellInfo, an object containing cell info + // - state, an object containing the table state (optional) + // + // output: + // - content to render (e.g. an HTML string) + return `
${cellInfo.value}
` + } + """ + ), + html=True, # to render as HTML +) + +Reactable(data, columns={"manufacturer": column}) + +``` + +::: {.callout-note} + + +## `cellInfo` properties + + +```{python} +# | echo: false +import polars as pl + +cell_info_props = pl.DataFrame( + { + "Property": [ + "value", + "row", + "column", + "index", + "viewIndex", + "aggregated", + "expanded", + "filterValue", + "subRows", + "level", + "selected", + ], + "Example": [ + '"setosa"', + '{ Petal.Length: 1.7, Species: "setosa" }', + '{ id: "Petal.Length" }', + "20", + "0", + "true", + "true", + '"petal"', + '[{ Petal.Length: 1.7, Species: "setosa" }, ...]', + "0", + "true", + ], + "Description": [ + "cell value", + "row data", + "column info object", + "row index (zero-based)", + "row index within the page (zero-based)", + "whether the row is aggregated", + "whether the row is expanded", + "column filter value", + "sub rows data (aggregated cells only)", + "row nesting depth (zero-based)", + "whether the row is selected", + ], + } +) + +Reactable(cell_info_props, pagination=False) +``` + +::: + +## Headers + + +```{python} +column = Column( + header=JS( + """ + function(column, state) { + // input: + // - column, an object containing column properties + // - state, an object containing the table state (optional) + // + // output: + // - content to render (e.g. an HTML string) + return `
${column.name}
` + } + """ + ), + html=True, # to render as HTML +) + +Reactable( + data, + columns={"price": column}, +) +``` + +::: {.callout-note} +## `column` properties + +```{python} +# | echo: false +column_props = pl.DataFrame( + { + "Property": [ + "id", + "name", + "filterValue", + "setFilter", + "column", + "data", + ], + "Example": [ + '"Petal.Length"', + '"Petal Length"', + '"petal"', + "function setFilter(value: any)", + '{ id: "Petal.Length", name: "Petal Length", filterValue: "petal" }', + "[{ Petal.Length: 1.7, Petal.Width: 0.2, _subRows: [] }, ...]", + ], + "Description": [ + "column ID", + "column display name", + "column filter value", + "function to set the column filter value (set to undefined to clear the filter)", + "column info object (deprecated in v0.3.0)", + "current row data in the table (deprecated in v0.3.0)", + ], + } +) + +Reactable(column_props) +``` +::: + + +## Footers + +```{python} +column = Column( + footer=JS( + """ + function(column, state) { + // input: + // - column, an object containing column properties + // - state, an object containing the table state (optional) + // + // output: + // - content to render (e.g. an HTML string) + return `
Rows: ${state.sortedData.length}
` + } + """ + ), + html=True, # to render as HTML +) + +Reactable(data, columns={"price": column}) +``` + +::: {.callout-note} +## `column` properties + +```{python} +# | echo: false +Reactable(column_props) +``` +::: + +## Expandable row details + +```{python} +Reactable( + data, + details=JS( + """ + function(rowInfo, state) { + // input: + // - rowInfo, an object containing row info + // - state, an object containing the table state (optional) + // + // output: + // - content to render (e.g. an HTML string) + return `
Details for row: ${rowInfo.index}
` + } + """ + ), +) +``` + +::: {.callout-note} + +## `rowInfo` properties + +```{python} +# | echo: false +row_info_props = pl.DataFrame( + { + "Property": [ + "values", + "row", + "index", + "viewIndex", + "expanded", + "level", + "selected", + ], + "Example": [ + '{ Petal.Length: 1.7, Species: "setosa" }', + '{ Petal.Length: 1.7, Species: "setosa" }', + "20", + "0", + "true", + "0", + "true", + ], + "Description": [ + "row data values", + "same as values (deprecated in v0.3.0)", + "row index (zero-based)", + "row index within the page (zero-based)", + "whether the row is expanded", + "row nesting depth (zero-based)", + "whether the row is selected", + ], + } +) + +Reactable(row_info_props) +``` +::: + +## Javascript `state` properties + + +```{python} +# | echo: false +state_props = pl.DataFrame( + { + "Property": [ + "sorted", + "page", + "pageSize", + "pages", + "filters", + "searchValue", + "selected", + "pageRows", + "sortedData", + "data", + "meta", + "hiddenColumns", + ], + "Example": [ + '[{ id: "Petal.Length", desc: true }, ...]', + "2", + "10", + "5", + '[{ id: "Species", value: "petal" }]', + '"petal"', + "[0, 1, 4]", + '[{ Petal.Length: 1.7, Species: "setosa" }, ...]', + '[{ Petal.Length: 1.7, Species: "setosa" }, ...]', + '[{ Petal.Length: 1.7, Species: "setosa" }, ...]', + "{ custom: 123 }", + '["Petal.Length"]', + ], + "Description": [ + "columns being sorted in the table", + "page index (zero-based)", + "page size", + "number of pages", + "column filter values", + "table search value", + "selected row indices (zero-based)", + "current row data on the page", + "current row data in the table (after sorting, filtering, grouping)", + "original row data in the table", + "custom table metadata from reactable() (new in v0.4.0)", + "columns being hidden in the table", + ], + } +) + +Reactable(state_props) +``` \ No newline at end of file diff --git a/docs/get-started/format-custom-rendering.qmd b/docs/get-started/format-custom-rendering.qmd index 0e0fa2d..f0864ec 100644 --- a/docs/get-started/format-custom-rendering.qmd +++ b/docs/get-started/format-custom-rendering.qmd @@ -1,24 +1,8 @@ --- -title: Custom Rendering +title: Custom rendering (python) jupyter: python3 --- -Control functions: - -* aggregate -* filterMethod -* onClick - - -Rendering functions: - -* cell -* header - - also ColGroup -* footer -* details -* class (Js Cell) - ```{python} import polars as pl import htmltools as html @@ -116,304 +100,3 @@ Reactable( }, ) ``` - -## Javascript renderers - -### Cell - -```{python} -column = Column( - cell=JS( - """ - function(cellInfo, state) { - // input: - // - cellInfo, an object containing cell info - // - state, an object containing the table state (optional) - // - // output: - // - content to render (e.g. an HTML string) - return `
${cellInfo.value}
` - } - """ - ), - html=True, # to render as HTML -) - -Reactable(data, columns={"manufacturer": column}) - -``` - -::: {.callout-note} - - -#### `cellInfo` properties - - -```{python} -# | echo: false -import polars as pl - -cell_info_props = pl.DataFrame( - { - "Property": [ - "value", - "row", - "column", - "index", - "viewIndex", - "aggregated", - "expanded", - "filterValue", - "subRows", - "level", - "selected", - ], - "Example": [ - '"setosa"', - '{ Petal.Length: 1.7, Species: "setosa" }', - '{ id: "Petal.Length" }', - "20", - "0", - "true", - "true", - '"petal"', - '[{ Petal.Length: 1.7, Species: "setosa" }, ...]', - "0", - "true", - ], - "Description": [ - "cell value", - "row data", - "column info object", - "row index (zero-based)", - "row index within the page (zero-based)", - "whether the row is aggregated", - "whether the row is expanded", - "column filter value", - "sub rows data (aggregated cells only)", - "row nesting depth (zero-based)", - "whether the row is selected", - ], - } -) - -Reactable(cell_info_props, pagination=False) -``` - -::: - -### Headers - - -```{python} -column = Column( - header=JS( - """ - function(column, state) { - // input: - // - column, an object containing column properties - // - state, an object containing the table state (optional) - // - // output: - // - content to render (e.g. an HTML string) - return `
${column.name}
` - } - """ - ), - html=True, # to render as HTML -) - -Reactable( - data, - columns={"price": column}, -) -``` - -::: {.callout-note} -#### `column` properties - -```{python} -# | echo: false -column_props = pl.DataFrame( - { - "Property": [ - "id", - "name", - "filterValue", - "setFilter", - "column", - "data", - ], - "Example": [ - '"Petal.Length"', - '"Petal Length"', - '"petal"', - "function setFilter(value: any)", - '{ id: "Petal.Length", name: "Petal Length", filterValue: "petal" }', - "[{ Petal.Length: 1.7, Petal.Width: 0.2, _subRows: [] }, ...]", - ], - "Description": [ - "column ID", - "column display name", - "column filter value", - "function to set the column filter value (set to undefined to clear the filter)", - "column info object (deprecated in v0.3.0)", - "current row data in the table (deprecated in v0.3.0)", - ], - } -) - -Reactable(column_props) -``` -::: - - -### Footers - -```{python} -column = Column( - footer=JS( - """ - function(column, state) { - // input: - // - column, an object containing column properties - // - state, an object containing the table state (optional) - // - // output: - // - content to render (e.g. an HTML string) - return `
Rows: ${state.sortedData.length}
` - } - """ - ), - html=True, # to render as HTML -) - -Reactable(data, columns={"price": column}) -``` - -::: {.callout-note} -#### `column` properties - -```{python} -# | echo: false -Reactable(column_props) -``` -::: - -### Expandable row details - -```{python} -Reactable( - data, - details=JS( - """ - function(rowInfo, state) { - // input: - // - rowInfo, an object containing row info - // - state, an object containing the table state (optional) - // - // output: - // - content to render (e.g. an HTML string) - return `
Details for row: ${rowInfo.index}
` - } - """ - ), -) -``` - -::: {.callout-note} - -#### `rowInfo` properties - -```{python} -# | echo: false -row_info_props = pl.DataFrame( - { - "Property": [ - "values", - "row", - "index", - "viewIndex", - "expanded", - "level", - "selected", - ], - "Example": [ - '{ Petal.Length: 1.7, Species: "setosa" }', - '{ Petal.Length: 1.7, Species: "setosa" }', - "20", - "0", - "true", - "0", - "true", - ], - "Description": [ - "row data values", - "same as values (deprecated in v0.3.0)", - "row index (zero-based)", - "row index within the page (zero-based)", - "whether the row is expanded", - "row nesting depth (zero-based)", - "whether the row is selected", - ], - } -) - -Reactable(row_info_props) -``` -::: - -## Javascript `state` properties - - -```{python} -# | echo: false -state_props = pl.DataFrame( - { - "Property": [ - "sorted", - "page", - "pageSize", - "pages", - "filters", - "searchValue", - "selected", - "pageRows", - "sortedData", - "data", - "meta", - "hiddenColumns", - ], - "Example": [ - '[{ id: "Petal.Length", desc: true }, ...]', - "2", - "10", - "5", - '[{ id: "Species", value: "petal" }]', - '"petal"', - "[0, 1, 4]", - '[{ Petal.Length: 1.7, Species: "setosa" }, ...]', - '[{ Petal.Length: 1.7, Species: "setosa" }, ...]', - '[{ Petal.Length: 1.7, Species: "setosa" }, ...]', - "{ custom: 123 }", - '["Petal.Length"]', - ], - "Description": [ - "columns being sorted in the table", - "page index (zero-based)", - "page size", - "number of pages", - "column filter values", - "table search value", - "selected row indices (zero-based)", - "current row data on the page", - "current row data in the table (after sorting, filtering, grouping)", - "original row data in the table", - "custom table metadata from reactable() (new in v0.4.0)", - "columns being hidden in the table", - ], - } -) - -Reactable(state_props) -``` diff --git a/docs/get-started/format-details.qmd b/docs/get-started/format-details.qmd new file mode 100644 index 0000000..46d84e8 --- /dev/null +++ b/docs/get-started/format-details.qmd @@ -0,0 +1,33 @@ +--- +title: Rendering details +--- + + +```{python} +import polars as pl +import htmltools as html +from react_tables.data import cars_93 +from react_tables import Reactable, reactable, Props, embed_css +from react_tables.models import Column, ColInfo, CellInfo, HeaderCellInfo, RowInfo, JS + +embed_css() + +data = cars_93[20:25, ["manufacturer", "model", "type", "price"]] +``` + +## Basic expandable row details + + +```{python} +def fmt_details(ci: RowInfo): + return html.div( + f"Details for row: {ci.row_index}", + reactable(Props(data[ci.row_index, :])), + ) + + +Reactable( + data[["model"]], + details=fmt_details, +) +``` \ No newline at end of file diff --git a/docs/get-started/format-header-footer.qmd b/docs/get-started/format-header-footer.qmd new file mode 100644 index 0000000..db8cd1c --- /dev/null +++ b/docs/get-started/format-header-footer.qmd @@ -0,0 +1,45 @@ +--- +title: Rendering header and footer +--- + +```{python} +import polars as pl +import htmltools as html +from react_tables.data import cars_93 +from react_tables import Reactable, reactable, Props, embed_css +from react_tables.models import Column, ColInfo, CellInfo, HeaderCellInfo, RowInfo, JS + +embed_css() + +data = cars_93[20:25, ["manufacturer", "model", "type", "price"]] +``` + +## Headers + +```{python} +def fmt_header(ci: HeaderCellInfo): + return html.div(f"name: {ci.name}", html.br(), f"value: {ci.value}") + + +Reactable( + data, + columns={"manufacturer": Column(header=fmt_header, name="Manufacturer")}, +) +``` + +## Footers + +```{python} +def fmt_footer(ci: ColInfo): + ttl = sum(ci.values) + return f"${ttl:.2f}" + + +Reactable( + data, + searchable=True, + columns={"price": Column(footer=fmt_footer)}, +) +``` + +Note that a sum of `$79.10` appears at the bottom right of the table. Importantly, if you filter the rows by typing into the search box, the sum won't update. This is because Python footers are rendered only once, when generating the initial table. \ No newline at end of file diff --git a/docs/get-started/style-conditional-js.qmd b/docs/get-started/style-conditional-js.qmd new file mode 100644 index 0000000..be67710 --- /dev/null +++ b/docs/get-started/style-conditional-js.qmd @@ -0,0 +1,27 @@ +--- +title: Javascript styling +--- + +{{< embed ../examples/index.qmd#setup echo=true >}} + +## Cell style + +{{< embed ../examples/index.qmd#style-cond-cell-js echo=true >}} + +## Row style + +{{< embed ../examples/index.qmd#style-cond-row-js echo=true >}} + +## Metadata + +You can pass arbitrary data from Python to JavaScript style functions using the `meta` argument in reactable(). + +meta should be a named list of values that can also be JS() expressions or functions. Custom metadata can be accessed from JavaScript using the state.meta property, and updated using updateReactable() in Shiny or Reactable.setMeta() in the JavaScript API. + +Use custom metadata to: + +Simplify JavaScript style functions that need access to data outside of the table +Dynamically change how data is styled without rerendering the table +Share JavaScript code or data between different style functions + +{{< embed ../examples/index.qmd#style-metadata echo=true >}} \ No newline at end of file diff --git a/docs/get-started/style-conditional.qmd b/docs/get-started/style-conditional.qmd index 543e2df..091e474 100644 --- a/docs/get-started/style-conditional.qmd +++ b/docs/get-started/style-conditional.qmd @@ -1,10 +1,130 @@ --- -title: Conditional styling +title: Conditional styling (python) +jupyter: python3 --- -{{< embed ../examples/index.qmd#setup echo=true >}} -{{< embed ../examples/index.qmd#style-cond-cell-py echo=true >}} -{{< embed ../examples/index.qmd#style-cond-cell-js echo=true >}} -{{< embed ../examples/index.qmd#style-cond-row-py echo=true >}} -{{< embed ../examples/index.qmd#style-cond-row-js echo=true >}} -{{< embed ../examples/index.qmd#style-metadata echo=true >}} \ No newline at end of file +You can conditionally style a table using functions that return inline styles or CSS classes. Just like with custom rendering, style functions can either be in Python or JavaScript. + +* `Column()` arguments `style=` and `class_=` customize cell styles. +* `Reactable()` arguments `row_style=` and `row_class=` customize row styles. + + +```{python} +from react_tables import Reactable, Column, embed_css +from react_tables.data import sleep + +embed_css() +``` + +## Cell styles + +Pass a function to the `Column()` parameter `style=` to set conditional CSS styles. The function should take a single `CellInfo` argument. + +Below is an example function, which sets the `color` and `font-weight` styles. + +```{python} +# | label: style-cond-cell-py +from react_tables.models import CellInfo + + +def cond_style(ci: CellInfo): + return { + "color": "#008800" if ci.value > 0 else "#e00000", + "font-weight": "bold", + } +``` + +Notice these 3 pieces: + +* `cond_style` will be applied to every cell in a column. +* The value of the CSS color depends on `ci.value`, which is the value of the current cell. +* CSS rules are returned as a dictionary. + +Here's the function above used to conditionarlly style the `"extra"` column: + +```{python} +Reactable( + sleep[:6, :], + columns=[ + Column( + id="extra", + style=cond_style, + ) + ], +) +``` + +## Cell class + +Pass a function to the `Column()` parameter `class_=` to set a class attribute on each cell in a column. Similar to `style=`, the function should take a `CellInfo` object. + +Here's an example, which sets some CSS, and then renders a table with class set to `"big"` when the extra column is 3.4. + +```{python} +# | echo: false +from IPython.display import display, HTML + +css = """.big { + font-weight: bold; + color: red; +}""" + +display(HTML(f"\n\n")) +``` + +```{python} +# | output: asis +# | echo: false + +print(f"```css\n{css}\n```") +``` + +```{python} +def big_class(ci: CellInfo): + return "big" if ci.value == 3.4 else None + + +Reactable( + sleep[:6, :], + columns={ + "extra": Column(class_=big_class), + }, +) + +``` + +## Row styles and class + +Pass a function to `row_style=` or `row_class=` to set row-based styles or class names, respectively. The function should take row number as its only argument. + +```{python} +# | output: asis +# | echo: false +css = """.bold { + font-weight: bold; +}""" + +print(f"\n\n") + +print(f"```css\n{css}\n```") +``` + +```{python} +# define conditional row style and row class functions ---- +def f_row_style(indx: int): + if sleep[indx, "extra"] > 0: + return {"background": "rgba(0, 0, 0, 0.05)"} + + +def f_row_class(indx: int): + if sleep[indx, "extra"] > 0: + return "bold" + + +# generate table ---- +Reactable( + sleep[:6, :], + row_style=f_row_style, + row_class=f_row_class, +) +``` \ No newline at end of file diff --git a/docs/get-started/style-custom-sort-indicators.qmd b/docs/get-started/style-custom-sort-indicators.qmd index b6ba5c4..6524cae 100644 --- a/docs/get-started/style-custom-sort-indicators.qmd +++ b/docs/get-started/style-custom-sort-indicators.qmd @@ -18,9 +18,20 @@ from react_tables.data import cars_93 embed_css() ``` -To use a custom sort indicator, you can hide the default sort icon using reactable(`show_sort_icon=False`) and add your own sort indicator. +Sometimes, table state is available in ways that allow for easy styling. For example, sorting uses an `aria-sort` proprety, that can be targeted with CSS rules. -This also hides the sort icon when a header is focused, so be sure to add a visual focus indicator to ensure your table is accessible to keyboard users (to test this, click the first table header then press the Tab key to navigate to other headers). +This page illustrates how to add custom sort indicators by two steps: + +* hiding sort icons. +* targetting headers with the `area-sort` property. + +## Hiding sort icon + +To hide the default sort indicator, you can hide the default sort icon using reactable(`show_sort_icon=False`). + +This also hides the sort icon when a header is focused, so it needs some visual focus indicator to ensure your table is accessible to keyboard users (to test this, click the first table header then press the Tab key to navigate to other headers). + +## Example Here’s an example that changes the sort indicator to a bar on the top or bottom of the header (indicating an ascending or descending sort), and adds a light background to headers when hovered or focused. diff --git a/docs/get-started/style-theming.qmd b/docs/get-started/style-theming.qmd index 5a4fe6e..fe3ac02 100644 --- a/docs/get-started/style-theming.qmd +++ b/docs/get-started/style-theming.qmd @@ -2,6 +2,16 @@ title: Theming --- +Themes provide a powerful way to customize table styling that can be reused across tables. You can either set theme variables to change the default styles (e.g., row stripe color), or add your own custom CSS to specific elements of the table. + +To apply a theme, provide a `Theme()` as the `theme=` argument: + {{< embed ../examples/index.qmd#setup echo=true >}} {{< embed ../examples/index.qmd#theme echo=true >}} + +## Global theme + +To set the default theme for all tables, set the global `reactable.options.theme` attribute: + + {{< embed ../examples/index.qmd#theme-global echo=true >}} diff --git a/docs/reference/index.qmd b/docs/reference/index.qmd index ee374a3..7473430 100644 --- a/docs/reference/index.qmd +++ b/docs/reference/index.qmd @@ -1,9 +1,34 @@ # Function reference {.doc .doc-index} -## Some functions +## Create tables -Functions to inspect docstrings. +Classes to build reactable tables. | | | | --- | --- | -| [Reactable](Reactable.qmd#react_tables.Reactable) | A reactive table. | \ No newline at end of file +| [Reactable](Reactable.qmd#react_tables.Reactable) | A reactive table. | + +## Customize columns + +| | | +| --- | --- | +| [Column](Column.qmd#react_tables.Column) | | +| [ColGroup](ColGroup.qmd#react_tables.ColGroup) | | +| [ColFormat](ColFormat.qmd#react_tables.ColFormat) | | +| [ColFormatGroupBy](ColFormatGroupBy.qmd#react_tables.ColFormatGroupBy) | | + +## Customize tables + +| | | +| --- | --- | +| [Theme](Theme.qmd#react_tables.Theme) | | +| [Language](Language.qmd#react_tables.Language) | | + +## Rendering + +Classes used in custom rendering functions. + +| | | +| --- | --- | +| [CellInfo](CellInfo.qmd#react_tables.CellInfo) | | +| [RowInfo](RowInfo.qmd#react_tables.RowInfo) | | \ No newline at end of file diff --git a/react_tables/__init__.py b/react_tables/__init__.py index d63d080..c703bce 100644 --- a/react_tables/__init__.py +++ b/react_tables/__init__.py @@ -1,6 +1,17 @@ from __future__ import annotations -from .models import Props, Reactable, Column, ColFormat +from .models import ( + Props, + Reactable, + Column, + ColGroup, + ColFormat, + ColFormatGroupBy, + CellInfo, + RowInfo, + Theme, + Language, +) from .widgets import BigblockWidget, embed_css, bigblock, RT from .options import options from .render_gt import render @@ -10,8 +21,15 @@ __all__ = [ "Reactable", "Column", + "ColGroup", + "ColFormat", + "ColFormatGroupBy", + "CellInfo", + "RowInfo", "bigblock", "reactable", "options", "Props", + "Theme", + "Language", ] diff --git a/react_tables/models.py b/react_tables/models.py index e0951bd..88f7012 100644 --- a/react_tables/models.py +++ b/react_tables/models.py @@ -304,7 +304,7 @@ def __post_init__( # TODO: will fail for data with no columns n_rows = len(list(self.data.values())[0]) - # derived ---- + # simple derived properties ---- self.default_sort_desc = default_sort_order == "desc" self.inline = not full_width self.nowrap = not wrap @@ -571,6 +571,10 @@ def init_data(self, data: dict[str, list[Any]]) -> Column: if callable(self.cell): new_col.cell = self._apply_transform(col_data, self.cell) + # class to apply to cells ---- + if callable(self.class_): + new_col.class_ = self._apply_transform(col_data, self.class_) + # header: transform or set string as react tag ---- if callable(self.header): # TODO: confusing that the column data name is called id, and label is called name? @@ -601,7 +605,7 @@ def init_data(self, data: dict[str, list[Any]]) -> Column: # style: transform ---- # overall style if callable(self.style): - new_col.style = [self.style(x) for x in col_data] + new_col.style = self._apply_transform(col_data, self.style) elif isinstance(self.style, list): if len(self.style) != n_rows: raise ValueError( @@ -625,7 +629,11 @@ def to_props(self) -> dict[str, Any]: renamed = rename( as_props(self), - **{"class": "class_", "selectable": "_selectable", "header_class": "header_class_name"}, + **{ + "class_": "class_name", + "selectable": "_selectable", + "header_class": "header_class_name", + }, ) return to_camel_dict(filter_none(renamed))