├── .eslintignore ├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md ├── screenshots │ ├── Autoprofiler_social_tag.png │ ├── demo.gif │ └── profiler_sc.png └── workflows │ ├── build.yml │ └── deploy_juplite.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── RELEASE.md ├── TROUBLESHOOTING.md ├── digautoprofiler ├── __init__.py ├── _version.py ├── profile_lib.py └── utils.py ├── environment.yml ├── examples ├── SF Housing Demo.ipynb └── df_housing_sample.csv ├── install.json ├── jupyterlite-requirements.txt ├── package-lock.json ├── package.json ├── postcss.config.cjs ├── pyproject.toml ├── requirements.txt ├── setup.py ├── src ├── ProfilePanel.ts ├── common │ └── exchangeInterfaces.ts ├── components │ ├── ColumnEntry.svelte │ ├── ColumnProfile.svelte │ ├── DFProfile.svelte │ ├── ProfileView.ts │ ├── Profiler.svelte │ ├── actions │ │ ├── outline.ts │ │ └── scrub-action-factory.ts │ ├── create-chart │ │ ├── AddChartButton.svelte │ │ ├── BivariateChartEntry.svelte │ │ ├── BivariateWrapper.svelte │ │ ├── CreateChartMenu.svelte │ │ └── DropdownMenu.svelte │ ├── data-types │ │ ├── Base.svelte │ │ ├── DataTypeIcon.svelte │ │ ├── FormattedDataType.svelte │ │ ├── Interval.svelte │ │ ├── Number.svelte │ │ ├── Timestamp.svelte │ │ ├── Varchar.svelte │ │ ├── duckdb-data-types.ts │ │ └── pandas-data-types.ts │ ├── export-code │ │ ├── ExportChartButton.svelte │ │ ├── ExportFactButton.svelte │ │ └── ExportableCode.ts │ ├── fact-panel │ │ ├── DisplayFact.svelte │ │ ├── NumericalStats.svelte │ │ ├── StringStats.svelte │ │ ├── TempStats.svelte │ │ └── VizOrStats.svelte │ ├── icons │ │ ├── AlertIcon.svelte │ │ ├── BivariateButton.svelte │ │ ├── BivariateIcon.svelte │ │ ├── BooleanType.svelte │ │ ├── Cancel.svelte │ │ ├── CaretDownIcon.svelte │ │ ├── CategoricalType.svelte │ │ ├── Delete.svelte │ │ ├── Done.svelte │ │ ├── EditIcon.svelte │ │ ├── ExportIcon.svelte │ │ ├── FloatType.svelte │ │ ├── IntegerType.svelte │ │ ├── List.svelte │ │ ├── Parquet.svelte │ │ ├── Pin.svelte │ │ ├── RefreshIcon.svelte │ │ ├── RightArrow.svelte │ │ ├── SettingsIcon.svelte │ │ └── TimestampType.svelte │ ├── nav │ │ ├── CollapsibleCard.svelte │ │ ├── ExpanderButton.svelte │ │ ├── RefreshData.svelte │ │ └── SettingsMenu.svelte │ ├── tooltip │ │ ├── FloatingElement.svelte │ │ ├── Portal.svelte │ │ ├── Tooltip.svelte │ │ ├── TooltipContent.svelte │ │ ├── types.d.ts │ │ └── utils.ts │ ├── utils │ │ ├── constants.ts │ │ ├── convertTypes.ts │ │ ├── formatters.ts │ │ ├── guid.ts │ │ ├── sizes.ts │ │ └── sort-utils.ts │ └── viz │ │ ├── BarAndLabel.svelte │ │ ├── ExploreChart.svelte │ │ ├── TopKSummary.svelte │ │ ├── bivariate │ │ ├── Area.svelte │ │ ├── AxisX.svelte │ │ ├── AxisY.svelte │ │ ├── Bar.svelte │ │ ├── BiHistogram.svelte │ │ ├── BiLine.svelte │ │ ├── Column.svelte │ │ └── Line.svelte │ │ ├── histogram │ │ ├── HistogramBase.svelte │ │ ├── NumericHistogram.svelte │ │ ├── SmallHistogram.svelte │ │ ├── SummaryStatLabel.svelte │ │ ├── TimestampHistogram.svelte │ │ └── histogram-tooltip │ │ │ └── HistogramTooltip.svelte │ │ └── timestamp │ │ ├── TimestampBound.svelte │ │ ├── TimestampDetail.svelte │ │ ├── TimestampMouseoverAnnotation.svelte │ │ ├── TimestampPaths.svelte │ │ ├── TimestampProfileSummary.svelte │ │ ├── ZoomWindow.svelte │ │ ├── extremum-resolution-store.ts │ │ └── utils.ts ├── dataAPI │ ├── ProfileModel.ts │ └── jupyter │ │ ├── PythonExecutor.ts │ │ ├── cell.ts │ │ └── notebook.ts ├── index.ts ├── stores.ts └── svg.d.ts ├── style ├── base.css ├── index.css ├── index.js └── logo.svg ├── tailwind.config.cjs ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | **/*.d.ts 5 | tests 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/eslint-recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:prettier/recommended' 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { 10 | project: 'tsconfig.json', 11 | sourceType: 'module' 12 | }, 13 | plugins: ['@typescript-eslint', 'svelte3'], 14 | rules: { 15 | '@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }], 16 | '@typescript-eslint/no-explicit-any': 'off', 17 | '@typescript-eslint/no-namespace': 'off', 18 | '@typescript-eslint/no-use-before-define': 'off', 19 | '@typescript-eslint/quotes': [ 20 | 'error', 21 | 'single', 22 | { avoidEscape: true, allowTemplateLiterals: false } 23 | ], 24 | curly: ['error', 'all'], 25 | eqeqeq: 'error', 26 | 'prefer-arrow-callback': 'error' 27 | }, 28 | overrides: [ 29 | { 30 | files: ['**/*.svelte'], 31 | processor: 'svelte3/svelte3' 32 | } 33 | ] 34 | }; 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **OS, browser, jupyter, and AutoProfiler versions (please complete the following information):** 27 | - OS: [e.g. Chrome] 28 | - Browser [e.g. chrome, safari] 29 | - AutoProfiler version [e.g. 0.2.2]: 30 | - Jupyter version: 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Please describe your use case and feature request** 11 | Screenshots are appreciated if applicable! 12 | 13 | If you have general feedback about your experience using AutoProfiler or would be willing to chat with us about your use case please consider filling out our feedback form: https://forms.gle/V3ejpXxMcQXqYJG48 14 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Functionality 2 | 3 | 4 | 5 | ## Issues addressed 6 | 7 | -------------------------------------------------------------------------------- /.github/screenshots/Autoprofiler_social_tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmudig/AutoProfiler/32f612c23d1056bf8797fc4ac515cac1a35ac1fe/.github/screenshots/Autoprofiler_social_tag.png -------------------------------------------------------------------------------- /.github/screenshots/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmudig/AutoProfiler/32f612c23d1056bf8797fc4ac515cac1a35ac1fe/.github/screenshots/demo.gif -------------------------------------------------------------------------------- /.github/screenshots/profiler_sc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cmudig/AutoProfiler/32f612c23d1056bf8797fc4ac515cac1a35ac1fe/.github/screenshots/profiler_sc.png -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Base Setup 17 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 18 | 19 | - name: Install dependencies 20 | run: python -m pip install -U jupyterlab~=3.1 check-manifest 21 | 22 | - name: Build the extension 23 | run: | 24 | set -eux 25 | jlpm 26 | python -m pip install . 27 | 28 | jupyter labextension list 2>&1 | grep -ie "AutoProfiler.*OK" 29 | python -m jupyterlab.browser_check 30 | 31 | check-manifest -v 32 | 33 | pip install build 34 | python -m build --sdist 35 | cp dist/*.tar.gz myextension.tar.gz 36 | pip uninstall -y "digautoprofiler" jupyterlab 37 | rm -rf myextension 38 | 39 | - uses: actions/upload-artifact@v2 40 | with: 41 | name: myextension-sdist 42 | path: myextension.tar.gz 43 | 44 | test_isolated: 45 | needs: build 46 | runs-on: ubuntu-latest 47 | 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v2 51 | - name: Install Python 52 | uses: actions/setup-python@v2 53 | with: 54 | python-version: '3.8' 55 | architecture: 'x64' 56 | - uses: actions/download-artifact@v2 57 | with: 58 | name: myextension-sdist 59 | - name: Install and Test 60 | run: | 61 | set -eux 62 | # Remove NodeJS, twice to take care of system and locally installed node versions. 63 | sudo rm -rf $(which node) 64 | sudo rm -rf $(which node) 65 | pip install myextension.tar.gz 66 | pip install jupyterlab 67 | jupyter labextension list 2>&1 | grep -ie "AutoProfiler.*OK" 68 | python -m jupyterlab.browser_check --no-chrome-test -------------------------------------------------------------------------------- /.github/workflows/deploy_juplite.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy JupyterLite 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Setup Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.8 21 | - name: Install the dependencies 22 | run: | 23 | python -m pip install -r jupyterlite-requirements.txt 24 | python -m pip install -r requirements.txt 25 | - name: Build the JupyterLite site 26 | run: | 27 | jupyter lite build --contents examples --output-dir jlExamplesDist 28 | - name: Upload artifact 29 | uses: actions/upload-pages-artifact@v1 30 | with: 31 | path: ./jlExamplesDist 32 | 33 | deploy: 34 | needs: build 35 | if: github.ref == 'refs/heads/main' 36 | permissions: 37 | pages: write 38 | id-token: write 39 | 40 | environment: 41 | name: github-pages 42 | url: ${{steps.deployment.outputs.page_url}} 43 | 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v1 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | .eslintcache 5 | .stylelintcache 6 | *.egg-info/ 7 | .ipynb_checkpoints 8 | *.tsbuildinfo 9 | digautoprofiler/labextension 10 | 11 | # Created by https://www.gitignore.io/api/python 12 | # Edit at https://www.gitignore.io/?templates=python 13 | 14 | ### Python ### 15 | # Byte-compiled / optimized / DLL files 16 | __pycache__/ 17 | *.py[cod] 18 | *$py.class 19 | 20 | # C extensions 21 | *.so 22 | 23 | # Distribution / packaging 24 | .Python 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | downloads/ 29 | eggs/ 30 | .eggs/ 31 | lib/ 32 | lib64/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | pip-wheel-metadata/ 38 | share/python-wheels/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | # PyInstaller 44 | # Usually these files are written by a python script from a template 45 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 46 | *.manifest 47 | *.spec 48 | 49 | # Installer logs 50 | pip-log.txt 51 | pip-delete-this-directory.txt 52 | 53 | # Unit test / coverage reports 54 | htmlcov/ 55 | .tox/ 56 | .nox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | 66 | # Translations 67 | *.mo 68 | *.pot 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # pyenv 80 | .python-version 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | .spyproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # Mr Developer 96 | .mr.developer.cfg 97 | .project 98 | .pydevproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | .dmypy.json 106 | dmypy.json 107 | 108 | # Pyre type checker 109 | .pyre/ 110 | 111 | # End of https://www.gitignore.io/api/python 112 | 113 | # OSX files 114 | .DS_Store 115 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | digautoprofiler 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid", 5 | "tabWidth": 4, 6 | "plugins": [ 7 | "prettier-plugin-svelte" 8 | ] 9 | } -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-standard", 5 | "stylelint-prettier/recommended" 6 | ], 7 | "rules": { 8 | "property-no-vendor-prefix": null, 9 | "selector-no-vendor-prefix": null, 10 | "value-no-vendor-prefix": null, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "eslint.validate": [ 6 | "javascript", 7 | "typescript" 8 | ] 9 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Overview of AutoProfiler development 2 | 3 | AutoProfiler is a jupyter extension. It contains frontend typescript code and python code for executing pandas commands. These are bundled together into one jupyter extension that can be pip installed by a user. 4 | 5 | ## Frontend Code 6 | 7 | The frontend svelte code is contained in the `src/` directory. We use the [svelte](https://learn.svelte.dev/tutorial/welcome-to-svelte) framework for components and do styling with [tailwind](https://tailwindcss.com/). 8 | 9 | - [`index.ts`](src/index.ts) is the entrypoint for the extension that jupyter looks at to load the extension. 10 | - The code roughly follows a Model, View, Controller pattern because that is how the jupyter extension examples are structured. 11 | - [`ProfilePanel.ts`](src/ProfilePanel.ts) -- main wrapper that creates a [`ProfileModel`](src/dataAPI/ProfileModel.ts) and [`ProfileView`](src/components/ProfileView.ts) and listens for changes to the notebook. 12 | - [`ProfileView`](src/components/ProfileView.ts) creates a [`Profiler.svelte`](src/components/Profiler.svelte) component that is the root component. 13 | - [`ProfileModel`](src/dataAPI/ProfileModel.ts) handles jupyter interactions to execute python code in the kernel and listen for changes to the notebook. 14 | 15 | ## Python code 16 | 17 | The python code is contained in the `digautoprofiler/` directory. This is mostly utility functions that are called by the frontend code to execute pandas commands and return the results, along with automatically generated jupyter extension code in the `digautoprofiler/labextension` directory (don't manually edit this). 18 | 19 | # Development Setup and workflow 20 | 21 | To run AutoProfiler locally you need to install as a python library that is editable. 22 | 23 | ## First time 24 | 25 | - You will need [NodeJS and npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) installed to build the extension package. 26 | - It is best to make a new conda environment before following steps (e.g. `conda create --name autoprofiler python=3.11`) 27 | 28 | Then pip install the following: 29 | 30 | ```bash 31 | pip install jupyterlab jupyter-packaging 32 | ``` 33 | 34 | Now download and build extension: 35 | 36 | ```bash 37 | git clone https://github.com/cmudig/AutoProfiler.git 38 | cd AutoProfiler 39 | # Install package in development mode 40 | pip install -e . 41 | # Link your development version of the extension with JupyterLab 42 | jupyter labextension develop . --overwrite 43 | # Build Typescript source after making changes 44 | npm run build 45 | ``` 46 | 47 | Jupyter labextension can be weird sometimes; nuking the conda env and restarting tends to fix it. 48 | 49 | ## Devloop: changes to frontend code in `src/` 50 | 51 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. 52 | 53 | ### Terminal 1 54 | 55 | Watch the source directory in one terminal, automatically rebuilding when changes are made to `src/`. You will need to refresh the browser to see changes. 56 | 57 | ```bash 58 | cd AutoProfiler 59 | npm run watch 60 | ``` 61 | 62 | **Warning**: for some reason tailwind is not automatically re-loading classes during watch mode; if you add in new classes you will need to kill and restart the watch process or run `npm run build` to see changes. 63 | 64 | ### Terminal 2 65 | 66 | To test your code you can run a jupyter lab instance in another terminal. **Note**: this does NOT have to be in the same directory (I would encourage you to have separate testing directory); as long as the conda environment is the same, any update to the `digautoprofiler` package will be reflected in the running jupyter lab instance. 67 | 68 | ```bash 69 | cd /path/to/your/testing/directory 70 | conda activate autoprofiler 71 | jupyter lab 72 | ``` 73 | 74 | ## Devloop: changes to python code in `digautoprofiler/` 75 | 76 | Changes to the python utility functions will update when the python module reloads. If you restart the kernel in jupyter, this should update the python package (you don't even have to refresh the page, just kill and restart kernel). 77 | 78 | ## Development uninstall 79 | 80 | ```bash 81 | pip uninstall digautoprofiler 82 | ``` 83 | 84 | In development mode, you will also need to remove the symlink created by `jupyter labextension develop` 85 | command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` 86 | folder is located. Then you can remove the symlink named `digautoprofiler` within that folder. 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Will Epperson 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include *.md 3 | include pyproject.toml 4 | 5 | include package.json 6 | include install.json 7 | include ts*.json 8 | include yarn.lock 9 | include requirements.txt 10 | include jupyterlite-requirements.txt 11 | include environment.yml 12 | 13 | graft digautoprofiler/labextension 14 | 15 | # Javascript files 16 | graft src 17 | graft style 18 | prune **/node_modules 19 | prune lib 20 | prune binder 21 | 22 | # extra rules to make match vc 23 | graft examples 24 | prune .vscode 25 | 26 | # Patterns to exclude from any directory 27 | global-exclude *~ 28 | global-exclude *.pyc 29 | global-exclude *.pyo 30 | global-exclude .git 31 | global-exclude .ipynb_checkpoints 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | PyPi 6 | 7 | 8 | Binder 9 | 10 | 11 | Lite 12 | 13 |

14 | 15 | Profile your [Pandas](https://pandas.pydata.org) Dataframes! Autoprofiler will automatically visualize your Pandas dataframes after every execution, no extra code necessary. 16 | 17 | Autoprofiler allows you to spend less time specifying charts and more time interacting with your data by automatically showing you profiling information like: 18 | 19 | - Distribution of each column 20 | - Sample values 21 | - Summary statistics 22 | 23 | ## Updates profiles as your data updates 24 | 25 | ![screenshot of Autoprofiler](https://raw.githubusercontent.com/cmudig/AutoProfiler/main/.github/screenshots/profiler_sc.png) 26 | 27 | Autoprofiler reads your current Jupyter notebook and produces profiles for the Pandas Dataframes in your memory as they change. 28 | 29 | https://user-images.githubusercontent.com/13400543/199877605-ba50f9c8-87e5-46c9-8207-1c6496bb3b18.mov 30 | 31 | ## Install 32 | 33 | To instally locally use pip and then open jupyter lab and the extension will be running. 34 | 35 | ```bash 36 | pip install -U digautoprofiler 37 | ``` 38 | 39 | Please note, AutoProfiler only works in [JupyterLab](https://jupyter.org/install) with version >=3.x, < 4.0.0. 40 | 41 | ## Try it out 42 | 43 | To try out Autoprofiler in a hosted notebook, use one of the options below 44 | 45 | | Jupyter Lite | Binder | 46 | | :-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------: | 47 | | [![Lite](https://gist.githubusercontent.com/willeppy/35cdc20a3fc26e393ce76f1df35bcdfc/raw/a7fca1d0a2d62c2b49f60c0217dffbd0fe404471/lite-badge-launch-small.svg)](http://dig.cmu.edu/AutoProfiler) | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/cmudig/AutoProfiler/HEAD?labpath=examples%2FSF%20Housing%20Demo.ipynb) | 48 | 49 | **Browser support:** AutoProfiler has been developed and tested with Chrome. 50 | 51 | ## Development Install 52 | 53 | For development install instructions, see [CONTRIBUTING.md](CONTRIBUTING.md). 54 | 55 | If you're having install issues, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md). 56 | 57 | ## Acknowledgements 58 | 59 | Big thanks to the Rill Data team! Much of our profiler UI code is adapted from [Rill Developer](https://github.com/rilldata/rill-developer). 60 | 61 | ## Citation 62 | 63 | Please reference our [VIS'23 paper](https://arxiv.org/abs/2308.03964): 64 | 65 | ```bibtex 66 | @article{epperson23autoprofiler, 67 | title={Dead or Alive: Continuous Data Profiling for Interactive Data Science}, 68 | author={Will Epperson and Vaishnavi Goranla and Dominik Moritz and Adam Perer}, 69 | journal={IEEE Transactions on Visualization and Computer Graphics}, 70 | year={2023}, 71 | url={https://arxiv.org/abs/2308.03964} 72 | } 73 | ``` 74 | 75 | ## Let us know what you think! 📢 76 | 77 | We would love to hear your feedback on how you are using AutoProfiler! Please fill out [this form](https://forms.gle/V3ejpXxMcQXqYJG48) or email Will at [willepp@cmu.edu](mailto:willepp@cmu.edu). 78 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of AutoProfiler 2 | 3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 4 | 5 | ## Manual release 6 | 7 | ### Python package 8 | 9 | This extension can be distributed as Python 10 | packages. All of the Python 11 | packaging instructions in the `pyproject.toml` file to wrap your extension in a 12 | Python package. Before generating a package, we first need to install `build`. 13 | 14 | ```bash 15 | pip install build twine 16 | ``` 17 | 18 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 19 | 20 | ```bash 21 | npm install 22 | npm run build 23 | python3 -m build 24 | ``` 25 | 26 | 27 | 28 | Then to upload the package to PyPI, do: 29 | 30 | ```bash 31 | twine upload dist/* 32 | ``` 33 | 34 | ### NPM package 35 | 36 | To publish the frontend part of the extension as a NPM package, do: 37 | 38 | ```bash 39 | npm login 40 | npm publish --access public 41 | ``` 42 | 43 | 58 | 59 | ## Publishing to `conda-forge` 60 | 61 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html 62 | 63 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 64 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | Installing Jupyter extensions can be tricky, these are some common issues we've run into. If your issue isn't covered here please [open an issue](https://github.com/cmudig/AutoProfiler/issues) in the repo and we'll try to help you out. 4 | 5 | ### How do I load this into a fresh conda env? 6 | 7 | ```bash 8 | conda create -n autoprofiler-env python 9 | conda activate digautoprofiler-env 10 | pip install digautoprofiler 11 | ``` 12 | 13 | ### Why isn't anything showing up? 14 | 15 | After pip installing, if you're not seeing AutoProfiler in the side bar then the extension was not installed properly. Jupyter reads extensions from a directory wherever it is installed. 16 | 17 | Check 1: Run the command below in your terminal. 18 | 19 | ```bash 20 | jupyter labextension list 21 | ``` 22 | 23 | All of your extensions should list in the terminal like 24 | 25 | ```bash 26 | digautoprofiler v0.1.1 enabled OK (python, digautoprofiler) 27 | ``` 28 | 29 | If this is not showing, then the extension did not install to the same location that jupyter is looking for. One way to check this is to run 30 | 31 | ```bash 32 | which jupyter 33 | which pip 34 | ``` 35 | 36 | These should point to the same conda env if you're using conda. For example, if you made the conda env from earlier in the troubleshooting then it might be `~/opt/anaconda3/envs/digautoprofiler-env/bin/` 37 | 38 | Check 2: Another way to make sure the extension is installed is to run jupyter and then click on the puzzle piece in the left side bar. This shows all your activate extensions. These should be allowed to run and digautoprofiler should be listed. 39 | -------------------------------------------------------------------------------- /digautoprofiler/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from ._version import __version__ 5 | from .profile_lib import * 6 | 7 | HERE = Path(__file__).parent.resolve() 8 | 9 | with (HERE / "labextension" / "package.json").open() as fid: 10 | data = json.load(fid) 11 | 12 | 13 | def _jupyter_labextension_paths(): 14 | return [{ 15 | "src": "labextension", 16 | "dest": data["name"] 17 | }] -------------------------------------------------------------------------------- /digautoprofiler/_version.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | __all__ = ["__version__"] 5 | 6 | def _fetchVersion(): 7 | HERE = Path(__file__).parent.resolve() 8 | 9 | for settings in HERE.rglob("package.json"): 10 | try: 11 | with settings.open() as f: 12 | version = json.load(f)["version"] 13 | return ( 14 | version.replace("-alpha.", "a") 15 | .replace("-beta.", "b") 16 | .replace("-rc.", "rc") 17 | ) 18 | except FileNotFoundError: 19 | pass 20 | 21 | raise FileNotFoundError(f"Could not find package.json under dir {HERE!s}") 22 | 23 | __version__ = _fetchVersion() 24 | -------------------------------------------------------------------------------- /digautoprofiler/utils.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | 3 | def convertVC(vc: pd.Series, colName: str): 4 | # this behavior changed in pandas 2.0.0 5 | if pd.__version__ >= "2.0.0": 6 | return vc.reset_index().rename(columns={colName: "value"}).to_dict('records') 7 | 8 | return vc.reset_index().rename( 9 | columns={"index": "value", 10 | colName: "count"}).to_dict('records') 11 | 12 | def convertDescribe(statistics: pd.Series): 13 | s = statistics.to_dict() 14 | 15 | return { 16 | "min": s["min"], 17 | "q25": s["25%"], 18 | "q50": s["50%"], 19 | "q75": s["75%"], 20 | "max": s["max"], 21 | "mean": s["mean"], 22 | } 23 | 24 | def convertBinned(numVC: pd.Series, true_min): 25 | """ 26 | numVC has interval index from binned value counts. Replace far left low 27 | with true min because pandas cuts below min 28 | """ 29 | d = pd.DataFrame( 30 | {"low": numVC.index.left, 31 | "high": numVC.index.right, 32 | "count": numVC.values 33 | }) 34 | d = d.reset_index().rename(columns={"index": "bucket"}) 35 | d_dict = d.to_dict('records') 36 | 37 | if len(d_dict) > 0: 38 | d_dict[0]['low'] = true_min 39 | 40 | return d_dict -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: digautoprofiler-env 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python 6 | - pip 7 | - pip: 8 | - digautoprofiler 9 | -------------------------------------------------------------------------------- /examples/SF Housing Demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "ed4ecfc3", 6 | "metadata": {}, 7 | "source": [ 8 | "# Autoprofiler demo: SF Housing" 9 | ] 10 | }, 11 | { 12 | "cell_type": "code", 13 | "execution_count": null, 14 | "id": "ca93f31d-656d-4824-bca6-5d09df6957f0", 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import pandas as pd" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "id": "15c95ba0-2bd8-4a38-9f31-770f3625cbd6", 24 | "metadata": {}, 25 | "source": [ 26 | "## What is AutoProfiler?\n", 27 | "AutoProfiler shows you data _profiles_ for any pandas dataframe in the left side bar of Jupyter. Whenever you load or update a dataframe, autoprofiler will show you information like distributions, common values, missing values etc. \n", 28 | "\n", 29 | "__To install and use locally: `pip install -U digautoprofiler`__\n", 30 | "\n", 31 | "## This demo\n", 32 | "This dataset is a sample of Bay Area rents from 2000-2018 gathered from [this site](https://www.katepennington.org/data). Try exploring the dataset and open **Autoprofiler** in the left panel to see profiling charts!" 33 | ] 34 | }, 35 | { 36 | "cell_type": "markdown", 37 | "id": "1d2d08bf", 38 | "metadata": {}, 39 | "source": [ 40 | "If this demo is running in jupyterlite, we have to read the dataframe and install dependencies in a slighly different way, if not we can just load it normally:" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "id": "5fede952-b5bf-40a4-b080-9239eabd0ee0", 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "import sys\n", 51 | "\n", 52 | "if sys.platform == \"emscripten\":\n", 53 | " # running in jupyterlite, so we need to manually install dependencies\n", 54 | " import piplite\n", 55 | " await piplite.install('altair')\n", 56 | " await piplite.install('digautoprofiler')\n", 57 | "\n", 58 | " # read the data for jupyter lite\n", 59 | " import pyodide\n", 60 | " URL = \"https://raw.githubusercontent.com/cmudig/AutoProfiler/main/examples/df_housing_sample.csv\"\n", 61 | " df_housing = pd.read_csv(pyodide.http.open_url(URL))\n", 62 | "else:\n", 63 | " df_housing = pd.read_csv(\"./df_housing_sample.csv\")" 64 | ] 65 | }, 66 | { 67 | "cell_type": "markdown", 68 | "id": "ba9bc295-df10-4e75-b426-e96b60f25466", 69 | "metadata": {}, 70 | "source": [ 71 | "The profile charts will update whenever the data updates, including changing the type:" 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": null, 77 | "id": "66938dcd-fd82-4bf2-9a7f-85cfd009d8e4", 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [ 81 | "df_housing[\"date\"] = pd.to_datetime(df_housing[\"date\"], format=\"%Y%m%d\")" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "id": "37fbb000-61ca-4dc6-a8bd-08fcbd9c7ffe", 87 | "metadata": {}, 88 | "source": [ 89 | "Or whenever a new dataframe is created:" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": null, 95 | "id": "eca5c953-4d29-49f7-8866-c47582780ad5", 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "expensive_rents = df_housing[df_housing[\"price\"] > 5000]" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "id": "1db013fa", 105 | "metadata": {}, 106 | "source": [ 107 | "Try different pandas commands to...\n", 108 | "- See how your dataframes profile changes after a command\n", 109 | "- Compare dataframes\n", 110 | "- See most common values, distributions, cardinality, null values and more!" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "id": "7d9229d4-8407-46de-85e4-4032e16796f0", 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [] 120 | } 121 | ], 122 | "metadata": { 123 | "kernelspec": { 124 | "display_name": "Python 3.9.10 64-bit", 125 | "language": "python", 126 | "name": "python3" 127 | }, 128 | "language_info": { 129 | "codemirror_mode": { 130 | "name": "ipython", 131 | "version": 3 132 | }, 133 | "file_extension": ".py", 134 | "mimetype": "text/x-python", 135 | "name": "python", 136 | "nbconvert_exporter": "python", 137 | "pygments_lexer": "ipython3", 138 | "version": "3.9.10" 139 | }, 140 | "vscode": { 141 | "interpreter": { 142 | "hash": "aee8b7b246df8f9039afb4144a1f6fd8d2ca17a180786b69acc140d282b71a49" 143 | } 144 | } 145 | }, 146 | "nbformat": 4, 147 | "nbformat_minor": 5 148 | } 149 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "digautoprofiler", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package digautoprofiler" 5 | } -------------------------------------------------------------------------------- /jupyterlite-requirements.txt: -------------------------------------------------------------------------------- 1 | jupyterlite>=0.1.0b13 2 | digautoprofiler>=0.2.8 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digautoprofiler", 3 | "version": "3.0.1", 4 | "description": "Automatically profile your pandas dataframes in jupyter lab.", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/cmudig/AutoProfiler", 11 | "bugs": { 12 | "url": "https://github.com/cmudig/AutoProfiler/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "Will Epperson", 17 | "email": "willepp@live.com" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 22 | ], 23 | "main": "lib/index.js", 24 | "types": "lib/index.d.ts", 25 | "style": "style/index.css", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/cmudig/AutoProfiler.git" 29 | }, 30 | "scripts": { 31 | "build": "jlpm build:lib && jlpm build:labextension:dev", 32 | "build:prod": "jlpm clean && npm ci && tsc && webpack --mode=production && jlpm build:labextension", 33 | "build:labextension": "jupyter labextension build .", 34 | "build:labextension:dev": "jupyter labextension build --development True .", 35 | "build:lib": "tsc && webpack --mode=development", 36 | "clean": "jlpm clean:lib clean:labextension", 37 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 38 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 39 | "clean:labextension": "rimraf digautoprofiler/labextension", 40 | "clean:rest": "rimraf digautoprofiler.egg-info node_modules dist", 41 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache && jlpm clean:rest", 42 | "eslint": "jlpm eslint:check --fix", 43 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 44 | "install:extension": "npm ci && jlpm build", 45 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 46 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 47 | "prettier": "jlpm prettier:base --write --list-different", 48 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md,.svelte}\"", 49 | "prettier:check": "jlpm prettier:base --check", 50 | "stylelint": "jlpm stylelint:check --fix", 51 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 52 | "watch": "run-p watch:src watch:labextension", 53 | "watch:src": "webpack --watch --mode=development", 54 | "watch:labextension": "jupyter labextension watch ." 55 | }, 56 | "dependencies": { 57 | "@jupyterlab/application": "^3.4.8", 58 | "@jupyterlab/notebook": "^3.4.8", 59 | "d3-format": "^3.1.0", 60 | "d3-scale": "^4.0.2", 61 | "d3-shape": "^3.1.0", 62 | "d3-time": "^3.0.0", 63 | "d3-time-format": "^4.1.0", 64 | "layercake": "^7.2.2", 65 | "lodash": "^4.17.21" 66 | }, 67 | "devDependencies": { 68 | "@babel/core": "^7.19.3", 69 | "@babel/preset-env": "^7.19.3", 70 | "@jupyterlab/builder": "^3.4.8", 71 | "@tsconfig/svelte": "^3.0.0", 72 | "@types/lodash": "^4.14.186", 73 | "@types/webpack-env": "^1.18.0", 74 | "@typescript-eslint/eslint-plugin": "^5.39.0", 75 | "@typescript-eslint/parser": "^5.39.0", 76 | "arquero": "^4.8.8", 77 | "autoprefixer": "^10.4.14", 78 | "css-loader": "^6.7.1", 79 | "eslint": "^8.24.0", 80 | "eslint-config-prettier": "^8.5.0", 81 | "eslint-plugin-prettier": "^4.2.1", 82 | "eslint-plugin-svelte3": "^4.0.0", 83 | "npm-run-all": "^4.1.5", 84 | "postcss": "^8.4.24", 85 | "prettier": "^2.7.1", 86 | "prettier-plugin-svelte": "^2.7.1", 87 | "rimraf": "^3.0.2", 88 | "source-map-loader": "^4.0.0", 89 | "style-loader": "^3.3.1", 90 | "stylelint": "^14.13.0", 91 | "stylelint-config-prettier": "^9.0.3", 92 | "stylelint-config-recommended": "^7.0.0", 93 | "stylelint-config-standard": "~25.0.0", 94 | "stylelint-prettier": "^2.0.0", 95 | "svelte": "^3.50.1", 96 | "svelte-collapse": "^0.1.1", 97 | "svelte-loader": "^3.1.3", 98 | "svelte-loading-spinners": "^0.1.7", 99 | "svelte-preprocess": "^4.10.7", 100 | "svg-url-loader": "^8.0.0", 101 | "tailwindcss": "^3.3.2", 102 | "ts-loader": "^9.4.1", 103 | "typescript": "^4.8.4", 104 | "webpack": "^5.74.0", 105 | "webpack-cli": "^4.10.0" 106 | }, 107 | "sideEffects": [ 108 | "style/*.css", 109 | "style/index.js" 110 | ], 111 | "styleModule": "style/index.js", 112 | "publishConfig": { 113 | "access": "public" 114 | }, 115 | "jupyterlab": { 116 | "extension": true, 117 | "outputDir": "digautoprofiler/labextension", 118 | "sharedPackages": { 119 | "@jupyter-widgets/base": { 120 | "bundled": false, 121 | "singleton": true 122 | }, 123 | "@jupyterlab/notebook": { 124 | "bundled": false, 125 | "singleton": true 126 | }, 127 | "@lumino/widgets": { 128 | "bundled": false, 129 | "singleton": true 130 | } 131 | } 132 | }, 133 | "jupyter-releaser": { 134 | "hooks": { 135 | "before-build-npm": [ 136 | "python -m pip install jupyterlab~=3.1", 137 | "jlpm" 138 | ], 139 | "before-build-python": [ 140 | "jlpm clean:all" 141 | ] 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["jupyter_packaging~=0.10,<2", "jupyterlab~=3.1"] 3 | build-backend = "jupyter_packaging.build_api" 4 | 5 | [tool.jupyter-packaging.options] 6 | skip-if-exists = ["digautoprofiler/labextension/static/style.js"] 7 | ensured-targets = ["digautoprofiler/labextension/static/style.js", "digautoprofiler/labextension/package.json"] 8 | 9 | [tool.jupyter-packaging.builder] 10 | factory = "jupyter_packaging.npm_builder" 11 | 12 | [tool.jupyter-packaging.build-args] 13 | build_cmd = "build:prod" 14 | npm = ["jlpm"] 15 | 16 | [tool.check-manifest] 17 | ignore = ["digautoprofiler/labextension/**", "yarn.lock", ".*", "package-lock.json", "*.cjs", "*.config.js"] 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas>=1.4.2 2 | altair 3 | jupyterlab >=3,<4.0.0 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | digautoprofiler setup 3 | """ 4 | import json 5 | import sys 6 | from pathlib import Path 7 | # from os import path 8 | 9 | import setuptools 10 | 11 | HERE = Path(__file__).parent.resolve() 12 | 13 | # Get the package info from package.json 14 | pkg_json = json.loads((HERE / "package.json").read_bytes()) 15 | 16 | with open((HERE / "requirements.txt")) as fp: 17 | install_requires = fp.read() 18 | 19 | # install_requires = [ 20 | # "jupyterlab>=3.3.3", 21 | # "pandas>=1.4.2" 22 | # ] 23 | 24 | # The name of the project 25 | name = "digautoprofiler" 26 | 27 | lab_path = (HERE / pkg_json["jupyterlab"]["outputDir"]) 28 | 29 | # Representative files that should exist after a successful build 30 | ensured_targets = [ 31 | str(lab_path / "package.json"), 32 | str(lab_path / "static/style.js") 33 | ] 34 | 35 | labext_name = pkg_json["name"] 36 | 37 | data_files_spec = [ 38 | ("share/jupyter/labextensions/%s" % labext_name, str(lab_path.relative_to(HERE)), "**"), 39 | ("share/jupyter/labextensions/%s" % labext_name, str("."), "install.json"), 40 | ] 41 | 42 | long_description = (HERE / "README.md").read_text() 43 | 44 | version = ( 45 | pkg_json["version"] 46 | .replace("-alpha.", "a") 47 | .replace("-beta.", "b") 48 | .replace("-rc.", "rc") 49 | ) 50 | 51 | setup_args = dict( 52 | name=name, # pypi name 53 | version=version, 54 | url=pkg_json["homepage"], 55 | author=pkg_json["author"]["name"], 56 | author_email=pkg_json["author"]["email"], 57 | description=pkg_json["description"], 58 | license=pkg_json["license"], 59 | license_file="LICENSE", 60 | long_description=long_description, 61 | long_description_content_type="text/markdown", 62 | packages=setuptools.find_packages(), 63 | install_requires=install_requires, 64 | zip_safe=False, 65 | include_package_data=True, 66 | python_requires=">=3.7", 67 | platforms="Linux, Mac OS X, Windows", 68 | keywords=["Jupyter", "JupyterLab", "JupyterLab3"], 69 | classifiers=[ 70 | "License :: OSI Approved :: BSD License", 71 | "Programming Language :: Python", 72 | "Programming Language :: Python :: 3", 73 | "Programming Language :: Python :: 3.7", 74 | "Programming Language :: Python :: 3.8", 75 | "Programming Language :: Python :: 3.9", 76 | "Programming Language :: Python :: 3.10", 77 | "Framework :: Jupyter", 78 | "Framework :: Jupyter :: JupyterLab", 79 | "Framework :: Jupyter :: JupyterLab :: 3", 80 | "Framework :: Jupyter :: JupyterLab :: Extensions", 81 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 82 | "Intended Audience :: Developers", 83 | "Intended Audience :: Science/Research", 84 | "Intended Audience :: Other Audience", 85 | "Topic :: Scientific/Engineering :: Information Analysis", 86 | "Topic :: Scientific/Engineering :: Visualization", 87 | ], 88 | ) 89 | 90 | try: 91 | from jupyter_packaging import ( 92 | wrap_installers, 93 | npm_builder, 94 | get_data_files 95 | ) 96 | post_develop = npm_builder( 97 | build_cmd="install:extension", source_dir="src", build_dir=lab_path 98 | ) 99 | setup_args["cmdclass"] = wrap_installers(post_develop=post_develop, ensured_targets=ensured_targets) 100 | setup_args["data_files"] = get_data_files(data_files_spec) 101 | except ImportError as e: 102 | import logging 103 | logging.basicConfig(format="%(levelname)s: %(message)s") 104 | logging.warning("Build tool `jupyter-packaging` is missing. Install it with pip or conda.") 105 | if not ("--name" in sys.argv or "--version" in sys.argv): 106 | raise e 107 | 108 | if __name__ == "__main__": 109 | setuptools.setup(**setup_args) 110 | -------------------------------------------------------------------------------- /src/ProfilePanel.ts: -------------------------------------------------------------------------------- 1 | import { StackedPanel } from '@lumino/widgets'; 2 | import type { ISessionContext } from '@jupyterlab/apputils'; 3 | import type { Message } from '@lumino/messaging'; 4 | import type { NotebookAPI } from './dataAPI/jupyter/notebook'; 5 | import { ProfileModel } from './dataAPI/ProfileModel'; 6 | import { ProfileView } from './components/ProfileView'; 7 | import { LabIcon } from '@jupyterlab/ui-components'; 8 | import appIconStr from '../style/logo.svg'; 9 | 10 | export class ProfilePanel extends StackedPanel { 11 | constructor() { 12 | super(); 13 | this.addClass('auto-profile-wrapper'); 14 | this.id = 'auto-profile-app'; 15 | this.title.caption = 'Autoprofiler'; // shown on hover 16 | this.title.iconClass = 'autoprofile-logo'; 17 | 18 | const icon = new LabIcon({ 19 | name: 'auto-profile-app:app-icon', 20 | svgstr: appIconStr 21 | }); 22 | 23 | this.title.icon = icon; 24 | 25 | // MODEL init 26 | this._profileModel = new ProfileModel(this._sessionContext); 27 | 28 | // VIEW init 29 | this._profileView = new ProfileView(this._profileModel); 30 | this.addWidget(this._profileView); 31 | } 32 | 33 | // ~~~~~~~~~ Variables, getters, setters ~~~~~~~~~ 34 | private _sessionContext: ISessionContext; 35 | private _profileModel: ProfileModel; 36 | private _profileView: ProfileView; 37 | 38 | get session(): ISessionContext { 39 | return this._sessionContext; 40 | } 41 | 42 | set session(session: ISessionContext) { 43 | this._sessionContext = session; 44 | } 45 | 46 | public async connectNotebook(notebook: NotebookAPI) { 47 | if (notebook.hasConnection) { 48 | this.session = notebook.panel.sessionContext; 49 | } 50 | await this._profileModel.connectNotebook(notebook, () => { return this.isVisible }); 51 | } 52 | 53 | // ~~~~~~~~~ Lifecycle methods for closing panel ~~~~~~~~~ 54 | dispose(): void { 55 | this._sessionContext.dispose(); 56 | super.dispose(); 57 | } 58 | 59 | protected onCloseRequest(msg: Message): void { 60 | super.onCloseRequest(msg); 61 | this.dispose(); 62 | } 63 | 64 | /** 65 | * Called before the widget is made visible. 66 | * NOTE: when using beforeShow, this.isVisible is false during update. 67 | * 68 | * other useful state messages are onAfterShow, 69 | * onBeforeHide, onAfterHide. 70 | * @param msg 71 | */ 72 | protected onBeforeShow(msg: Message): void { 73 | this._profileModel.updateAll(); 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/common/exchangeInterfaces.ts: -------------------------------------------------------------------------------- 1 | export type IColTypeTuple = { 2 | colName: string; 3 | colType: string; 4 | colIsIndex: boolean; 5 | }; 6 | 7 | export type Warning = { 8 | warnMsg: string; 9 | } 10 | 11 | export type IDFColMap = { 12 | [key: string]: { 13 | columns: IColTypeTuple[]; 14 | python_id: string; 15 | warnings: Warning[] 16 | }; 17 | }; 18 | 19 | // data fetched from kernel for frontend 20 | export type IDFProfileWStateMap = { 21 | [dfname: string]: IDFProfileWState; 22 | } | undefined; 23 | 24 | 25 | export type IDFProfileData = { 26 | profile: ColumnProfileData[]; 27 | shape: number[]; 28 | dfName: string; 29 | } 30 | 31 | 32 | export type IDFProfileState = { 33 | lastUpdatedTime: number; 34 | isPinned: boolean; 35 | warnings: Warning[] 36 | } 37 | 38 | export type IDFProfileWState = IDFProfileData & IDFProfileState 39 | 40 | export type ColumnProfileData = IColTypeTuple & { 41 | nullCount: number, 42 | summary: AnySummary; 43 | } 44 | 45 | export type AnySummary = NumericSummary | CategoricalSummary | BoolSummary | TemporalSummary; 46 | 47 | export type NumericSummary = { 48 | summaryType: "numeric"; 49 | histogram: IHistogram; 50 | quantMeta: IQuantMeta; 51 | } 52 | 53 | export type CategoricalSummary = { // string or boolean 54 | summaryType: "categorical"; 55 | cardinality: number; // num unique 56 | topK: ValueCount[]; 57 | stringMeta: IStringMeta; 58 | } 59 | 60 | export type BoolSummary = { // string or boolean 61 | summaryType: "boolean"; 62 | cardinality: number; // num unique 63 | topK: ValueCount[]; 64 | } 65 | 66 | export type TemporalSummary = { 67 | summaryType: "temporal"; 68 | histogram: IHistogram; 69 | timeInterval: Interval; 70 | temporalMeta: ITemporalMeta; 71 | } 72 | 73 | // type guards 74 | export function isNumericSummary(s: AnySummary): s is NumericSummary { 75 | return (s as NumericSummary).summaryType === "numeric" 76 | } 77 | 78 | export function isCategoricalSummary(s: AnySummary): s is CategoricalSummary { 79 | return (s as CategoricalSummary).summaryType === "categorical" 80 | } 81 | 82 | export function isBooleanSummary(s: AnySummary): s is BoolSummary { 83 | return (s as BoolSummary).summaryType === "boolean" 84 | } 85 | 86 | export function isTemporalSummary(s: AnySummary): s is TemporalSummary { 87 | return (s as TemporalSummary).summaryType === "temporal" 88 | } 89 | 90 | // ~~~~~~~ Individual data type info ~~~~~~~ 91 | export type IQuantMeta = { 92 | min: number; 93 | q25: number; 94 | q50: number; 95 | q75: number; 96 | max: number; 97 | mean: number; 98 | sd_outlier: number; 99 | iqr_outlier: number; 100 | sortedness: string; 101 | n_zero: number; 102 | n_positive: number; 103 | n_negative: number; 104 | }; 105 | 106 | export type IStringMeta = { 107 | minLength: number; 108 | maxLength: number; 109 | meanLength: number; 110 | }; 111 | 112 | export type ITemporalMeta = { 113 | sortedness: string; 114 | num_outliers: number; 115 | } 116 | 117 | export type TimeBin = { 118 | count: number; 119 | ts_start: Date; 120 | ts_end: Date; 121 | }; 122 | 123 | export type ValueCount = { 124 | value: any; 125 | count: number; 126 | }; 127 | 128 | export type IHistogramBin = { 129 | bucket: number; 130 | low: number; 131 | high: number; 132 | count: number; 133 | }; 134 | 135 | export type IHistogram = IHistogramBin[]; 136 | 137 | export type Interval = { 138 | months: number; 139 | days: number; 140 | micros: number; 141 | }; 142 | 143 | export enum PreviewRollupInterval { 144 | ms = '1 millisecond', 145 | second = '1 second', 146 | minute = '1 minute', 147 | hour = '1 hour', 148 | day = '1 day', 149 | month = '1 month', 150 | year = '1 year' 151 | } 152 | 153 | // ~~~ Bivariate chart types ~~~ 154 | 155 | export type BivariateTimestampBin = { 156 | 'period': string; 157 | 'value': number; 158 | 'bucket': number; 159 | } 160 | 161 | export type BivariateTimestampInfo = { 162 | data: BivariateTimestampBin[]; 163 | timestep: TimeOffset; 164 | } 165 | 166 | export type TimeOffset = "Y" | "M" | "W" | "D" | "H" | "T" | "S"; 167 | export type AggrType = "count" | "mean" | "sum" | "min" | "max"; 168 | 169 | type BivariateDataBase = { 170 | aggrType: AggrType; 171 | xColumn: IColTypeTuple; 172 | yColumn: IColTypeTuple; 173 | filledOut: boolean; 174 | } 175 | 176 | export type IBivariateHistogramData = BivariateDataBase & { 177 | chartType: 'histogram'; 178 | data: ValueCount[]; 179 | } 180 | 181 | export type IBivariateLinechartData = BivariateDataBase & { 182 | chartType: 'linechart'; 183 | data: BivariateTimestampBin[]; 184 | } 185 | 186 | export type IBivariateData = IBivariateHistogramData | IBivariateLinechartData; 187 | 188 | export type ChartSelection = { 189 | x: ColumnProfileData; 190 | y: ColumnProfileData; 191 | } 192 | -------------------------------------------------------------------------------- /src/components/ColumnEntry.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 50 |
51 | 52 |
53 |
54 | 55 | 60 | -------------------------------------------------------------------------------- /src/components/ProfileView.ts: -------------------------------------------------------------------------------- 1 | import { Widget } from '@lumino/widgets'; 2 | import type { ProfileModel } from '../dataAPI/ProfileModel'; 3 | import Profiler from './Profiler.svelte'; 4 | 5 | export class ProfileView extends Widget { 6 | constructor(model: ProfileModel) { 7 | super(); 8 | this.addClass('auto-profile-app'); 9 | 10 | new Profiler({ 11 | target: this.node, 12 | props: { 13 | profileModel: model 14 | } 15 | }); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/actions/outline.ts: -------------------------------------------------------------------------------- 1 | /** this action appends another text DOM element 2 | * that gives an outlined / punched-out look to whatever 3 | * svg text node it is applied to. It will then listen to 4 | * any of the relevant attributes / the content itself 5 | * and update accordingly via a basic MutationObserver. 6 | */ 7 | 8 | interface OutlineAction { 9 | destroy: () => void; 10 | } 11 | 12 | export function outline( 13 | node: SVGElement, 14 | args = { color: 'white' } 15 | ): OutlineAction { 16 | const enclosingSVG = node.ownerSVGElement; 17 | 18 | // create a clone of the element. 19 | const clonedElement = node.cloneNode(true) as SVGElement; 20 | node.parentElement.insertBefore(clonedElement, node); 21 | clonedElement.setAttribute('fill', args.color); 22 | clonedElement.style.fill = args.color; 23 | clonedElement.setAttribute('filter', 'url(#outline-filter)'); 24 | // apply the filter to this svg element. 25 | let outlineFilter = enclosingSVG.querySelector('#outline-filter'); 26 | if (outlineFilter === null) { 27 | outlineFilter = document.createElementNS( 28 | 'http://www.w3.org/2000/svg', 29 | 'filter' 30 | ); 31 | outlineFilter.id = 'outline-filter'; 32 | 33 | const morph = document.createElementNS( 34 | 'http://www.w3.org/2000/svg', 35 | 'feMorphology' 36 | ); 37 | morph.setAttribute('operator', 'dilate'); 38 | morph.setAttribute('radius', '2'); 39 | morph.setAttribute('in', 'SourceGraphic'); 40 | morph.setAttribute('result', 'THICKNESS'); 41 | 42 | const composite = document.createElementNS( 43 | 'http://www.w3.org/2000/svg', 44 | 'feComposite' 45 | ); 46 | composite.setAttribute('operator', 'out'); 47 | composite.setAttribute('in', 'THICKNESS'); 48 | composite.setAttribute('in2', 'SourceGraphic'); 49 | 50 | outlineFilter.appendChild(morph); 51 | outlineFilter.appendChild(composite); 52 | enclosingSVG.prepend(outlineFilter); 53 | } 54 | 55 | const config = { 56 | attributes: true, 57 | childList: true, 58 | subtree: true, 59 | characterData: true 60 | }; 61 | const observer = new MutationObserver(() => { 62 | clonedElement.setAttribute('x', node.getAttribute('x')); 63 | clonedElement.setAttribute('y', node.getAttribute('y')); 64 | clonedElement.setAttribute( 65 | 'text-anchor', 66 | node.getAttribute('text-anchor') 67 | ); 68 | if (node.getAttribute('dx')) { 69 | clonedElement.setAttribute('dx', node.getAttribute('dx')); 70 | } 71 | if (node.getAttribute('dy')) { 72 | clonedElement.setAttribute('dy', node.getAttribute('dy')); 73 | } 74 | // clone any animations that may be applied via svelte transitions. 75 | clonedElement.style.animation = node.style.animation; 76 | // copy the contents of the node. 77 | clonedElement.innerHTML = node.innerHTML; 78 | }); 79 | observer.observe(node, config); 80 | 81 | return { 82 | destroy() { 83 | clonedElement.remove(); 84 | } 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/components/create-chart/AddChartButton.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/components/create-chart/BivariateChartEntry.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if biData.data.length !== 0} 17 | {#if biData.chartType === 'histogram'} 18 | 23 | {:else if biData.chartType === 'linechart'} 24 | 29 | {/if} 30 | 31 | {#if allowEdit} 32 |
33 | 34 | 43 | Edit 44 | 45 |
46 | {/if} 47 | {:else} 48 |
No aggregated data to show for the columns.
49 | {/if} 50 | -------------------------------------------------------------------------------- /src/components/create-chart/BivariateWrapper.svelte: -------------------------------------------------------------------------------- 1 | 72 | 73 | 74 | 75 | 76 |
77 | 78 |
79 | 80 | XY chart 81 |
82 |
83 | 84 | 85 | {title} 86 | 87 | 88 | 89 | {#if active} 90 |
94 | 95 | {#if inEditMode} 96 |
97 | { 104 | handleColumnEdit( 105 | event.detail.xVariable, 106 | event.detail.yVariable 107 | ); 108 | }} 109 | on:aggrEdit={event => { 110 | handleAggrChange(event.detail.aggrType); 111 | }} 112 | on:delete={() => { 113 | dispatch('delete'); 114 | }} 115 | /> 116 |
117 | {/if} 118 | 119 | 120 | {#if biData.filledOut} 121 | { 125 | inEditMode = true; 126 | }} 127 | /> 128 | {/if} 129 |
130 | {/if} 131 |
132 |
133 | -------------------------------------------------------------------------------- /src/components/create-chart/CreateChartMenu.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 |
40 |
41 | { 45 | xVariable = event?.detail; 46 | dispatchColumnUpdate(); 47 | }} 48 | clickable={true} 49 | title={'Column one'} 50 | /> 51 | 52 | { 56 | yVariable = event?.detail; 57 | dispatchColumnUpdate(); 58 | }} 59 | clickable={!_.isNil(xVariable)} 60 | filteringColumn={xVariable} 61 | title={'Column two'} 62 | /> 63 | 64 | 78 |
79 | 80 |
81 | 82 | 83 | 93 | Done 94 | 95 | 96 | 97 | 98 | 107 | Delete 108 | 109 |
110 |
111 | -------------------------------------------------------------------------------- /src/components/create-chart/DropdownMenu.svelte: -------------------------------------------------------------------------------- 1 | 85 | 86 |
87 | 101 |
102 | -------------------------------------------------------------------------------- /src/components/data-types/Base.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | {#if isNull} 11 | ∅ null 12 | {:else} 13 | 14 | {/if} 15 | 16 | -------------------------------------------------------------------------------- /src/components/data-types/DataTypeIcon.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 |
41 |
42 | 43 |
44 |
45 | -------------------------------------------------------------------------------- /src/components/data-types/FormattedDataType.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 37 | {#if value === undefined} 38 | 39 | {:else} 40 | {formatDataType(value, type)} 41 | {/if} 42 | 43 | -------------------------------------------------------------------------------- /src/components/data-types/Interval.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/data-types/Number.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/data-types/Timestamp.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/data-types/Varchar.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/data-types/duckdb-data-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides mappings from duckdb's data types to conceptual types we use in the application: 3 | * CATEGORICALS, NUMERICS, and TIMESTAMPS. 4 | */ 5 | 6 | export const INTEGERS = new Set([ 7 | 'BIGINT', 8 | 'HUGEINT', 9 | 'SMALLINT', 10 | 'INTEGER', 11 | 'TINYINT', 12 | 'UBIGINT', 13 | 'UINTEGER', 14 | 'UTINYINT', 15 | 'INT1', 16 | 'INT4', 17 | 'INT', 18 | 'SIGNED', 19 | 'SHORT' 20 | ]); 21 | 22 | export const FLOATS = new Set([ 23 | 'DOUBLE', 24 | 'DECIMAL', 25 | 'FLOAT8', 26 | 'NUMERIC', 27 | 'FLOAT' 28 | ]); 29 | 30 | export const NUMERICS = new Set([...INTEGERS, ...FLOATS]); 31 | export const BOOLEANS = new Set(['BOOLEAN', 'BOOL', 'LOGICAL']); 32 | export const TIMESTAMPS = new Set(['TIMESTAMP', 'TIME', 'DATETIME', 'DATE']); 33 | export const INTERVALS = new Set(['INTERVAL']); 34 | export const CATEGORICALS = new Set([ 35 | 'BYTE_ARRAY', 36 | 'VARCHAR', 37 | 'CHAR', 38 | 'BPCHAR', 39 | 'TEXT', 40 | 'STRING' 41 | ]); 42 | 43 | interface IColorTokens { 44 | textClass: string; 45 | bgClass: string; 46 | vizFillClass: string; 47 | vizStrokeClass: string; 48 | } 49 | 50 | export const CATEGORICAL_TOKENS: IColorTokens = { 51 | textClass: 'text-sky-800', 52 | bgClass: 'bg-sky-200', 53 | vizFillClass: 'fill-sky-800', 54 | vizStrokeClass: 'fill-sky-800' 55 | }; 56 | 57 | export const NUMERIC_TOKENS: IColorTokens = { 58 | textClass: 'text-red-800', 59 | bgClass: 'bg-red-200', 60 | vizFillClass: 'fill-red-300', 61 | vizStrokeClass: 'stroke-red-300' 62 | }; 63 | 64 | export const TIMESTAMP_TOKENS: IColorTokens = { 65 | textClass: 'text-teal-800', 66 | bgClass: 'bg-teal-200', 67 | vizFillClass: 'fill-teal-500', 68 | vizStrokeClass: 'stroke-teal-500' 69 | }; 70 | 71 | export const INTERVAL_TOKENS: IColorTokens = TIMESTAMP_TOKENS; 72 | 73 | function setTypeTailwindStyles( 74 | list: string[], 75 | // a tailwind class, for now. 76 | colorTokens: IColorTokens 77 | ) { 78 | return list.reduce((acc, v) => { 79 | acc[v] = { ...colorTokens }; 80 | return acc; 81 | }, {}); 82 | } 83 | 84 | export const DATA_TYPE_COLORS = { 85 | ...setTypeTailwindStyles(Array.from(CATEGORICALS), CATEGORICAL_TOKENS), 86 | ...setTypeTailwindStyles(Array.from(NUMERICS), NUMERIC_TOKENS), 87 | ...setTypeTailwindStyles(Array.from(TIMESTAMPS), TIMESTAMP_TOKENS), 88 | ...setTypeTailwindStyles(Array.from(INTERVALS), INTERVAL_TOKENS), 89 | ...setTypeTailwindStyles(Array.from(BOOLEANS), CATEGORICAL_TOKENS) 90 | }; 91 | -------------------------------------------------------------------------------- /src/components/data-types/pandas-data-types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides mappings from pandas data types to conceptual types we use in the application: 3 | * CATEGORICALS, NUMERICS, and TIMESTAMPS. 4 | */ 5 | 6 | // TODO use built in pandas type checking? 7 | 8 | export const INTEGERS = new Set([ 9 | 'int', 10 | 'int8', 11 | 'int16', 12 | 'int32', 13 | 'int64', 14 | 'uint8', 15 | 'uint16', 16 | 'uint32', 17 | 'uint64', 18 | 'Int8', 19 | 'Int16', 20 | 'Int32', 21 | 'Int64', 22 | 'UInt8', 23 | 'UInt16', 24 | 'UInt32', 25 | 'UInt64' // unclear if these are necessary 26 | ]); 27 | 28 | export const FLOATS = new Set([ 29 | 'float', 30 | 'float_', 31 | 'float16', 32 | 'float32', 33 | 'float64' 34 | ]); 35 | 36 | export const NUMERICS = new Set([...INTEGERS, ...FLOATS]); 37 | export const BOOLEANS = new Set(['bool', '_bool']); 38 | export const TIMESTAMPS = new Set(['datetime64', 'datetime64[ns]']); 39 | export const INTERVALS = new Set([]); // TODO add pandas.Interval support 40 | export const CATEGORICALS = new Set(['object', 'string', 'str', 'category']); 41 | 42 | interface IColorTokens { 43 | textClass: string; 44 | bgClass: string; 45 | vizFillClass: string; 46 | vizStrokeClass: string; 47 | vizHoverClass?: string; 48 | } 49 | 50 | export const CATEGORICAL_TOKENS: IColorTokens = { 51 | textClass: 'text-sky-800', 52 | bgClass: 'bg-sky-200', 53 | vizFillClass: 'fill-sky-800', 54 | vizStrokeClass: 'fill-sky-800' 55 | }; 56 | 57 | export const NUMERIC_TOKENS: IColorTokens = { 58 | textClass: 'text-red-800', 59 | bgClass: 'bg-red-200', 60 | vizFillClass: 'fill-red-300', 61 | vizStrokeClass: 'stroke-red-300', 62 | vizHoverClass: 'fill-red-400' 63 | }; 64 | 65 | export const TIMESTAMP_TOKENS: IColorTokens = { 66 | textClass: 'text-teal-800', 67 | bgClass: 'bg-teal-200', 68 | vizFillClass: 'fill-teal-500', 69 | vizStrokeClass: 'stroke-teal-500' 70 | }; 71 | 72 | export const INTERVAL_TOKENS: IColorTokens = TIMESTAMP_TOKENS; 73 | 74 | function setTypeTailwindStyles( 75 | list: string[], 76 | // a tailwind class, for now. 77 | colorTokens: IColorTokens 78 | ) { 79 | return list.reduce((acc, v) => { 80 | acc[v] = { ...colorTokens }; 81 | return acc; 82 | }, {}); 83 | } 84 | 85 | export const DATA_TYPE_COLORS = { 86 | ...setTypeTailwindStyles(Array.from(CATEGORICALS), CATEGORICAL_TOKENS), 87 | ...setTypeTailwindStyles(Array.from(BOOLEANS), CATEGORICAL_TOKENS), 88 | ...setTypeTailwindStyles(Array.from(NUMERICS), NUMERIC_TOKENS), 89 | ...setTypeTailwindStyles(Array.from(TIMESTAMPS), TIMESTAMP_TOKENS), 90 | // ...setTypeTailwindStyles(Array.from(INTERVALS), INTERVAL_TOKENS), 91 | }; 92 | -------------------------------------------------------------------------------- /src/components/export-code/ExportChartButton.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 |
46 |
47 | 48 |
49 | 50 | 57 | 58 | Export chart to code 61 | 62 |
63 |
64 | -------------------------------------------------------------------------------- /src/components/export-code/ExportFactButton.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | 50 | 51 | {tooltipText} 52 | 53 | -------------------------------------------------------------------------------- /src/components/fact-panel/DisplayFact.svelte: -------------------------------------------------------------------------------- 1 | 3 | 4 |
  • 5 |
    6 |
    7 | 8 |
    9 |
    10 | 11 |
    12 |
    13 |
  • 14 | -------------------------------------------------------------------------------- /src/components/fact-panel/NumericalStats.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 |
      50 | 51 | 52 | There {stats?.sd_outlier === 1 ? 'is' : 'are'} 53 | 54 | {stats?.sd_outlier} outlier{stats?.sd_outlier === 1 ? '' : 's'} 55 | more than 3 std away from the mean. 56 | 57 | 58 | 65 | 66 | 67 | 68 | 69 | 70 | There {stats?.iqr_outlier === 1 ? 'is' : 'are'} 71 | 72 | {stats?.iqr_outlier} outlier{stats?.iqr_outlier === 1 73 | ? '' 74 | : 's'} 75 | more than 1.5 * IQR below q1 or above q3. 76 | 77 | 78 | 85 | 86 | 87 | 88 | 89 | 90 | This column is {formatSort(stats?.sortedness)}. 91 | 92 | 93 | 94 | 95 | 96 | {formatNumberString( 97 | stats?.n_zero, 98 | stats?.n_positive, 99 | stats?.n_negative 100 | )} 101 | 102 | 103 |
    104 | -------------------------------------------------------------------------------- /src/components/fact-panel/StringStats.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
      16 | 17 | 18 | Entries are {stats?.minLength}-{stats?.maxLength} characters long, mean 19 | of {formatFloat(stats?.meanLength)}. 20 | 21 | 22 | 23 | 24 | 25 | {formatFloat((unique / rows) * 100.0)}% of the entires are unique ({unique} 26 | out of {rows} non-null rows). 27 | 28 | 29 | 36 | 37 | 38 |
    39 | -------------------------------------------------------------------------------- /src/components/fact-panel/TempStats.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
      14 | 15 | 16 | This column is {formatSort(facts?.sortedness)}. 17 | 18 | 19 | 20 | 21 | 22 | There are {facts?.num_outliers} time-series outliers with respect to 23 | count using the hampel identifier method. 24 | 25 | 26 | 33 | 34 | 35 |
    36 | -------------------------------------------------------------------------------- /src/components/icons/AlertIcon.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/icons/BivariateButton.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | 15 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/icons/BivariateIcon.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 20 | 21 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/icons/BooleanType.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/icons/Cancel.svelte: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 18 | 19 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/icons/CaretDownIcon.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/icons/CategoricalType.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/icons/Delete.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/icons/Done.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 22 | 23 | 29 | 34 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/icons/EditIcon.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/icons/ExportIcon.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 26 | 33 | 40 | 41 | -------------------------------------------------------------------------------- /src/components/icons/FloatType.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/components/icons/IntegerType.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 16 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/icons/List.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/icons/Parquet.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/icons/Pin.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/icons/RefreshIcon.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/icons/RightArrow.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /src/components/icons/SettingsIcon.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/icons/TimestampType.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 17 | 21 | 22 | -------------------------------------------------------------------------------- /src/components/nav/CollapsibleCard.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
    23 |
    dispatch('header-hover', { over: true })} 26 | on:mouseleave={() => dispatch('header-hover', { over: false })} 27 | > 28 |
    29 | 30 |
    31 | 32 |
    33 | 34 |
    35 |
    36 | 37 |
    38 | 39 |
    40 |
    41 | 42 | 48 | -------------------------------------------------------------------------------- /src/components/nav/ExpanderButton.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | -------------------------------------------------------------------------------- /src/components/nav/RefreshData.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
    17 |
    18 | 19 |
    20 | 21 | 28 | 29 | Manually refresh data 32 | 33 |
    34 |
    35 | -------------------------------------------------------------------------------- /src/components/nav/SettingsMenu.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 |
    16 | 25 | {#if active} 26 | 27 |
    28 | 33 |
    36 |
    37 | 38 | Visualize DF index 39 |
    40 |
    41 |
    42 |
    43 |
    44 | {/if} 45 |
    46 | 47 | 71 | -------------------------------------------------------------------------------- /src/components/tooltip/FloatingElement.svelte: -------------------------------------------------------------------------------- 1 | 9 | 117 | 118 | 119 | 120 | 121 |
    129 | 130 |
    131 |
    132 | -------------------------------------------------------------------------------- /src/components/tooltip/Portal.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 39 | -------------------------------------------------------------------------------- /src/components/tooltip/Tooltip.svelte: -------------------------------------------------------------------------------- 1 | 20 | 60 | 61 |
    { 65 | waitUntil(() => { 66 | active = true; 67 | }, activeDelay); 68 | }} 69 | on:mouseleave={() => { 70 | waitUntil(() => { 71 | active = false; 72 | }, nonActiveDelay); 73 | }} 74 | > 75 | 76 | {#if active && !suppress && !$childRequestedTooltipSuppression} 77 | 78 |
    79 | 86 | 87 | 88 |
    89 |
    90 | {/if} 91 |
    92 | -------------------------------------------------------------------------------- /src/components/tooltip/TooltipContent.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
    9 | 10 |
    11 | -------------------------------------------------------------------------------- /src/components/tooltip/types.d.ts: -------------------------------------------------------------------------------- 1 | export type Location = "left" | "right" | "top" | "bottom"; 2 | export type Alignment = "start" | "middle" | "end"; 3 | export type FloatingElementRelationship = "parent" | "direct" | "mouse"; 4 | -------------------------------------------------------------------------------- /src/components/tooltip/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | function minmax(v, min, max) { 3 | return Math.max(min, Math.min(v, max)); 4 | } 5 | 6 | export function mouseLocationToBoundingRect({ x, y, width = 0, height = 0 }) { 7 | return { 8 | width, 9 | height, 10 | left: x, 11 | right: x + width, 12 | top: y, 13 | bottom: y + height, 14 | }; 15 | 16 | } 17 | 18 | export function placeElement({ 19 | location, 20 | alignment, 21 | parentPosition, // using getBoundingClientRect // DOMRect 22 | elementPosition, // using getBoundingClientRect // DOMRect 23 | distance = 0, 24 | x = 0, 25 | y = 0, 26 | windowWidth = window.innerWidth, 27 | windowHeight = window.innerHeight, 28 | pad = 16 * 2, 29 | }) { 30 | let left; 31 | let top; 32 | 33 | const elementWidth = elementPosition.width; 34 | const elementHeight = elementPosition.height; 35 | 36 | const parentRight = parentPosition.right + x; 37 | const parentLeft = parentPosition.left + x; 38 | const parentTop = parentPosition.top + y; 39 | const parentBottom = parentPosition.bottom + y; 40 | const parentWidth = parentPosition.width; 41 | const parentHeight = parentPosition.height; 42 | 43 | // Task 1: check if we need to reflect agains the location axis. 44 | if (location === "bottom") { 45 | if (parentBottom + elementHeight + distance + pad > windowHeight + y) { 46 | top = parentTop - elementHeight - distance; 47 | } else { 48 | top = parentBottom + distance; 49 | } 50 | } else if (location === "top") { 51 | if (parentTop - elementHeight - distance - pad < y) { 52 | top = parentBottom + distance; 53 | } else { 54 | top = parentTop - elementHeight - distance; 55 | } 56 | } else if (location === "left") { 57 | if (parentLeft - distance - elementWidth - pad < x) { 58 | // reflect 59 | left = parentRight + distance; 60 | } else { 61 | left = parentLeft - elementWidth - distance; 62 | } 63 | } else if (location === "right") { 64 | if (parentRight + elementWidth + distance + pad > windowWidth + x) { 65 | left = parentLeft - elementWidth - distance; 66 | } else { 67 | left = parentRight + distance; 68 | } 69 | } 70 | 71 | // OUR SECOND JOB IS RE-ALIGNMENT ALONG THE ALIGNMENT ACTION. 72 | let alignmentValue; 73 | 74 | const rightLeft = location === "right" || location === "left"; 75 | 76 | switch (alignment) { 77 | case "start": { 78 | alignmentValue = rightLeft 79 | ? parentTop // right / left 80 | : parentLeft; // top / bottom 81 | break; 82 | } 83 | case "end": { 84 | alignmentValue = rightLeft 85 | ? parentBottom - elementHeight // right / left 86 | : parentRight - elementWidth; // top / bottom 87 | break; 88 | } 89 | default: { 90 | // 'middle' 91 | alignmentValue = rightLeft 92 | ? parentTop - (elementHeight - parentHeight) / 2 // right / left 93 | : parentLeft - (elementWidth - parentWidth) / 2; // top / bottom 94 | break; 95 | } 96 | } 97 | const alignMin = pad + (rightLeft ? y : x); 98 | const alignMax = rightLeft 99 | ? y + windowHeight - elementHeight - pad 100 | : x + windowWidth - elementWidth - pad; 101 | 102 | const value = minmax(alignmentValue, alignMin, alignMax); 103 | 104 | if (rightLeft) { 105 | top = value; 106 | } else { 107 | left = value; 108 | } 109 | 110 | return [left, top]; 111 | } 112 | -------------------------------------------------------------------------------- /src/components/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export interface DomainCoordinates { 2 | x: number; //| Date; 3 | y: number; //| Date; 4 | } 5 | 6 | export const DEFAULT_COORDINATES: DomainCoordinates = { 7 | x: undefined, 8 | y: undefined 9 | }; 10 | 11 | export interface PlotConfig { 12 | top: number; 13 | bottom: number; 14 | left: number; 15 | right: number; 16 | buffer: number; 17 | width: number; 18 | height: number; 19 | devicePixelRatio: number; 20 | plotTop: number; 21 | plotBottom: number; 22 | plotLeft: number; 23 | plotRight: number; 24 | fontSize: number; 25 | textGap: number; 26 | id: any; 27 | } 28 | -------------------------------------------------------------------------------- /src/components/utils/convertTypes.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import type { IHistogram, TimeBin } from '../../common/exchangeInterfaces'; 4 | 5 | export function convertToTimeBin(histogram: IHistogram): TimeBin[] { 6 | if (histogram == undefined) return []; 7 | 8 | return histogram.map(b => ({ 9 | count: b.count, 10 | ts_start: new Date(b.low * 1000), 11 | ts_end: new Date(b.high * 1000) 12 | })); 13 | } -------------------------------------------------------------------------------- /src/components/utils/formatters.ts: -------------------------------------------------------------------------------- 1 | import { 2 | INTERVALS, 3 | INTEGERS, 4 | FLOATS, 5 | CATEGORICALS, 6 | TIMESTAMPS, 7 | BOOLEANS 8 | } from '../data-types/pandas-data-types'; 9 | import { 10 | type Interval, 11 | PreviewRollupInterval 12 | } from '../../common/exchangeInterfaces'; 13 | import { format } from 'd3-format'; 14 | import { timeFormat } from 'd3-time-format'; 15 | 16 | const zeroPad = format('02d'); 17 | const msPad = format('03d'); 18 | export const formatInteger = format(','); 19 | const formatRate = format('.1f'); 20 | 21 | export function formatNumeric(type: string, value) { 22 | if (INTEGERS.has(type)) { 23 | return formatInt(value); 24 | } else if (FLOATS.has(type)) { 25 | return formatFloat(value); 26 | } 27 | 28 | return value 29 | } 30 | 31 | 32 | export function formatInt(v) { 33 | return format('d')(v) 34 | } 35 | 36 | export function formatFloat(v) { 37 | if (Math.abs(v) >= 1) { 38 | return v.toFixed(1); 39 | } 40 | 41 | // 2 significant digits 42 | return format('.2r')(v); 43 | } 44 | 45 | export const formatPercentage = format('.1%'); 46 | 47 | /** 48 | * changes precision depending on the 49 | */ 50 | export function formatBigNumberPercentage(v) { 51 | if (v < 0.0001) { 52 | const f = format('.4%')(v); 53 | if (f === '0.0000%') { 54 | return '~ 0%'; 55 | } else { 56 | return f; 57 | } 58 | } else { 59 | return format('.2%')(v); 60 | } 61 | } 62 | 63 | export function removeTimezoneOffset(dt) { 64 | return new Date(dt.getTime() + dt.getTimezoneOffset() * 60000); 65 | } 66 | 67 | export const standardTimestampFormat = (v, type = 'TIMESTAMP') => { 68 | let fmt = timeFormat('%b %d, %Y %I:%M:%S'); 69 | if (type === 'DATE') { 70 | fmt = timeFormat('%b %d, %Y'); 71 | } 72 | return fmt(removeTimezoneOffset(new Date(v))); 73 | }; 74 | export const datePortion = timeFormat('%b %d, %Y'); 75 | export const timePortion = timeFormat('%I:%M:%S'); 76 | 77 | export function microsToTimestring(microseconds: number) { 78 | // to format micros, we need to translate this to hh:mm:ss. 79 | // start with hours/ 80 | const sign = Math.sign(microseconds); 81 | const micros = Math.abs(microseconds); 82 | const hours = ~~(micros / 1000 / 1000 / 60 / 60); 83 | let remaining = micros - hours * 1000 * 1000 * 60 * 60; 84 | const minutes = ~~(remaining / 1000 / 1000 / 60); 85 | //const seconds = (remaining - (minutes * 1000 * 1000 * 60)) / 1000 / 1000; 86 | remaining -= minutes * 1000 * 1000 * 60; 87 | const seconds = ~~(remaining / 1000 / 1000); 88 | remaining -= seconds * 1000 * 1000; 89 | const ms = ~~(remaining / 1000); 90 | if (hours === 0 && minutes === 0 && seconds === 0 && ms > 0) { 91 | return `${sign === 1 ? '' : '-'}${ms}ms`; 92 | } 93 | return `${sign === 1 ? '' : '-'}${zeroPad(hours)}:${zeroPad( 94 | minutes 95 | )}:${zeroPad(seconds)}.${msPad(ms)}`; 96 | } 97 | 98 | export function intervalToTimestring(interval: Interval) { 99 | const months = interval.months 100 | ? `${formatInteger(interval.months)} month${interval.months > 1 ? 's' : '' 101 | } ` 102 | : ''; 103 | const days = interval.days 104 | ? `${formatInteger(interval.days)} day${interval.days > 1 ? 's' : ''} ` 105 | : ''; 106 | const time = 107 | interval.months > 0 || interval.days > 1 108 | ? '' 109 | : microsToTimestring(interval.micros); 110 | // if only days && days > 365, convert to years? 111 | if (interval.months === 0 && interval.days > 0 && interval.days > 365) { 112 | return `${formatRate(interval.days / 365)} years`; 113 | } 114 | return `${months}${days}${time}`; 115 | } 116 | 117 | export function formatCompactInteger(n: number) { 118 | let fmt: any; 119 | if (n <= 1000) { 120 | fmt = formatInteger; 121 | return fmt(~~n); 122 | } else { 123 | fmt = format('.3s'); 124 | return fmt(n); 125 | } 126 | } 127 | 128 | export function formatDataType(value: any, type: string) { 129 | if (INTEGERS.has(type)) { 130 | return value; 131 | } else if (FLOATS.has(type)) { 132 | return value; 133 | } else if (CATEGORICALS.has(type)) { 134 | return value; 135 | } else if (TIMESTAMPS.has(type)) { 136 | return standardTimestampFormat(value, type); 137 | } else if (INTERVALS.has(type)) { 138 | return intervalToTimestring(value); 139 | } else if (BOOLEANS.has(type)) { 140 | return value; 141 | } 142 | } 143 | 144 | /** These will be used in the string */ 145 | export const PreviewRollupIntervalFormatter = { 146 | [PreviewRollupInterval.ms]: 147 | 'millisecond-level' /** showing rows binned by ms */, 148 | [PreviewRollupInterval.second]: 149 | 'second-level' /** showing rows binned by second */, 150 | [PreviewRollupInterval.minute]: 151 | 'minute-level' /** showing rows binned by minute */, 152 | [PreviewRollupInterval.hour]: 'hourly' /** showing hourly counts */, 153 | [PreviewRollupInterval.day]: 'daily' /** showing daily counts */, 154 | [PreviewRollupInterval.month]: 'monthly' /** showing monthly counts */, 155 | [PreviewRollupInterval.year]: 'yearly' /** showing yearly counts */ 156 | }; 157 | 158 | export function formatSort(sortedness: string) { 159 | if (sortedness === 'noSort') { 160 | return 'not sorted'; 161 | } 162 | return `sorted (${sortedness})`; 163 | } 164 | -------------------------------------------------------------------------------- /src/components/utils/guid.ts: -------------------------------------------------------------------------------- 1 | export function guidGenerator() { 2 | const S4 = function () { 3 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 4 | }; 5 | return ( 6 | S4() + 7 | S4() + 8 | '-' + 9 | S4() + 10 | '-' + 11 | S4() + 12 | '-' + 13 | S4() + 14 | '-' + 15 | S4() + 16 | S4() + 17 | S4() 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/components/utils/sizes.ts: -------------------------------------------------------------------------------- 1 | export const config = { 2 | nullPercentageWidth: 75, 3 | mediumCutoff: 300, 4 | compactBreakpoint: 350, 5 | largeBreakpoint: 500, 6 | summaryVizWidth: { default: 120, large: 240 }, 7 | exampleWidth: { medium: 204, small: 132 } 8 | }; 9 | 10 | 11 | export function getSummarySize(containerWidth: number): number { 12 | if (containerWidth > config.largeBreakpoint) { 13 | return config.summaryVizWidth.large; 14 | } 15 | 16 | return config.summaryVizWidth.default; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/utils/sort-utils.ts: -------------------------------------------------------------------------------- 1 | import { BOOLEANS, CATEGORICALS, INTEGERS, FLOATS, TIMESTAMPS } from "../data-types/pandas-data-types"; 2 | import type { IDFProfileWState } from "../../common/exchangeInterfaces" 3 | 4 | // SORT COLUMNS 5 | export function sortByCardinality(a, b) { 6 | if (a.summary && b.summary) { 7 | if (a.summary.cardinality < b.summary.cardinality) { 8 | return 1; 9 | } else if (a.summary.cardinality > b.summary.cardinality) { 10 | return -1; 11 | } else { 12 | return sortByType(a, b); 13 | } 14 | } else { 15 | return sortByType(a, b); 16 | } 17 | } 18 | 19 | export function sortByNullity(a, b) { 20 | if (a.nullCount !== undefined && b.nullCount !== undefined) { 21 | if (a.nullCount < b.nullCount) { 22 | return 1; 23 | } else if ((a.nullCount > b.nullCount)) { 24 | return -1; 25 | } else { 26 | const byType = sortByType(a, b); 27 | if (byType) return byType; 28 | return sortByName(a, b); 29 | } 30 | } 31 | 32 | return sortByName(a, b); 33 | } 34 | 35 | export function sortByType(a, b) { 36 | if (BOOLEANS.has(a.type) && !BOOLEANS.has(b.type)) return 1; 37 | else if (!BOOLEANS.has(a.type) && BOOLEANS.has(b.type)) return -1; 38 | else if (CATEGORICALS.has(a.type) && !CATEGORICALS.has(b.type)) return 1; 39 | else if (!CATEGORICALS.has(a.type) && CATEGORICALS.has(b.type)) return -1; 40 | else if (FLOATS.has(a.type) && !FLOATS.has(b.type)) return 1; 41 | else if (!FLOATS.has(a.type) && FLOATS.has(b.type)) return -1; 42 | else if (INTEGERS.has(a.type) && !INTEGERS.has(b.type)) return 1; 43 | else if (!INTEGERS.has(a.type) && INTEGERS.has(b.type)) return -1; 44 | else if (TIMESTAMPS.has(a.type) && TIMESTAMPS.has(b.type)) { 45 | return -1; 46 | } else if (!TIMESTAMPS.has(a.type) && TIMESTAMPS.has(b.type)) { 47 | return 1; 48 | } 49 | return 0;//sortByName(a, b); 50 | } 51 | 52 | export function sortByName(a, b) { 53 | return (a.name > b.name) ? 1 : -1; 54 | } 55 | 56 | export function defaultSort(a, b) { 57 | const byType = sortByType(a, b); 58 | if (byType !== 0) return byType; 59 | if ( 60 | CATEGORICALS.has(a.type) && !CATEGORICALS.has(b.type) 61 | ) return sortByNullity(b, a); 62 | return sortByCardinality(a, b); 63 | } 64 | 65 | // SORT DFs 66 | 67 | function compareString(a, b, prop) { 68 | if (a[prop] < b[prop]) return -1; 69 | if (a[prop] > b[prop]) return 1; 70 | 71 | return 0; 72 | } 73 | 74 | function comparePinned(a, b) { 75 | return b.isPinned - a.isPinned; 76 | } 77 | 78 | export function sortDFArr(arr: IDFProfileWState[], by = 'dfName') { 79 | let compare; 80 | if (by === 'dfName') { 81 | compare = (a, b) => compareString(a, b, by); 82 | } 83 | if (by === 'lastUpdatedTime') { 84 | // invert so more recent at top 85 | compare = (a, b) => b[by] - a[by]; 86 | } 87 | 88 | return arr.sort((a, b) => comparePinned(a, b) || compare(a, b)); 89 | } 90 | -------------------------------------------------------------------------------- /src/components/viz/BarAndLabel.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
    19 |
    20 |
    21 |
    22 | 23 | 42 | -------------------------------------------------------------------------------- /src/components/viz/TopKSummary.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 |
    53 |
    62 | {#each topK.slice(0, 10) as { value, count }} 63 | {@const printValue = value === null ? ' null ∅' : value} 64 |
    handleClick(e, value)} 67 | on:mouseover={() => handleColHover(value)} 68 | on:focus={() => handleColHover(value)} 69 | on:mouseleave={() => handleColHover(undefined)} 70 | on:blur={() => handleColHover(undefined)} 71 | > 72 | 73 | 82 | 83 | Export rows to code 86 | 87 |

    90 | {printValue} 91 |

    92 |
    93 | 94 | {@const negligiblePercentage = count / totalRows < 0.0002} 95 | {@const percentage = negligiblePercentage 96 | ? '<.01%' 97 | : formatPercentage(count / totalRows)} 98 |
    99 | 100 | 101 | {formatCount(count)} 102 | 103 | {#if percentage.length < 6} {/if}{#if percentage.length < 5} {/if} ({percentage}) 107 | 108 | 109 |
    110 | {/each} 111 | 112 | {#if cardinality > 10} 113 | {@const numExtra = cardinality - 10} 114 |
    115 |

    116 | + {formatCount(numExtra)} more unique value{numExtra > 1 117 | ? 's' 118 | : ''} 119 |

    120 | 121 | 128 | 129 | Export unique values to code 132 | 133 |
    134 | {/if} 135 |
    136 |
    137 | -------------------------------------------------------------------------------- /src/components/viz/bivariate/Area.svelte: -------------------------------------------------------------------------------- 1 | 5 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/components/viz/bivariate/AxisX.svelte: -------------------------------------------------------------------------------- 1 | 5 | 71 | 72 | 73 | {#each tickVals as tick, i (tick)} 74 | 78 | {#if gridlines !== false} 79 | 80 | {/if} 81 | {#if tickMarks === true} 82 | 89 | {/if} 90 | {formatTick(tick)} 98 | 99 | {/each} 100 | 106 | {xLabel} 107 | 108 | {#if baseline === true} 109 | 116 | {/if} 117 | 118 | 119 | 150 | -------------------------------------------------------------------------------- /src/components/viz/bivariate/AxisY.svelte: -------------------------------------------------------------------------------- 1 | 5 | 61 | 62 | 63 | {#each tickVals as tick (tick)} 64 | 70 | {#if gridlines !== false} 71 | 77 | {/if} 78 | {#if tickMarks === true} 79 | 86 | {/if} 87 | {formatTick(tick)} 94 | 95 | 101 | {tick} 102 | 103 | 104 | {/each} 105 | 111 | {yLabel} 112 | 113 | 114 | 115 | 140 | -------------------------------------------------------------------------------- /src/components/viz/bivariate/Bar.svelte: -------------------------------------------------------------------------------- 1 | 5 | 12 | 13 | 14 | {#each $data as d, i} 15 | 24 | {/each} 25 | 26 | -------------------------------------------------------------------------------- /src/components/viz/bivariate/BiHistogram.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
    21 | d.value)} 27 | xDomain={[0, null]} 28 | {data} 29 | > 30 | 31 | 32 | 33 | 34 | 35 | 36 |
    37 | 38 | 44 | -------------------------------------------------------------------------------- /src/components/viz/bivariate/BiLine.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    15 | d.period)} 21 | {data} 22 | > 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
    31 | 32 | 38 | -------------------------------------------------------------------------------- /src/components/viz/bivariate/Column.svelte: -------------------------------------------------------------------------------- 1 | 5 | 32 | 33 | 34 | {#each $data as d, i} 35 | {@const colHeight = columnHeight(d)} 36 | {@const xGot = $xGet(d)} 37 | {@const xPos = Array.isArray(xGot) ? xGot[0] : xGot} 38 | {@const colWidth = $xScale.bandwidth 39 | ? $xScale.bandwidth() 40 | : columnWidth(d)} 41 | {@const yValue = $y(d)} 42 | 55 | {#if showLabels && yValue} 56 | {yValue} 61 | {/if} 62 | {/each} 63 | 64 | 65 | 70 | -------------------------------------------------------------------------------- /src/components/viz/bivariate/Line.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | 27 | -------------------------------------------------------------------------------- /src/components/viz/histogram/NumericHistogram.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 | 300} 42 | bind:buffer 43 | {top} 44 | fillColor={NUMERIC_TOKENS.vizFillClass} 45 | hoverColor={NUMERIC_TOKENS.vizHoverClass} 46 | baselineStrokeColor={NUMERIC_TOKENS.vizStrokeClass} 47 | {data} 48 | {left} 49 | {right} 50 | width={effectiveWidth} 51 | height={height + 6 * (fontSize + buffer + anchorBuffer) + anchorBuffer} 52 | bottom={6 * (fontSize + buffer + anchorBuffer / 2)} 53 | > 54 | 55 | 56 | 62 | 63 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | {#each [['min', min], ['25%', qlow], ['median', median], ['mean', mean], ['75%', qhigh], ['max', max]] as [label, value], i} 78 | {@const yi = 79 | y(0) + 80 | anchorBuffer + 81 | i * (fontSize + buffer + anchorBuffer / 2) + 82 | anchorBuffer * 2} 83 | 84 | 92 | 99 | {/each} 100 | 101 | 102 | {#each [['min', min], ['25%', qlow], ['median', median], ['mean', mean], ['75%', qhigh], ['max', max]] as [label, value], i} 103 | {@const yi = 104 | y(0) + 105 | anchorBuffer + 106 | i * (fontSize + buffer + anchorBuffer / 2) + 107 | anchorBuffer * 2} 108 | {@const anchor = x(value) < width / 2 ? 'start' : 'end'} 109 | {@const anchorPlacement = 110 | anchor === 'start' ? anchorBuffer : -anchorBuffer} 111 | 112 | 130 | {/each} 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/components/viz/histogram/SmallHistogram.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /src/components/viz/histogram/SummaryStatLabel.svelte: -------------------------------------------------------------------------------- 1 | 66 | 67 | 68 | handleClick(e, label)} 71 | on:mouseenter={handleHover} 72 | on:mouseleave={handleUnhover} 73 | > 74 | 80 | {label} 81 | 82 | 83 | 88 | 89 | 90 | {formatDisplay(type, label, value)} 98 | 105 | 106 | 107 | 112 | -------------------------------------------------------------------------------- /src/components/viz/histogram/TimestampHistogram.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | {#if interval} 28 |
    29 |
    30 | This column spans {timeLength}. 31 |
    32 |
    33 | {/if} 34 | 300} 36 | fillColor={TIMESTAMP_TOKENS.vizFillClass} 37 | baselineStrokeColor={TIMESTAMP_TOKENS.vizStrokeClass} 38 | {data} 39 | left={0} 40 | right={0} 41 | width={effectiveWidth} 42 | {height} 43 | bottom={40} 44 | > 45 | 46 | {@const yStart = y.range()[0] + fontSize + buffer * 1.5} 47 | {@const yEnd = y.range()[0] + fontSize * 2 + buffer * 1.75} 48 | {@const xStart = x.range()[0]} 49 | {@const xEnd = x.range()[1]} 50 | {@const start = removeTimezoneOffset(new Date(x.domain()[0] * 1000))} 51 | {@const end = removeTimezoneOffset(new Date(x.domain()[1] * 1000))} 52 | {@const isSameDay = 53 | start.getFullYear() === end.getFullYear() && 54 | start.getMonth() === end.getMonth() && 55 | start.getDate() === end.getDate()} 56 | {@const emphasize = 'font-semibold'} 57 | {@const deEmphasize = 'fill-gray-500 italic'} 58 | 59 | 64 | {datePortion(start)} 65 | 66 | {#if type !== 'DATE'} 67 | 72 | {timePortion(start)} 73 | 74 | {/if} 75 | 76 | 77 | 83 | {datePortion(end)} 84 | 85 | {#if type !== 'DATE'} 86 | 92 | {timePortion(end)} 93 | 94 | {/if} 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /src/components/viz/histogram/histogram-tooltip/HistogramTooltip.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#if !(value == undefined)} 16 | 17 | 24 | {leftBinInclusive ? '[' : '('}{formatFloat(value.low)}, {formatFloat( 25 | value.high 26 | )}{']'}: {formatInteger(value.count)} 27 | row{#if value.count !== 1}s{/if} (click bar to export) 28 | 29 | 30 | {/if} 31 | -------------------------------------------------------------------------------- /src/components/viz/timestamp/TimestampBound.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 30 | -------------------------------------------------------------------------------- /src/components/viz/timestamp/TimestampMouseoverAnnotation.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 36 | 43 | {#each [[yAccessor, 'rgb(100,100,100)']] as [accessor, color]} 44 | {@const cx = $X(point[xAccessor])} 45 | {@const cy = $Y(point[accessor])} 46 | {#if cx && cy} 47 | 48 | {/if} 49 | {/each} 50 | 56 | 62 | {standardTimestampFormat(xStartLabel)} - {standardTimestampFormat( 63 | xEndLabel 64 | )} 65 | 66 | 72 | {formatInteger(~~point[yAccessor])} row{#if point[yAccessor] !== 1}s{/if} 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/components/viz/timestamp/TimestampPaths.svelte: -------------------------------------------------------------------------------- 1 | 119 | 120 | 121 | 129 | 130 | 131 | $plotConfig.width * $plotConfig.devicePixelRatio 136 | ? 1 137 | : 0} 138 | > 139 | 146 | 153 | 154 | -------------------------------------------------------------------------------- /src/components/viz/timestamp/TimestampProfileSummary.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
    27 |
    28 |
    29 | {#if interval} 30 | Range is {intervalToTimestring(interval)} 31 | {/if} 32 |
    33 |
    34 | {#if zoomed} 35 | Zoomed to {numZoomedRows} rows ({formatBigNumberPercentage( 36 | numZoomedRows / numTotalRows 37 | )}). 38 | {/if} 39 |
    40 |
    41 | 42 |
    Ctrl + Click + Drag to zoom
    43 |
    44 | -------------------------------------------------------------------------------- /src/components/viz/timestamp/ZoomWindow.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | 32 | -------------------------------------------------------------------------------- /src/components/viz/timestamp/extremum-resolution-store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module extremum-resolution-store 3 | * This specialized store handles the resolution of plot bounds based 4 | * on multiple extrema. If multiple components set a maximum value within 5 | * the same namespace, the store will automatically pick the largest 6 | * for the store value. This enables us to determine, for instance, if 7 | * multiple lines are on the same chart, which ones determine the bounds. 8 | */ 9 | import { max, min } from 'd3-array'; 10 | import { cubicOut } from 'svelte/easing'; 11 | import { tweened } from 'svelte/motion'; 12 | import { derived, get, writable, type Writable } from 'svelte/store'; 13 | import type { EasingFunction } from 'svelte/transition'; 14 | 15 | const LINEAR_SCALE_STORE_DEFAULTS = { 16 | duration: 0, 17 | easing: cubicOut, 18 | direction: 'min', 19 | namespace: undefined, 20 | alwaysOverrideInitialValue: false 21 | }; 22 | 23 | interface extremumArgs { 24 | duration?: number; 25 | easing?: EasingFunction; 26 | direction?: string; 27 | alwaysOverrideInitialValue?: boolean; 28 | } 29 | 30 | interface Extremum { 31 | value: number | Date; 32 | override?: boolean; 33 | } 34 | 35 | interface ExtremaStoreValue { 36 | [key: string]: Extremum; 37 | } 38 | 39 | const extremaFunctions = { min, max }; 40 | 41 | export function createExtremumResolutionStore( 42 | initialValue: number | Date = undefined, 43 | passedArgs: extremumArgs = {} 44 | ) { 45 | const args = { ...LINEAR_SCALE_STORE_DEFAULTS, ...passedArgs }; 46 | const storedValues: Writable = writable({}); 47 | let tweenProps = { 48 | duration: args.duration, 49 | easing: args.easing 50 | }; 51 | const valueTween = tweened(initialValue, tweenProps); 52 | function _update(key: string, value: number | Date, override = false) { 53 | // FIXME: there's an odd bug where if I don't check for equality first, I tend 54 | // to get an infinite loop with dates and the downstream scale. 55 | // This is easily fixed by only updating if the value has in fact changed. 56 | const extremum = get(storedValues)[key]; 57 | if (extremum?.value === value && extremum?.override === override) { 58 | return; 59 | } 60 | storedValues.update(storeValue => { 61 | if (!(key in storeValue)) { 62 | storeValue[key] = { value: undefined, override: false }; 63 | } 64 | storeValue[key].value = value; 65 | storeValue[key].override = override; 66 | return storeValue; 67 | }); 68 | } 69 | /** add the initial value as its own key, if set by user. */ 70 | if (initialValue && args.alwaysOverrideInitialValue === false) { 71 | _update('__initial_value__', initialValue); 72 | } 73 | 74 | function _remove(key: string) { 75 | storedValues.update(storeValue => { 76 | delete storeValue[key]; 77 | return storeValue; 78 | }); 79 | } 80 | 81 | const domainExtremum = derived( 82 | storedValues, 83 | $storedValues => { 84 | let extremum; 85 | const extrema: Extremum[] = [...Object.values($storedValues)]; 86 | for (const entry of extrema) { 87 | if (entry.override) { 88 | extremum = entry.value; 89 | break; 90 | } else { 91 | extremum = extremaFunctions[args.direction]([ 92 | entry.value, 93 | extremum 94 | ]); 95 | } 96 | } 97 | return extremum; 98 | }, 99 | initialValue 100 | ); 101 | 102 | // set the final tween with the value. 103 | domainExtremum.subscribe(value => { 104 | if (value !== undefined) { 105 | valueTween.set(value, tweenProps); 106 | } 107 | }); 108 | 109 | const returnedStore = { 110 | subscribe: valueTween.subscribe, 111 | setWithKey(key, value = undefined, override = undefined) { 112 | _update(key, value, override); 113 | }, 114 | removeKey(key: string) { 115 | _remove(key); 116 | }, 117 | setTweenProps(tweenPropsArgs) { 118 | tweenProps = tweenPropsArgs; 119 | } 120 | }; 121 | return returnedStore; 122 | } 123 | -------------------------------------------------------------------------------- /src/components/viz/timestamp/utils.ts: -------------------------------------------------------------------------------- 1 | import { line, area, curveLinear, curveStep } from 'd3-shape'; 2 | import type { ScaleLinear, ScaleTime } from 'd3-scale'; 3 | 4 | export type GraphicScale = 5 | | ScaleLinear 6 | | ScaleTime; 7 | 8 | /** 9 | * Creates a string to be fed into the d attribute of a path, 10 | * producing a single path definition for one circle. 11 | * These completed, segmented arcs will not overlap in a way where 12 | * we can overplot if part of the same path. 13 | */ 14 | export function circlePath(cx: number, cy: number, r: number): string { 15 | return ` 16 | M ${cx - r}, ${cy} 17 | a ${r},${r} 0 1,0 ${r * 2},0 18 | a ${r},${r} 0 1,0 ${-r * 2},0 19 | `; 20 | } 21 | 22 | const curves = { 23 | curveLinear, 24 | curveStep 25 | }; 26 | 27 | export function pathDoesNotDropToZero(yAccessor: string) { 28 | return (d, i: number, arr) => { 29 | return ( 30 | !isNaN(d[yAccessor]) && 31 | d[yAccessor] !== undefined && 32 | // remove all zeroes where the previous or next value is also zero. 33 | // these do not add to our understanding. 34 | (!(i !== 0 && d[yAccessor] === 0 && arr[i - 1][yAccessor] === 0) || 35 | !( 36 | i !== arr.length - 1 && 37 | d[yAccessor] === 0 && 38 | arr[i + 1][yAccessor] === 0 39 | )) 40 | ); 41 | }; 42 | } 43 | 44 | interface LineGeneratorArguments { 45 | xAccessor: string; 46 | xScale: ScaleLinear | ScaleTime; 47 | yScale: ScaleLinear | ScaleTime; 48 | curve: string; 49 | pathDefined?: ( 50 | datum: object, 51 | i: number, 52 | arr: ArrayLike 53 | ) => boolean; 54 | } 55 | 56 | /** 57 | * A convenience function to generate a nice SVG path for a time series. 58 | * FIXME: rename to timeSeriesLineFactory. 59 | * FIXME: once we've gotten the data generics in place and threaded into components, let's make sure to type this. 60 | */ 61 | export function lineFactory(args: LineGeneratorArguments) { 62 | return (yAccessor: string) => 63 | line() 64 | .x(d => args.xScale(d[args.xAccessor])) 65 | .y(d => args.yScale(d[yAccessor])) 66 | .curve(curves[args.curve] || curveLinear) 67 | .defined(args.pathDefined || pathDoesNotDropToZero(yAccessor)); 68 | } 69 | 70 | /** 71 | * A convenience function to generate a nice SVG area path for a time series. 72 | * FIXME: rename to timeSeriesAreaFactory. 73 | * FIXME: once we've gotten the data generics in place and threaded into components, let's make sure to type this. 74 | */ 75 | export function areaFactory(args: LineGeneratorArguments) { 76 | return (yAccessor: string) => 77 | area() 78 | .x(d => ~~args.xScale(d[args.xAccessor])) 79 | .y0(~~args.yScale(0)) 80 | .y1(d => ~~args.yScale(d[yAccessor])) 81 | .curve(curves[args.curve] || curveLinear) 82 | .defined(args.pathDefined || pathDoesNotDropToZero(yAccessor)); 83 | } 84 | 85 | /** 86 | * Return a list of ticks to be represented on the 87 | * axis or grid depending on axis-side, it's length and 88 | * the data type of field 89 | */ 90 | export function getTicks( 91 | xOrY: string, 92 | scale: GraphicScale, 93 | axisLength: number, 94 | isDate: boolean 95 | ) { 96 | const tickCount = ~~(axisLength / (xOrY === 'x' ? 150 : 50)); 97 | let ticks = scale.ticks(tickCount); 98 | 99 | if (ticks.length <= 1) { 100 | if (isDate) { 101 | ticks = scale.domain(); 102 | } else { 103 | ticks = scale.nice().domain(); 104 | } 105 | } 106 | 107 | return ticks; 108 | } 109 | -------------------------------------------------------------------------------- /src/dataAPI/jupyter/cell.ts: -------------------------------------------------------------------------------- 1 | import type { ICellModel, ICodeCellModel } from '@jupyterlab/cells'; 2 | import { Signal } from '@lumino/signaling'; 3 | 4 | export default class CellAPI { 5 | _runSignal = new Signal(this); 6 | model: ICellModel; 7 | index: number; 8 | 9 | constructor(model: ICellModel, index: number) { 10 | this.model = model; 11 | this.index = index; 12 | } 13 | 14 | set text(text: string) { 15 | this.model.value.text = text; 16 | } 17 | 18 | get text() { 19 | return this.model.value.text; 20 | } 21 | 22 | get type() { 23 | return this.model.type; 24 | } 25 | 26 | // an event emitted when the code/markdown changes 27 | edited() { 28 | return this.model.value.changed; 29 | } 30 | 31 | // an event emitted when this cell is run 32 | run() { 33 | return this._runSignal; 34 | } 35 | 36 | getExecutionCount(): number | undefined { 37 | 38 | if (this.type === "code") { 39 | return (this.model as ICodeCellModel).executionCount 40 | } 41 | 42 | return undefined 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/dataAPI/jupyter/notebook.ts: -------------------------------------------------------------------------------- 1 | import { NotebookPanel, Notebook, NotebookActions } from '@jupyterlab/notebook'; 2 | import { PromiseDelegate } from '@lumino/coreutils'; 3 | import { PathExt } from '@jupyterlab/coreutils'; 4 | import { Signal, type ISignal } from '@lumino/signaling'; 5 | import _ from 'lodash'; 6 | 7 | import CellAPI from './cell'; 8 | // import KernelAPI from './kernel' 9 | 10 | export class NotebookAPI { 11 | private readonly _ready: PromiseDelegate; 12 | private _changed = new Signal(this); 13 | private _hasConnection = false; 14 | 15 | panel: NotebookPanel | undefined; 16 | // kernel: KernelAPI; 17 | cells: CellAPI[]; 18 | mostRecentExecutionCount: number; 19 | 20 | constructor(notebookPanel: NotebookPanel) { 21 | this.panel = notebookPanel; 22 | this._ready = new PromiseDelegate(); 23 | 24 | if (!(this.panel == undefined)) { 25 | this.listenToSession(); 26 | this.panel.revealed.then(() => { 27 | this.listenToCells(); 28 | this._hasConnection = true 29 | this._ready.resolve(); 30 | }); 31 | } else { 32 | this._ready.resolve(); 33 | } 34 | } 35 | 36 | saveToNotebookMetadata(key: string, value: any) { 37 | this.panel.model.metadata.set(key, value); 38 | } 39 | 40 | // ready is a Promise that resolves once the notebook is done loading 41 | get ready(): Promise { 42 | return this._ready.promise; 43 | } 44 | 45 | get hasConnection(): boolean { 46 | return this._hasConnection; 47 | } 48 | 49 | // changed is a signal emitted when various things in this notebook change 50 | get changed(): ISignal { 51 | return this._changed; 52 | } 53 | 54 | get notebook(): Notebook | undefined { 55 | return this.panel?.content; 56 | } 57 | 58 | get language(): string | undefined { 59 | const meta = this.notebook?.model?.metadata; 60 | if (meta?.has('language_info')) { 61 | const val = meta?.get('language_info')?.valueOf(); 62 | if (val instanceof Object) { 63 | return val['name']; 64 | } 65 | } 66 | } 67 | 68 | get path(): string | undefined { 69 | return this.panel?.sessionContext?.path; 70 | } 71 | 72 | get name(): string | undefined { 73 | return this.path ? PathExt.basename(this.path) : undefined; 74 | } 75 | 76 | get activeCell(): CellAPI | undefined { 77 | if (this.notebook && this.cells) { 78 | const active = this.notebook.activeCell; 79 | return this.cells.find(c => c.model.id === active.model.id); 80 | } 81 | return undefined; 82 | } 83 | 84 | public addCell(kind: 'code' | 'markdown', text: string, index?: number) { 85 | let cell; 86 | if (kind === 'code') { 87 | cell = this.notebook.model.contentFactory.createCodeCell({}); 88 | } else { 89 | cell = this.notebook.model.contentFactory.createMarkdownCell({}); 90 | } 91 | cell.value.text = text; 92 | 93 | // if no index provided, insert at current position 94 | if (index == undefined) { 95 | const active = this.notebook.activeCell; 96 | index = this.cells.findIndex(c => c.model.id === active.model.id); 97 | index += 1 98 | } 99 | 100 | this.notebook.model.cells.insert(index, cell); 101 | } 102 | 103 | // Various notebook level events you can listen to 104 | private listenToCells() { 105 | this.loadCells(); 106 | 107 | // event fires when cells are added, deleted, or moved 108 | this.notebook.model.cells.changed.connect(() => { 109 | this.loadCells(); 110 | this._changed.emit('cells'); 111 | }); 112 | 113 | // event fires when the user selects a cell 114 | this.notebook.activeCellChanged.connect((_, cell) => { 115 | this._changed.emit('activeCell'); 116 | }); 117 | 118 | // event fires when any cell is run 119 | NotebookActions.executed.connect((_, args) => { 120 | // can get execution signals from other notebooks 121 | if (args.notebook.id === this.notebook.id) { 122 | const cell = this.cells.find( 123 | c => c.model.id === args.cell.model.id 124 | ); 125 | if (cell) { 126 | const exCount = cell.getExecutionCount() 127 | if (exCount) { 128 | this.mostRecentExecutionCount = exCount 129 | } 130 | cell._runSignal.emit(); 131 | this._changed.emit('cell run'); 132 | } 133 | } 134 | }); 135 | 136 | this.notebook.model.metadata.changed.connect((_, args) => { 137 | this._changed.emit('language changed'); 138 | }) 139 | 140 | } 141 | 142 | private listenToSession() { 143 | // event fires when the user moves or renames their notebook 144 | this.panel.sessionContext.propertyChanged.connect((_, prop) => { 145 | if (prop === 'path') { 146 | this._changed.emit('path'); 147 | } 148 | if (prop === 'name') { 149 | this._changed.emit('name'); 150 | } 151 | }); 152 | } 153 | 154 | private loadCells() { 155 | this.cells = []; 156 | for (let i = 0; i < this.notebook.model.cells.length; i++) { 157 | this.cells.push(new CellAPI(this.notebook.model.cells.get(i), i)); 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application'; 5 | import { INotebookTracker } from '@jupyterlab/notebook'; 6 | import { NotebookAPI } from './dataAPI/jupyter/notebook'; 7 | import { ProfilePanel } from './ProfilePanel'; 8 | 9 | /** 10 | * Initialization data for the AutoProfiler extension. 11 | */ 12 | const plugin: JupyterFrontEndPlugin = { 13 | id: 'AutoProfiler:plugin', 14 | autoStart: true, 15 | activate: (app: JupyterFrontEnd, notebookTracker: INotebookTracker) => { 16 | const panel: ProfilePanel = new ProfilePanel(); 17 | app.shell.add(panel, 'right', { rank: 1000 }); 18 | 19 | // emitted when the user's notebook changes, null if all notebooks close 20 | notebookTracker.currentChanged.connect((_, widget) => { 21 | const notebook = new NotebookAPI(widget); 22 | notebook.ready.then(async () => { 23 | // connect panel to notebook 24 | await panel.connectNotebook(notebook); 25 | }); 26 | }); 27 | }, 28 | requires: [INotebookTracker] 29 | }; 30 | 31 | export default plugin; 32 | -------------------------------------------------------------------------------- /src/stores.ts: -------------------------------------------------------------------------------- 1 | import { type Writable, writable } from 'svelte/store'; 2 | 3 | // UI stores 4 | export const currentHoveredCol: Writable = writable(undefined); 5 | export const showIndex: Writable = writable(false); 6 | -------------------------------------------------------------------------------- /src/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const value: string; 3 | export default value; 4 | } 5 | -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | /* 2 | See the JupyterLab Developer Guide for useful CSS Patterns: 3 | 4 | https://jupyterlab.readthedocs.io/en/stable/developer/css.html 5 | */ 6 | .auto-profile-app { 7 | /* css for the main side panel view container */ 8 | background: var(--jp-layout-color1); 9 | color: var(--jp-ui-font-color1); 10 | 11 | /* This is needed so that all font sizing of children done in ems is 12 | * relative to this base size */ 13 | font-size: var(--jp-ui-font-size1); 14 | 15 | /* text-align: center; */ 16 | overflow-y: auto; 17 | min-width: 400px; 18 | width: 450px; 19 | } 20 | 21 | .auto-profile-wrapper { 22 | min-width: 400px; 23 | } 24 | 25 | #header-icon { 26 | background: url('./logo.svg'); 27 | min-width: 100%; 28 | min-height: 6em; 29 | background-size: contain; 30 | display: inline-block; 31 | background-repeat: no-repeat; 32 | background-position: center; 33 | margin-top: 1em; 34 | } 35 | 36 | .autoprofile-logo { 37 | align-items: center; 38 | display: flex; 39 | } -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /style/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | content: [ 4 | './src/**/*.{html,js,svelte,ts}', 5 | ], 6 | theme: { 7 | extend: {}, 8 | }, 9 | plugins: [], 10 | variants: { 11 | opacity: ({ after }) => after(["disabled"]), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "allowSyntheticDefaultImports": true, 5 | "composite": true, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "incremental": true, 9 | "jsx": "react", 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "noEmitOnError": true, 13 | "noImplicitAny": false, 14 | "noUnusedLocals": true, 15 | "preserveWatchOutput": true, 16 | "resolveJsonModule": true, 17 | "outDir": "lib", 18 | "rootDir": "src", 19 | "strict": false, 20 | "strictNullChecks": false, 21 | "target": "es2017", 22 | "types": [ 23 | "svelte", 24 | "node" 25 | ], 26 | "isolatedModules": true, 27 | "forceConsistentCasingInFileNames": true 28 | }, 29 | "include": [ 30 | "src/**/*", 31 | "style" 32 | ] 33 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const version = require('./package.json').version; 3 | const SveltePreprocess = require('svelte-preprocess'); 4 | 5 | // Custom webpack rules 6 | const rules = [ 7 | { test: /\.ts$/, loader: 'ts-loader' }, 8 | { 9 | test: /\.js$/, 10 | enforce: 'pre', 11 | loader: 'source-map-loader' 12 | }, 13 | { test: /\.css$/, use: ['style-loader', 'css-loader'] }, 14 | { 15 | test: /\.svelte$/, 16 | loader: 'svelte-loader', 17 | options: { 18 | preprocess: SveltePreprocess({ 19 | postcss: { 20 | plugins: [require('tailwindcss'), require('autoprefixer')] 21 | } 22 | }) 23 | } 24 | }, 25 | { 26 | test: /\.svg$/, 27 | loader: 'svg-url-loader' 28 | } 29 | ]; 30 | 31 | // Packages that shouldn't be bundled but loaded at runtime 32 | const externals = [ 33 | '@jupyter-widgets/base', 34 | '@jupyterlab/notebook', 35 | '@lumino/widgets' 36 | ]; 37 | 38 | const resolve = { 39 | extensions: [ 40 | '.webpack.js', 41 | '.web.js', 42 | '.ts', 43 | '.js', 44 | '.svelte', 45 | '.css', 46 | '.svg' 47 | ], 48 | mainFields: ['svelte', 'browser', 'module', 'main'] 49 | }; 50 | 51 | module.exports = [ 52 | /** 53 | * Lab extension 54 | * 55 | * This builds the lib/ folder with the JupyterLab extension. 56 | */ 57 | { 58 | entry: './src/index.ts', 59 | output: { 60 | filename: 'index.js', 61 | path: path.resolve(__dirname, 'lib'), 62 | libraryTarget: 'amd', 63 | publicPath: '' 64 | }, 65 | module: { 66 | rules: rules 67 | }, 68 | externals, 69 | resolve, 70 | ignoreWarnings: [/Failed to parse source map/] 71 | } 72 | ]; 73 | --------------------------------------------------------------------------------