diff --git a/docs/examples/widgets/data_table.py b/docs/examples/widgets/data_table.py index f409bb45c..dae29b267 100644 --- a/docs/examples/widgets/data_table.py +++ b/docs/examples/widgets/data_table.py @@ -21,9 +21,8 @@ class TableApp(App): def on_mount(self) -> None: table = self.query_one(DataTable) - rows = iter(ROWS) - table.add_columns(*next(rows)) - table.add_rows(rows) + table.add_columns(*ROWS[0]) + table.add_rows(ROWS[1:]) app = TableApp() diff --git a/docs/examples/widgets/data_table_cursors.py b/docs/examples/widgets/data_table_cursors.py new file mode 100644 index 000000000..8c33f2be5 --- /dev/null +++ b/docs/examples/widgets/data_table_cursors.py @@ -0,0 +1,40 @@ +from itertools import cycle + +from textual.app import App, ComposeResult +from textual.widgets import DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (5, "Chad le Clos", "South Africa", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), + (10, "Darren Burns", "Scotland", 51.84), +] + +cursors = cycle(["column", "row", "cell"]) + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable() + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.cursor_type = next(cursors) + table.zebra_stripes = True + table.add_columns(*ROWS[0]) + table.add_rows(ROWS[1:]) + + def key_c(self): + table = self.query_one(DataTable) + table.cursor_type = next(cursors) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/data_table_fixed.py b/docs/examples/widgets/data_table_fixed.py new file mode 100644 index 000000000..a928a68e5 --- /dev/null +++ b/docs/examples/widgets/data_table_fixed.py @@ -0,0 +1,25 @@ +from textual.app import App, ComposeResult +from textual.widgets import DataTable + + +class TableApp(App): + CSS = "DataTable {height: 1fr}" + + def compose(self) -> ComposeResult: + yield DataTable() + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.focus() + table.add_columns("A", "B", "C") + for number in range(1, 100): + table.add_row(str(number), str(number * 2), str(number * 3)) + table.fixed_rows = 2 + table.fixed_columns = 1 + table.cursor_type = "row" + table.zebra_stripes = True + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/data_table_labels.py b/docs/examples/widgets/data_table_labels.py new file mode 100644 index 000000000..316d4709a --- /dev/null +++ b/docs/examples/widgets/data_table_labels.py @@ -0,0 +1,34 @@ +from rich.text import Text + +from textual.app import App, ComposeResult +from textual.widgets import DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (5, "Chad le Clos", "South Africa", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), + (10, "Darren Burns", "Scotland", 51.84), +] + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable() + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.add_columns(*ROWS[0]) + for number, row in enumerate(ROWS[1:], start=1): + label = Text(str(number), style="#B0FC38 italic") + table.add_row(*row, label=label) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/docs/examples/widgets/data_table_renderables.py b/docs/examples/widgets/data_table_renderables.py new file mode 100644 index 000000000..f1a07ea3e --- /dev/null +++ b/docs/examples/widgets/data_table_renderables.py @@ -0,0 +1,37 @@ +from rich.text import Text + +from textual.app import App, ComposeResult +from textual.widgets import DataTable + +ROWS = [ + ("lane", "swimmer", "country", "time"), + (4, "Joseph Schooling", "Singapore", 50.39), + (2, "Michael Phelps", "United States", 51.14), + (5, "Chad le Clos", "South Africa", 51.14), + (6, "László Cseh", "Hungary", 51.14), + (3, "Li Zhuhao", "China", 51.26), + (8, "Mehdy Metella", "France", 51.58), + (7, "Tom Shields", "United States", 51.73), + (1, "Aleksandr Sadovnikov", "Russia", 51.84), + (10, "Darren Burns", "Scotland", 51.84), +] + + +class TableApp(App): + def compose(self) -> ComposeResult: + yield DataTable() + + def on_mount(self) -> None: + table = self.query_one(DataTable) + table.add_columns(*ROWS[0]) + for row in ROWS[1:]: + # Adding styled and justified `Text` objects instead of plain strings. + styled_row = [ + Text(str(cell), style="italic #03AC13", justify="right") for cell in row + ] + table.add_row(*styled_row) + + +app = TableApp() +if __name__ == "__main__": + app.run() diff --git a/docs/widgets/data_table.md b/docs/widgets/data_table.md index d70e43b35..215d0541b 100644 --- a/docs/widgets/data_table.md +++ b/docs/widgets/data_table.md @@ -5,9 +5,13 @@ A table widget optimized for displaying a lot of data. - [x] Focusable - [ ] Container -## Example +## Guide -The example below populates a table with CSV data. +### Adding data + +The following example shows how to fill a table with data. +First, we use [add_columns][textual.widgets.DataTable.add_rows] to include the `lane`, `swimmer`, `country`, and `time` columns in the table. +After that, we use the [add_rows][textual.widgets.DataTable.add_rows] method to insert the rows into the table. === "Output" @@ -20,11 +24,141 @@ The example below populates a table with CSV data. --8<-- "docs/examples/widgets/data_table.py" ``` +To add a single row or column use [add_row][textual.widgets.DataTable.add_row] and [add_column][textual.widgets.DataTable.add_column], respectively. + +#### Styling and justifying cells + +Cells can contain more than just plain strings - [Rich](https://rich.readthedocs.io/en/stable/introduction.html) renderables such as [`Text`](https://rich.readthedocs.io/en/stable/text.html?highlight=Text#rich-text) are also supported. +`Text` objects provide an easy way to style and justify cell content: + +=== "Output" + + ```{.textual path="docs/examples/widgets/data_table_renderables.py"} + ``` + +=== "data_table_renderables.py" + + ```python + --8<-- "docs/examples/widgets/data_table_renderables.py" + ``` + +### Keys + +When adding a row to the table, you can supply a _key_ to [add_row][textual.widgets.DataTable.add_row]. +A key is a unique identifier for that row. +If you don't supply a key, Textual will generate one for you and return it from `add_row`. +This key can later be used to reference the row, regardless of its current position in the table. + +When working with data from a database, for example, you may wish to set the row `key` to the primary key of the data to ensure uniqueness. +The method [add_column][textual.widgets.DataTable.add_column] also accepts a `key` argument and works similarly. + +Keys are important because cells in a data table can change location due to factors like row deletion and sorting. +Thus, using keys instead of coordinates allows us to refer to data without worrying about its current location in the table. + +If you want to change the table based solely on coordinates, you can use the [coordinate_to_cell_key][textual.widgets.DataTable.coordinate_to_cell_key] method to convert a coordinate to a _cell key_, which is a `(row_key, column_key)` pair. + +### Cursors + +The coordinate of the cursor is exposed via the `cursor_coordinate` reactive attribute. +Three types of cursors are supported: `cell`, `row`, and `column`. +Change the cursor type by assigning to the `cursor_type` reactive attribute. + +=== "Column Cursor" + + ```{.textual path="docs/examples/widgets/data_table_cursors.py"} + ``` + +=== "Row Cursor" + + ```{.textual path="docs/examples/widgets/data_table_cursors.py" press="c"} + ``` + +=== "Cell Cursor" + + ```{.textual path="docs/examples/widgets/data_table_cursors.py" press="c,c"} + ``` + +=== "data_table_cursors.py" + + ```python + --8<-- "docs/examples/widgets/data_table_cursors.py" + ``` + +You can change the position of the cursor using the arrow keys, ++page-up++, ++page-down++, ++home++ and ++end++, +or by assigning to the `cursor_coordinate` reactive attribute. + +### Updating data + +Cells can be updated in the `DataTable` by using the [update_cell][textual.widgets.DataTable.update_cell] and [update_cell_at][textual.widgets.DataTable.update_cell_at] methods. + +### Removing data + +To remove all data in the table, use the [clear][textual.widgets.DataTable.clear] method. +To remove individual rows, use [remove_row][textual.widgets.DataTable.remove_row]. +The `remove_row` method accepts a `key` argument, which identifies the row to be removed. + +If you wish to remove the row below the cursor in the `DataTable`, use `coordinate_to_cell_key` to get the row key of +the row under the current `cursor_coordinate`, then supply this key to `remove_row`: + +```python +# Get the keys for the row and column under the cursor. +row_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate) +# Supply the row key to `remove_row` to delete the row. +table.remove_row(row_key) +``` + +### Fixed data + +You can fix a number of rows and columns in place, keeping them pinned to the top and left of the table respectively. +To do this, assign an integer to the `fixed_rows` or `fixed_columns` reactive attributes of the `DataTable`. + +=== "Fixed data" + + ```{.textual path="docs/examples/widgets/data_table_fixed.py" press="end"} + ``` + +=== "data_table_fixed.py" + + ```python + --8<-- "docs/examples/widgets/data_table_fixed.py" + ``` + +In the example above, we set `fixed_rows` to `2`, and `fixed_columns` to `1`, +meaning the first two rows and the leftmost column do not scroll - they always remain +visible as you scroll through the data table. + +### Sorting + +The `DataTable` can be sorted using the [sort][textual.widgets.DataTable.sort] method. +In order to sort your data by a column, you must have supplied a `key` to the `add_column` method +when you added it. +You can then pass this key to the `sort` method to sort by that column. +Additionally, you can sort by multiple columns by passing multiple keys to `sort`. + +### Labelled rows + +A "label" can be attached to a row using the [add_row][textual.widgets.DataTable.add_row] method. +This will add an extra column to the left of the table which the cursor cannot interact with. +This column is similar to the leftmost column in a spreadsheet containing the row numbers. +The example below shows how to attach simple numbered labels to rows. + +=== "Labelled rows" + + ```{.textual path="docs/examples/widgets/data_table_labels.py"} + ``` + +=== "data_table_labels.py" + + ```python + --8<-- "docs/examples/widgets/data_table_labels.py" + ``` + ## Reactive Attributes | Name | Type | Default | Description | -| ------------------- | ------------------------------------------- | ------------------ | ----------------------------------------------------- | +|---------------------|---------------------------------------------|--------------------|-------------------------------------------------------| | `show_header` | `bool` | `True` | Show the table header | +| `show_row_labels` | `bool` | `True` | Show the row labels (if applicable) | | `fixed_rows` | `int` | `0` | Number of fixed rows (rows which do not scroll) | | `fixed_columns` | `int` | `0` | Number of fixed columns (columns which do not scroll) | | `zebra_stripes` | `bool` | `False` | Display alternating colors on rows | @@ -43,6 +177,7 @@ The example below populates a table with CSV data. - [DataTable.ColumnHighlighted][textual.widgets.DataTable.ColumnHighlighted] - [DataTable.ColumnSelected][textual.widgets.DataTable.ColumnSelected] - [DataTable.HeaderSelected][textual.widgets.DataTable.HeaderSelected] +- [DataTable.RowLabelSelected][textual.widgets.DataTable.RowLabelSelected] ## Bindings