├── .github ├── ISSUE_TEMPLATE.md ├── release-drafter-config.yml └── workflows │ ├── main.yml │ ├── master-merge.yml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── build.rb ├── conf.py ├── environment.yml ├── images │ ├── events_api.gif │ ├── filtering_demo.gif │ └── linked_to_scatter.gif └── index.rst ├── js ├── package.json ├── src │ ├── embed.js │ ├── extension.js │ ├── index.js │ ├── jupyterlab-plugin.js │ ├── qgrid.booleanfilter.js │ ├── qgrid.css │ ├── qgrid.datefilter.js │ ├── qgrid.editors.js │ ├── qgrid.filterbase.js │ ├── qgrid.sliderfilter.js │ ├── qgrid.textfilter.js │ └── qgrid.widget.js └── webpack.config.js ├── qgrid ├── __init__.py ├── _version.py ├── grid.py ├── pd_json │ ├── __init__.py │ ├── json.py │ ├── normalize.py │ └── table_schema.py └── tests │ ├── __init__.py │ └── test_grid.py ├── readthedocs.yml ├── requirements.txt ├── setup.cfg └── setup.py /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Environment 2 | 3 | * Operating System: 4 | * Python Version: `$ python --version` 5 | * How did you install Qgrid: (`pip`, `conda`, or `other (please explain)`) 6 | * Python packages: `$ pip freeze` or `$ conda list` (please include qgrid, notebook, and jupyterlab versions) 7 | * Jupyter lab packages (if applicable): `$ jupyter labextension list` 8 | 9 | ### Description of Issue 10 | 11 | * What did you expect to happen? 12 | * What happened instead? 13 | 14 | ### Reproduction Steps 15 | 16 | 1. 17 | 2. 18 | 3. 19 | ... 20 | 21 | ### What steps have you taken to resolve this already? 22 | 23 | ... 24 | 25 | ### Anything else? 26 | 27 | ... 28 | -------------------------------------------------------------------------------- /.github/release-drafter-config.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | template: | 4 | ## What’s Changed 5 | 6 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest] 19 | python-version: [2.7, 3.5, 3.7, 3.8] 20 | include: 21 | - python-version: 2.7 22 | pandas: 0.18.1 23 | numpy: 1.11.3 24 | - python-version: 3.5 25 | pandas: 0.18.1 26 | numpy: 1.11.3 27 | - python-version: 3.7 28 | pandas: 1.0.1 29 | numpy: 1.18.1 30 | - python-version: 3.8 31 | pandas: 1.0.1 32 | numpy: 1.18.1 33 | 34 | steps: 35 | - uses: actions/checkout@v1 36 | - name: Set up Python ${{ matrix.python-version }} 37 | uses: actions/setup-python@v1 38 | with: 39 | python-version: ${{ matrix.python-version }} 40 | - uses: actions/cache@v1 41 | with: 42 | path: ~/.cache/pip 43 | key: ${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.pandas }}-${{ matrix.numpy }}-pip-${{ hashFiles('**/setup.py') }} 44 | - name: Install dependencies 45 | env: 46 | PYTHONWARNINGS: ignore:DEPRECATION::pip._internal.cli.base_command 47 | run: | 48 | python -m pip install --upgrade pip 49 | pip install pandas==${{ matrix.pandas }} numpy==${{ matrix.numpy }} 50 | pip install -e .[test] 51 | - name: Lint with flake8 52 | run: | 53 | flake8 54 | - name: Run the tests 55 | run: | 56 | pytest -------------------------------------------------------------------------------- /.github/workflows/master-merge.yml: -------------------------------------------------------------------------------- 1 | name: On Master Merge 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | draft-release-publish: 10 | name: Draft a new release 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | with: 16 | config-name: release-drafter-config.yml 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python 🐍 distributions 📦 to PyPI 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build-n-publish: 8 | name: Build and publish Python 🐍 distributions 📦 to TestPyPI 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - uses: actions/checkout@master 12 | - name: Set up Python 3.7 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: 3.7 16 | 17 | - name: Build sdist 18 | run: python setup.py sdist 19 | 20 | - name: Publish distribution 📦 to Test PyPI 21 | uses: pypa/gh-action-pypi-publish@master 22 | with: 23 | password: ${{ secrets.test_pypi_password }} 24 | repository_url: https://test.pypi.org/legacy/ 25 | 26 | - name: Install from test and test running 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install --extra-index-url https://test.pypi.org/simple qgrid 30 | python -c 'import qgrid;print(qgrid.__version__)' 31 | pip uninstall -y qgrid 32 | 33 | - name: Publish distribution 📦 to PyPI 34 | uses: pypa/gh-action-pypi-publish@master 35 | with: 36 | password: ${{ secrets.pypi_password }} 37 | 38 | - name: Install and test running 39 | run: | 40 | pip install qgrid 41 | python -c 'import qgrid;print(qgrid.__version__)' 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | *.pyc 3 | # IntelliJ IDEA (IDE-related files) 4 | *.iml 5 | .idea/* 6 | .ipynb_checkpoints/* 7 | 8 | # Setuptools. 9 | dist 10 | *.egg-info 11 | 12 | # pytest 13 | .cache/* 14 | .pytest_cache/* 15 | 16 | # Jupyter notebook 17 | jupyterhub.sqlite 18 | jupyterhub_cookie_secret 19 | index.ipynb 20 | 21 | docs/_build/* 22 | 23 | build/ 24 | *.py[cod] 25 | node_modules/ 26 | package-lock.json 27 | js/package-lock.json 28 | jslab/package-lock.json 29 | 30 | # Compiled javascript 31 | qgrid/static/ 32 | 33 | # OS X 34 | .DS_Store 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing to Qgrid 2 | ======================= 3 | For developers of qgrid, people who want to contribute to the qgrid codebase or documentation, or people who want 4 | to install from source and make local changes to their copy of qgrid, please refer to the 5 | `Running from source & testing your changes`__ section of the `README`__ page for this repository. 6 | 7 | All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. 8 | We `track issues`__ on `GitHub`__ and that's also the best place to ask any questions you may have. 9 | 10 | __ https://github.com/quantopian/qgrid#running-from-source-testing-your-changes 11 | __ https://github.com/quantopian/qgrid 12 | __ https://github.com/quantopian/qgrid/issues 13 | __ https://github.com/ 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2018 Quantopian, Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include requirements*.txt 4 | 5 | recursive-include qgrid/static *.* 6 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://media.quantopian.com/logos/open_source/qgrid-logo-03.png 2 | :target: https://qgrid.readthedocs.io 3 | :width: 190px 4 | :align: center 5 | :alt: qgrid 6 | 7 | ===== 8 | qgrid 9 | ===== 10 | Qgrid is a Jupyter notebook widget which uses `SlickGrid `_ to render pandas 11 | DataFrames within a Jupyter notebook. This allows you to explore your DataFrames with intuitive scrolling, sorting, and 12 | filtering controls, as well as edit your DataFrames by double clicking cells. 13 | 14 | Qgrid was developed for use in `Quantopian's hosted research environment 15 | `_ 16 | and is available for use in that environment as of June 2018. 17 | Quantopian also offers a `fully managed service for professionals `_ 18 | that includes Qgrid, Zipline, Alphalens, Pyfolio, FactSet data, and more. 19 | 20 | Announcements: Qgrid Webinar 21 | ---------------------------- 22 | Qgrid author Tim Shawver recently did a live webinar about Qgrid, and the recording of the webinar is `now available on YouTube `_. 23 | 24 | This talk will be interesting both for people that are new to Qgrid, as well as longtime fans that are interested in learning more about the project. 25 | 26 | Demo 27 | ---- 28 | Click the badge below to try out the latest beta of qgrid in Quantopian's hosted research environment. If you're already signed into Quantopian you'll be brought directly to the demo notebook. Otherwise you'll be prompted to register (it's free): 29 | 30 | .. image:: https://img.shields.io/badge/launch-quantopian-red.svg?colorB=d33015 31 | :target: https://www.quantopian.com/clone_notebook?id=5b2baee1b3d6870048620188&utm_source=github&utm_medium=web&utm_campaign=qgrid-repo 32 | | 33 | Click the badge below to try out qgrid using binder: 34 | 35 | .. image:: https://beta.mybinder.org/badge.svg 36 | :target: https://mybinder.org/v2/gh/quantopian/qgrid-notebooks/master?filepath=index.ipynb 37 | | 38 | Click the following badge to try out qgrid in Jupyterlab, also using binder: 39 | 40 | .. image:: https://mybinder.org/badge.svg 41 | :target: https://mybinder.org/v2/gh/quantopian/qgrid-notebooks/master?urlpath=lab 42 | | 43 | *For both binder links, you'll see a brief loading screen while a server is being created for you in the cloud. This shouldn't take more than a minute, and usually completes in under 10 seconds.* 44 | 45 | *The binder demos generally will be using the most recent stable release of qgrid, so features that were added in a recent beta version may not be available in those demos.* 46 | 47 | For people who would rather not go to another page to try out qgrid for real, here's the tldr; version: 48 | 49 | .. figure:: docs/images/filtering_demo.gif 50 | :align: left 51 | :target: docs/images/filtering_demo.gif 52 | :width: 200px 53 | 54 | A brief demo showing filtering, editing, and the `get_changed_df()` method 55 | 56 | API Documentation 57 | ----------------- 58 | API documentation is hosted on `readthedocs `_. 59 | 60 | Installation 61 | ------------ 62 | 63 | Installing with pip:: 64 | 65 | pip install qgrid 66 | jupyter nbextension enable --py --sys-prefix qgrid 67 | 68 | # only required if you have not enabled the ipywidgets nbextension yet 69 | jupyter nbextension enable --py --sys-prefix widgetsnbextension 70 | 71 | Installing with conda:: 72 | 73 | # only required if you have not added conda-forge to your channels yet 74 | conda config --add channels conda-forge 75 | 76 | conda install qgrid 77 | 78 | Jupyterlab Installation 79 | ----------------------- 80 | 81 | First, go through the normal installation steps above as you normally would when using qgrid in the notebook. 82 | If you haven't already install jupyterlab and enabled ipywidgets, do that first with the following lines:: 83 | 84 | pip install jupyterlab 85 | jupyter labextension install @jupyter-widgets/jupyterlab-manager 86 | 87 | Install the qgrid-jupyterlab extension and enable:: 88 | 89 | jupyter labextension install qgrid2 90 | 91 | At this point if you run jupyter lab normally with the 'jupyter lab' command, you should be 92 | able to use qgrid in notebooks as you normally would. 93 | 94 | *Please Note: Jupyterlab support has been tested with jupyterlab 0.30.5 and jupyterlab-manager 0.31.3, so if you're 95 | having trouble, try installing those versions. Feel free to file an issue if you find that qgrid isn't working 96 | with a newer version of either dependency.* 97 | 98 | What's New 99 | ---------- 100 | **Column-specific options (as of 1.1.0)**: 101 | Thanks to a significant `PR from the community `_, Qgrid users now have the ability to set a number of options on a per column basis. This allows you to do things like explicitly specify which column should be sortable, editable, etc. For example, if you wanted to prevent editing on all columns except for a column named `'A'`, you could do the following:: 102 | 103 | col_opts = { 'editable': False } 104 | col_defs = { 'A': { 'editable': True } } 105 | qgrid.show_grid(df, column_options=col_opts, column_definitions=col_defs) 106 | 107 | See the updated `show_grid `_ documentation for more information. 108 | 109 | **Disable editing on a per-row basis (as of 1.1.0)**: 110 | This feature can be thought of as the first row-specific option that qgrid supports. In particular it allows a user to specify, using python code, whether or not a particular row should be editable. For example, to make it so only rows in the grid where the `'status'` column is set to `'active'` are editable, you might use the following code:: 111 | 112 | def can_edit_row(row): 113 | return row['status'] == 'active' 114 | 115 | qgrid.show_grid(df, row_edit_callback=can_edit_row) 116 | 117 | **New API methods for dynamically updating an existing qgrid widget (as of 1.1.0)**: 118 | Adds the following new methods, which can be used to update the state of an existing Qgrid widget without having to call `show_grid` to completely rebuild the widget: 119 | 120 | - `edit_cell `_ 121 | - `change_selection `_ 122 | - `toggle_editable `_ 123 | - `change_grid_option `_ (experimental) 124 | 125 | **Improved MultiIndex Support (as of 1.0.6-beta.6)**: 126 | Qgrid now displays multi-indexed DataFrames with some of the index cells merged for readability, as is normally done when viewing DataFrames as a static html table. The following image shows qgrid displaying a multi-indexed DataFrame that was returned from Quantopian's `Pipeline API `_: 127 | 128 | .. figure:: https://s3.amazonaws.com/quantopian-forums/pipeline_with_qgrid.png 129 | :align: left 130 | :target: https://s3.amazonaws.com/quantopian-forums/pipeline_with_qgrid.png 131 | :width: 100px 132 | 133 | Dependencies 134 | ------------ 135 | 136 | Qgrid runs on `Python 2 or 3 `_. You'll also need 137 | `pip `_ for the installation steps below. 138 | 139 | Qgrid depends on the following three Python packages: 140 | 141 | `Jupyter notebook `_ 142 | This is the interactive Python environment in which qgrid runs. 143 | 144 | `ipywidgets `_ 145 | In order for Jupyter notebooks to be able to run widgets, you have to also install this ipywidgets package. 146 | It's maintained by the Jupyter organization, the same people who created Jupyter notebook. 147 | 148 | `Pandas `_ 149 | A powerful data analysis / manipulation library for Python. Qgrid requires that the data to be rendered as an 150 | interactive grid be provided in the form of a pandas DataFrame. 151 | 152 | These are listed in `requirements.txt `_ 153 | and will be automatically installed (if necessary) when qgrid is installed via pip. 154 | 155 | Compatibility 156 | ------------- 157 | 158 | ================= =========================== ============================== ============================== 159 | qgrid IPython / Jupyter notebook ipywidgets Jupyterlab 160 | ================= =========================== ============================== ============================== 161 | 0.2.0 2.x N/A N/A 162 | 0.3.x 3.x N/A N/A 163 | 0.3.x 4.0 4.0.x N/A 164 | 0.3.x 4.1 4.1.x N/A 165 | 0.3.2 4.2 5.x N/A 166 | 0.3.3 5.x 6.x N/A 167 | 1.0.x 5.x 7.x 0.30.x 168 | ================= =========================== ============================== ============================== 169 | 170 | 171 | Running the demo notebooks locally 172 | ---------------------------------- 173 | 174 | There are a couple of demo notebooks in the `qgrid-notebooks `_ repository 175 | which will help you get familiar with the functionality that qgrid provides. Here are the steps to clone the 176 | qgrid-notebooks repository and open a demo notebook: 177 | 178 | #. Install qgrid by following the instructions in the `Installation`_ section above, if you haven't already 179 | 180 | #. Clone the qgrid-notebooks repository from GitHub:: 181 | 182 | git clone https://github.com/quantopian/qgrid-notebooks.git 183 | 184 | #. Install the dev requirements for the repository and start the notebook server:: 185 | 186 | cd qgrid-notebooks 187 | pip install -r requirements_dev.txt 188 | jupyter notebook 189 | 190 | #. Click on one of the two notebooks (`index.ipynb `_ or `experimental.ipynb `_) that you see listed in the notebook UI in your browser. 191 | 192 | Running from source & testing your changes 193 | ------------------------------------------ 194 | 195 | If you'd like to contribute to qgrid, or just want to be able to modify the source code for your own purposes, you'll 196 | want to clone this repository and run qgrid from your local copy of the repository. The following steps explain how 197 | to do this. 198 | 199 | #. Clone the repository from GitHub and ``cd`` into the top-level directory:: 200 | 201 | git clone https://github.com/quantopian/qgrid.git 202 | cd qgrid 203 | 204 | #. Install the current project in `editable `_ 205 | mode:: 206 | 207 | pip install -e . 208 | 209 | #. Install the node packages that qgrid depends on and build qgrid's javascript using webpack:: 210 | 211 | cd js && npm install . 212 | 213 | #. Install and enable qgrid's javascript in your local jupyter notebook environment:: 214 | 215 | jupyter nbextension install --py --symlink --sys-prefix qgrid && jupyter nbextension enable --py --sys-prefix qgrid 216 | 217 | #. If desired, install the labextension:: 218 | 219 | jupyter labextension link js/ 220 | 221 | #. Run the notebook as you normally would with the following command:: 222 | 223 | jupyter notebook 224 | 225 | Manually testing server-side changes 226 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 227 | If the code you need to change is in qgrid's python code, then restart the kernel of the notebook you're in and 228 | rerun any qgrid cells to see your changes take effect. 229 | 230 | Manually testing client-side changes 231 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 232 | If the code you need to change is in qgrid's javascript or css code, repeat step 3 to rebuild qgrid's npm package, 233 | then refresh the browser tab where you're viewing your notebook to see your changes take effect. 234 | 235 | Running automated tests 236 | ^^^^^^^^^^^^^^^^^^^^^^^ 237 | There is a small python test suite which can be run locally by running the command ``pytest`` in the root folder 238 | of the repository. 239 | 240 | Building docs 241 | ^^^^^^^^^^^^^ 242 | The read-the-docs page is generated using sphinx. If you change any doc strings or want to add something to the 243 | read-the-docs page, you can preview your changes locally before submitting a PR using the following commands:: 244 | 245 | pip install sphinx sphinx_rtd_theme 246 | cd docs && make html 247 | 248 | This will result in the ``docs/_build/html`` folder being populated with a new version of the read-the-docs site. If 249 | you open the ``index.html`` file in your browser, you should be able to preview your changes. 250 | 251 | Events API 252 | ---------- 253 | As of qgrid 1.0.3 there are new ``on`` and ``off`` methods in qgrid which can be used to attach/detach event handlers. They're available on both the ``qgrid`` module (see `qgrid.on `_), and on individual QgridWidget instances (see `qgrid.QgridWidget.on `_). Previously the only way to listen for events was to use undocumented parts of the API. 254 | 255 | Having the ability to attach event handlers allows us to do some interesting things in terms of using qgrid in conjunction with other widgets/visualizations. One example is using qgrid to filter a DataFrame that's also being displayed by another visualization. 256 | 257 | If you previously used the ``observe`` method to respond to qgrid events, lets see how your code might be updated to use the new ``on`` method:: 258 | 259 | # Before upgrading to 1.0.3 260 | def handle_df_change(change): 261 | print(change['new']) 262 | 263 | qgrid_widget.observe(handle_df_change, names=['_df']) 264 | 265 | When you upgrade to 1.0.3, you have more granular control over which events you do an don't listen to, but you can also replicate the previous behavior of calling ``print`` every time the state of the internal DataFrame is changed. Here's what that would look like using the new ``on`` method:: 266 | 267 | # After upgrading to 1.0.3 268 | def handle_json_updated(event, qgrid_widget): 269 | # exclude 'viewport_changed' events since that doesn't change the DataFrame 270 | if (event['triggered_by'] != 'viewport_changed'): 271 | print(qgrid_widget.get_changed_df()) 272 | 273 | qgrid_widget.on('json_updated', handle_json_updated) 274 | 275 | See the `events notebook `_ for more examples of using these new API methods. 276 | 277 | For people who would rather not go to another page to try out the events notebook, here are a couple of gifs to give you an idea of what you can do with it. 278 | 279 | The first gif shows how you can use qgrid to filter the data that's being shown by a matplotlib scatter plot: 280 | 281 | .. figure:: docs/images/linked_to_scatter.gif 282 | :align: left 283 | :target: docs/images/linked_to_scatter.gif 284 | :width: 600px 285 | 286 | A brief demo showing qgrid hooked up to a matplotlib plot 287 | 288 | The second gif shows how you can move qgrid to a separate view in JupyterLab, which makes it more convenient 289 | to use in conjunction with other visualizations (in this case, a couple of ``Output`` widgets): 290 | 291 | .. figure:: docs/images/events_api.gif 292 | :align: left 293 | :target: docs/images/events_api.gif 294 | :width: 600px 295 | 296 | A brief demo showing qgrid's events api 297 | 298 | Continuing to use qgrid 0.3.3 299 | ----------------------------- 300 | If you're looking for the installation and usage instructions for qgrid 0.3.3 and the sample notebook that goes 301 | along with it, please see the `qgrid 0.3.3 tag `_ in this 302 | repository. The installation steps will be mostly the same. The only difference is that when you run "pip install" 303 | you'll have to explicitly specify that you want to install version 0.3.3, like this:: 304 | 305 | pip install qgrid==0.3.3 306 | 307 | If you're looking for the API docs, you can find them on the 308 | `readthedocs page for qgrid 0.3.3 `_. 309 | 310 | If you're looking for the demo notebook for 0.3.3, it's still availabe `in nbviewer 311 | `_. 312 | 313 | Qgrid 0.3.3 is not compatible with ipywidgets 7, so if you need support for ipywidgets 7, you'll need to use 314 | qgrid 1.0. 315 | 316 | Contributing 317 | ------------ 318 | All contributions, bug reports, bug fixes, documentation improvements, enhancements, and ideas are welcome. See the 319 | `Running from source & testing your changes`_ section above for more details on local qgrid development. 320 | 321 | If you are looking to start working with the qgrid codebase, navigate to the GitHub issues tab and start looking 322 | through interesting issues. 323 | 324 | Feel free to ask questions by submitting an issue with your question. 325 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html -a -E $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/qgrid.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/qgrid.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/qgrid" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/qgrid" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/build.rb: -------------------------------------------------------------------------------- 1 | require 'github/markup' 2 | 3 | output = File.open('readme.html','w') 4 | 5 | path = File.join(File.dirname(__FILE__), '../README.rst') 6 | output << GitHub::Markup.render(path, File.read(path)) 7 | output.close -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # qgrid documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Sep 1 11:25:28 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | sys.path.append(os.path.abspath('..')) 24 | # sys.path.append(os.path.abspath('../qgrid')) 25 | 26 | # -- General configuration ------------------------------------------------ 27 | 28 | # If your documentation needs a minimal Sphinx version, state it here. 29 | #needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | 'sphinx.ext.autodoc', 'sphinx.ext.napoleon' 36 | ] 37 | 38 | napoleon_use_rtype = False 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix(es) of source filenames. 44 | # You can specify multiple suffix as a list of string: 45 | # source_suffix = ['.rst', '.md'] 46 | source_suffix = '.rst' 47 | 48 | # The encoding of source files. 49 | #source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = u'qgrid' 56 | copyright = u'2015, Quantopian, Inc.' 57 | author = u'Quantopian, Inc.' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = '1.0' 65 | # The full version, including alpha/beta/rc tags. 66 | release = '1.0' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = None 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | #today = '' 78 | # Else, today_fmt is used as the format for a strftime call. 79 | #today_fmt = '%B %d, %Y' 80 | 81 | # List of patterns, relative to source directory, that match files and 82 | # directories to ignore when looking for source files. 83 | exclude_patterns = ['_build'] 84 | 85 | # The reST default role (used for this markup: `text`) to use for all 86 | # documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | #modindex_common_prefix = [] 105 | 106 | # If true, keep warnings as "system message" paragraphs in the built documents. 107 | #keep_warnings = False 108 | 109 | # If true, `todo` and `todoList` produce output, else they produce nothing. 110 | todo_include_todos = False 111 | 112 | 113 | # -- Options for HTML output ---------------------------------------------- 114 | 115 | # The theme to use for HTML and HTML Help pages. See the documentation for 116 | # a list of builtin themes. 117 | html_theme = 'sphinx_rtd_theme' 118 | 119 | # Theme options are theme-specific and customize the look and feel of a theme 120 | # further. For a list of options available for each theme, see the 121 | # documentation. 122 | #html_theme_options = {} 123 | 124 | # Add any paths that contain custom themes here, relative to this directory. 125 | #html_theme_path = [] 126 | 127 | # The name for this set of Sphinx documents. If None, it defaults to 128 | # " v documentation". 129 | #html_title = None 130 | 131 | # A shorter title for the navigation bar. Default is the same as html_title. 132 | #html_short_title = None 133 | 134 | # The name of an image file (relative to this directory) to place at the top 135 | # of the sidebar. 136 | #html_logo = None 137 | 138 | # The name of an image file (within the static path) to use as favicon of the 139 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 140 | # pixels large. 141 | #html_favicon = None 142 | 143 | # Add any paths that contain custom static files (such as style sheets) here, 144 | # relative to this directory. They are copied after the builtin static files, 145 | # so a file named "default.css" will overwrite the builtin "default.css". 146 | html_static_path = ['_static'] 147 | 148 | # Add any extra paths that contain custom files (such as robots.txt or 149 | # .htaccess) here, relative to this directory. These files are copied 150 | # directly to the root of the documentation. 151 | #html_extra_path = [] 152 | 153 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 154 | # using the given strftime format. 155 | #html_last_updated_fmt = '%b %d, %Y' 156 | 157 | # If true, SmartyPants will be used to convert quotes and dashes to 158 | # typographically correct entities. 159 | #html_use_smartypants = True 160 | 161 | # Custom sidebar templates, maps document names to template names. 162 | #html_sidebars = {} 163 | 164 | # Additional templates that should be rendered to pages, maps page names to 165 | # template names. 166 | #html_additional_pages = {} 167 | 168 | # If false, no module index is generated. 169 | #html_domain_indices = True 170 | 171 | # If false, no index is generated. 172 | #html_use_index = True 173 | 174 | # If true, the index is split into individual pages for each letter. 175 | #html_split_index = False 176 | 177 | # If true, links to the reST sources are added to the pages. 178 | #html_show_sourcelink = True 179 | 180 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 181 | #html_show_sphinx = True 182 | 183 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 184 | #html_show_copyright = True 185 | 186 | # If true, an OpenSearch description file will be output, and all pages will 187 | # contain a tag referring to it. The value of this option must be the 188 | # base URL from which the finished HTML is served. 189 | #html_use_opensearch = '' 190 | 191 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 192 | #html_file_suffix = None 193 | 194 | # Language to be used for generating the HTML full-text search index. 195 | # Sphinx supports the following languages: 196 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 197 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 198 | #html_search_language = 'en' 199 | 200 | # A dictionary with options for the search language support, empty by default. 201 | # Now only 'ja' uses this config value 202 | #html_search_options = {'type': 'default'} 203 | 204 | # The name of a javascript file (relative to the configuration directory) that 205 | # implements a search results scorer. If empty, the default will be used. 206 | #html_search_scorer = 'scorer.js' 207 | 208 | # Output file base name for HTML help builder. 209 | htmlhelp_basename = 'qgriddoc' 210 | 211 | # -- Options for LaTeX output --------------------------------------------- 212 | 213 | latex_elements = { 214 | # The paper size ('letterpaper' or 'a4paper'). 215 | #'papersize': 'letterpaper', 216 | 217 | # The font size ('10pt', '11pt' or '12pt'). 218 | #'pointsize': '10pt', 219 | 220 | # Additional stuff for the LaTeX preamble. 221 | #'preamble': '', 222 | 223 | # Latex figure (float) alignment 224 | #'figure_align': 'htbp', 225 | } 226 | 227 | # Grouping the document tree into LaTeX files. List of tuples 228 | # (source start file, target name, title, 229 | # author, documentclass [howto, manual, or own class]). 230 | latex_documents = [ 231 | (master_doc, 'qgrid.tex', u'qgrid Documentation', 232 | u'Quantopian, Inc.', 'manual'), 233 | ] 234 | 235 | # The name of an image file (relative to this directory) to place at the top of 236 | # the title page. 237 | #latex_logo = None 238 | 239 | # For "manual" documents, if this is true, then toplevel headings are parts, 240 | # not chapters. 241 | #latex_use_parts = False 242 | 243 | # If true, show page references after internal links. 244 | #latex_show_pagerefs = False 245 | 246 | # If true, show URL addresses after external links. 247 | #latex_show_urls = False 248 | 249 | # Documents to append as an appendix to all manuals. 250 | #latex_appendices = [] 251 | 252 | # If false, no module index is generated. 253 | #latex_domain_indices = True 254 | 255 | 256 | # -- Options for manual page output --------------------------------------- 257 | 258 | # One entry per manual page. List of tuples 259 | # (source start file, name, description, authors, manual section). 260 | man_pages = [ 261 | (master_doc, 'qgrid', u'qgrid Documentation', 262 | [author], 1) 263 | ] 264 | 265 | # If true, show URL addresses after external links. 266 | #man_show_urls = False 267 | 268 | 269 | # -- Options for Texinfo output ------------------------------------------- 270 | 271 | # Grouping the document tree into Texinfo files. List of tuples 272 | # (source start file, target name, title, author, 273 | # dir menu entry, description, category) 274 | texinfo_documents = [ 275 | (master_doc, 'qgrid', u'qgrid Documentation', 276 | author, 'qgrid', 'One line description of project.', 277 | 'Miscellaneous'), 278 | ] 279 | 280 | # Documents to append as an appendix to all manuals. 281 | #texinfo_appendices = [] 282 | 283 | # If false, no module index is generated. 284 | #texinfo_domain_indices = True 285 | 286 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 287 | #texinfo_show_urls = 'footnote' 288 | 289 | # If true, do not generate a @detailmenu in the "Top" node's menu. 290 | #texinfo_no_detailmenu = False 291 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | name: qgrid_docs 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - qgrid 6 | -------------------------------------------------------------------------------- /docs/images/events_api.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantopian/qgrid/877b420d3bd83297bbcc97202b914001a85afff2/docs/images/events_api.gif -------------------------------------------------------------------------------- /docs/images/filtering_demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantopian/qgrid/877b420d3bd83297bbcc97202b914001a85afff2/docs/images/filtering_demo.gif -------------------------------------------------------------------------------- /docs/images/linked_to_scatter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quantopian/qgrid/877b420d3bd83297bbcc97202b914001a85afff2/docs/images/linked_to_scatter.gif -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. qgrid documentation master file, created by 2 | sphinx-quickstart on Tue Sep 1 11:25:28 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. image:: https://media.quantopian.com/logos/open_source/qgrid-logo-03.png 7 | :target: https://qgrid.readthedocs.io 8 | :width: 190px 9 | :align: center 10 | :alt: qgrid 11 | 12 | Qgrid API Documentation 13 | ======================= 14 | 15 | Qgrid is a Jupyter notebook widget which uses `SlickGrid `_ to render pandas 16 | DataFrames within a Jupyter notebook. This allows you to explore your DataFrames with intuitive scrolling, sorting, and 17 | filtering controls, as well as edit your DataFrames by double clicking cells. 18 | 19 | Qgrid was developed for use in `Quantopian's hosted research environment 20 | `_ 21 | and is available for use in that environment as of June 2018. 22 | 23 | 24 | Other qgrid resources 25 | --------------------- 26 | 27 | This page hosts only the API docs for the project. You might also be interested in these other qgrid-related 28 | resources: 29 | 30 | `qgrid on GitHub `_ 31 | This is where you'll find the source code and the rest of the documentation for the project, including the 32 | instructions for installing and running qgrid. 33 | 34 | `qgrid demo on Quantopian `_ 35 | Click the badge below try out the latest beta of qgrid in Quantopian's hosted research environment. If you're 36 | already signed into Quantopian you'll be brought directly to the demo notebook. Otherwise you'll be prompted 37 | to register (it's free): 38 | 39 | .. image:: https://img.shields.io/badge/launch-quantopian-red.svg?colorB=d33015 40 | :target: https://www.quantopian.com/clone_notebook?id=5b2baee1b3d6870048620188&utm_source=github&utm_medium=web&utm_campaign=qgrid-repo 41 | 42 | `qgrid demo on binder `_ 43 | Click the badge below or the link above to try out qgrid using binder. You'll see a brief loading screen and 44 | then a notebook will appear: 45 | 46 | .. image:: https://beta.mybinder.org/badge.svg 47 | :target: https://beta.mybinder.org/v2/gh/quantopian/qgrid-notebooks/master?filepath=index.ipynb 48 | 49 | 50 | :mod:`qgrid` Module 51 | ------------------- 52 | 53 | .. automodule:: qgrid 54 | :members: 55 | :exclude-members: QgridWidget 56 | 57 | .. autoclass:: QgridWidget(df=None, grid_options=None, precision=None, show_toolbar=None) 58 | :members: 59 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qgrid2", 3 | "version": "1.1.3", 4 | "description": "An Interactive Grid for Sorting and Filtering DataFrames in Jupyter Notebook", 5 | "author": "Quantopian Inc.", 6 | "main": "src/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/Quantopian/qgrid.git" 10 | }, 11 | "keywords": [ 12 | "jupyter", 13 | "widgets", 14 | "ipython", 15 | "ipywidgets" 16 | ], 17 | "scripts": { 18 | "clean": "rimraf dist/", 19 | "prepare": "webpack", 20 | "test": "echo \"Error: no test specified\" && exit 1" 21 | }, 22 | "devDependencies": { 23 | "css-loader": "^3.4.2", 24 | "expose-loader": "^0.7.5", 25 | "file-loader": "^6.0.0", 26 | "jshint": "^2.11.0", 27 | "json-loader": "^0.5.7", 28 | "rimraf": "^3.0.2", 29 | "style-loader": "^1.1.3", 30 | "webpack": "^4.42.0", 31 | "webpack-cli": "^3.3.11" 32 | }, 33 | "dependencies": { 34 | "@jupyter-widgets/base": "^1.1 || ^2 || ^3", 35 | "@jupyter-widgets/controls": "^1 || ^2", 36 | "jquery": "^3.2.1", 37 | "jquery-ui-dist": "^1.12.1", 38 | "moment": "^2.24.0", 39 | "slickgrid-qgrid": "0.0.5", 40 | "underscore": "^1.9.2" 41 | }, 42 | "jshintConfig": { 43 | "esversion": 6 44 | }, 45 | "files": [ 46 | "dist/", 47 | "src/" 48 | ], 49 | "jupyterlab": { 50 | "extension": "src/jupyterlab-plugin" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /js/src/embed.js: -------------------------------------------------------------------------------- 1 | // Entry point for the unpkg bundle containing custom model definitions. 2 | // 3 | // It differs from the notebook bundle in that it does not need to define a 4 | // dynamic baseURL for the static assets and may load some css that would 5 | // already be loaded by the notebook otherwise. 6 | 7 | // Export widget models and views, and the npm package version number. 8 | module.exports = require('./qgrid.widget.js'); 9 | module.exports.version = require('../package.json').version; 10 | -------------------------------------------------------------------------------- /js/src/extension.js: -------------------------------------------------------------------------------- 1 | // This file contains the javascript that is run when the notebook is loaded. 2 | // It contains some requirejs configuration and the `load_ipython_extension` 3 | // which is required for any notebook extension. 4 | 5 | // Configure requirejs 6 | if (window.require) { 7 | window.require.config({ 8 | map: { 9 | "*" : { 10 | "qgrid": "nbextensions/qgrid/index" 11 | } 12 | } 13 | }); 14 | } 15 | 16 | // Export the required load_ipython_extension 17 | module.exports = { 18 | load_ipython_extension: function() {} 19 | }; 20 | -------------------------------------------------------------------------------- /js/src/index.js: -------------------------------------------------------------------------------- 1 | // Entry point for the notebook bundle containing custom model definitions. 2 | // Export widget models and views, and the npm package version number. 3 | module.exports = require('./qgrid.widget.js'); 4 | module.exports.version = require('../package.json').version; 5 | -------------------------------------------------------------------------------- /js/src/jupyterlab-plugin.js: -------------------------------------------------------------------------------- 1 | var qgrid = require('./index'); 2 | 3 | var base = require('@jupyter-widgets/base'); 4 | 5 | /** 6 | * The widget manager provider. 7 | */ 8 | module.exports = { 9 | id: 'qgrid', 10 | requires: [base.IJupyterWidgetRegistry], 11 | activate: function(app, widgets) { 12 | widgets.registerWidget({ 13 | name: 'qgrid', 14 | version: qgrid.version, 15 | exports: qgrid 16 | }); 17 | }, 18 | autoStart: true 19 | }; 20 | -------------------------------------------------------------------------------- /js/src/qgrid.booleanfilter.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var filter_base = require('./qgrid.filterbase.js'); 3 | 4 | class BooleanFilter extends filter_base.FilterBase { 5 | 6 | get_filter_html() { 7 | return ` 8 |
9 |

10 | 11 | 12 |

13 | 26 | 29 |
30 | `; 31 | } 32 | 33 | update_min_max(col_info, has_active_filter) { 34 | this.values = col_info.values; 35 | this.length = col_info.length; 36 | if('filter_info' in col_info){ 37 | this.selected = col_info.filter_info.selected; 38 | } else { 39 | this.selected = null; 40 | } 41 | this.show_filter(); 42 | } 43 | 44 | initialize_controls() { 45 | super.initialize_controls(); 46 | this.radio_buttons = this.filter_elem.find('.bool-filter-radio'); 47 | 48 | this.filter_elem.find('label').click((e) => { 49 | var radio_id = $(e.currentTarget).attr('for'); 50 | this.radio_buttons.filter(`#${radio_id}`).click(); 51 | }); 52 | 53 | if (this.selected == null) { 54 | this.radio_buttons.prop('checked', false); 55 | } else { 56 | this.radio_buttons.filter( 57 | `#radio-${this.selected}` 58 | ).prop('checked', true); 59 | } 60 | 61 | this.radio_buttons.change(() => { 62 | var checked_radio = this.radio_buttons.filter(':checked'); 63 | var old_selected_value = this.selected; 64 | if (checked_radio.length == 0) { 65 | this.selected = null; 66 | } else { 67 | this.selected = checked_radio.val() == 'true'; 68 | } 69 | if (this.selected != old_selected_value) { 70 | this.send_filter_changed(); 71 | } 72 | }); 73 | } 74 | 75 | is_active() { 76 | return this.selected != null; 77 | } 78 | 79 | reset_filter() { 80 | this.radio_buttons.prop('checked', false); 81 | this.selected = null; 82 | this.send_filter_changed(); 83 | } 84 | 85 | get_filter_info() { 86 | return { 87 | "field": this.field, 88 | "type": "boolean", 89 | "selected": this.selected 90 | }; 91 | } 92 | } 93 | 94 | module.exports = {'BooleanFilter': BooleanFilter}; 95 | -------------------------------------------------------------------------------- /js/src/qgrid.css: -------------------------------------------------------------------------------- 1 | 2 | .q-grid-container * { 3 | -webkit-box-sizing: content-box; 4 | -moz-box-sizing: content-box; 5 | box-sizing: content-box; 6 | } 7 | 8 | .q-grid-container { 9 | position: relative; 10 | overflow-x: auto; 11 | width: auto; 12 | height: auto; 13 | border-radius: 4px; 14 | } 15 | 16 | .q-grid-container .hidden { 17 | display: none !important; 18 | } 19 | 20 | .q-grid-container a { 21 | color: #337ab7 !important; 22 | text-decoration: none; 23 | line-height: 20px; 24 | } 25 | 26 | .q-grid-container a:hover, 27 | .q-grid-container a:focus { 28 | color: #23527c; 29 | text-decoration: underline; 30 | } 31 | 32 | .q-grid { 33 | position: relative; 34 | border: 1px solid #d3d3d3; 35 | margin-top: 6px; 36 | margin-bottom: 2px; 37 | box-sizing: border-box; 38 | } 39 | 40 | .q-grid-toolbar { 41 | margin: 4px 0 42 | } 43 | 44 | .q-grid-toolbar .btn { 45 | background-color: #EEEEEE; 46 | border-color: #E0E0E0; 47 | margin-right: 6px; 48 | padding: 4px 40px; 49 | /* taken from bootstrap's btn class, since 50 | jupyterlab doesn't include bootstrap css */ 51 | display: inline-block; 52 | margin-bottom: 0; 53 | font-weight: normal; 54 | text-align: center; 55 | vertical-align: middle; 56 | touch-action: manipulation; 57 | cursor: pointer; 58 | background-image: none; 59 | border: 1px solid #E0E0E0; 60 | white-space: nowrap; 61 | font-size: 13px; 62 | line-height: 1.42857143; 63 | border-radius: 2px; 64 | } 65 | 66 | .q-grid-toolbar .btn:hover { 67 | background-color: #DDDDDD; 68 | border-color: #CCCCCC; 69 | } 70 | 71 | .q-grid-toolbar .btn:focus { 72 | outline: none; 73 | } 74 | 75 | .q-grid-toolbar .close-modal-btn { 76 | display: none; 77 | } 78 | 79 | .qgrid-modal .q-grid-toolbar .close-modal-btn { 80 | display: block; 81 | } 82 | 83 | .qgrid-modal .full-screen-btn, 84 | .qgrid-modal .modal-header, 85 | .qgrid-modal .modal-footer { 86 | display: none; 87 | } 88 | 89 | .qgrid-modal { 90 | width: 100%; 91 | height: 100%; 92 | margin: 0px; 93 | } 94 | 95 | .qgrid-modal .modal-dialog { 96 | position: absolute; 97 | top: 20px; 98 | left: 20px; 99 | bottom: 20px; 100 | right: 20px; 101 | margin: 0px; 102 | width: auto; 103 | height: auto; 104 | } 105 | 106 | .qgrid-modal .modal-content { 107 | position: absolute; 108 | left: 0px; 109 | right: 0px; 110 | top: 0px; 111 | bottom: 0px; 112 | } 113 | 114 | .qgrid-modal .modal-body { 115 | position: absolute; 116 | padding: 15px; 117 | top: 0px; 118 | left: 0px; 119 | bottom: 0px; 120 | right: 0px; 121 | } 122 | 123 | .qgrid-modal .modal-body button.close { 124 | margin-top: 7px; 125 | margin-right: 14px; 126 | } 127 | 128 | .qgrid-modal .q-grid-container { 129 | bottom: 25px; 130 | left: 20px; 131 | right: 20px; 132 | position: absolute; 133 | top: 15px; 134 | } 135 | 136 | .qgrid-modal .q-grid { 137 | position: absolute; 138 | top: 33px; 139 | bottom: 0px; 140 | left: 0px; 141 | right: 0px; 142 | height: auto !important; 143 | } 144 | 145 | .q-grid-toolbar .full-screen-btn, 146 | .q-grid-toolbar .close-modal-btn { 147 | height: 18px; 148 | width: 12px; 149 | padding: 4px 7px; 150 | float: right; 151 | margin-right: 0px; 152 | } 153 | 154 | .output_scroll .q-grid { 155 | height: 315px !important; 156 | } 157 | 158 | .q-grid-toolbar { 159 | display: none; 160 | } 161 | 162 | .show-toolbar .q-grid-toolbar { 163 | display: block; 164 | } 165 | 166 | .output_scroll .show-toolbar .q-grid { 167 | height: 284px !important; 168 | } 169 | 170 | .q-grid.force-fit-columns .slick-viewport { 171 | overflow-x: hidden !important; 172 | } 173 | 174 | .q-grid.hide-scrollbar > .slick-header { 175 | margin-bottom: -1px; 176 | } 177 | 178 | .q-grid.hide-scrollbar > .slick-viewport { 179 | overflow-y: hidden !important; 180 | height: 100%; 181 | } 182 | 183 | .q-grid.hide-scrollbar.force-fit-columns > .slick-viewport { 184 | overflow: hidden !important; 185 | } 186 | 187 | .q-grid.hide-scrollbar > .slick-viewport > .grid-canvas { 188 | margin-top: 1px; 189 | } 190 | 191 | .q-grid .slick-header { 192 | border-top: none; 193 | border-left: none; 194 | border-right: none; 195 | } 196 | 197 | .q-grid .slick-header-columns, 198 | .text-filter-grid .slick-header-columns { 199 | background: none; 200 | background-color: rgb(245, 245, 245); 201 | border-bottom: none; 202 | } 203 | 204 | .q-grid.force-fit-columns .slick-header-column:last-child .filter-button:hover, 205 | .q-grid.force-fit-columns .slick-header-column.active:last-child .filter-button, 206 | .q-grid.force-fit-columns .slick-header-column:last-child .filter-button.filter-active { 207 | border-right: 1px solid #ccc; 208 | margin-right: -4px; 209 | } 210 | 211 | .q-grid.force-fit-columns .slick-header-column:last-child .filter-button { 212 | margin-right: -4px; 213 | border-right: 1px solid transparent; 214 | } 215 | 216 | .q-qgrid.hide-scrollbar .slick-header-column:last-child .slick-resizable-handle { 217 | right: -4px; 218 | } 219 | 220 | .q-grid.hide-scrollbar > .slick_header .slick-header-column:last-child .filter-button, 221 | .q-grid.hide-scrollbar > .slick-header .slick-header-column:last-child .filter-button:hover, 222 | .q-grid.hide-scrollbar > .slick-header .slick-header-column.active:last-child .filter-button:hover, 223 | .q-grid.hide-scrollbar > .slick-header .slick-header-column.active:last-child .filter-button.filter-active { 224 | border-right: 1px solid transparent; 225 | } 226 | 227 | .q-grid .slick-header-column .filter-button.filter-active, 228 | .q-grid .slick-header-column .filter-button.filter-active:hover{ 229 | background-color: #deeaf7; 230 | border-left: 1px solid #cccccc; 231 | } 232 | 233 | .q-grid .slick-header-column { 234 | background: rgb(245, 245, 245); 235 | color: #565656; 236 | border-right-style: none; 237 | font-size: 13px; 238 | font-weight: bold; 239 | padding: 6px 2px !important; 240 | } 241 | 242 | .q-grid .slick-header-column.active { 243 | background-color: #deeaf7; 244 | } 245 | 246 | .q-grid .slick-header-column-sorted { 247 | font-style: inherit !important; 248 | } 249 | 250 | .q-grid .slick-header-column:first-child .slick-column-name { 251 | margin-left: 6px !important; 252 | } 253 | 254 | .q-grid .slick-sort-indicator { 255 | height: 8px !important; 256 | margin-left: 6px; 257 | } 258 | 259 | .q-grid .slick-sort-indicator.fa-sort-asc { 260 | margin-top: 4px; 261 | color: #5862ff; 262 | } 263 | 264 | .q-grid .slick-sort-indicator.fa-sort-desc { 265 | margin-top: 1px; 266 | color: #5862ff; 267 | } 268 | 269 | .q-grid .slick-sort-indicator.fa-spinner { 270 | height: auto !important;; 271 | width: auto !important;; 272 | margin-top: 3px; 273 | } 274 | 275 | .q-grid .fa-check { 276 | color: #17a700; 277 | } 278 | 279 | .q-grid .slick-column-name { 280 | margin-bottom: 0px; 281 | float: left; 282 | } 283 | 284 | .q-grid .slick-resizable-handle { 285 | width: 1px; 286 | background-color: #c5c5c5; 287 | border-left: 3px solid rgb(245, 245, 245); 288 | border-right: 3px solid rgb(245, 245, 245); 289 | } 290 | 291 | .q-grid .slick-resizable-handle.active { 292 | border-right-color: #deeaf7; 293 | } 294 | 295 | .q-grid .slick-cell { 296 | border-bottom: 1px solid #e1e8ed; 297 | border-right: none; 298 | border-left: 1px solid transparent; 299 | font-size: 13px; 300 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 301 | padding-left: 0px; 302 | padding-top: 4px; 303 | border-top: none; 304 | } 305 | 306 | .q-grid.highlight-selected-row .slick-cell.selected { 307 | background-color: #deeaf7; 308 | } 309 | 310 | .q-grid.highlight-selected-cell .slick-cell.active { 311 | border: 2px solid rgb(65, 165, 245); 312 | padding-top: 2px; 313 | padding-bottom: 1px; 314 | padding-left: 3px; 315 | } 316 | 317 | .q-grid .slick-cell.editable:not(.idx-col) { 318 | border: 2px solid rgb(65, 165, 245); 319 | padding-top: 2px; 320 | padding-bottom: 1px; 321 | padding-left: 3px !important; 322 | background-color: #FFF; 323 | -webkit-box-shadow: 0 2px 5px rgba(0,0,0,0.4); 324 | -moz-box-shadow: 0 2px 5px rgba(0,0,0,0.4); 325 | box-shadow: 0 2px 5px rgba(0,0,0,0.4); 326 | } 327 | 328 | .q-grid .slick-cell.editable .editor-select:focus { 329 | outline-style: none; 330 | } 331 | 332 | .q-grid .slick-cell.idx-col { 333 | font-weight: bold; 334 | margin-right: 3px; 335 | } 336 | 337 | .q-grid .slick-cell.idx-col.last-idx-col { 338 | border-right: 1px solid rgb(225, 232, 237); 339 | } 340 | 341 | .q-grid .slick-cell.idx-col:not(.first-idx-col) { 342 | font-weight: bold; 343 | border-left: 1px solid rgb(225, 232, 237); 344 | margin-right: 3px; 345 | } 346 | 347 | .q-grid .idx-col.group-top { 348 | border-bottom-color: transparent; 349 | background-color: #FFF; 350 | } 351 | 352 | .q-grid .idx-col.group-middle { 353 | border-top-color: transparent; 354 | border-bottom-color: transparent; 355 | color: transparent; 356 | background-color: #FFF; 357 | } 358 | 359 | .q-grid .idx-col.group-bottom { 360 | border-top-color: transparent; 361 | color: transparent; 362 | background-color: #FFF; 363 | } 364 | 365 | .q-grid .idx-col.group-single { 366 | background-color: transparent; 367 | } 368 | 369 | .q-grid .slick-cell.l0.r0 { 370 | border-left: none; 371 | padding-left: 6px; 372 | z-index: 85; 373 | } 374 | 375 | .q-grid .slick-cell.l1.r1 { 376 | z-index: 86; 377 | } 378 | 379 | .q-grid .slick-cell.l2.r2 { 380 | z-index: 87; 381 | } 382 | 383 | .q-grid .slick-cell.l3.r3 { 384 | z-index: 88; 385 | } 386 | 387 | .q-grid .slick-cell.l4.r4 { 388 | z-index: 89; 389 | } 390 | 391 | .q-grid .slick-cell.l5.r5 { 392 | z-index: 90; 393 | } 394 | 395 | .q-grid .slick-cell.editable { 396 | z-index: 91 !important; 397 | } 398 | 399 | .q-grid .slick-cell.selected { 400 | background-color: transparent; 401 | } 402 | 403 | /* Filter button */ 404 | 405 | .q-grid .slick-header-column.active .filter-button, .filter-button:hover { 406 | background-color: #deeaf7; 407 | border-left: 1px solid #cccccc; 408 | } 409 | 410 | .q-grid .slick-header-column.filter-button-disabled .filter-button { 411 | color: #bababa; 412 | } 413 | 414 | .q-grid .filter-button { 415 | position: absolute; 416 | right: 4px; 417 | top: 0px; 418 | line-height: 29px; 419 | height: 28px; 420 | width: 21px; 421 | text-align: center; 422 | z-index: 10; 423 | background-image: none; 424 | margin-top: 0px; 425 | } 426 | 427 | .q-grid .filter-button:hover { 428 | background-color: #efefef; 429 | border-left: 1px solid #cccccc; 430 | } 431 | 432 | /* Filter dropdowns */ 433 | 434 | .q-grid-container .grid-filter { 435 | z-index: 1000; 436 | position: absolute; 437 | padding: 0px; 438 | margin: 0px; 439 | max-height: 400px; 440 | display: block; 441 | border-radius: 0px; 442 | /* we previously got these style from bootstrap */ 443 | float: left; 444 | min-width: 160px; 445 | list-style: none; 446 | font-size: 13px; 447 | text-align: left; 448 | background-color: #fff; 449 | border: 1px solid rgba(0, 0, 0, 0.15); 450 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 451 | background-clip: padding-box; 452 | } 453 | 454 | .q-grid-container .filter-error-msg { 455 | padding: 8px 14px; 456 | width: 160px; 457 | } 458 | 459 | .q-grid-container .grid-filter h3.qgrid-popover-title { 460 | padding: 5px 30px 5px 11px; 461 | border-bottom: 1px solid #d3d3d3; 462 | border-radius: 0px; 463 | /* we previously got these style from bootstrap */ 464 | margin: 0; 465 | font-size: 13px; 466 | background-color: #f7f7f7; 467 | font-family: inherit; 468 | font-weight: 500; 469 | line-height: 1.1; 470 | color: inherit; 471 | display: block; 472 | } 473 | 474 | .q-grid-container .grid-filter .dropdown-title { 475 | font-size: 13px; 476 | } 477 | 478 | .q-grid-container .grid-filter i.close-button { 479 | position: absolute; 480 | right: 11px; 481 | top: 7px; 482 | padding: 0; 483 | color: #ababab; 484 | cursor: pointer; 485 | background-image: none; 486 | margin-top: 0px; 487 | } 488 | 489 | .q-grid-container .grid-filter i.close-button:hover { 490 | color: #777777; 491 | } 492 | 493 | .q-grid-container .grid-filter .dropdown-footer { 494 | background-color: #f7f7f7; 495 | border-top: 1px solid #d3d3d3; 496 | padding: 4px 12px; 497 | height: 19px; 498 | } 499 | 500 | .q-grid-container .grid-filter .dropdown-footer a.reset-link { 501 | padding: 0px; 502 | float: right; 503 | } 504 | 505 | .q-grid-container .grid-filter .dropdown-footer a.select-all-link { 506 | padding: 0px; 507 | } 508 | 509 | .q-grid-container .date-range-filter .dropdown-body { 510 | margin: 15px 11px; 511 | width: 210px; 512 | } 513 | 514 | .q-grid-container .date-range-filter .start-date { 515 | margin-right: 4px; 516 | width: 80px; 517 | } 518 | 519 | .q-grid-container .date-range-filter .end-date { 520 | margin-left: 4px; 521 | width: 80px; 522 | } 523 | 524 | .q-grid-container .numerical-filter .dropdown-body { 525 | margin: 20px 23px 10px 20px; 526 | width: 207px; 527 | text-align: center; 528 | } 529 | 530 | .q-grid-container .numerical-filter .slider-range { 531 | margin-bottom: 4px; 532 | } 533 | 534 | 535 | .q-grid-container .numerical-filter .ui-slider-handle { 536 | width: 18px; 537 | padding: 0; 538 | } 539 | 540 | .q-grid-container .numerical-filter .range-separator { 541 | font-size: 28px; 542 | font-weight: 300; 543 | padding: 0 7px; 544 | color: #646a76; 545 | position: relative; 546 | top: 4px; 547 | } 548 | 549 | .text-filter-grid * { 550 | -webkit-box-sizing: border-box; 551 | -moz-box-sizing: border-box; 552 | box-sizing: border-box; 553 | } 554 | 555 | .text-filter .slick-viewport { 556 | overflow-y: auto !important; 557 | overflow-x: hidden !important; 558 | } 559 | 560 | .text-filter .input-area { 561 | padding: 12px; 562 | } 563 | 564 | .text-filter .no-results { 565 | margin-left: 12px; 566 | margin-bottom: 10px; 567 | } 568 | 569 | .text-filter .search-input { 570 | height: 30px; 571 | width: 100%; 572 | margin-bottom: 0px; 573 | box-sizing: border-box; 574 | } 575 | 576 | .text-filter .text-filter-grid { 577 | width: 300px; 578 | height: 250px; 579 | box-sizing: border-box; 580 | border-top: 1px solid #d3d3d3; 581 | } 582 | 583 | .text-filter .slick-header { 584 | border: none; 585 | } 586 | 587 | .text-filter .slick-header-columns { 588 | height: 0px; 589 | } 590 | 591 | .text-filter .slick-cell { 592 | line-height: 30px; 593 | border-left: none; 594 | border-right: none; 595 | background-color: #ffffff !important; 596 | border-bottom: 1px solid #e1e8ed !important; 597 | padding: 0; 598 | text-align: left; 599 | cursor: pointer; 600 | } 601 | 602 | .text-filter .slick-row.odd .slick-cell { 603 | background: #fafafa !important; 604 | } 605 | 606 | .text-filter .slick-row.active .slick-cell { 607 | background-color: #deeaf7 !important; 608 | border-top: 1px solid transparent !important; 609 | } 610 | 611 | .text-filter .check-box-cell { 612 | text-align: center; 613 | line-height: 28px; 614 | } 615 | 616 | .text-filter .check-box-cell input { 617 | margin: 0; 618 | } 619 | 620 | /* from custom-bootstrap *****************/ 621 | 622 | .q-grid-container input.datepicker { 623 | padding: 4px 4px 2px 4px; 624 | border: 1px solid lightgrey; 625 | font-size: 13px; 626 | line-height: 18px; 627 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 628 | } 629 | 630 | /* jquery-ui overrides */ 631 | 632 | .ui-widget, .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { 633 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 634 | font-size: 13px; 635 | } 636 | 637 | .ui-tooltip { 638 | opacity: 1.0 !important; 639 | } 640 | 641 | /* jquery-ui datepicker style overrides */ 642 | 643 | #ui-datepicker-div { 644 | z-index: 9999 !important; 645 | } 646 | 647 | #ui-datepicker-div.ui-datepicker { 648 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 649 | /*@include box-shadow(0 5px 10px rgba(0, 0, 0, 0.2))*/ 650 | z-index: 99 !important; 651 | padding: 0 !important; 652 | background-color: white; 653 | border: 1px solid gray; 654 | } 655 | 656 | #ui-datepicker-div.ui-datepicker .ui-icon { 657 | display: inline-block; 658 | font: normal normal normal 14px/1 FontAwesome; 659 | font-size: inherit; 660 | text-rendering: auto; 661 | -webkit-font-smoothing: antialiased; 662 | -moz-osx-font-smoothing: grayscale; 663 | text-indent: 0px; 664 | margin-left: -4px; 665 | margin-top: -6px; 666 | background-image: none; 667 | } 668 | 669 | #ui-datepicker-div.ui-datepicker .ui-icon-circle-triangle-w:before { 670 | content: "\f053"; 671 | } 672 | 673 | #ui-datepicker-div.ui-datepicker .ui-icon-circle-triangle-e:before { 674 | content: "\f054"; 675 | } 676 | 677 | 678 | /* from slickgrid editing example */ 679 | input.editor-text { 680 | width: 100%; 681 | height: 100%; 682 | border: 0; 683 | margin: -1px 0 0 0; 684 | background: transparent; 685 | outline: 0; 686 | padding: 0; 687 | } 688 | 689 | .ui-datepicker-trigger { 690 | width: 16px; 691 | height: 16px; 692 | margin-top: 2px; 693 | padding: 0; 694 | vertical-align: top; 695 | left: 4px; 696 | position: relative; 697 | } 698 | 699 | .slick-cell.boolean { 700 | text-align: center; 701 | } 702 | 703 | .bool-radio-wrapper { 704 | width: 150px; 705 | position: relative; 706 | margin: 8px auto 4px auto; 707 | } 708 | 709 | .label-true { 710 | padding: 4px 28px 4px 13px; 711 | } 712 | 713 | .label-false { 714 | padding: 4px 28px 4px 4px; 715 | margin-left: 15px; 716 | } 717 | 718 | input.bool-filter-radio { 719 | margin-left: -23px; 720 | } 721 | 722 | .slick-cell { 723 | -webkit-touch-callout: none; /* iOS Safari */ 724 | -webkit-user-select: none; /* Safari */ 725 | -khtml-user-select: none; /* Konqueror HTML */ 726 | -moz-user-select: none; /* Firefox */ 727 | -ms-user-select: none; /* Internet Explorer/Edge */ 728 | user-select: none; 729 | } 730 | 731 | .slick-row .slick-cell:not(:first-child) { 732 | padding-left: 5px; 733 | margin-left: -4px; 734 | } 735 | 736 | .q-grid .slick-sort-indicator-desc, 737 | .q-grid .slick-sort-indicator-asc { 738 | background-image: none; 739 | } 740 | 741 | .jupyter-widgets { 742 | overflow: auto !important; 743 | } 744 | -------------------------------------------------------------------------------- /js/src/qgrid.datefilter.js: -------------------------------------------------------------------------------- 1 | var $ = require('jquery'); 2 | var filter_base = require('./qgrid.filterbase.js'); 3 | 4 | class DateFilter extends filter_base.FilterBase { 5 | 6 | get_filter_html() { 7 | return ` 8 |
9 |

10 | 11 | 12 |

13 | 18 | 21 |
22 | `; 23 | } 24 | 25 | update_min_max(col_info, has_active_filter) { 26 | this.min_value = col_info.filter_min; 27 | this.max_value = col_info.filter_max; 28 | 29 | var filter_info = col_info.filter_info; 30 | if (filter_info) { 31 | this.filter_start_date = filter_info.min || this.min_value; 32 | this.filter_end_date = filter_info.max || this.max_value; 33 | } else { 34 | this.filter_start_date = this.min_value; 35 | this.filter_end_date = this.max_value; 36 | } 37 | 38 | this.has_multiple_values = this.min_value != this.max_value; 39 | this.show_filter(); 40 | if (has_active_filter) { 41 | this.update_filter_button_disabled(); 42 | } 43 | } 44 | 45 | reset_filter() { 46 | this.start_date_control.datepicker("setDate", this.min_date); 47 | this.end_date_control.datepicker("setDate", this.max_date); 48 | 49 | this.start_date_control.datepicker("option", "maxDate", this.max_date); 50 | this.end_date_control.datepicker("option", "minDate", this.min_date); 51 | 52 | this.filter_start_date = null; 53 | this.filter_end_date = null; 54 | this.send_filter_changed(); 55 | } 56 | 57 | initialize_controls() { 58 | super.initialize_controls(); 59 | this.min_date = new Date(this.min_value); 60 | this.max_date = new Date(this.max_value); 61 | 62 | this.start_date_control = this.filter_elem.find(".start-date"); 63 | this.end_date_control = this.filter_elem.find(".end-date"); 64 | 65 | var date_options = { 66 | "dayNamesMin": ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], 67 | "prevText": "", 68 | "nextText": "", 69 | minDate: this.min_date, 70 | maxDate: this.max_date, 71 | beforeShow: (input, inst) => { 72 | // align the datepicker with the right edge of the input it drops down from 73 | var clicked_elem = $(inst); 74 | clicked_elem.closest(".qgrid-dropdown-menu").addClass("calendar-open"); 75 | 76 | var widget = clicked_elem.datepicker('widget'); 77 | widget.css('margin-left', $(input).outerWidth() - widget.outerWidth()); 78 | widget.addClass("stay-open-on-click filter-child-elem"); 79 | }, 80 | onSelect: (dateText, instance) => { 81 | // pull the values from the datepickers 82 | var start_date_string = this.start_date_control.val(); 83 | var end_date_string = this.end_date_control.val(); 84 | 85 | var start_date = new Date(start_date_string); 86 | var end_date = new Date(end_date_string); 87 | 88 | start_date = Date.UTC(start_date.getUTCFullYear(), start_date.getUTCMonth(), start_date.getUTCDate()); 89 | end_date = Date.UTC(end_date.getUTCFullYear(), end_date.getUTCMonth(), end_date.getUTCDate()); 90 | end_date += (1000 * 60 * 60 * 24) - 1; 91 | 92 | this.filter_start_date = start_date; 93 | this.filter_end_date = end_date; 94 | 95 | this.send_filter_changed(); 96 | 97 | var datepicker = $(instance.input); 98 | setTimeout((function () { 99 | datepicker.blur(); 100 | }), 100); 101 | 102 | if (datepicker.hasClass("start-date")) { 103 | // update the end date's min 104 | this.end_date_control.datepicker("option", "minDate", start_date); 105 | } 106 | if (datepicker.hasClass("end-date")) { 107 | // update the start date's max 108 | this.start_date_control.datepicker("option", "maxDate", new Date(end_date_string)); 109 | } 110 | } 111 | }; 112 | 113 | this.filter_elem.find(".datepicker").datepicker(date_options); 114 | 115 | if (this.filter_start_date != null){ 116 | this.start_date_control.datepicker("setDate", this.get_utc_date(this.filter_start_date)); 117 | } else { 118 | this.start_date_control.datepicker("setDate", this.min_date); 119 | } 120 | 121 | if (this.filter_end_date != null){ 122 | this.end_date_control.datepicker("setDate", this.get_utc_date(this.filter_end_date)); 123 | } else { 124 | this.end_date_control.datepicker("setDate", this.max_date); 125 | } 126 | } 127 | 128 | get_utc_date(date_ms) { 129 | var date = new Date(date_ms); 130 | return new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()); 131 | } 132 | 133 | get_filter_info() { 134 | return { 135 | "field": this.field, 136 | "type": "date", 137 | "min": this.filter_start_date, 138 | "max": this.filter_end_date 139 | }; 140 | } 141 | 142 | is_active() { 143 | return this.filter_start_date || this.filter_end_date; 144 | } 145 | } 146 | 147 | module.exports = {'DateFilter': DateFilter}; 148 | -------------------------------------------------------------------------------- /js/src/qgrid.editors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Input handlers 3 | * 4 | * Adapted from https://github.com/mleibman/SlickGrid/blob/master/slick.editors.js 5 | * MIT License, Copyright (c) 2010 Michael Leibman 6 | */ 7 | var $ = require('jquery'); 8 | require('slickgrid-qgrid/slick.editors.js'); 9 | 10 | class IndexEditor { 11 | constructor(args){ 12 | this.column_info = args.column; 13 | this.$cell = $(args.container); 14 | this.$cell.attr('title', 15 | 'Editing index columns is not supported'); 16 | this.$cell.tooltip(); 17 | this.$cell.tooltip('enable'); 18 | this.$cell.tooltip("open"); 19 | // automatically hide it after 4 seconds 20 | setTimeout((event, ui) => { 21 | this.$cell.tooltip('destroy'); 22 | args.cancelChanges(); 23 | }, 3000); 24 | } 25 | 26 | destroy() {} 27 | 28 | focus() {} 29 | 30 | loadValue(item) { 31 | this.$cell.text( 32 | this.column_info.formatter( 33 | null, null, item[this.column_info.field], this.column_info, null 34 | ) 35 | ); 36 | } 37 | 38 | serializeValue() {} 39 | 40 | applyValue(item, state) {} 41 | 42 | isValueChanged() { 43 | return false; 44 | } 45 | 46 | validate() { 47 | return { 48 | valid: true, 49 | msg: null 50 | }; 51 | } 52 | } 53 | 54 | // http://stackoverflow.com/a/22118349 55 | class SelectEditor { 56 | constructor(args) { 57 | this.column_info = args.column; 58 | this.options = []; 59 | if (this.column_info.editorOptions.options) { 60 | this.options = this.column_info.editorOptions.options; 61 | } else { 62 | this.options = ["yes", "no"]; 63 | } 64 | 65 | var option_str = ""; 66 | 67 | this.elem = $(" 17 | 18 |
19 | 20 |
21 | 25 | 26 | `; 27 | } 28 | 29 | handle_msg(msg) { 30 | var column_info = msg.col_info; 31 | if (msg.type == 'update_data_view_filter'){ 32 | this.update_data_view(column_info); 33 | } 34 | super.handle_msg(msg); 35 | } 36 | 37 | update_min_max(col_info, has_active_filter) { 38 | this.values = col_info.values; 39 | this.length = col_info.length; 40 | this.value_range = col_info.value_range; 41 | this.selected_rows = []; 42 | for (var i = 0; i < col_info.selected_length; i++) { 43 | this.selected_rows.push(i); 44 | } 45 | this.ignore_selection_changed = true; 46 | this.show_filter(); 47 | this.ignore_selection_changed = false; 48 | } 49 | 50 | update_data_view(col_info) { 51 | if (this.update_timeout) { 52 | clearTimeout(this.update_timeout); 53 | } 54 | 55 | this.update_timeout = setTimeout(() => { 56 | this.values = col_info.values; 57 | this.length = col_info.length; 58 | this.value_range = col_info.value_range; 59 | 60 | if (this.length === 0) { 61 | this.filter_elem.find('.no-results').removeClass('hidden'); 62 | this.filter_grid_elem.addClass('hidden'); 63 | return; 64 | } 65 | 66 | this.filter_elem.find('.no-results').addClass('hidden'); 67 | this.filter_grid_elem.removeClass('hidden'); 68 | this.ignore_selection_changed = true; 69 | this.update_slick_grid_data(); 70 | this.filter_grid.setData(this.data_view); 71 | this.selected_rows = []; 72 | for (var i = 0; i < col_info.selected_length; i++) { 73 | this.selected_rows.push(i); 74 | this.row_selection_model.setSelectedRows(this.selected_rows); 75 | } 76 | this.filter_grid.render(); 77 | this.ignore_selection_changed = false; 78 | }, 100); 79 | } 80 | 81 | update_slick_grid_data() { 82 | this.grid_items = this.values.map(function (value, index) { 83 | return { 84 | id: value, 85 | value: value 86 | }; 87 | }); 88 | 89 | this.data_view = { 90 | getLength: () => { 91 | return this.length; 92 | }, 93 | getItem: (i) => { 94 | var default_row = { 95 | id: 'row' + i, 96 | value: '' 97 | }; 98 | if (i >= this.value_range[0] && i < this.value_range[1]) { 99 | return this.grid_items[i - this.value_range[0]] || default_row; 100 | } else { 101 | return default_row; 102 | } 103 | } 104 | }; 105 | } 106 | 107 | initialize_controls() { 108 | super.initialize_controls(); 109 | this.filter_grid_elem = this.filter_elem.find(".text-filter-grid"); 110 | this.search_string = ""; 111 | 112 | this.update_slick_grid_data(); 113 | 114 | this.sort_comparer = (x, y) => { 115 | var x_value = x.value; 116 | var y_value = y.value; 117 | 118 | // selected row should be sorted to the top 119 | if (x.selected != y.selected) { 120 | return x.selected ? -1 : 1; 121 | } 122 | 123 | return x_value > y_value ? 1 : -1; 124 | }; 125 | 126 | var text_filter = (item, args) => { 127 | if (this.search_string) { 128 | if (item.value.toLowerCase().indexOf(this.search_string.toLowerCase()) == -1) { 129 | return false; 130 | } 131 | } 132 | return true; 133 | }; 134 | 135 | var row_formatter = function (row, cell, value, columnDef, dataContext) { 136 | return "" + dataContext.value + ""; 137 | }; 138 | 139 | var checkboxSelector = new Slick.CheckboxSelectColumn({ 140 | cssClass: "check-box-cell" 141 | }); 142 | 143 | var columns = [ 144 | checkboxSelector.getColumnDefinition(), 145 | { 146 | id: "name", 147 | name: "Name", 148 | field: "name", 149 | formatter: row_formatter, 150 | sortable: true 151 | }]; 152 | 153 | var options = { 154 | enableCellNavigation: true, 155 | fullWidthRows: true, 156 | syncColumnCellResize: true, 157 | rowHeight: 32, 158 | forceFitColumns: true, 159 | enableColumnReorder: false 160 | }; 161 | 162 | var max_height = options.rowHeight * 8; 163 | // Subtract 110 from viewport height to account for the height of the header + search box + footer 164 | // of the filter control. This value can't be calculated dynamically because the filter control 165 | // hasn't been shown yet. 166 | var qgrid_viewport_height = this.column_header_elem.closest('.slick-header').siblings('.slick-viewport').height() - 115; 167 | if (qgrid_viewport_height < max_height) { 168 | max_height = qgrid_viewport_height; 169 | } 170 | 171 | var grid_height = max_height; 172 | // totalRowHeight is how tall the grid would have to be to fit all of the rows in the dataframe. 173 | var total_row_height = (this.grid_items.length) * options.rowHeight; 174 | 175 | if (total_row_height <= max_height) { 176 | grid_height = total_row_height; 177 | this.filter_grid_elem.addClass('hide-scrollbar'); 178 | } 179 | this.filter_grid_elem.height(grid_height); 180 | 181 | this.filter_grid = new Slick.Grid( 182 | this.filter_grid_elem, this.data_view, columns, options 183 | ); 184 | this.filter_grid.registerPlugin(checkboxSelector); 185 | 186 | this.row_selection_model = new Slick.RowSelectionModel({ 187 | selectActiveRow: false 188 | }); 189 | this.row_selection_model.onSelectedRangesChanged.subscribe( 190 | (e, args) => this.handle_selection_changed(e, args) 191 | ); 192 | 193 | this.filter_grid.setSelectionModel(this.row_selection_model); 194 | this.row_selection_model.setSelectedRows(this.selected_rows); 195 | 196 | if (this.column_type != 'any') { 197 | this.filter_grid.onViewportChanged.subscribe((e, args) => { 198 | if (this.viewport_timeout) { 199 | clearTimeout(this.viewport_timeout); 200 | } 201 | this.viewport_timeout = setTimeout(() => { 202 | var vp = args.grid.getViewport(); 203 | var msg = { 204 | 'type': 'change_filter_viewport', 205 | 'field': this.field, 206 | 'top': vp.top, 207 | 'bottom': vp.bottom 208 | }; 209 | this.widget_model.send(msg); 210 | this.viewport_timeout = null; 211 | }, 100); 212 | }); 213 | } 214 | 215 | this.filter_grid.render(); 216 | 217 | this.security_search = this.filter_elem.find(".search-input"); 218 | this.security_search.keyup((e) => this.handle_text_input_key_up(e)); 219 | this.security_search.click((e) => this.handle_text_input_click(e)); 220 | 221 | this.filter_grid.onClick.subscribe( 222 | (e, args) => this.handle_grid_clicked(e, args) 223 | ); 224 | this.filter_grid.onKeyDown.subscribe( 225 | (e, args) => this.handle_grid_key_down(e, args) 226 | ); 227 | 228 | this.filter_elem.find("a.select-all-link").click((e) => { 229 | this.ignore_selection_changed = true; 230 | this.reset_filter(); 231 | this.filter_list = "all"; 232 | var all_row_indices = []; 233 | for (var i = 0; i < this.length; i++) { 234 | all_row_indices.push(i); 235 | } 236 | this.row_selection_model.setSelectedRows(all_row_indices); 237 | this.ignore_selection_changed = false; 238 | this.send_filter_changed(); 239 | return false; 240 | }); 241 | 242 | setTimeout(() => { 243 | this.filter_grid.setColumns(this.filter_grid.getColumns()); 244 | this.filter_grid.resizeCanvas(); 245 | }, 10); 246 | } 247 | 248 | toggle_row_selected(row_index) { 249 | var old_selected_rows = this.row_selection_model.getSelectedRows(); 250 | // if the row is already selected, remove it from the selected rows array. 251 | var selected_rows = old_selected_rows.filter(function (word) { 252 | return word !== row_index; 253 | }); 254 | // otherwise add it to the selected rows array so it gets selected 255 | if (selected_rows.length == old_selected_rows.length) { 256 | selected_rows.push(row_index); 257 | } 258 | this.row_selection_model.setSelectedRows(selected_rows); 259 | } 260 | 261 | handle_grid_clicked(e, args) { 262 | this.toggle_row_selected(args.row); 263 | var active_cell = this.filter_grid.getActiveCell(); 264 | if (!active_cell) { 265 | e.stopImmediatePropagation(); 266 | } 267 | } 268 | 269 | handle_grid_key_down(e, args) { 270 | var active_cell = this.filter_grid.getActiveCell(); 271 | if (active_cell) { 272 | if (e.keyCode == 13) { // enter key 273 | this.toggle_row_selected(active_cell.row); 274 | return; 275 | } 276 | 277 | // focus on the search box for any key other than the up/down arrows 278 | if (e.keyCode != 40 && e.keyCode != 38) { 279 | this.focus_on_search_box(); 280 | return; 281 | } 282 | 283 | // also focus on the search box if we're at the top of the grid and this is the up arrow 284 | else if (active_cell.row == 0 && e.keyCode == 38) { 285 | this.focus_on_search_box(); 286 | e.preventDefault(); 287 | return; 288 | } 289 | } 290 | } 291 | 292 | focus_on_search_box() { 293 | this.security_search.focus().val(this.search_string); 294 | this.filter_grid.resetActiveCell(); 295 | } 296 | 297 | handle_text_input_key_up(e) { 298 | var old_search_string = this.search_string; 299 | if (e.keyCode == 40) { // down arrow 300 | this.filter_grid.focus(); 301 | this.filter_grid.setActiveCell(0, 0); 302 | return; 303 | } 304 | if (e.keyCode == 13) { // enter key 305 | if (this.security_grid.getDataLength() > 0) { 306 | this.toggle_row_selected(0); 307 | this.security_search.val(""); 308 | } 309 | } 310 | 311 | this.search_string = this.security_search.val(); 312 | if (old_search_string != this.search_string) { 313 | var msg = { 314 | 'type': 'show_filter_dropdown', 315 | 'field': this.field, 316 | 'search_val': this.search_string 317 | }; 318 | this.widget_model.send(msg); 319 | } 320 | } 321 | 322 | handle_text_input_click(e) { 323 | this.filter_grid.resetActiveCell(); 324 | } 325 | 326 | handle_selection_changed(e, args) { 327 | if (this.ignore_selection_changed) { 328 | return false; 329 | } 330 | 331 | var rows = this.row_selection_model.getSelectedRows(); 332 | rows = _.sortBy(rows, function (i) { 333 | return i; 334 | }); 335 | this.excluded_rows = []; 336 | if (this.filter_list == 'all') { 337 | var j = 0; 338 | for (var i = 0; i < this.data_view.getLength(); i++) { 339 | if (rows[j] == i) { 340 | j += 1; 341 | continue; 342 | } else { 343 | this.excluded_rows.push(i); 344 | } 345 | } 346 | } else { 347 | this.filter_list = rows.length > 0 ? rows : null; 348 | } 349 | 350 | this.send_filter_changed(); 351 | } 352 | 353 | is_active() { 354 | return this.filter_list != null; 355 | } 356 | 357 | reset_filter() { 358 | this.ignore_selection_changed = true; 359 | this.search_string = ""; 360 | this.excluded_rows = null; 361 | this.security_search.val(""); 362 | this.row_selection_model.setSelectedRows([]); 363 | this.filter_list = null; 364 | this.send_filter_changed(); 365 | var msg = { 366 | 'type': 'show_filter_dropdown', 367 | 'field': this.field, 368 | 'search_val': this.search_string 369 | }; 370 | this.widget_model.send(msg); 371 | this.ignore_selection_changed = false; 372 | } 373 | 374 | get_filter_info() { 375 | return { 376 | "field": this.field, 377 | "type": "text", 378 | "selected": this.filter_list, 379 | "excluded": this.excluded_rows 380 | }; 381 | } 382 | } 383 | 384 | module.exports = {'TextFilter': TextFilter}; 385 | -------------------------------------------------------------------------------- /js/src/qgrid.widget.js: -------------------------------------------------------------------------------- 1 | var widgets = require('@jupyter-widgets/base'); 2 | var _ = require('underscore'); 3 | var moment = require('moment'); 4 | window.$ = window.jQuery = require('jquery'); 5 | var date_filter = require('./qgrid.datefilter.js'); 6 | var slider_filter = require('./qgrid.sliderfilter.js'); 7 | var text_filter = require('./qgrid.textfilter.js'); 8 | var boolean_filter = require('./qgrid.booleanfilter.js'); 9 | var editors = require('./qgrid.editors.js'); 10 | var dialog = null; 11 | try { 12 | dialog = require('base/js/dialog'); 13 | } catch (e) { 14 | console.warn("Qgrid was unable to load base/js/dialog. " + 15 | "Full screen button won't be available"); 16 | } 17 | var jquery_ui = require('jquery-ui-dist/jquery-ui.min.js'); 18 | 19 | require('slickgrid-qgrid/slick.core.js'); 20 | require('slickgrid-qgrid/lib/jquery.event.drag-2.3.0.js'); 21 | require('slickgrid-qgrid/plugins/slick.rowselectionmodel.js'); 22 | require('slickgrid-qgrid/plugins/slick.checkboxselectcolumn.js'); 23 | require('slickgrid-qgrid/slick.dataview.js'); 24 | require('slickgrid-qgrid/slick.grid.js'); 25 | require('slickgrid-qgrid/slick.editors.js'); 26 | require('style-loader!slickgrid-qgrid/slick.grid.css'); 27 | require('style-loader!slickgrid-qgrid/slick-default-theme.css'); 28 | require('style-loader!jquery-ui-dist/jquery-ui.min.css'); 29 | require('style-loader!./qgrid.css'); 30 | 31 | // Model for the qgrid widget 32 | class QgridModel extends widgets.DOMWidgetModel { 33 | defaults() { 34 | return _.extend(widgets.DOMWidgetModel.prototype.defaults(), { 35 | _model_name : 'QgridModel', 36 | _view_name : 'QgridView', 37 | _model_module : 'qgrid', 38 | _view_module : 'qgrid', 39 | _model_module_version : '^1.1.3', 40 | _view_module_version : '^1.1.3', 41 | _df_json: '', 42 | _columns: {} 43 | }); 44 | } 45 | } 46 | 47 | 48 | // View for the qgrid widget 49 | class QgridView extends widgets.DOMWidgetView { 50 | render() { 51 | // subscribe to incoming messages from the QGridWidget 52 | this.model.on('msg:custom', this.handle_msg, this); 53 | this.initialize_qgrid(); 54 | } 55 | 56 | /** 57 | * Main entry point for drawing the widget, 58 | * including toolbar buttons if necessary. 59 | */ 60 | initialize_qgrid() { 61 | this.$el.empty(); 62 | if (!this.$el.hasClass('q-grid-container')){ 63 | this.$el.addClass('q-grid-container'); 64 | } 65 | this.initialize_toolbar(); 66 | this.initialize_slick_grid(); 67 | } 68 | 69 | initialize_toolbar() { 70 | if (!this.model.get('show_toolbar')){ 71 | this.$el.removeClass('show-toolbar'); 72 | } else { 73 | this.$el.addClass('show-toolbar'); 74 | } 75 | 76 | if (this.toolbar){ 77 | return; 78 | } 79 | 80 | this.toolbar = $("
").appendTo(this.$el); 81 | 82 | let append_btn = (btn_info) => { 83 | return $(` 84 | 91 | `).appendTo(this.toolbar); 92 | }; 93 | 94 | append_btn({ 95 | loading_text: 'Adding...', 96 | event_type: 'add_row', 97 | text: 'Add Row' 98 | }); 99 | 100 | append_btn({ 101 | loading_text: 'Removing...', 102 | event_type: 'remove_row', 103 | text: 'Remove Row' 104 | }); 105 | 106 | this.buttons = this.toolbar.find('.btn'); 107 | this.buttons.attr('title', 108 | 'Not available while there is an active filter'); 109 | this.buttons.tooltip(); 110 | this.buttons.tooltip({ 111 | show: {delay: 300} 112 | }); 113 | this.buttons.tooltip({ 114 | hide: {delay: 100, 'duration': 0} 115 | }); 116 | this.buttons.tooltip('disable'); 117 | 118 | this.full_screen_btn = null; 119 | if (dialog) { 120 | this.full_screen_modal = $('body').find('.qgrid-modal'); 121 | if (this.full_screen_modal.length == 0) { 122 | this.full_screen_modal = $(` 123 | 130 | `).appendTo($('body')); 131 | } 132 | this.full_screen_btn = $(` 133 |