From 73e6fa48d604cbb7271ae3fef725a4c54139a18f Mon Sep 17 00:00:00 2001 From: Marc Wouts Date: Sat, 16 Mar 2024 14:43:52 +0000 Subject: [PATCH] ITables Version 2.0 (#235) * Bundle DataTables and a few extensions using esbuild * Remove the css option and document the styling * export big integers as bigint in JS * Transition to hatch and pyproject.toml * Add support for Python 3.12 --- .github/workflows/continuous-integration.yml | 57 +-- .github/workflows/publish-book.yml | 8 +- .github/workflows/publish.yml | 19 +- .gitignore | 8 +- .pre-commit-config.yaml | 20 +- README.md | 13 +- docs/_config.yml | 1 + docs/_toc.yml | 37 +- docs/advanced_parameters.md | 310 +++-------------- docs/changelog.md | 14 + docs/custom_css.md | 175 +++++++++- docs/custom_extensions.md | 104 ++++++ docs/downsampling.md | 7 +- docs/extensions.md | 205 +++++++++++ docs/formatting.md | 172 ++++++++++ docs/pandas_style.md | 5 +- docs/polars_dataframes.md | 4 - docs/quarto/quarto_html.qmd | 12 +- docs/quarto/quarto_revealjs.qmd | 12 +- docs/quick_start.md | 39 +-- docs/references.md | 2 +- docs/sample_dataframes.md | 4 - docs/supported_editors.md | 5 +- environment.yml | 16 +- itables/datatables_format.py | 71 ++-- itables/dt_for_itables/LICENSE | 21 ++ itables/dt_for_itables/README.md | 30 ++ itables/dt_for_itables/package-lock.json | 324 ++++++++++++++++++ itables/dt_for_itables/package.json | 32 ++ itables/dt_for_itables/src.js | 31 ++ itables/html/datatables_template.html | 25 +- .../html/datatables_template_connected.html | 25 -- .../html/initialize_offline_datatable.html | 3 - itables/html/itables.css | 27 -- itables/javascript.py | 150 +++++--- itables/options.py | 29 +- itables/shiny.py | 16 +- itables/utils.py | 9 +- itables/version.py | 2 +- pyproject.toml | 89 ++++- requirements-dev.txt | 13 - requirements.txt | 2 - setup.cfg | 19 - setup.py | 73 ---- tests/test_connected_notebook_is_small.py | 2 +- tests/test_datatables_format.py | 33 +- tests/test_javascript.py | 5 - tests/test_sample_dfs.py | 12 +- 48 files changed, 1555 insertions(+), 737 deletions(-) create mode 100644 docs/custom_extensions.md create mode 100644 docs/extensions.md create mode 100644 docs/formatting.md create mode 100644 itables/dt_for_itables/LICENSE create mode 100644 itables/dt_for_itables/README.md create mode 100644 itables/dt_for_itables/package-lock.json create mode 100644 itables/dt_for_itables/package.json create mode 100644 itables/dt_for_itables/src.js delete mode 100644 itables/html/datatables_template_connected.html delete mode 100644 itables/html/initialize_offline_datatable.html delete mode 100644 itables/html/itables.css delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 3d7ae113..ed90a54e 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -17,10 +17,10 @@ jobs: pre-commit: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 - - uses: pre-commit/action@v3.0.0 + uses: actions/setup-python@v5 + - uses: pre-commit/action@v3.0.1 codeql: runs-on: ubuntu-latest @@ -28,48 +28,45 @@ jobs: security-events: write steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: python, javascript - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 pytest: strategy: fail-fast: false matrix: - python-version: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"] + python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"] pandas-version: [latest] include: - - python-version: 3.6 - pandas-version: 0.22 + - python-version: 3.7 + pandas-version: '<1.0' - python-version: 3.9 - pandas-version: 1.5 - - python-version: "3.11" + pandas-version: '<2.0' + - python-version: "3.12" pandas-version: pre polars: true - - python-version: "3.11" + - python-version: "3.12" uninstall_jinja2: true runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-dev.txt - - name: Install a development version of 'itables' - run: pip install -e . + run: pip install -e .[test] - name: Install pandas latest if: matrix.pandas-version == 'latest' @@ -79,7 +76,7 @@ jobs: run: pip install pandas --pre - name: Install pandas ${{ matrix.pandas-version }} if: matrix.pandas-version != 'pre' && matrix.pandas-version != 'latest' - run: pip install pandas==${{ matrix.pandas-version }} + run: pip install 'pandas${{ matrix.pandas-version }}' - name: Install polars if: matrix.polars @@ -97,3 +94,19 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v3 + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout source + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Install hatch + run : pip install hatch + - name: Build package + run: hatch build diff --git a/.github/workflows/publish-book.yml b/.github/workflows/publish-book.yml index fba9fb03..5cfbc0f0 100644 --- a/.github/workflows/publish-book.yml +++ b/.github/workflows/publish-book.yml @@ -15,11 +15,13 @@ jobs: deploy-book: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 - # Install dependencies - name: Set up Python 3.11 - uses: actions/setup-python@v1 + uses: actions/setup-python@v5 with: python-version: 3.11 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d1576ca1..58a83441 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,26 +3,29 @@ on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+-rc[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+rc[0-9]+" + - "v[0-9]+.[0-9]+.[0-9]+dev[0-9]+" jobs: publish: runs-on: ubuntu-latest environment: name: pypi - url: https://pypi.org/p/jupytext + url: https://pypi.org/p/itables permissions: id-token: write steps: - name: Checkout source - uses: actions/checkout@v3 - - name: Set up Python 3.9 - uses: actions/setup-python@v4 + uses: actions/checkout@v4 + - name: Set up Node + uses: actions/setup-node@v4 + - name: Set up Python + uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.11 - name: Build package run: | - pip install wheel requests - python setup.py sdist bdist_wheel + python -m pip install wheel build + python -m build - name: Publish uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore index 422e63d5..f63987e9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,10 +16,12 @@ dist # Jupyter Book _build -# External dependencies -itables/external - # Quarto .jupyter_cache docs/quarto/*.html docs/quarto/*_files/ + +# DataTables bundle +node_modules +dt_bundle.js +dt_bundle.css diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 501a5ba3..2490c2e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,27 +11,31 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/pycqa/flake8 - rev: 6.0.0 - hooks: - - id: flake8 - - repo: https://github.com/timothycrosley/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 22.3.0 hooks: - id: black + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.3.1 + hooks: + - id: ruff + args: ["--fix", "--show-fixes"] + - repo: https://github.com/mwouts/jupytext rev: v1.14.5 hooks: - id: jupytext + exclude: dt_for_itables/ types: ["markdown"] - args: ["--pipe", "black"] + args: ["--pipe", "isort {} --treat-comment-as-code '# %%' --profile black", "--pipe", "black", "--check", "ruff check {} --ignore E402"] additional_dependencies: - black==22.3.0 # Matches hook + - ruff==0.3.1 + - isort==5.13.2 diff --git a/README.md b/README.md index 78c4634e..006964c5 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,15 @@ ![CI](https://github.com/mwouts/itables/actions/workflows/continuous-integration.yml/badge.svg?branch=main) [![codecov.io](https://codecov.io/github/mwouts/itables/coverage.svg?branch=main)](https://codecov.io/github/mwouts/itables?branch=main) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/mwouts/itables.svg)](https://lgtm.com/projects/g/mwouts/itables/context:python) [![Pypi](https://img.shields.io/pypi/v/itables.svg)](https://pypi.python.org/pypi/itables) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/itables.svg)](https://anaconda.org/conda-forge/itables) [![pyversions](https://img.shields.io/pypi/pyversions/itables.svg)](https://pypi.python.org/pypi/itables) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) -## Turn your Python DataFrames into Interactive Tables +## Turn your Python DataFrames into Interactive DataTables This packages changes how Pandas and Polars DataFrames are rendered in Jupyter Notebooks. -With `itables` you can display your tables as interactive [datatables](https://datatables.net/) +With `itables` you can display your tables as interactive [DataTables](https://datatables.net/) that you can sort, paginate, scroll or filter. ITables is just about how tables are displayed. You can turn it on and off in just two lines, @@ -24,7 +23,7 @@ work with Polars DataFrames). ## Documentation Browse the [documentation](https://mwouts.github.io/itables/) to see -examples of Pandas or Polars DataFrames rendered as interactive datatables. +examples of Pandas or Polars DataFrames rendered as interactive DataTables. ## Quick start @@ -50,7 +49,7 @@ and then render any DataFrame as an interactive table that you can sort, search If you prefer to render only selected DataFrames as interactive tables, use `itables.show` to show just one Series or DataFrame as an interactive table: ![show](docs/show_df.png) -Since `itables==1.0.0`, the [jquery](https://jquery.com/) and [datatables.net](https://datatables.net/) libraries and CSS +Since `itables==1.0.0`, the [jQuery](https://jquery.com/) and [DataTables](https://datatables.net/) libraries and CSS are injected in the notebook when you execute `init_notebook_mode` with its default argument `connected=False`. Thanks to this the interactive tables will work even without a connection to the internet. @@ -67,6 +66,8 @@ execute `init_notebook_mode`. - Google Colab - VS Code (for both Jupyter Notebooks and Python scripts) - PyCharm (for Jupyter Notebooks) +- Quarto +- Shiny for Python ## Try ITables on Binder @@ -90,7 +91,7 @@ and decide whether you should upgrade `itables`. When the data in a table is larger than `maxBytes`, which is equal to 64KB by default, `itables` will display only a subset of the table - one that fits into `maxBytes`. If you wish, you can deactivate the limit with `maxBytes=0`, change the value of `maxBytes`, or similarly set a limit on the number of rows (`maxRows`, defaults to 0) or columns (`maxColumns`, defaults to `pd.get_option('display.max_columns')`). -Note that datatables support [server-side processing](https://datatables.net/examples/data_sources/server_side). At a later stage we may implement support for larger tables using this feature. +Note that DataTables support [server-side processing](https://datatables.net/examples/data_sources/server_side). At a later stage we may implement support for larger tables using this feature. ```{code-cell} from itables.sample_dfs import get_indicators diff --git a/docs/_config.yml b/docs/_config.yml index f8f02972..26d5c62d 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,5 +1,6 @@ title: Interactive Tables author: Marc Wouts +copyright: "2019-2024" execute: execute_notebooks: force sphinx: diff --git a/docs/_toc.yml b/docs/_toc.yml index b9355d41..1df44b5b 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -1,16 +1,25 @@ format: jb-book root: quick_start -chapters: -- file: supported_editors -- file: advanced_parameters -- file: pandas_style -- file: custom_css -- file: downsampling -- file: sample_dataframes -- file: polars_dataframes -- file: quarto -- file: references -- file: contributing -- file: developing -- file: troubleshooting -- file: changelog +parts: + - caption: How to use DataTables + chapters: + - file: advanced_parameters + - file: formatting + - file: custom_css + - file: extensions + - file: custom_extensions + - caption: ITables + chapters: + - file: supported_editors + - file: quarto + - file: downsampling + - file: references + - file: contributing + - file: developing + - file: troubleshooting + - file: changelog + - caption: Example DataFrames + chapters: + - file: sample_dataframes + - file: polars_dataframes + - file: pandas_style diff --git a/docs/advanced_parameters.md b/docs/advanced_parameters.md index bb773c4b..1c26e512 100644 --- a/docs/advanced_parameters.md +++ b/docs/advanced_parameters.md @@ -12,11 +12,11 @@ kernelspec: name: itables --- -# Advanced parameters +# The DataTable Arguments -The `itables` package is a wrapper for the Javascript [datatables.net](https://datatables.net/) library, which has a great [documentation](https://datatables.net/), a huge collection of [examples](https://datatables.net/examples/index), and a useful [forum](https://datatables.net/forums/). +ITables is a wrapper for the Javascript [DataTables](https://datatables.net/) library, which has a great [documentation](https://datatables.net/), a huge collection of [examples](https://datatables.net/examples/index), and a useful [forum](https://datatables.net/forums/). -Below we give a few examples of how the datatables.net examples can be ported to Python with `itables`. +Below we give a series of examples of how the DataTables examples can be ported to Python with `itables`. As always, we initialize the `itables` library with @@ -30,32 +30,13 @@ Then we create two sample dataframes: ```{code-cell} import pandas as pd + from itables.sample_dfs import get_countries df_small = pd.DataFrame({"a": [2, 1]}) df = get_countries(html=False) ``` -## Position and width - -The default value for the table CSS is `table-layout:auto;width:auto;margin:auto;caption-side:bottom`. -Without `width:auto`, tables with few columns still take the full notebook width in Jupyter. -Using `margin:auto` makes non-wide tables centered in Jupyter. - -You can change the CSS used for a single table with e.g. - -```{code-cell} -show(df_small, style="table-layout:auto;width:50%;float:right") -``` - -or you can also change it for all tables by changing `itables.options.style`: - -```python -import itables.options as opt - -opt.style = "table-layout:auto;width:auto" -``` - ```{code-cell} :tags: [remove-cell] @@ -64,34 +45,6 @@ import itables.options as opt opt.lengthMenu = [5, 10, 20, 50, 100, 200, 500] ``` -## Theme - -Select how your table looks like with the `classes` argument (defaults to `"display nowrap"`) of the `show` function, or by changing `itables.options.classes`. - -Add `"compact"` if you want a denser table: - -```{code-cell} -:tags: [full-width] - -show(df, classes="display nowrap compact") -``` - -Remove `"nowrap"` if you want the cell content to be wrapped: - -```{code-cell} -:tags: [full-width] - -show(df, classes="display") -``` - -[More options](https://datatables.net/manual/styling/classes#Table-classes) like `"cell-border"` are available: - -```{code-cell} -:tags: [full-width] - -show(df, classes="display nowrap cell-border") -``` - ## Caption You can set additional `tags` on the table like e.g. a [caption](https://datatables.net/blog/2014-11-07): @@ -102,48 +55,60 @@ You can set additional `tags` on the table like e.g. a [caption](https://datatab show(df, "Countries from the World Bank Database") ``` -The caption appears at the bottom of the table by default. This is governed by `caption-side:bottom` -in the `style` option which you can change. You can also override the location of the caption in the caption tag itself: +The caption appears at the bottom of the table by default (except +in Jupyter Book). This is governed by `caption-side:bottom` +in the [`style` option](style). + +You can also override the location of the caption in the caption tag itself: ```{code-cell} :tags: [full-width] show( df, - tags='Countries from the World Bank Database', + tags='Countries from the World Bank Database', ) ``` -```{code-cell} -:tags: [remove-input] - -opt.lengthMenu = [5, 10, 20, 50, 100, 200, 500] -``` - -## Removing the search box +(layout)= +## Table layout By default, datatables that don't fit in one page come with a search box, a pagination control, a table summary, etc. You can select which elements are actually displayed using -DataTables' [`dom` option](https://datatables.net/reference/option/dom) with e.g.: +DataTables' [`layout` option](https://datatables.net/reference/option/layout) with e.g.: ```{code-cell} -show(df_small, dom="ti") +show(df_small, layout={"topStart": "search", "topEnd": None}) ``` -The available elements are: -- `l`: length changing input control -- `f`: filtering input -- `t`: the table itself -- `i`: table information summary -- `p`: pagination control -- `r`: processing display element +The available positions are `topStart, topEnd, bottomStart, bottomEnd`. You can also use `top2Start`, etc... (see more +in the [DataTables documentation](https://datatables.net/reference/option/layout)). Like for the other arguments of `show`, you can change the default value of the dom option with e.g.: ``` import itables.options as opt -opt.dom = "lfrtip" # (default value) +opt.layout = { + "topStart": "pageLength", + "topEnd": "search", + "bottomStart": "info", + "bottomEnd": "paging" +} # (default value) +``` + +```{tip} +The `layout` option was introduced with `itables==2.0` and `DataTables==2.0` +and deprecates the former [`dom` option](https://datatables.net/reference/option/dom). +If you wish to continue using the `dom` option, set `opt.warn_on_dom = False`. +``` + +## Search + +The [search option](https://datatables.net/reference/option/search) let you control the initial value for the search field, and whether the query should be treated as a regular expression or not: + +```{code-cell} +show(df, search={"regex": True, "caseInsensitive": True, "search": "s.ain"}) ``` ## Pagination @@ -180,7 +145,7 @@ show(df, scrollY="200px", scrollCollapse=True, paging=False) In the context of the notebook, a horizontal scroll bar should appear when the table is too wide. In other contexts like here in Jupyter Book, you might want to use `scrollX = True`. -## Table footer +## Footer Use `footer = True` if you wish to display a table footer. @@ -193,14 +158,14 @@ show(df, footer=True) ## Column filters Use `column_filters = "header"` or `"footer"` if you wish to display individual column filters -(remove the global search box with [`dom='lrtip'`](https://datatables.net/reference/option/dom) if desired). +(remove the global search box with a [`layout`](layout) modifier if desired). ```{code-cell} alpha_numeric_df = pd.DataFrame( [["one", 1.5], ["two", 2.3]], columns=["string", "numeric"] ) -show(alpha_numeric_df, column_filters="footer", dom="lrtip") +show(alpha_numeric_df, column_filters="footer", layout={"topEnd": None}) ``` As always you can set activate column filters by default with e.g. @@ -223,62 +188,6 @@ get_dict_of_test_dfs()["multiindex"] opt.column_filters = False ``` -## Pandas formatting - -`itables` builds the HTML representation of your Pandas dataframes using Pandas itself, so -you can use [Pandas' formatting options](https://pandas.pydata.org/pandas-docs/stable/user_guide/options.html). -For instance, you can change the precision used to display floating numbers: - -```{code-cell} -import math -import pandas as pd - -with pd.option_context("display.float_format", "{:,.2f}".format): - show(pd.Series([i * math.pi for i in range(1, 6)])) -``` - -Or you can use a custom formatter: - -```{code-cell} -with pd.option_context("display.float_format", "${:,.2f}".format): - show(pd.Series([i * math.pi for i in range(1, 6)])) -``` - -```{tip} -ITables in version 1.6.0+ can also render -[Pandas Style](https://pandas.pydata.org/docs/user_guide/style.html) -objects as interactive datatables. - -This way, you can easily add background color, and even -tooltips to your dataframes, and still get them -displayed using datatables.net - see our [example](pandas_style.md). -``` - -## Javascript formatting - -Numbers are formatted using Pandas, then are converted back to float to ensure they come in the right order when sorted. -Therefore, to achieve a particular formatting you might have to resort to the -[`columns.render` option](https://datatables.net/examples/advanced_init/column_render.html) -of datatables. - -For instance, this [example](https://datatables.net/forums/discussion/61407/how-to-apply-a-numeric-format-to-a-column) -can be ported like this: - -```{code-cell} -from itables import JavascriptCode - - -show( - pd.Series([i * math.pi * 1e4 for i in range(1, 6)]), - columnDefs=[ - { - "targets": "_all", - "render": JavascriptCode("$.fn.dataTable.render.number(',', '.', 3, '$')"), - } - ], -) -``` - ## Row order Since `itables>=1.3.0`, the interactive datatable shows the rows in the same order as the original dataframe: @@ -317,144 +226,3 @@ or locally by passing an argument `showIndex` to the `show` function: df_with_range_index = pd.DataFrame({"letter": list("abcd")}) show(df_with_range_index, showIndex=True) ``` - -## Advanced cell formatting with JS callbacks - -You can use Javascript callbacks to set the cell or row style depending on the cell content. - -The example below, in which we color in red the cells with negative numbers, is directly inspired by the corresponding datatables.net [example](https://datatables.net/reference/option/columns.createdCell). - -Note how the Javascript callback is declared as `JavascriptFunction` object below. - -```{code-cell} -from itables import JavascriptFunction - -show( - pd.DataFrame([[-1, 2, -3, 4, -5], [6, -7, 8, -9, 10]], columns=list("abcde")), - columnDefs=[ - { - "targets": "_all", - "createdCell": JavascriptFunction( - """ -function (td, cellData, rowData, row, col) { - if (cellData < 0) { - $(td).css('color', 'red') - } -} -""" - ), - } - ], -) -``` - -```{tip} -Since `itables==1.6.0`, you can also render -[Pandas style](pandas_style.md) objects as interactive datatables - -that might be a simpler alternative to the JavaScript callbacks - documented here. -``` - -## Column width - -The [`columnDefs.width`](https://datatables.net/reference/option/columns.width) argument let you adjust the column widths. - -Note that the default value of `style`, or of `autoWidth` (defaults to `True`), might override custom column widths, -so you might have to change their values as in the examples below. - -You can set a fixed width for all the columns with `"targets": "_all"`: - -```{code-cell} -:tags: [full-width] - -show( - df, - columnDefs=[{"width": "120px", "targets": "_all"}], - scrollX=True, - style="width:1200px", - autoWidth=False, -) -``` - -You can also adjust the width of selected columns only: - -```{code-cell} -:tags: [full-width] - -show( - df, - columnDefs=[{"width": "30%", "targets": [2, 3]}], - style="width:100%;margin:auto", -) -``` - -If you wish you can also set a value for `columnDefs` permanently in `itables.options` as demonstrated in the cell alignment example below. - -## Cell alignment - -You can use the datatables.net [cell classes](https://datatables.net/manual/styling/classes#Cell-classes) like `dt-left`, `dt-center`, `dt-right` etc. to set the cell alignment. Specify it for one table by using the `columnDefs` argument of `show` - -```{code-cell} -show(df, columnDefs=[{"className": "dt-center", "targets": "_all"}]) -``` - -or globally by setting `opt.columnDefs`: - -```{code-cell} -opt.columnDefs = [{"className": "dt-center", "targets": "_all"}] -df -``` - -```{code-cell} -del opt.columnDefs -``` - -## HTML in cells - -```{code-cell} -pd.Series( - [ - "bold", - "italic", - 'link', - ], - name="HTML", -) -``` - -## Images in cells - -Since HTML is supported you can display images in your tables. -You can use either -- `` with an url -- `` with a [base64 encoded image](https://stackoverflow.com/a/8499716/9817073). - -```{code-cell} -pd.Series( - { - "url": 'MNIST', - "base64": 'Red dot', - }, - name="Images", -) -``` - -## The search option - -The [search option](https://datatables.net/reference/option/search) let you control the initial value for the search field, and whether the query should be treated as a regular expression or not: - -```{code-cell} -show(df, search={"regex": True, "caseInsensitive": True, "search": "s.ain"}) -``` - -## Select rows - -Not currently implemented. May be made available at a later stage using the [select](https://datatables.net/extensions/select/) extension for datatables. - -+++ - -## Copy, CSV, PDF and Excel buttons - -Not currently implemented. May be made available at a later stage thanks to the [buttons](https://datatables.net/extensions/buttons/) extension for datatable. diff --git a/docs/changelog.md b/docs/changelog.md index c2004c3c..8c00520e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,20 @@ ITables ChangeLog ================= +2.0.0 (2024-03-16) +------------------ + +**Added** +- The CSV, Excel and Print buttons are now included ([#50](https://github.com/mwouts/itables/issues/50), [#155](https://github.com/mwouts/itables/issues/155)) +- We have included a few other extensions like SearchBuilder and SearchPanes and documented how to add more ([#178](https://github.com/mwouts/itables/issues/178), [#207](https://github.com/mwouts/itables/issues/207), [#208](https://github.com/mwouts/itables/issues/208), [#231](https://github.com/mwouts/itables/issues/231)) +- ITables is now tested with Python 3.12 + +**Changed** +- ITables uses the latest version `2.0.2` of `DataTables` ([#121](https://github.com/mwouts/itables/issues/121)) +- Large Python integers are now mapped to JavaScript `BigInt` ([#172](https://github.com/mwouts/itables/issues/172)) +- ITables is build using `hatch` and `pyproject.toml` + + 1.7.1 (2024-03-05) ------------------ diff --git a/docs/custom_css.md b/docs/custom_css.md index 462f4ad1..7878dd25 100644 --- a/docs/custom_css.md +++ b/docs/custom_css.md @@ -12,30 +12,177 @@ kernelspec: name: itables --- -# Custom CSS +# Styling -You can change the global CSS used to render the tables -by either passing a custom CSS to the `show` function, or by -changing `opt.css`. - -Note that the CSS must be the same for all the tables -in a given notebook. To change the CSS for just one table, -use the [`style`](advanced_parameters.html#position-and-width) argument of the `show` function. +As usual, we initialize ITables with `init_notebook_mode`, and we create two sample DataFrames: ```{code-cell} +:tags: [hide-input] + +import pandas as pd + +import itables.options as opt from itables import init_notebook_mode, show from itables.sample_dfs import get_countries -import itables.options as opt +df = get_countries(html=False) +df_small = pd.DataFrame({"a": [2, 1]}) + +init_notebook_mode(all_interactive=True) +``` + +## Classes + +Select how your table looks like with the `classes` argument (defaults to `"display nowrap"`) of the `show` function, or by changing `itables.options.classes`. + +Add `"compact"` if you want a denser table: + +```{code-cell} +:tags: [full-width] + +show(df, classes="display nowrap compact") +``` + +Remove `"nowrap"` if you want the cell content to be wrapped: + +```{code-cell} +:tags: [full-width] + +show(df, classes="display") +``` -opt.css = """ -.itables table td { font-style: italic; } -.itables table th { font-style: oblique; } +[More options](https://datatables.net/manual/styling/classes#Table-classes) like `"cell-border"` are available: + +```{code-cell} +:tags: [full-width] + +show(df, classes="display nowrap cell-border") +``` + +## CSS + +You can use CSS to alter how the interactive DataTables are rendered. + +For instance, we change the +[font size](https://developer.mozilla.org/en-US/docs/Web/CSS/font-size) +for all the tables in the document with this code: + +```{code-cell} +from IPython.display import HTML, display + +css = """ +.dt-container { + font-size: small; +} """ +display(HTML(f"" "")) +``` -init_notebook_mode(all_interactive=True) +This is helpful for instance in the context of +[Quarto presentations](quarto.md). + +With this over CSS, we change _every datatable_ table header +in the notebook to bold/italic. + +```{code-cell} +css = """ +.dataTable th { + font-weight: bolder; + font-style: italic; +} +""" +display(HTML(f"" "")) +``` + +You might also want to alter the style of specific tables only. +To do this, add a new class to the target tables, as +in the example below: + +```{code-cell} +class_specific_css = ".table_with_monospace_font { font-family: courier, monospace }" +display(HTML(f"" "")) +``` + +```{code-cell} +show(df, classes="display nowrap table_with_monospace_font") +``` + +(style)= +## The style argument + +The `show` function has a `style` argument that determines the +style for that particular table. + +The default value for `style` is `table-layout:auto;width:auto;margin:auto;caption-side:bottom`. +Without `width:auto`, tables with few columns still take the full notebook width in Jupyter. +Using `margin:auto` makes non-wide tables centered in Jupyter. + +## Position and width + +You can set a specific width or position for a table using with the `style` argument of the show function: + +```{code-cell} +show(df_small, style="table-layout:auto;width:50%;float:right") +``` + +or you can also change it for all tables by changing `itables.options.style`: + +```python +import itables.options as opt + +opt.style = "table-layout:auto;width:auto" ``` +```{tip} +For ajusting the height of a table, see the section on [pagination](advanced_parameters.md#pagination). +``` + +## Column width + +The [`columnDefs.width`](https://datatables.net/reference/option/columns.width) argument let you adjust the column widths. + +Note that the default value of `style`, or of `autoWidth` (defaults to `True`), might override custom column widths, +so you might have to change their values as in the examples below. + +You can set a fixed width for all the columns with `"targets": "_all"`: + +```{code-cell} +:tags: [full-width] + +show( + df, + columnDefs=[{"width": "120px", "targets": "_all"}], + scrollX=True, + style="width:1200px", + autoWidth=False, +) +``` + +You can also adjust the width of selected columns only: + +```{code-cell} +:tags: [full-width] + +show( + df, + columnDefs=[{"width": "30%", "targets": [2, 3]}], + style="width:100%;margin:auto", +) +``` + +If you wish you can also set a value for `columnDefs` permanently in `itables.options` as demonstrated in the cell alignment example below. + +## Cell alignment + +You can use the datatables.net [cell classes](https://datatables.net/manual/styling/classes#Cell-classes) like `dt-left`, `dt-center`, `dt-right` etc. to set the cell alignment. Specify it for one table by using the `columnDefs` argument of `show` + +```{code-cell} +show(df, columnDefs=[{"className": "dt-center", "targets": "_all"}]) +``` + +or globally by setting `opt.columnDefs`: + ```{code-cell} -get_countries() +opt.columnDefs = [{"className": "dt-center", "targets": "_all"}] +df ``` diff --git a/docs/custom_extensions.md b/docs/custom_extensions.md new file mode 100644 index 00000000..8d644589 --- /dev/null +++ b/docs/custom_extensions.md @@ -0,0 +1,104 @@ +--- +jupytext: + formats: md:myst + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.14.5 +kernelspec: + display_name: itables + language: python + name: itables +--- + +# Custom Extensions + +## Internationalisation + +```{code-cell} +from itables import show +from itables.sample_dfs import get_countries + +df = get_countries(html=False) +``` + +DataTables controls can use a different language than English. To +display the table controls in another language, go to the [internationalisation](https://datatables.net/plug-ins/i18n/) +plug-ins page and find the language URL, like e.g. + +```{code-cell} +show( + df, + language={"url": "https://cdn.datatables.net/plug-ins/2.0.2/i18n/fr-FR.json"}, +) +``` + +```tip +You can also use the internationalization in the offline mode. Download the translation file, +then set `opt.language` accordingly: + +~~~python +import json +import itables.options as opt + +with open("fr-FR.json") as fp: + opt.language = json.load(fp) +~~~ +``` + +## Creating a custom DataTables bundle + +To use custom extensions in the offline mode, you will need +to create a bundle of jQuery, DataTables, and the desired extensions. + +To do so, make a copy of +[`itables/dt_for_itables`](https://github.com/mwouts/itables/tree/main/itables/dt_for_itables): +```bash +$ ls itables/dt_for_itables/ +package.json package-lock.json README.md src.js +``` + +Add or remove the desired extensions in `package.json` and `src.js`. To do this, +you can use the [DataTables download](https://datatables.net/download/) page and +follow the instructions for the _NPM_ download method. + +For instance, say you want to bundle the PDF export button. Change +`src.js` to this code: +```javascript +import JSZip from 'jszip'; +import jQuery from 'jquery'; +import pdfMake from 'pdfmake'; +import DataTable from 'datatables.net-dt'; +import 'datatables.net-dt/css/dataTables.dataTables.min.css'; + +import 'datatables.net-buttons-dt'; +import 'datatables.net-buttons/js/buttons.html5.mjs'; +import 'datatables.net-buttons/js/buttons.print.mjs'; +import 'datatables.net-buttons-dt/css/buttons.dataTables.min.css'; + +DataTable.Buttons.jszip(JSZip); +DataTable.Buttons.pdfMake(pdfMake); + +pdfMake.vfs = pdfFonts.pdfMake.vfs; + +export { DataTable, jQuery }; +``` + +and run these commands: +```bash +# Install the dependencies in package.json +npm install + +# Install the additional dependencies +npm install pdfmake --save + +# Create dt_bundle.js and dt_bundle.css +npm run build +``` + +Finally, you can either deploy `dt_bundle.js` and `dt_bundle.css` on an +http server and pass the URL of `dt_bundle.js` as the `dt_url` option to `show`, +or, in the offline mode, pass the path to `dt_bundle.js` +as the `dt_bundle` argument of the `init_notebook_mode` method (in either +case you can set the values permanently on `itables.options`). diff --git a/docs/downsampling.md b/docs/downsampling.md index 3136d2df..70de9a44 100644 --- a/docs/downsampling.md +++ b/docs/downsampling.md @@ -21,16 +21,15 @@ When the data in a table is larger than `maxBytes`, which is equal to 64KB by de If you wish, you can increase the value of `maxBytes` or even deactivate the limit (with `maxBytes=0`). Similarly, you can set a limit on the number of rows (`maxRows`, defaults to 0) or columns (`maxColumns`, defaults to `200`). ```{code-cell} +import itables.options as opt from itables import init_notebook_mode, show +from itables.downsample import nbytes +from itables.sample_dfs import get_indicators init_notebook_mode(all_interactive=True) ``` ```{code-cell} -from itables.sample_dfs import get_indicators -from itables.downsample import nbytes -import itables.options as opt - opt.lengthMenu = [2, 5, 10, 20, 50, 100, 200, 500] opt.maxBytes = 10000 diff --git a/docs/extensions.md b/docs/extensions.md new file mode 100644 index 00000000..786a5701 --- /dev/null +++ b/docs/extensions.md @@ -0,0 +1,205 @@ +--- +jupytext: + formats: md:myst + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.14.5 +kernelspec: + display_name: itables + language: python + name: itables +--- + +# DataTables Extensions + +DataTables comes with a series of [extensions](https://datatables.net/extensions/), which are supported by ITables since v2.0. +A selection of these extensions are included in ITables. + +As usual, we activate ITables with: + +```{code-cell} +from itables import init_notebook_mode, show + +init_notebook_mode() +``` + +and then, we create a few example dataframes: + +```{code-cell} +:tags: [hide-input] + +import string + +import numpy as np +import pandas as pd + +from itables.sample_dfs import get_countries + +df = get_countries(html=False) +# Add columns for the searchPanes demo +df["climate_zone"] = np.where( + df["latitude"].abs() < 23.43615, + "Tropical", + np.where( + df["latitude"].abs() < 35, + "Sub-tropical", + # Artic circle is 66.563861 but there is no capital there => using 64 + np.where(df["latitude"].abs() < 64, "Temperate", "Frigid"), + ), +) +df["hemisphere"] = np.where(df["latitude"] > 0, "North", "South") +wide_df = pd.DataFrame( + { + letter: np.random.normal(size=100) + for letter in string.ascii_lowercase + string.ascii_uppercase + } +) +``` + +## Buttons + +The DataTables [buttons](https://datatables.net/extensions/buttons/) let you copy the table data, or export it as CSV or Excel files. + +To display the buttons, you need to pass a `buttons` argument to the `show` function: + +```{code-cell} +:tags: [full-width] + +show(df, buttons=["copyHtml5", "csvHtml5", "excelHtml5"]) +``` + +You can also specify a [`layout`](layout) modifier that will decide +the location of the buttons (the default is `layout={"topStart": "buttons"}`). And if +you want to keep the pagination control too, you can add `"pageLength"` to the list of buttons. + +As always, it is possible to set default values for these parameters by setting these on `itables.options`. For instance, set +```python +opt.buttons = ["copyHtml5", "csvHtml5", "excelHtml5"] +``` +to get the buttons for all tables. + + +By default, the exported file name is the name of the HTML page. To change it, set a +[`title` option](https://datatables.net/extensions/buttons/examples/html5/filename.html) on the buttons, like +here: + +```{code-cell} +:tags: [full-width] + +show( + df, + buttons=[ + "pageLength", + {"extend": "csvHtml5", "title": "Countries"}, + {"extend": "excelHtml5", "title": "Countries"}, + ], +) +``` + +```{warning} +The PDF button is not included in ITables's DataTable bundle. This is because the required PDF libraries +have a large footprint on the bundle size. Still, you can add it to your custom bundle, see the next chapter. +``` + +## SearchPanes + +[SearchPanes](https://datatables.net/extensions/searchpanes/) is an extension that lets you select rows based on +unique values. In the example below we have activated the cascade filtering through the +[`searchPanes.cascadePanes`](https://datatables.net/extensions/searchpanes/examples/initialisation/cascadePanes.html) argument. +Note that, in Jupyter, the [`searchPanes.layout`](https://datatables.net/extensions/searchpanes/layout) +argument is required (otherwise the search panes are too wide). + +```{code-cell} +:tags: [full-width] + +show( + df, + layout={"top1": "searchPanes"}, + searchPanes={"layout": "columns-3", "cascadePanes": True}, +) +``` + +```{warning} +When searching, please keep in mind that ITables will [downsample](downsampling.md) your table if it is larger than `maxBytes`, +so you might not see the full dataset - pay attention to the downsampling message at the bottom left of the table. +``` + +## SearchBuilder + +[SearchBuilder](https://datatables.net/extensions/searchbuilder/) let you build complex search queries. You just need to add it to the layout +by passing e.g. `layout={"top1": "searchBuilder"}`. + +It is possible to set a predefined search, as we do in the below: + +```{code-cell} +:tags: [full-width] + +show( + df, + layout={"top1": "searchBuilder"}, + searchBuilder={ + "preDefined": { + "criteria": [ + {"data": "climate_zone", "condition": "=", "value": ["Sub-tropical"]} + ] + } + }, +) +``` + +## FixedColumns + +[FixedColumn](https://datatables.net/extensions/fixedcolumns/) is an extension +that let you fix some columns as you scroll horizontally. + +```{code-cell} +:tags: [full-width] + +show( + wide_df, + fixedColumns={"start": 1, "end": 2}, + scrollX=True, +) +``` + +## KeyTable + +With the [KeyTable](https://datatables.net/extensions/keytable/) extension you can navigate in a table using the arrow keys: + +```{code-cell} +:tags: [full-width] + +show(df, keys=True) +``` + +```{tip} +You can activate this option for all your tables with + +~~~python +import itables.options as opt + +opt.keys = True +~~~ +``` + +```{warning} +The KeyTable extension works in Jupyter Book (try it here in the documentation) but not in JupyterLab. +``` + +## RowGroup + +Use the [RowGroup](https://datatables.net/extensions/rowgroup/) extension to group +the data according to the content of one colum. Optionally, you can hide the content +of that column to avoid duplicating the information. + +```{code-cell} +:tags: [full-width] + +show( + df.sort_values("region"), + rowGroup={"dataSrc": 1}, + columnDefs=[{"targets": 1, "visible": False}], +) +``` diff --git a/docs/formatting.md b/docs/formatting.md new file mode 100644 index 00000000..5822de15 --- /dev/null +++ b/docs/formatting.md @@ -0,0 +1,172 @@ +--- +jupytext: + formats: md:myst + text_representation: + extension: .md + format_name: myst + format_version: 0.13 + jupytext_version: 1.14.5 +kernelspec: + display_name: itables + language: python + name: itables +--- + +# Formatting + +## Formatting with Pandas + +`itables` builds the HTML representation of your Pandas dataframes using Pandas itself, so +you can use [Pandas' formatting options](https://pandas.pydata.org/pandas-docs/stable/user_guide/options.html). +For instance, you can change the precision used to display floating numbers: + +```{code-cell} +from itables import init_notebook_mode, show +from itables.sample_dfs import get_countries + +init_notebook_mode(all_interactive=True) +``` + +```{code-cell} +import math + +import pandas as pd + +with pd.option_context("display.float_format", "{:,.2f}".format): + show(pd.Series([i * math.pi for i in range(1, 6)])) +``` + +Or you can use a custom formatter: + +```{code-cell} +with pd.option_context("display.float_format", "${:,.2f}".format): + show(pd.Series([i * math.pi for i in range(1, 6)])) +``` + +## Formatting with Javascript + +Numbers are formatted using Pandas, then are converted back to float to ensure they come in the right order when sorted. +Therefore, to achieve a particular formatting you might have to resort to the +[`columns.render` option](https://datatables.net/examples/advanced_init/column_render.html) +of DataTables. + +For instance, this [example](https://datatables.net/forums/discussion/61407/how-to-apply-a-numeric-format-to-a-column) +can be ported like this: + +```{code-cell} +from itables import JavascriptCode + +show( + pd.Series([i * math.pi * 1e4 for i in range(1, 6)]), + columnDefs=[ + { + "targets": "_all", + "render": JavascriptCode("$.fn.dataTable.render.number(',', '.', 3, '$')"), + } + ], +) +``` + +## Colors based on cell values + +You can use Javascript callbacks to set the cell or row style depending on the cell content. + +The example below, in which we color in red the cells with negative numbers, is directly inspired by the corresponding datatables.net [example](https://datatables.net/reference/option/columns.createdCell). + +Note how the Javascript callback is declared as `JavascriptFunction` object below. + +```{code-cell} +from itables import JavascriptFunction + +show( + pd.DataFrame([[-1, 2, -3, 4, -5], [6, -7, 8, -9, 10]], columns=list("abcde")), + columnDefs=[ + { + "targets": "_all", + "createdCell": JavascriptFunction( + """ +function (td, cellData, rowData, row, col) { + if (cellData < 0) { + $(td).css('color', 'red') + } +} +""" + ), + } + ], +) +``` + +## Formatting with Pandas style + +ITables in version 1.6 and above can render +[Pandas Style](https://pandas.pydata.org/docs/user_guide/style.html) +objects as interactive DataTables. + +This way, you can easily add background color, and even +tooltips to your dataframes, and still get them +displayed using DataTables - see our [examples](pandas_style.md). + +```{warning} +Please note that Pandas Style objects are rendered using +their `.to_html()` method, which is less efficient that +the default JS data export used by ITables. +``` + +## HTML in cells + +### A simple example + +HTML content is supported, which means that you can have formatted text, +links or even images in your tables: + +```{code-cell} +pd.Series( + [ + "bold", + "italic", + 'link', + ], + name="HTML", +) +``` + +### Images in a table + +```{code-cell} +:tags: [full-width] + +df = get_countries(html=False) + +df["flag"] = [ + '' + ''.format(code=code.lower(), country=country) + for code, country in zip(df.index, df["country"]) +] +df["country"] = [ + '{}'.format(country, country) + for country in df["country"] +] +df["capital"] = [ + '{}'.format(capital, capital) + for capital in df["capital"] +] +df +``` + +### Base64 images + +[Base64 encoded image](https://stackoverflow.com/a/8499716/9817073) are supported, too: + +```{code-cell} +pd.Series( + { + "url": 'MNIST', + "base64": 'Red dot', + }, + name="Images", +) +``` diff --git a/docs/pandas_style.md b/docs/pandas_style.md index ddc6f528..8db0feab 100644 --- a/docs/pandas_style.md +++ b/docs/pandas_style.md @@ -12,14 +12,15 @@ kernelspec: name: itables --- -# Pandas Style +# Pandas Style examples Starting with `itables>=1.6.0`, ITables provides support for [Pandas Style](https://pandas.pydata.org/docs/user_guide/style.html). ```{code-cell} -import pandas as pd import numpy as np +import pandas as pd + from itables import init_notebook_mode init_notebook_mode(all_interactive=True) diff --git a/docs/polars_dataframes.md b/docs/polars_dataframes.md index 753d2081..f4967844 100644 --- a/docs/polars_dataframes.md +++ b/docs/polars_dataframes.md @@ -168,9 +168,5 @@ show(dict_of_test_dfs["named_column_index"]) ## big_integers ```{code-cell} -import itables.options as opt - -opt.warn_on_int_to_str_conversion = False - show(dict_of_test_dfs["big_integers"]) ``` diff --git a/docs/quarto/quarto_html.qmd b/docs/quarto/quarto_html.qmd index d6321682..11e3c86e 100644 --- a/docs/quarto/quarto_html.qmd +++ b/docs/quarto/quarto_html.qmd @@ -26,17 +26,18 @@ jupyter: ```{python} #| echo: false +from IPython.display import display, HTML import itables.options as opt # show 5 rows per 'page' opt.lengthMenu = [5] # don't show the length control -opt.dom = "frtip" +opt.layout["topStart"] = None # use a smaller font (default is medium) # see https://developer.mozilla.org/en-US/docs/Web/CSS/font-size -opt.css += ".dataTables_wrapper { font-size: small; }" +display(HTML("")) ``` ```{python} @@ -70,6 +71,7 @@ get_countries(html=False) ## This document uses ```python +from IPython.display import display, HTML import itables.options as opt @@ -77,11 +79,11 @@ import itables.options as opt opt.lengthMenu = [5] # don't show the length control -opt.dom = "frtip" +opt.layout["topStart"] = None # use a smaller font (default is medium) # see https://developer.mozilla.org/en-US/docs/Web/CSS/font-size -opt.css += ".dataTables_wrapper { font-size: x-small; }" +display(HTML("")) ``` ::: @@ -122,7 +124,7 @@ under a MIT license. ITables renders Pandas or Polars DataFrames as interactive HTML tables using the JavaScript -[datatables-net](https://datatables.net/) library. +[DataTables](https://datatables.net/) library. ::: ::: {.callout-tip} diff --git a/docs/quarto/quarto_revealjs.qmd b/docs/quarto/quarto_revealjs.qmd index 890798cb..bd5e5653 100644 --- a/docs/quarto/quarto_revealjs.qmd +++ b/docs/quarto/quarto_revealjs.qmd @@ -26,17 +26,18 @@ jupyter: ```{python} #| echo: false +from IPython.display import display, HTML import itables.options as opt # show 5 rows per 'page' opt.lengthMenu = [5] # don't show the length control -opt.dom = "frtip" +opt.layout["topStart"] = None # use a smaller font (default is medium) # see https://developer.mozilla.org/en-US/docs/Web/CSS/font-size -opt.css += ".dataTables_wrapper { font-size: small; }" +display(HTML("")) ``` ```{python} @@ -70,6 +71,7 @@ get_countries(html=False) ## This document uses ```python +from IPython.display import display, HTML import itables.options as opt @@ -77,11 +79,11 @@ import itables.options as opt opt.lengthMenu = [5] # don't show the length control -opt.dom = "frtip" +opt.layout["topStart"] = None # use a smaller font (default is medium) # see https://developer.mozilla.org/en-US/docs/Web/CSS/font-size -opt.css += ".dataTables_wrapper { font-size: small; }" +display(HTML("")) ``` ::: @@ -122,7 +124,7 @@ under a MIT license. ITables renders Pandas or Polars DataFrames as interactive HTML tables using the JavaScript -[datatables-net](https://datatables.net/) library. +[DataTables](https://datatables.net/) library. ::: ::: {.callout-tip} diff --git a/docs/quick_start.md b/docs/quick_start.md index da7a323a..f44e0d30 100644 --- a/docs/quick_start.md +++ b/docs/quick_start.md @@ -16,7 +16,6 @@ kernelspec: ![CI](https://github.com/mwouts/itables/actions/workflows/continuous-integration.yml/badge.svg?branch=main) [![codecov.io](https://codecov.io/github/mwouts/itables/coverage.svg?branch=main)](https://codecov.io/github/mwouts/itables?branch=main) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/mwouts/itables.svg)](https://lgtm.com/projects/g/mwouts/itables/context:python) [![Pypi](https://img.shields.io/pypi/v/itables.svg)](https://pypi.python.org/pypi/itables) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/itables.svg)](https://anaconda.org/conda-forge/itables) [![pyversions](https://img.shields.io/pypi/pyversions/itables.svg)](https://pypi.python.org/pypi/itables) @@ -24,10 +23,10 @@ kernelspec: Star -## Turn your Python DataFrames into Interactive Tables +## Turn your Python DataFrames into Interactive DataTables This packages changes how Pandas and Polars DataFrames are rendered in Jupyter Notebooks. -With `itables` you can display your tables as interactive [datatables](https://datatables.net/) +With `itables` you can display your tables as interactive [DataTables](https://datatables.net/) that you can sort, paginate, scroll or filter. ITables is just about how tables are displayed. You can turn it on and off in just two lines, @@ -61,7 +60,7 @@ init_notebook_mode(all_interactive=True) ``` After this, any Pandas or Polars DataFrame, or Series, -is displayed as an interactive [datatables.net](https://datatables.net/) table, +is displayed as an interactive [DataTables](https://datatables.net/), which lets you explore, filter or sort your data. ```{code-cell} @@ -74,7 +73,7 @@ df ## Offline mode versus connected mode ITables use two Javascript libraries: -[jquery](https://jquery.com/) and [datatables.net](https://datatables.net/). +[jQuery](https://jquery.com/) and [DataTables](https://datatables.net/). By default `itables` works offline. No internet connection is required as the two libraries are embedded into the notebook itself @@ -86,11 +85,10 @@ To do so, add the argument `connected=True` when you execute `init_notebook_mode`. This will also make your notebook lighter by about [700kB](https://github.com/mwouts/itables/blob/main/tests/test_connected_notebook_is_small.py). -## Formatting specific tables only +## Using ITables for specific tables only If you prefer to render only certain series or dataframes using `itables`, -or you want to use the [advanced parameters](advanced_parameters.md), then -use `init_notebook_mode(all_interactive=False)` then `show`: +then call `init_notebook_mode(all_interactive=False)` then `show`: ```{code-cell} from itables import show @@ -98,31 +96,6 @@ from itables import show show(df, lengthMenu=[2, 5, 10, 25, 50, 100, 250]) ``` -## HTML content - -HTML content is supported, which means that you can have formatted text, -links or even images in your tables: - -```{code-cell} -:tags: [full-width] - -df["flag"] = [ - '' - ''.format(code=code.lower(), country=country) - for code, country in zip(df.index, df["country"]) -] -df["country"] = [ - '{}'.format(country, country) - for country in df["country"] -] -df["capital"] = [ - '{}'.format(capital, capital) - for capital in df["capital"] -] -df -``` - ## Try ITables on Binder You can run our examples notebooks directly on [![Lab](https://img.shields.io/badge/Binder-JupyterLab-blue.svg)](https://mybinder.org/v2/gh/mwouts/itables/main?urlpath=lab/tree/docs/quick_start.md), without having to install anything on your side. diff --git a/docs/references.md b/docs/references.md index 5ce18ab3..dd3eb118 100644 --- a/docs/references.md +++ b/docs/references.md @@ -3,7 +3,7 @@ ## DataTables - DataTables is a plug-in for the jQuery Javascript library. It has a great [documentation](https://datatables.net/manual/), and a large set of [examples](https://datatables.net/examples/index). -- The R package [DT](https://rstudio.github.io/DT/) uses [datatables.net](https://datatables.net/) as the underlying library for both R notebooks and Shiny applications. In addition to the standard functionalities of the library (display, sort, filtering and row selection), RStudio seems to have implemented cell edition. +- The R package [DT](https://rstudio.github.io/DT/) uses [DataTables](https://datatables.net/) as the underlying library for both R notebooks and Shiny applications. In addition to the standard functionalities of the library (display, sort, filtering and row selection), RStudio seems to have implemented cell edition. ## Alternatives diff --git a/docs/sample_dataframes.md b/docs/sample_dataframes.md index 1083f62d..4cbdd726 100644 --- a/docs/sample_dataframes.md +++ b/docs/sample_dataframes.md @@ -179,9 +179,5 @@ show(dict_of_test_dfs["named_column_index"]) ## big_integers ```{code-cell} -import itables.options as opt - -opt.warn_on_int_to_str_conversion = False - show(dict_of_test_dfs["big_integers"]) ``` diff --git a/docs/supported_editors.md b/docs/supported_editors.md index 3b455b50..4b4e8077 100644 --- a/docs/supported_editors.md +++ b/docs/supported_editors.md @@ -32,7 +32,7 @@ A short sample notebook is available [here](https://colab.research.google.com/dr ## VS Code -In VS Code, `itables` works both for Jupyter Notebooks and Python scripts +In VS Code, `itables` works both for Jupyter Notebooks and Python scripts. ![](images/code.png) @@ -51,7 +51,7 @@ ITables works well with Quarto - check out our `html` and `revealjs` [examples]( # Exporting a DataFrame to an HTML table -To get the HTML representation of a Pandas DataFrame `df` as an interactive [datatable](https://datatables.net/), you can use `to_html_datatable` as below: +To get the HTML representation of a Pandas DataFrame `df` as an interactive [DataTable](https://datatables.net/), you can use `to_html_datatable` as below: ```python from itables import to_html_datatable from itables.sample_dfs import get_countries @@ -65,6 +65,7 @@ html = to_html_datatable(df) You can use ITables in Web applications generated with [Shiny](https://shiny.rstudio.com/py/) for Python with e.g. ```python from shiny import ui + from itables.shiny import DT ui.HTML(DT(df)) diff --git a/environment.yml b/environment.yml index 6a1061da..8a19b03d 100644 --- a/environment.yml +++ b/environment.yml @@ -3,30 +3,26 @@ channels: - defaults - conda-forge dependencies: - - python + - nodejs + - python<=3.10 - jupyter - - jupyterlab - - jupytext>=1.13.8 - - markdown-it-py>=2.0 + - jupyterlab>=4 + - jupytext - nbconvert - ipykernel - pandas + - matplotlib - polars - pyarrow - pytest - pytest-xdist - pytest-cov - pre-commit - - pylint - - flake8 - - black - - isort - pip - setuptools - twine - ghp-import - # Interactive applications - shiny - pip: - world_bank_data - - jupyter_book>=0.12 # jupyter-book-0.12.2-pyhd8ed1ab_0 requires jupytext >=1.11.2,<1.12 + - jupyter_book diff --git a/itables/datatables_format.py b/itables/datatables_format.py index c313d880..c2599c50 100644 --- a/itables/datatables_format.py +++ b/itables/datatables_format.py @@ -1,4 +1,5 @@ import json +import re import warnings import numpy as np @@ -65,42 +66,6 @@ def default(self, obj): return TableValuesEncoder -def convert_bigints_to_str(df, warn_on_int_to_str_conversion): - """In Javascript, integers have to remain between JS_MIN_SAFE_INTEGER and JS_MAX_SAFE_INTEGER.""" - converted = [] - for i, col in enumerate(df.columns): - try: - x = df.iloc[:, i] - if ( - x.dtype.kind == "i" - and ( - ~x.isnull() - & ((x < JS_MIN_SAFE_INTEGER) | (x > JS_MAX_SAFE_INTEGER)) - ).any() - ): - _isetitem(df, i, x.astype(str)) - converted.append(col) - except AttributeError: - # Polars - x = df[col] - if x.dtype in pl.INTEGER_DTYPES and ( - (x.min() < JS_MIN_SAFE_INTEGER) or (x.max() > JS_MAX_SAFE_INTEGER) - ): - df = df.with_columns(pl.col(col).cast(pl.Utf8)) - converted.append(col) - - if converted and warn_on_int_to_str_conversion: - warnings.warn( - "The columns {} contains integers that are too large for Javascript.\n" - "They have been converted to str. See https://github.com/mwouts/itables/issues/172.\n" - "To silence this warning, please run:\n" - " import itables.options as opt\n" - " opt.warn_on_int_to_str_conversion = False".format(converted) - ) - - return df - - def _isetitem(df, i, value): """Older versions of Pandas don't have df.isetitem""" try: @@ -109,12 +74,8 @@ def _isetitem(df, i, value): df.iloc[:, i] = value -def datatables_rows( - df, count=None, warn_on_unexpected_types=False, warn_on_int_to_str_conversion=False -): +def datatables_rows(df, count=None, warn_on_unexpected_types=False): """Format the values in the table and return the data, row by row, as requested by DataTables""" - df = convert_bigints_to_str(df, warn_on_int_to_str_conversion) - # We iterate over columns using an index rather than the column name # to avoid an issue in case of duplicated column names #89 if count is None or len(df.columns) == count: @@ -128,8 +89,32 @@ def datatables_rows( try: # Pandas DataFrame data = list(zip(*(empty_columns + [_format_column(x) for _, x in df.items()]))) - return json.dumps(data, cls=generate_encoder(warn_on_unexpected_types)) + has_bigints = any( + x.dtype.kind == "i" + and ((x > JS_MAX_SAFE_INTEGER).any() or (x < JS_MIN_SAFE_INTEGER).any()) + for _, x in df.items() + ) + js = json.dumps(data, cls=generate_encoder(warn_on_unexpected_types)) except AttributeError: # Polars DataFrame data = list(df.iter_rows()) - return json.dumps(data, cls=generate_encoder(False)) + import polars as pl + + has_bigints = any( + x.dtype in [pl.Int64, pl.UInt64] + and ((x > JS_MAX_SAFE_INTEGER).any() or (x < JS_MIN_SAFE_INTEGER).any()) + for x in (df[col] for col in df.columns) + ) + js = json.dumps(data, cls=generate_encoder(False)) + + if has_bigints: + js = n_suffix_for_bigints(js) + + return js + + +def n_suffix_for_bigints(js): + def n_suffix(matchobj): + return 'BigInt("' + matchobj.group(1) + '")' + matchobj.group(2) + + return re.sub(r"(-?\d{16,})(,|])", n_suffix, js) diff --git a/itables/dt_for_itables/LICENSE b/itables/dt_for_itables/LICENSE new file mode 100644 index 00000000..c9789501 --- /dev/null +++ b/itables/dt_for_itables/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Marc Wouts + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/itables/dt_for_itables/README.md b/itables/dt_for_itables/README.md new file mode 100644 index 00000000..47966eff --- /dev/null +++ b/itables/dt_for_itables/README.md @@ -0,0 +1,30 @@ +This package is a ESM bundle of [DataTables](https://datatables.net/) +and some of its extensions for [ITables](https://github.com/mwouts/itables/). + +# How to compile the bundle + +Run the following commands: +```bash +npm install +npm run build +``` + +# How to update the dependencies + +Run +```bash +npm update +``` +and check whether there are any outdated package with `npm outdated`. + +# How to publish a new version + +Update the dependencies, bump the version in `package.json`, and then: + +```bash +# Package the extension +npm pack + +# Publish the package on npm with +npm publish --access=public +``` diff --git a/itables/dt_for_itables/package-lock.json b/itables/dt_for_itables/package-lock.json new file mode 100644 index 00000000..e19e0def --- /dev/null +++ b/itables/dt_for_itables/package-lock.json @@ -0,0 +1,324 @@ +{ + "name": "dt_for_itables", + "version": "2.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dt_for_itables", + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "datatables.net-buttons": "^3.0.1", + "datatables.net-buttons-dt": "^3.0.1", + "datatables.net-dt": "^2.0.0", + "datatables.net-fixedcolumns-dt": "^5.0.0", + "datatables.net-keytable-dt": "^2.12.0", + "datatables.net-rowgroup-dt": "^1.5.0", + "datatables.net-searchbuilder-dt": "^1.7.0", + "datatables.net-searchpanes-dt": "^2.3.0", + "datatables.net-select-dt": "^2.0.0", + "jquery": "^3.7.1", + "jszip": "^3.10.1" + }, + "devDependencies": { + "esbuild": "*" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/datatables.net": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.0.2.tgz", + "integrity": "sha512-uQM+s1oXhpTajUw5DCxarpfAtQJMh0MKFCLZMCc+UWLWPg8ipe6L2zgDMbRC8n9UCwGVpHOwUYlQu2JU1/PhSg==", + "dependencies": { + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-buttons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/datatables.net-buttons/-/datatables.net-buttons-3.0.1.tgz", + "integrity": "sha512-VjTEooHtjHW2VY5/iddTLMRy2G2TtT2GyYwN9EmaMsG4MClD5lWfYdkpv/NMwz175gx7fe1lh/NVAYT4W3jMSg==", + "dependencies": { + "datatables.net": "^2", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-buttons-dt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/datatables.net-buttons-dt/-/datatables.net-buttons-dt-3.0.1.tgz", + "integrity": "sha512-kfZrc+954qpg6krz1pcVKVhxOzbZByaGHwwO0CcjKdIREBX4la4HkoIZGWsENSYSfdHUkHu9WWa84p9Rl+JtiQ==", + "dependencies": { + "datatables.net-buttons": "3.0.1", + "datatables.net-dt": "^2", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-dt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/datatables.net-dt/-/datatables.net-dt-2.0.2.tgz", + "integrity": "sha512-/Zpy7ZWGgCbCJB9qJZvaB+KFrdo+8JjjOdxL59/QiG3jquz2ZCsE9u814kbaGKTM1ARkvZVUxDB9IlH5qmHaqw==", + "dependencies": { + "datatables.net": "2.0.2", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-fixedcolumns": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/datatables.net-fixedcolumns/-/datatables.net-fixedcolumns-5.0.0.tgz", + "integrity": "sha512-7dTJrVDkZCicx9g//N3ufuEjvwrZpcmpjbkwrtC5LiQFCnuL/hMOiOb4CBcvTg5OiTU7VmtbBuQkeAJGs3QM5g==", + "dependencies": { + "datatables.net": ">=2.0.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-fixedcolumns-dt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/datatables.net-fixedcolumns-dt/-/datatables.net-fixedcolumns-dt-5.0.0.tgz", + "integrity": "sha512-5KVH6EHeYSXrxrEhKq8HMKqf4iRW4T1dtiDYZRomiwfAh/0iCeaamxgT5OFN8VJ/qpiwnyrDh3SqXA8Te0kE7w==", + "dependencies": { + "datatables.net-dt": ">=2.0.0", + "datatables.net-fixedcolumns": "5.0.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-keytable": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/datatables.net-keytable/-/datatables.net-keytable-2.12.0.tgz", + "integrity": "sha512-vijxMw7ZIB/fDe5FWGiDqe8CPiPXg3lvqK4lL48vQh1zoiE3+0C3za82qM9g/2zbwtIXmOLwBZc2ivrErNVPkA==", + "dependencies": { + "datatables.net": ">=1.11.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-keytable-dt": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/datatables.net-keytable-dt/-/datatables.net-keytable-dt-2.12.0.tgz", + "integrity": "sha512-FubrNaitx13Mq2cny2fxuV/Nk6bbmGw6VrH9Df81CT/jVfwAEkwIFirETEoL/hZfQDcD76ujj0b1wNDZ5Bsv9Q==", + "dependencies": { + "datatables.net-dt": ">=1.11.0", + "datatables.net-keytable": "2.12.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-rowgroup": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/datatables.net-rowgroup/-/datatables.net-rowgroup-1.5.0.tgz", + "integrity": "sha512-V/CLJu7rMjxwfhZAv59emZOPw58cwnYmd8NXTTJSnqBDgOZsaC9mtVo0ejBpdqvNw5WmMPy3AJceH+ay6JQ3hA==", + "dependencies": { + "datatables.net": ">=1.11.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-rowgroup-dt": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/datatables.net-rowgroup-dt/-/datatables.net-rowgroup-dt-1.5.0.tgz", + "integrity": "sha512-Dmn+ToLeUXDDg58Skie25xiWDy0GfgAMbiB8CMnOWKXH/gnGRjoU1V80R7ll8HbxGB6YfppXkPsPn00XU7c2xA==", + "dependencies": { + "datatables.net-dt": ">=1.11.0", + "datatables.net-rowgroup": "1.5.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-searchbuilder": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/datatables.net-searchbuilder/-/datatables.net-searchbuilder-1.7.0.tgz", + "integrity": "sha512-JGqxDVudgRxrHs/J+VM9O2rcwPgaUjpqBL1MHJAVJw+4vxvAY5Sbb0qP6ayo8h4yZyAE2+aPRm/smJ24N0nztw==", + "dependencies": { + "datatables.net": ">=1.11.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-searchbuilder-dt": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/datatables.net-searchbuilder-dt/-/datatables.net-searchbuilder-dt-1.7.0.tgz", + "integrity": "sha512-FKeL5xDEO4dF0CA+Vp4KkAdOdhfoCB3KaWdHiscki/9x4r03e0lD8g0V2oynHe4EshLHxFb/ug1Rd5GLcW/yAg==", + "dependencies": { + "datatables.net-dt": ">=1.11.0", + "datatables.net-searchbuilder": "1.7.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-searchpanes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/datatables.net-searchpanes/-/datatables.net-searchpanes-2.3.0.tgz", + "integrity": "sha512-AAl03TQXatzQh6gqNot1BAzenMxQ0/mxX+Nzn770mdTUmhVy2VX8pa7/vZlwe0tRbTcZ9VZMBMErCb66i5X3rA==", + "dependencies": { + "datatables.net": ">=1.11.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-searchpanes-dt": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/datatables.net-searchpanes-dt/-/datatables.net-searchpanes-dt-2.3.0.tgz", + "integrity": "sha512-ee4sEYYPRb+uXtAGL9KmIyjyyh8Ox4EI0a/GKHsCRrO1BTrnh7lZ+z8hJUc/TZJFDkgbYHh8XGPQXLOZUpIXHA==", + "dependencies": { + "datatables.net-dt": ">=1.11.0", + "datatables.net-searchpanes": "2.3.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-select": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/datatables.net-select/-/datatables.net-select-2.0.0.tgz", + "integrity": "sha512-sKMNoTlJejk5FfZo6Niwdn2/bHSDYiIt5WuMSsXzMGiCTIPtnDiYjNHF843vToKiTTsi+6T0zUuWddHLGPRsxA==", + "dependencies": { + "datatables.net": ">=2.0.0", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-select-dt": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/datatables.net-select-dt/-/datatables.net-select-dt-2.0.0.tgz", + "integrity": "sha512-w6uP7Q4/U675dkrC2Lg0B3P7cwUTxfyUgcc+Qt4XUyAqa+6rk/YjiYHfsrDZ9vUZdu66DFdj7hNZtZfkBBPjLg==", + "dependencies": { + "datatables.net-dt": ">=2.0.0", + "datatables.net-select": "2.0.0", + "jquery": ">=1.7" + } + }, + "node_modules/esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + } + } +} diff --git a/itables/dt_for_itables/package.json b/itables/dt_for_itables/package.json new file mode 100644 index 00000000..fdedb3ee --- /dev/null +++ b/itables/dt_for_itables/package.json @@ -0,0 +1,32 @@ +{ + "name": "dt_for_itables", + "version": "2.0.1", + "description": "DataTables bundle for itables", + "main": "src.js", + "scripts": { + "build": "esbuild src.js --format=esm --bundle --outfile=dt_bundle.js --minify" + }, + "author": "Marc Wouts", + "license": "MIT", + "dependencies": { + "datatables.net-buttons": "^3.0.1", + "datatables.net-buttons-dt": "^3.0.1", + "datatables.net-dt": "^2.0.0", + "datatables.net-fixedcolumns-dt": "^5.0.0", + "datatables.net-keytable-dt": "^2.12.0", + "datatables.net-rowgroup-dt": "^1.5.0", + "datatables.net-searchbuilder-dt": "^1.7.0", + "datatables.net-searchpanes-dt": "^2.3.0", + "datatables.net-select-dt": "^2.0.0", + "jquery": "^3.7.1", + "jszip": "^3.10.1" + }, + "devDependencies": { + "esbuild": "*" + }, + "homepage": "https://mwouts.github.io/itables", + "repository": { + "type": "git", + "url": "https://github.com/mwouts/itables.git" + } +} diff --git a/itables/dt_for_itables/src.js b/itables/dt_for_itables/src.js new file mode 100644 index 00000000..3b2a178f --- /dev/null +++ b/itables/dt_for_itables/src.js @@ -0,0 +1,31 @@ +import JSZip from 'jszip'; +import jQuery from 'jquery'; +import DataTable from 'datatables.net-dt'; +import 'datatables.net-dt/css/dataTables.dataTables.min.css'; + +import 'datatables.net-buttons-dt'; +import 'datatables.net-buttons/js/buttons.html5.mjs'; +import 'datatables.net-buttons/js/buttons.print.mjs'; +import 'datatables.net-buttons-dt/css/buttons.dataTables.min.css'; + +DataTable.Buttons.jszip(JSZip); + +import 'datatables.net-fixedcolumns-dt'; +import 'datatables.net-fixedcolumns-dt/css/fixedColumns.dataTables.min.css'; + +import 'datatables.net-keytable-dt'; +import 'datatables.net-keytable-dt/css/keyTable.dataTables.min.css'; + +import 'datatables.net-rowgroup-dt'; +import 'datatables.net-rowgroup-dt/css/rowGroup.dataTables.min.css'; + +import 'datatables.net-searchbuilder-dt'; +import 'datatables.net-searchbuilder-dt/css/searchBuilder.dataTables.min.css'; + +import 'datatables.net-searchpanes-dt'; +import 'datatables.net-searchpanes-dt/css/searchPanes.dataTables.min.css'; + +import 'datatables.net-select-dt'; +import 'datatables.net-select-dt/css/select.dataTables.min.css'; + +export { DataTable, jQuery }; diff --git a/itables/html/datatables_template.html b/itables/html/datatables_template.html index dced0b7a..4b5f9f72 100644 --- a/itables/html/datatables_template.html +++ b/itables/html/datatables_template.html @@ -1,22 +1,17 @@ -
-
A
+ -
diff --git a/itables/html/datatables_template_connected.html b/itables/html/datatables_template_connected.html deleted file mode 100644 index 3253542c..00000000 --- a/itables/html/datatables_template_connected.html +++ /dev/null @@ -1,25 +0,0 @@ -
- -
A
- - -
diff --git a/itables/html/initialize_offline_datatable.html b/itables/html/initialize_offline_datatable.html deleted file mode 100644 index a018b454..00000000 --- a/itables/html/initialize_offline_datatable.html +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/itables/html/itables.css b/itables/html/itables.css deleted file mode 100644 index 5ec8814f..00000000 --- a/itables/html/itables.css +++ /dev/null @@ -1,27 +0,0 @@ -.itables table td { - text-overflow: ellipsis; - overflow: hidden; -} - -.itables table th { - text-overflow: ellipsis; - overflow: hidden; -} - -.itables thead input { - width: 100%; - padding: 3px; - box-sizing: border-box; -} - -.itables tfoot input { - width: 100%; - padding: 3px; - box-sizing: border-box; -} - -.itables { font-size: medium; } - -.itables select, .itables input { - color: inherit; -} diff --git a/itables/javascript.py b/itables/javascript.py index af055cee..6c742f6b 100644 --- a/itables/javascript.py +++ b/itables/javascript.py @@ -6,10 +6,12 @@ import uuid import warnings from base64 import b64encode +from pathlib import Path import numpy as np import pandas as pd +from .utils import UNPKG_DT_BUNDLE_CSS, UNPKG_DT_BUNDLE_URL from .version import __version__ as itables_version try: @@ -44,8 +46,8 @@ "maxBytes", "maxRows", "maxColumns", + "warn_on_dom", "warn_on_unexpected_types", - "warn_on_int_to_str_conversion", } _ORIGINAL_DATAFRAME_REPR_HTML = pd.DataFrame._repr_html_ _ORIGINAL_DATAFRAME_STYLE_REPR_HTML = ( @@ -53,6 +55,14 @@ ) _ORIGINAL_POLARS_DATAFRAME_REPR_HTML = pl.DataFrame._repr_html_ _CONNECTED = True +DEFAULT_LAYOUT = { + "topStart": "pageLength", + "topEnd": "search", + "bottomStart": "info", + "bottomEnd": "paging", +} +DEFAULT_LAYOUT_CONTROLS = set(DEFAULT_LAYOUT.values()) + try: import google.colab @@ -68,14 +78,19 @@ def init_notebook_mode( - all_interactive=False, connected=GOOGLE_COLAB, warn_if_call_is_superfluous=True + all_interactive=False, + connected=GOOGLE_COLAB, + warn_if_call_is_superfluous=True, + dt_bundle=None, ): - """Load the datatables.net library and the corresponding css (if connected=False), - and (if all_interactive=True), activate the datatables representation for all the Pandas DataFrames and Series. + """Load the DataTables library and the corresponding css (if connected=False), + and (if all_interactive=True), activate the DataTables representation for all the Pandas DataFrames and Series. Warning: make sure you keep the output of this cell when 'connected=False', otherwise the interactive tables will stop working. """ + if dt_bundle is None: + dt_bundle = opt.dt_bundle global _CONNECTED if GOOGLE_COLAB and not connected: warnings.warn( @@ -117,25 +132,18 @@ def init_notebook_mode( del pl.Series._repr_html_ if not connected: - display(HTML(generate_init_offline_itables_html())) + display(HTML(generate_init_offline_itables_html(dt_bundle))) -def generate_init_offline_itables_html(): - dt_css = read_package_file("external/jquery.dataTables.min.css") - jquery_src = read_package_file("external/jquery.min.js") - dt64 = b64encode( - read_package_file("external/jquery.dataTables.mjs").encode("utf-8") - ).decode("ascii") +def generate_init_offline_itables_html(dt_bundle: Path): + assert dt_bundle.suffix == ".js" + dt_src = dt_bundle.read_text() + dt_css = dt_bundle.with_suffix(".css").read_text() + dt64 = b64encode(dt_src.encode("utf-8")).decode("ascii") - html = replace_value( - read_package_file("html/initialize_offline_datatable.html"), - "_datatables_src_for_itables", - DATATABLES_SRC_FOR_ITABLES, - ) - html = replace_value(html, "dt_css", dt_css) - html = replace_value(html, "jquery_src", jquery_src) - html = replace_value(html, "dt_src", "data:text/javascript;base64,{}".format(dt64)) - return html + return f""" + +""" def _table_header( @@ -225,7 +233,7 @@ def json_dumps(obj, eval_functions): return obj if eval_functions is None and obj.lstrip().startswith("function"): warnings.warn( - "One of the arguments passed to datatables starts with 'function'. " + "One of the arguments passed to DataTable starts with 'function'. " "To evaluate this function, change it into a 'JavascriptFunction' object " "or use the option 'eval_functions=True'. " "To silence this warning, use 'eval_functions=False'." @@ -283,18 +291,21 @@ def to_html_datatable( caption=None, tableId=None, connected=True, - import_jquery=True, use_to_html=False, **kwargs, ): check_table_id(tableId) + if "import_jquery" in kwargs: + raise TypeError( + "The argument 'import_jquery' was removed in ITables v2.0. " + "Please pass a custom 'dt_url' instead." + ) if use_to_html or (pd_style is not None and isinstance(df, pd_style.Styler)): return to_html_datatable_using_to_html( df=df, caption=caption, tableId=tableId, connected=connected, - import_jquery=import_jquery, **kwargs, ) @@ -332,7 +343,6 @@ def to_html_datatable( maxRows = kwargs.pop("maxRows", 0) maxColumns = kwargs.pop("maxColumns", pd.get_option("display.max_columns") or 0) warn_on_unexpected_types = kwargs.pop("warn_on_unexpected_types", False) - warn_on_int_to_str_conversion = kwargs.pop("warn_on_int_to_str_conversion", False) df, downsampling_warning = downsample( df, max_rows=maxRows, max_columns=maxColumns, max_bytes=maxBytes @@ -345,8 +355,38 @@ def to_html_datatable( ) ) - if "dom" not in kwargs and _df_fits_in_one_page(df, kwargs): - kwargs["dom"] = "ti" if downsampling_warning else "t" + has_default_layout = kwargs["layout"] == DEFAULT_LAYOUT + + if "dom" in kwargs: + if opt.warn_on_dom: + warnings.warn( + "The 'dom' argument has been deprecated in DataTables==2.0.", + DeprecationWarning, + ) + if not has_default_layout: + raise ValueError("You can pass both 'dom' and 'layout'") + del kwargs["layout"] + has_default_layout = False + + if has_default_layout and _df_fits_in_one_page(df, kwargs): + + def filter_control(control): + if control == "info" and downsampling_warning: + return control + if control not in DEFAULT_LAYOUT_CONTROLS: + return control + return None + + kwargs["layout"] = { + key: filter_control(control) for key, control in kwargs["layout"].items() + } + + if ( + "buttons" in kwargs + and "layout" in kwargs + and "buttons" not in kwargs["layout"].values() + ): + kwargs["layout"] = {**kwargs["layout"], "topStart": "buttons"} footer = kwargs.pop("footer") column_filters = kwargs.pop("column_filters") @@ -386,7 +426,6 @@ def to_html_datatable( df, column_count, warn_on_unexpected_types=warn_on_unexpected_types, - warn_on_int_to_str_conversion=warn_on_int_to_str_conversion, ) return html_table_from_template( @@ -395,7 +434,6 @@ def to_html_datatable( data=dt_data, kwargs=kwargs, connected=connected, - import_jquery=import_jquery, column_filters=column_filters, ) @@ -424,13 +462,17 @@ def set_default_options(kwargs, use_to_html): set(kwargs).intersection(options_not_available) ) ) + + # layout is updated using the arguments passed on to show + kwargs["layout"] = {**getattr(opt, "layout"), **kwargs.get("layout", {})} + # Default options for option in dir(opt): if ( (not use_to_html or (option not in _OPTIONS_NOT_AVAILABLE_WITH_TO_HTML)) and option not in kwargs and not option.startswith("__") - and option not in ["read_package_file"] + and option not in {"dt_bundle", "find_package_file", "UNPKG_DT_BUNDLE_URL"} ): kwargs[option] = getattr(opt, option) @@ -444,7 +486,7 @@ def set_default_options(kwargs, use_to_html): def to_html_datatable_using_to_html( - df=None, caption=None, tableId=None, connected=True, import_jquery=True, **kwargs + df=None, caption=None, tableId=None, connected=True, **kwargs ): """Return the HTML representation of the given dataframe as an interactive datatable, using df.to_html() rather than the underlying dataframe data.""" @@ -523,31 +565,44 @@ def to_html_datatable_using_to_html( data=None, kwargs=kwargs, connected=connected, - import_jquery=import_jquery, column_filters=None, ) def html_table_from_template( - html_table, table_id, data, kwargs, connected, import_jquery, column_filters + html_table, table_id, data, kwargs, connected, column_filters ): - css = kwargs.pop("css") + if "css" in kwargs: + TypeError( + "The 'css' argument has been deprecated, see the new " + "approach at https://mwouts.github.io/itables/custom_css.html." + ) eval_functions = kwargs.pop("eval_functions", None) pre_dt_code = kwargs.pop("pre_dt_code") + dt_url = kwargs.pop("dt_url") # Load the HTML template + output = read_package_file("html/datatables_template.html") if connected: - output = read_package_file("html/datatables_template_connected.html") - else: - output = read_package_file("html/datatables_template.html") - - if not import_jquery: - assert ( - connected - ), "In the offline mode, jQuery is imported through init_notebook_mode" + assert dt_url.endswith(".js") + output = replace_value(output, UNPKG_DT_BUNDLE_URL, dt_url) output = replace_value( - output, " import 'https://code.jquery.com/jquery-3.6.0.min.js';\n", "" + output, + UNPKG_DT_BUNDLE_CSS, + dt_url[:-3] + ".css", + ) + else: + connected_style = f'\n' + output = replace_value(output, connected_style, "") + connected_import = ( + "import {DataTable, jQuery as $} from '" + UNPKG_DT_BUNDLE_URL + "';" ) + local_import = ( + "const { DataTable, jQuery: $ } = await import(window." + + DATATABLES_SRC_FOR_ITABLES + + ");" + ) + output = replace_value(output, connected_import, local_import) output = replace_value( output, @@ -555,15 +610,6 @@ def html_table_from_template( html_table, ) output = replace_value(output, "#table_id", "#{}".format(table_id)) - output = replace_value( - output, - "", - "".format(css), - ) - if not connected: - output = replace_value( - output, "_datatables_src_for_itables", DATATABLES_SRC_FOR_ITABLES - ) if column_filters: # If the below was false, we would need to concatenate the JS code @@ -662,6 +708,6 @@ def safe_reset_index(df): def show(df=None, caption=None, **kwargs): """Show a dataframe""" - connected = kwargs.pop("connected", _CONNECTED) + connected = kwargs.pop("connected", ("dt_url" in kwargs) or _CONNECTED) html = to_html_datatable(df, caption=caption, connected=connected, **kwargs) display(HTML(html)) diff --git a/itables/options.py b/itables/options.py index 297703b6..fb728b89 100644 --- a/itables/options.py +++ b/itables/options.py @@ -1,15 +1,25 @@ -"""Global options for the Interactive Tables. +"""Global options for ITables. These parameters are documented at https://mwouts.github.io/itables/advanced_parameters.html """ -from .utils import read_package_file + +from .utils import UNPKG_DT_BUNDLE_URL, find_package_file + +"""Table layout, see https://datatables.net/reference/option/layout +NB: to remove a control, replace it by None""" +layout = { + "topStart": "pageLength", + "topEnd": "search", + "bottomStart": "info", + "bottomEnd": "paging", +} """Show the index? Possible values: True, False and 'auto'. In mode 'auto', the index is not shown if it has no name and its content is range(N)""" showIndex = "auto" -"""Default datatables classes. See https://datatables.net/manual/styling/classes""" +"""Default DataTables classes. See https://datatables.net/manual/styling/classes""" classes = "display nowrap" """Default table style. Use @@ -41,11 +51,14 @@ """Column filters""" column_filters = False -"""Table CSS""" -css = read_package_file("html/itables.css") - """Should a warning appear when we have to encode an unexpected type?""" warn_on_unexpected_types = True -"""Should a warning appear when we convert large integers to str?""" -warn_on_int_to_str_conversion = True +"""Should a warning appear when the deprecated 'dom' is used?""" +warn_on_dom = True + +"""The DataTables URL for the connected mode""" +dt_url = UNPKG_DT_BUNDLE_URL + +"""The DataTable bundle for the offline mode""" +dt_bundle = find_package_file("dt_for_itables/dt_bundle.js") diff --git a/itables/shiny.py b/itables/shiny.py index 37f55ebd..9e33f45f 100644 --- a/itables/shiny.py +++ b/itables/shiny.py @@ -2,19 +2,7 @@ def DT(df, caption=None, tableId=None, **kwargs): - """This is a version of 'to_html_datatable' that works in Shiny applications. - - In these applications, jquery is already loaded, so we call 'to_html_datatable' - with an argument 'import_jquery=False'. - - Cf. https://github.com/mwouts/itables/issues/181 - and https://github.com/rstudio/py-shiny/issues/502 - """ + """This is a version of 'to_html_datatable' that works in Shiny applications.""" return to_html_datatable( - df, - caption=caption, - tableId=tableId, - connected=True, - import_jquery=False, - **kwargs + df, caption=caption, tableId=tableId, connected=True, **kwargs ) diff --git a/itables/utils.py b/itables/utils.py index 33902930..7c4289d5 100644 --- a/itables/utils.py +++ b/itables/utils.py @@ -1,11 +1,14 @@ -import os from io import open +from pathlib import Path + +UNPKG_DT_BUNDLE_URL = "https://www.unpkg.com/dt_for_itables@2.0.1/dt_bundle.js" +UNPKG_DT_BUNDLE_CSS = UNPKG_DT_BUNDLE_URL.replace(".js", ".css") def find_package_file(*path): """Return the full path to a file from the itables package""" - current_path = os.path.dirname(__file__) - return os.path.join(current_path, *path) + current_path = Path(__file__).parent + return Path(current_path, *path) def read_package_file(*path): diff --git a/itables/version.py b/itables/version.py index 07eefa1d..77a9451a 100644 --- a/itables/version.py +++ b/itables/version.py @@ -1,3 +1,3 @@ """ITables' version number""" -__version__ = "1.7.1" +__version__ = "2.0.0" diff --git a/pyproject.toml b/pyproject.toml index 80b12c63..75be651d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,88 @@ [build-system] -build-backend = "setuptools.build_meta" -requires = ["setuptools", "requests", "pathlib"] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "itables" +authors = [{name = "Marc Wouts", email = "marc.wouts@gmail.com"}] +maintainers = [{name = "Marc Wouts", email = "marc.wouts@gmail.com"}] +description = "Pandas and Polar DataFrames as interactive DataTables" +readme = "README.md" +keywords = ["Pandas", "Polars", "Interactive", "Javascript", "DataTables"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Framework :: Jupyter", + "Intended Audience :: Developers", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] +requires-python = ">= 3.7" +dependencies = ["IPython", "pandas", "numpy"] +dynamic = ["version"] + +[tool.hatch.version] +path = "itables/version.py" + +[project.optional-dependencies] +test = [ + # Pytest + "pytest", + "pytest-cov", + # Sample dfs + "pytz", + "world_bank_data", + # Test the documentation + "ipykernel", + "nbconvert", + "jupytext", + # Pandas style + "matplotlib", + # Shiny test app + "shiny" +] +polars = ["polars", "pyarrow"] + +[project.urls] +Homepage = "https://mwouts.github.io/itables/" +Documentation = "https://mwouts.github.io/itables" +Repository = "https://github.com/mwouts/itables.git" +Issues = "https://github.com/mwouts/itables/issues" +Changelog = "https://github.com/mwouts/itables/blob/main/docs/changelog.md" + +[pycodestyle] +max-line-length = 88 + +[tool.coverage.report] +exclude_lines = [ + # Have to re-enable the standard pragma + "pragma: no cover", + + # Don't complain if tests don't hit defensive assertion code: + "raise NotImplementedError", + "except ImportError", + ] + +[tool.hatch.build.hooks.jupyter-builder] +enable-by-default = true +dependencies = ["hatch-jupyter-builder"] +build-function = "hatch_jupyter_builder.npm_builder" +ensured-targets = ["itables/dt_for_itables/dt_bundle.js", "itables/dt_for_itables/dt_bundle.css"] + +[tool.hatch.build.hooks.jupyter-builder.build-kwargs] +path = "itables/dt_for_itables" +build_cmd = "build" +npm = ["npm"] + +[tool.hatch.build.targets.sdist] +artifacts = ["itables/dt_for_itables/dt_bundle.js", "itables/dt_for_itables/dt_bundle.css"] + +[tool.hatch.build.targets.wheel] +artifacts = ["itables/dt_for_itables/dt_bundle.js", "itables/dt_for_itables/dt_bundle.css"] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index e8d6f97b..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,13 +0,0 @@ -pathlib -pre-commit -isort -flake8 -matplotlib -pytest -pytest-cov -jupytext -nbconvert -jupyter_client -ipykernel -world_bank_data -shiny diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 812d0ec0..00000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -IPython -pandas diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index b588e879..00000000 --- a/setup.cfg +++ /dev/null @@ -1,19 +0,0 @@ -[aliases] -test=pytest - -[coverage:run] -omit = - setup.py - -[coverage:report] -exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - - # Don't complain if tests don't hit defensive assertion code: - raise NotImplementedError - except ImportError - -[flake8] -max-line-length=127 -extend-ignore = E203 diff --git a/setup.py b/setup.py deleted file mode 100644 index 7d54d90e..00000000 --- a/setup.py +++ /dev/null @@ -1,73 +0,0 @@ -import re -from io import open -from pathlib import Path - -import requests -from setuptools import find_packages, setup - -this_directory = Path(__file__).parent -with open(str(this_directory / "README.md"), encoding="utf-8") as f: - long_description = f.read() - -with open(str(this_directory / "itables/version.py")) as f: - version_file = f.read() - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", version_file, re.M) - version = version_match.group(1) - -external = Path(__file__).parent / "itables" / "external" -if not external.is_dir(): - external.mkdir() -for name, url in [ - ("jquery.min.js", "https://code.jquery.com/jquery-3.6.0.min.js"), - ( - "jquery.dataTables.min.css", - "https://cdn.datatables.net/1.13.1/css/jquery.dataTables.min.css", - ), - ( - "jquery.dataTables.mjs", - "https://cdn.datatables.net/1.12.1/js/jquery.dataTables.mjs", - ), -]: - r = requests.get(url) - with open(str(external / name), "wb") as fp: - fp.write(r.content) - -setup( - name="itables", - version=version, - author="Marc Wouts", - author_email="marc.wouts@gmail.com", - description="Interactive Tables in Jupyter", - long_description=long_description, - long_description_content_type="text/markdown", - url="https://github.com/mwouts/itables", - packages=find_packages(exclude=["tests"]), - package_data={ - "itables": [ - "html/*", - "html/column_filters/*", - "samples/*.csv", - "external/*", - ] - }, - tests_require=["pytest", "pytz"], - install_requires=["IPython", "pandas", "numpy"], - extras_require={"polars": ["polars", "pyarrow"]}, - license="MIT", - classifiers=[ - "Development Status :: 5 - Production/Stable", - "License :: OSI Approved :: MIT License", - "Framework :: Jupyter", - "Intended Audience :: Developers", - "Intended Audience :: Science/Research", - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - ], -) diff --git a/tests/test_connected_notebook_is_small.py b/tests/test_connected_notebook_is_small.py index 9036c4ac..8b4836f1 100644 --- a/tests/test_connected_notebook_is_small.py +++ b/tests/test_connected_notebook_is_small.py @@ -30,4 +30,4 @@ def test_offline_notebook_is_not_too_large(tmp_path): nb_py.write_text(text_notebook(connected=False)) jupytext([str(nb_py), "--to", "ipynb", "--set-kernel", "itables", "--execute"]) assert nb_ipynb.exists() - assert 700000 < nb_ipynb.stat().st_size < 750000 + assert 700000 < nb_ipynb.stat().st_size < 800000 diff --git a/tests/test_datatables_format.py b/tests/test_datatables_format.py index 603377f8..91b91e97 100644 --- a/tests/test_datatables_format.py +++ b/tests/test_datatables_format.py @@ -7,7 +7,13 @@ import pandas as pd import pytest -from itables.datatables_format import datatables_rows, generate_encoder +from itables.datatables_format import ( + JS_MAX_SAFE_INTEGER, + JS_MIN_SAFE_INTEGER, + datatables_rows, + generate_encoder, + n_suffix_for_bigints, +) from itables.javascript import _column_count_in_header, _table_header from itables.sample_dfs import PANDAS_VERSION_MAJOR @@ -67,14 +73,14 @@ ( pd.DataFrame( { - "long": [ + "big_integers": [ 1234567890123456789, 2345678901234567890, 3456789012345678901, ] } ), - '[["1234567890123456789"], ["2345678901234567890"], ["3456789012345678901"]]', + '[[BigInt("1234567890123456789")], [BigInt("2345678901234567890")], [BigInt("3456789012345678901")]]', ), ], ids=[ @@ -125,3 +131,24 @@ def test_TableValuesEncoder(): json.dumps(Exception, cls=generate_encoder(False)) == "\"\"" ) + + +def test_encode_large_int_to_bigint(large=3456789012345678901): + assert ( + n_suffix_for_bigints(json.dumps([large])) == '[BigInt("3456789012345678901")]' + ) + assert ( + n_suffix_for_bigints(json.dumps([large * 100, large])) + == '[BigInt("345678901234567890100"), BigInt("3456789012345678901")]' + ) + + +@pytest.mark.parametrize("large", [JS_MIN_SAFE_INTEGER, JS_MAX_SAFE_INTEGER]) +def test_encode_max_int(large): + assert n_suffix_for_bigints(json.dumps([large])) == '[BigInt("{}")]'.format(large) + + +@pytest.mark.parametrize("large", [JS_MIN_SAFE_INTEGER, JS_MAX_SAFE_INTEGER]) +def test_encode_not_max_int(large): + large //= 10 + assert n_suffix_for_bigints(json.dumps([large])) == "[{}]".format(large) diff --git a/tests/test_javascript.py b/tests/test_javascript.py index 5dd2a760..ac738ad2 100644 --- a/tests/test_javascript.py +++ b/tests/test_javascript.py @@ -34,11 +34,6 @@ def test_warn_on_unexpected_types_not_in_html(df): assert "warn_on_unexpected_types" not in html -def test_warn_on_int_to_str_conversion_not_in_html(df): - html = to_html_datatable(df) - assert "warn_on_int_to_str_conversion" not in html - - def test_df_fits_in_one_page(df, lengthMenu): kwargs = dict(lengthMenu=lengthMenu) kwargs = {key: value for key, value in kwargs.items() if value is not None} diff --git a/tests/test_sample_dfs.py b/tests/test_sample_dfs.py index c1c0e150..eec1b337 100644 --- a/tests/test_sample_dfs.py +++ b/tests/test_sample_dfs.py @@ -1,8 +1,8 @@ import json -import sys import pandas as pd import pytest +from packaging import version from itables import show, to_html_datatable from itables.datatables_format import _format_column, generate_encoder @@ -55,8 +55,8 @@ def test_get_indicators(connected, use_to_html): @pytest.mark.skipif( - sys.version_info < (3, 7), - reason="AttributeError: 'Styler' object has no attribute 'to_html'", + version.parse(pd.__version__) < version.parse("1.0"), + reason="TypeError: Cannot interpret '' as a data type", ) @pytest.mark.skipif( pd_style is None, @@ -72,8 +72,6 @@ def kwargs_remove_none(**kwargs): def test_show_test_dfs(df, connected, use_to_html, lengthMenu, monkeypatch): - if "bigint" in df.columns: - monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False) show( df, connected=connected, @@ -83,8 +81,6 @@ def test_show_test_dfs(df, connected, use_to_html, lengthMenu, monkeypatch): def test_to_html_datatable(df, connected, use_to_html, lengthMenu, monkeypatch): - if "bigint" in df.columns: - monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False) html = to_html_datatable( df, connected=connected, @@ -111,8 +107,6 @@ def test_format_column(series_name, series): @pytest.mark.parametrize("series_name,series", get_dict_of_test_series().items()) def test_show_test_series(series_name, series, connected, use_to_html, monkeypatch): - if "bigint" in series_name: - monkeypatch.setattr("itables.options.warn_on_int_to_str_conversion", False) show(series, connected=connected, use_to_html=use_to_html)