diff --git a/docs/configuration.md b/docs/configuration.md index 810fa48..e7e1a9c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -105,25 +105,32 @@ parameters. The following table shows the parameter alternatives between these t !!! note - When the table mentions allowed values `yes` and `no`, the allowed values of functions parameters are actually + Acceptable values mentioned in the table below are primarily intended for `config.ini` and CLI arguments. + The function arguments might be slightly different, so it's recommended to see appropriate API documentation + of given function. For example, instead of values `yes` and `no`, the functions usually use Boolean values `True` and `False`. -| `config.ini` key | CLI argument | Function argument | Description | -|---------------------------|-----------------------------|------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------| -| `toc` | `--toc ` | _N/A_ | Include table of contents. It is included only when there are any headings. Allowed values: `yes`, `no`. | -| `align-tables` | `--align-tables` | `align` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Horizontal alignment of tables. Allowed values: `left`, `center`, `right`. | -| `table-captions-position` | `--table-captions-position` | `caption_position` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Caption position. Allowed values: `top`, `bottom`. | -| `numbered-tables` | `--numbered-tables` | `numbered` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Whether to number tables. Allowed values: `yes`, `no`. | -| `sortable-tables` | `--sortable-tables` | `sortable` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Whether to make columns in tables sortable. Allowed values: `yes`, `no`. | -| `full-tables` | `--full-tables` | `full_table` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Whether to show tables expanded. Allowed values: `yes`, `no`. | -| `align-plots` | `--align-plots` | `align` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Horizontal alignment of plots. Allowed values: `left`, `center`, `right`. | -| `plot-captions-position` | `--plot-captions-position` | `caption_position` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Caption position. Allowed values: `top`, `bottom`. | -| `numbered-plots` | `--numbered-plots` | `numbered` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Whether to number plots. Allowed values: `yes`, `no`. | -| `matplotlib-format` | `--matplotlib-format` | `matplotlib_format` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Format of matplotlib (and thus also seaborn) plots. Allowed values: `png`, `svg`. | -| `matplotlib-embedded` | `--matplotlib-embedded` | `embedded` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Whether to embedded matplotlib (and thus also seaborn) plots directly into HTML. Only for svg format. Allowed values: `yes`, `no`. | -| `numbered-headings` | `--numbered-headings` | _N/A_ | Whether to number headings. Allowed values: `yes`, `no`. | -| `page-width` | `--page-width` | _N/A_ | Width of the page container in percentage. Allowed values: An integer in the range 40..100. | -| `keep-stdout` | `--keep-stdout` | _N/A_ | Whether to print the output to stdout too: `yes`, `no`. | +| `config.ini` key | CLI argument | Function argument | Description | +|---------------------------|-----------------------------|------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `toc` | `--toc ` | _N/A_ | Include table of contents. It is included only when there are any headings. Allowed values: `yes`, `no`. | +| `align-tables` | `--align-tables` | `align` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Horizontal alignment of tables. Allowed values: `left`, `center`, `right`. | +| `table-captions-position` | `--table-captions-position` | `caption_position` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Caption position. Allowed values: `top`, `bottom`. | +| `numbered-tables` | `--numbered-tables` | `numbered` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Whether to number tables. Allowed values: `yes`, `no`. | +| `tables-display-option` | `--tables-display-option` | `display_option` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | How to display table. This option is useful for long tables, which should not be displayed fully. Allowed values are: `full` (show the full table), `scrolling` (show the table in scrolling mode on y-axis), `paging` (show the table in paging mode). | +| `tables-paging-sizes` | `--tables-paging-sizes` | `paging_sizes` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | The paging sizes that can be selected. Ignored when `tables-display-option` is not `paging`. Allowed values are integers and string `all` (no matter the case of letters), written as a non-empty comma-separated list. | +| `tables-scroll-y-height` | `--tables-scroll-y-height` | `scroll_y_height` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Height of the tables when `tables-display-option` is set to `scrolling`. Allowed values TODO | +| `tables-scroll-x` | `--tables-scroll-x` | `scroll_x` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Whether to allow scrolling on the x-axis. If turned off, a wide table is allowed to overflow the main container. It is recommended to turn this on, especially with `tables-display-option` set to `scrolling`, because otherwise the table header won't interact properly when scrolling horizontally. Allowed values: `yes`, `no`. | +| `sortable-tables` | `--sortable-tables` | `sortable` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Whether to make columns in tables sortable. Allowed values: `yes`, `no`. | +| `tables-search-box` | `--tables-search-box` | `show_search_box` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Whether to show the search box for the tables. Allowed values: `yes`, `no`. | +| `tables-datatables-style` | `--tables-datatables-style` | `datatables_style` in [`print_table()`](../api/pyreball_html/#pyreball.html.print_table) | Datatables class(es) that affect the tables styling. If multiple classes are provided, separate them either with commas or spaces. See [DataTables documentation](https://datatables.net/manual/styling/classes) with the possible values. | +| `align-plots` | `--align-plots` | `align` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Horizontal alignment of plots. Allowed values: `left`, `center`, `right`. | +| `plot-captions-position` | `--plot-captions-position` | `caption_position` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Caption position. Allowed values: `top`, `bottom`. | +| `numbered-plots` | `--numbered-plots` | `numbered` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Whether to number plots. Allowed values: `yes`, `no`. | +| `matplotlib-format` | `--matplotlib-format` | `matplotlib_format` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Format of matplotlib (and thus also seaborn) plots. Allowed values: `png`, `svg`. | +| `matplotlib-embedded` | `--matplotlib-embedded` | `embedded` in [`plot_graph()`](../api/pyreball_html/#pyreball.html.plot_graph) | Whether to embedded matplotlib (and thus also seaborn) plots directly into HTML. Only for svg format. Allowed values: `yes`, `no`. | +| `numbered-headings` | `--numbered-headings` | _N/A_ | Whether to number headings. Allowed values: `yes`, `no`. | +| `page-width` | `--page-width` | _N/A_ | Width of the page container in percentage. Allowed values: An integer in the range 40..100. | +| `keep-stdout` | `--keep-stdout` | _N/A_ | Whether to print the output to stdout too: `yes`, `no`. | The reason for having multiple options for setting these values is to allow the user to set some properties globally, while others locally as needed for particular scripts. diff --git a/docs/tables.md b/docs/tables.md index 4c0df57..71b3111 100644 --- a/docs/tables.md +++ b/docs/tables.md @@ -42,6 +42,8 @@ Tables can be horizontally aligned by `align` parameter, as shown in the followi ## Sorting +TODO: update this section. + It is possible to make the table sortable on all columns by setting `sortable` parameter to `True`, or by setting `sorting_definition` parameter, which also sorts the table initially on the specified column. @@ -52,3 +54,10 @@ or by setting `sorting_definition` parameter, which also sorts the table initial ## Dealing with Large Tables TBD + +## Styling + +TBD + +For more options, see the styling reference +in [Datatables documentation](https://datatables.net/manual/styling/classes). \ No newline at end of file diff --git a/pyreball/__main__.py b/pyreball/__main__.py index 12dfbe6..a0c4101 100644 --- a/pyreball/__main__.py +++ b/pyreball/__main__.py @@ -22,10 +22,12 @@ from pyreball.utils.utils import ( carefully_remove_directory_if_exists, check_and_fix_parameters, + check_paging_sizes_string_parameter, ChoiceParameter, get_file_config, IntegerParameter, merge_parameter_dictionaries, + StringParameter, Substitutor, ) @@ -100,18 +102,6 @@ """ -JAVASCRIPT_SORTABLE_TABLE = """ - - $(document).ready(function () { - $('.sortable_table').DataTable({ - "paging": false, - "searching": false, - "info": false, - }); - }); - -""" - def _replace_ids(html_path: Path) -> None: """ @@ -313,17 +303,56 @@ def insert_heading_title_and_toc(filename: Path, include_toc: bool = True): default="no", help="Number the tables.", ), + ChoiceParameter( + "--tables-display-option", + choices=["full", "paging", "scrolling"], + default="full", + help="How to display tables. Either full, with scrollbar, or with paging.", + ), + StringParameter( + "--tables-paging-sizes", + default="10,25,100,All", + help=( + "The paging sizes that can be selected. " + "Allowed values are integers and string 'all' (no matter the case of letters), " + "written as a non-empty comma-separated list. " + "Ignored when tables-display-option is not 'paging'." + ), + validation_function=check_paging_sizes_string_parameter, + ), + # TODO add more details what values can be passed here: + StringParameter( + "--tables-scroll-y-height", + default="300px", + help=( + "Height of the tables when 'scrolling' display option is set. Ignored with other display options." + ), + ), + ChoiceParameter( + "--tables-scroll-x", + choices=["yes", "no"], + default="yes", + help="Whether to allow horizontal scrolling on tables.", + ), ChoiceParameter( "--sortable-tables", choices=["yes", "no"], default="no", - help="Make the tables sortable.", + help="Whether to make the tables sortable.", ), ChoiceParameter( - "--full-tables", + "--tables-search-box", choices=["yes", "no"], default="no", - help="Force all tables to be expanded.", + help="Whether to show search box for tables.", + ), + StringParameter( + "--tables-datatables-style", + default="display", + help=( + "Datatables class(es) that affect the tables styling. If multiple classes are provided, " + "separate them either with commas or spaces." + ), ), ChoiceParameter( "--align-plots", @@ -537,13 +566,19 @@ def parse_arguments(args) -> Dict[str, Optional[Union[str, int]]]: nargs=argparse.REMAINDER, help="Remaining arguments that are passed to the Python script.", ) - return vars(parser.parse_args(args)) + variables = vars(parser.parse_args(args)) + # positional arguments must be renamed manually + variables['input_path'] = variables['input-path'] + variables['script_args'] = variables['script-args'] + del variables['input-path'] + del variables['script-args'] + return variables def main() -> None: args_dict = parse_arguments(sys.argv[1:]) - script_args_string = " ".join(cast(List[str], args_dict.pop("script-args"))) - input_path = cast(Path, args_dict.pop("input-path")) + script_args_string = " ".join(cast(List[str], args_dict.pop("script_args"))) + input_path = cast(Path, args_dict.pop("input_path")) input_path = input_path.expanduser().resolve() output_path = cast(Optional[Path], args_dict.pop("output_path")) config_path = cast(Optional[Path], args_dict.pop("config_path")) @@ -584,10 +619,7 @@ def main() -> None: carefully_remove_directory_if_exists(directory=Path(html_dir_path_str)) script_definitions = ( - JAVASCRIPT_CHANGE_EXPAND - + JAVASCRIPT_ON_LOAD - + JAVASCRIPT_SORTABLE_TABLE - + JAVASCRIPT_ROLLING_PLOTS + JAVASCRIPT_CHANGE_EXPAND + JAVASCRIPT_ON_LOAD + JAVASCRIPT_ROLLING_PLOTS ) css_definitions = get_css( diff --git a/pyreball/cfg/config.ini b/pyreball/cfg/config.ini index 5f56484..31433df 100644 --- a/pyreball/cfg/config.ini +++ b/pyreball/cfg/config.ini @@ -3,8 +3,13 @@ toc = yes align-tables = center table-captions-position = top numbered-tables = yes +tables-display-option = full +tables-paging-sizes = 10,25,100,All +tables-scroll-y-height = 300px +tables-scroll-x = yes sortable-tables = no -full-tables = yes +tables-search-box = no +tables-datatables-style = display align-plots = center plot-captions-position = bottom numbered-plots = yes diff --git a/pyreball/cfg/styles.template b/pyreball/cfg/styles.template index 0ccf0da..55c6ca4 100644 --- a/pyreball/cfg/styles.template +++ b/pyreball/cfg/styles.template @@ -21,14 +21,6 @@ img, svg, canvas.marks, div.vega-embed { display: block; } -table.dataframe { - border-style: none; - border-width: 0px; - border-collapse: separate; - border-spacing: 0; - font-size: 80%; -} - .text-centered { text-align: center; } @@ -40,41 +32,11 @@ table.dataframe { margin-bottom: 20px; } - .table-wrapper-inner { width: fit-content; max-width: 100%; } -.table-scroller -{ - overflow: auto; /* scrollable */ - margin-top: 10px; - margin-bottom: 10px; - max-height: none; /* don't show all cells - the last one should be hidden a bit */ - width: fit-content; - margin-left: auto; - margin-right: auto; - max-width: 100%; -} - -.table-scroller-collapsed -{ - overflow: auto; /* scrollable */ - margin-top: 10px; - margin-bottom: 10px; - max-height: 390px; /* don't show all cells - the last one should be hidden a bit */ - width: fit-content; - margin-left: auto; - margin-right: auto; - max-width: 100%; -} - -.table-expander { - background: rgb(250, 250, 250); - cursor: pointer; -} - .centered { margin-left:auto; margin-right:auto; @@ -94,49 +56,6 @@ table.dataframe { font-weight: bold; } -.dataframe tbody tr th:only-of-type { - vertical-align: middle; - border-width: 0px; - padding: 6px; -} - -.dataframe tbody tr th { - border-width: 0px; -} - -.dataframe tbody > tr:nth-child(odd) { - background: rgb(245, 245, 245); -} - -.dataframe tbody > tr:hover { - background: #c4e3f3; -} - -.dataframe td { - vertical-align: top; - border-width: 0px; - padding: 6px; -} - -.dataframe thead th { - border-bottom-width: 1px; - border-top-width: 0px; - border-left-width: 0px; - border-right-width: 0px; - /*top: 0; */ - position: sticky; - background: white; -} - -.dataframe thead tr:nth-child(1) th { position: sticky; top: 0; } /* first row of table header */ -.dataframe thead tr:nth-child(2) th { position: sticky; top: 28px; } /* second row of table header */ - -.dataframe tr, th { - text-align: right; - vertical-align: middle; - padding: 6px; -} - a.anchor-link:link { text-decoration: none; padding: 0px 20px; diff --git a/pyreball/html.py b/pyreball/html.py index 8d8bb1a..58fdb7c 100644 --- a/pyreball/html.py +++ b/pyreball/html.py @@ -1,6 +1,7 @@ """Main functions that serve as building blocks of the final html file.""" import builtins import io +import json import os import random import re @@ -443,32 +444,117 @@ def _prepare_caption_element( ) +def _compute_length_menu_for_datatables( + paging_sizes: List[Union[int, str]] +) -> Tuple[List[int], List[Union[int, str]]]: + arr_1 = [] + arr_2 = [] + for size in paging_sizes: + if isinstance(size, int): + arr_1.append(size) + arr_2.append(size) + elif isinstance(size, str) and size.lower() == "all": + arr_1.append(-1) + arr_2.append(size) + else: + raise ValueError(f"Unsupported value in paging_sizes: {size}") + return arr_1, arr_2 + + +def _gather_datatables_setup( + display_option: str = "full", + scroll_y_height: str = "300px", + scroll_x: bool = True, + sortable: bool = False, + sorting_definition: Optional[List[Tuple[int, str]]] = None, + paging_sizes: Optional[List[Union[int, str]]] = None, + show_search_box: bool = False, + datatables_definition: Optional[Dict[str, Any]] = None, +) -> Optional[Dict[str, Any]]: + if datatables_definition is not None: + return datatables_definition + + datatables_setup = {} + if display_option == "scrolling": + datatables_setup["paging"] = False + datatables_setup["scrollCollapse"] = True + datatables_setup["scrollY"] = scroll_y_height + elif display_option == "paging": + datatables_setup["paging"] = True + if paging_sizes is None: + paging_sizes = [10, 25, 100, "All"] + datatables_setup["lengthMenu"] = _compute_length_menu_for_datatables( + paging_sizes + ) + elif display_option == "full": + datatables_setup["paging"] = False + if scroll_x: + datatables_setup["scrollX"] = True + + # if show_search_box: + datatables_setup["searching"] = show_search_box + + if sortable or sorting_definition is not None: + if sorting_definition is None: + datatables_setup["order"] = [] + else: + datatables_setup["order"] = sorting_definition + else: + datatables_setup["ordering"] = False + + return datatables_setup + + def _prepare_table_html( df: "pandas.DataFrame", + tab_index: int = 0, caption: Optional[str] = None, + reference: Optional[Reference] = None, align: str = "center", caption_position: str = "top", - full_table: bool = True, numbered: bool = True, - reference: Optional[Reference] = None, + display_option: str = "full", + scroll_y_height: str = "300px", + scroll_x: bool = True, sortable: bool = False, - tab_index: int = 0, - sorting_definition: Optional[Tuple[str, str]] = None, + sorting_definition: Optional[List[Tuple[int, str]]] = None, + paging_sizes: Optional[List[Union[int, str]]] = None, + show_search_box: bool = False, + datatables_style: Union[str, List[str]] = "display", + datatables_definition: Optional[Dict[str, Any]] = None, **kwargs: Any, ) -> str: + # TODO: add a small margin between table caption and the table itself. + + # TODO: when creating an example with sorting_definition, use and recommend get_loc method + # https://saturncloud.io/blog/how-to-get-column-index-from-column-name-in-python-pandas/#method-1-using-the-get_loc-method + # Show that also for complex headers align_mapping = { "center": "centered", "left": "left-aligned", "right": "right-aligned", } - - if sorting_definition: - # if we have sorting definition, we turn on sortable - sortable = True table_classes = ["centered"] - if sortable and not sorting_definition: - # add this class only to sortable tables that don't have sorting definition - table_classes.append("sortable_table") + if isinstance(datatables_style, list): + table_classes += datatables_style + else: + table_classes.append(datatables_style) + + datatables_setup = _gather_datatables_setup( + display_option=display_option, + scroll_y_height=scroll_y_height, + scroll_x=scroll_x, + sortable=sortable, + sorting_definition=sorting_definition, + paging_sizes=paging_sizes, + show_search_box=show_search_box, + datatables_definition=datatables_definition, + ) + + if "border" not in kwargs: + # If border is not set explicitly, + # turn it off because it would clash with datatables styling + kwargs["border"] = 0 df_html = df.to_html(classes=table_classes, **kwargs) if reference: _check_and_mark_reference(reference) @@ -483,61 +569,41 @@ def _prepare_table_html( index=tab_index, anchor_link=anchor_link, ) - # div that is scrollable - scroller_id = "table-scroller-" + str(tab_index) - # div button that expands the table - expander_id = "table-expander-" + str(tab_index) - if full_table: - enclosing_div_class = "table-scroller" - style_attr = 'style="display: none;" ' - else: - enclosing_div_class = "table-scroller-collapsed" - style_attr = " " if caption_position == "top": table_html = caption_element else: table_html = "" - table_html += ( - f'