├── .coveragerc ├── .eslintignore ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .gitmodules ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CITATION.bib ├── CONTRIBUTING.md ├── COPYING ├── MANIFEST.in ├── README.md ├── babel.config.js ├── codecov.yml ├── css └── widget.css ├── docs ├── Makefile ├── environment.yml ├── make.bat └── source │ ├── _static │ └── helper.js │ ├── conf.py │ ├── develop-install.rst │ ├── examples │ ├── index.rst │ └── introduction.nblink │ ├── index.rst │ ├── installing.rst │ └── introduction.rst ├── examples ├── 1_introduction.ipynb ├── 2_numpy.ipynb ├── 3_spectral_indices_with_cubo_and_spyndex.ipynb ├── 4_google_earth_engine.ipynb └── 5_spectral_indices_with_open_eo.ipynb ├── install.json ├── jest.config.js ├── lexcube.json ├── lexcube ├── __init__.py ├── _frontend.py ├── _version.py ├── cube3d.py ├── lexcube_server │ ├── .dockerignore │ ├── .gitignore │ ├── .vscode │ │ └── launch.json │ ├── Dockerfile │ ├── config_example.json │ ├── requirements-core.txt │ └── src │ │ ├── lexcube_widget.py │ │ └── tile_server.py ├── nbextension │ └── extension.js └── tests │ ├── __init__.py │ ├── conftest.py │ ├── test_example.py │ └── test_nbextension_path.py ├── package-lock.json ├── package.json ├── pyproject.toml ├── pytest.ini ├── readme-media ├── disconnected.png ├── isometric.png ├── lexcube-demo.gif ├── lexcube-logo.png ├── post-installation-broken-widget.png ├── print-template.png └── sliders.png ├── readthedocs.yml ├── setup.py ├── src ├── __tests__ │ ├── index.spec.ts │ └── utils.ts ├── extension.ts ├── index.ts ├── lexcube-client │ ├── .gitattributes │ ├── .gitignore │ ├── .prettierrc │ ├── .vscode │ │ └── launch.json │ ├── LICENSE │ ├── deps │ │ └── numcodecs-0.2.5.tgz │ ├── package-lock.json │ ├── package.json │ └── src │ │ ├── client │ │ ├── OrbitControls.ts │ │ ├── client.ts │ │ ├── constants.ts │ │ ├── fast-line-segment-map.ts │ │ ├── geojson-loader.worker.ts │ │ ├── index.html │ │ ├── interaction.ts │ │ ├── networking.ts │ │ ├── rendering.ts │ │ ├── tiledata.ts │ │ ├── tsconfig.json │ │ ├── webpack.common.js │ │ ├── webpack.dev.js │ │ └── webpack.prod.js │ │ └── content │ │ ├── default-colormaps.json │ │ ├── parameterCustomColormaps.json │ │ └── parameterMetadataAttribution.json ├── plugin.ts ├── version.ts └── widget.ts ├── tbump.toml ├── tsconfig.eslint.json ├── tsconfig.json └── webpack.config.js /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = lexcube/tests/* 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | **/*.d.ts 5 | tests -------------------------------------------------------------------------------- /.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.eslint.json', 11 | sourceType: 'module' 12 | }, 13 | plugins: ['@typescript-eslint'], 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 | }; -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 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 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | docs/source/_static/embed-bundle.js 65 | docs/source/_static/embed-bundle.js.map 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # IPython Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # dotenv 80 | .env 81 | 82 | # virtualenv 83 | venv/ 84 | ENV/ 85 | 86 | # Spyder project settings 87 | .spyderproject 88 | 89 | # Rope project settings 90 | .ropeproject 91 | 92 | # ========================= 93 | # Operating System Files 94 | # ========================= 95 | 96 | # OSX 97 | # ========================= 98 | 99 | .DS_Store 100 | .AppleDouble 101 | .LSOverride 102 | 103 | # Thumbnails 104 | ._* 105 | 106 | # Files that might appear in the root of a volume 107 | .DocumentRevisions-V100 108 | .fseventsd 109 | .Spotlight-V100 110 | .TemporaryItems 111 | .Trashes 112 | .VolumeIcon.icns 113 | 114 | # Directories potentially created on remote AFP share 115 | .AppleDB 116 | .AppleDesktop 117 | Network Trash Folder 118 | Temporary Items 119 | .apdisk 120 | 121 | # Windows 122 | # ========================= 123 | 124 | # Windows image file caches 125 | Thumbs.db 126 | ehthumbs.db 127 | 128 | # Folder config file 129 | Desktop.ini 130 | 131 | # Recycle Bin used on file shares 132 | $RECYCLE.BIN/ 133 | 134 | # Windows Installer files 135 | *.cab 136 | *.msi 137 | *.msm 138 | *.msp 139 | 140 | # Windows shortcuts 141 | *.lnk 142 | 143 | 144 | # NPM 145 | # ---- 146 | 147 | **/node_modules/ 148 | lexcube/nbextension/index.* 149 | lexcube/labextension/*.tgz 150 | 151 | **/.tiles/ 152 | **/.venv/ 153 | **/node_modules/ 154 | **/build/ 155 | **/.ipynb_checkpoints/ 156 | **/__pycache__/ 157 | *.npy 158 | 159 | # Coverage data 160 | # ------------- 161 | **/coverage/ 162 | 163 | # Packed lab extensions 164 | lexcube/labextension 165 | .venv 166 | .yarn 167 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lexcube/client"] 2 | path = src/lexcube-client 3 | url = git@github.com:msoechting/lexcube-client.git 4 | [submodule "lexcube/lexcube-server"] 5 | path = lexcube/lexcube_server 6 | url = git@github.com:msoechting/lexcube-server.git 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | tests/ 4 | .jshintrc 5 | # Ignore any build output from python: 6 | dist/*.tar.gz 7 | dist/*.wheel 8 | 9 | **/.tiles/ 10 | **/.venv/ 11 | **/node_modules/ 12 | 13 | **/build/ 14 | **/.ipynb_checkpoints/ 15 | **/__pycache__/ 16 | *.npy 17 | readme-media/ 18 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /CITATION.bib: -------------------------------------------------------------------------------- 1 | @ARTICLE{soechting2024lexcube, 2 | author={Söchting, Maximilian and Mahecha, Miguel D. and Montero, David and Scheuermann, Gerik}, 3 | journal={IEEE Computer Graphics and Applications}, 4 | title={Lexcube: Interactive Visualization of Large Earth System Data Cubes}, 5 | year={2024}, 6 | volume={44}, 7 | number={1}, 8 | pages={25-37}, 9 | doi={10.1109/MCG.2023.3321989} 10 | } 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development Guidelines 2 | 3 | Lexcube for Jupyter has been released in January 2024 and is currently in **beta**. If you experience any issues or crashes, miss a feature or want to give any feedback, [opening an issue](https://github.com/msoechting/lexcube/issues/new/choose) is highly appreciated. Thank you. 4 | 5 | ## Development Installation 6 | 7 | Create a dev environment: 8 | 9 | ```bash 10 | # Using VENV only (if Node and Python are already installed) 11 | npm i 12 | python -m venv .venv 13 | .\\.venv\\Scripts\\activate # or your OS-equivalent 14 | pip install jupyterlab 15 | pip install -r ./lexcube/lexcube_server/requirements-core.txt 16 | 17 | # Using conda 18 | conda create -n lexcube-dev -c conda-forge nodejs yarn python jupyterlab 19 | python -m venv .venv 20 | pip install -r ./lexcube/lexcube_server/requirements-core.txt 21 | conda activate lexcube-dev 22 | ``` 23 | 24 | Install the python. This will also build the TS package. 25 | ```bash 26 | pip install -e ".[test, examples]" 27 | ``` 28 | 29 | When developing your extensions, you need to manually enable your extensions with the 30 | notebook / lab frontend. For lab, this is done by the command: 31 | 32 | ```bash 33 | jupyter labextension develop --overwrite . 34 | npm run build 35 | ``` 36 | 37 | For classic notebook, you need to run: 38 | 39 | ```bash 40 | jupyter nbextension install --sys-prefix --symlink --overwrite --py lexcube 41 | jupyter nbextension enable --sys-prefix --py lexcube 42 | ``` 43 | 44 | Note that the `--symlink` flag doesn't work on Windows, so you will here have to run 45 | the `install` command every time that you rebuild your extension (or enable developer mode to enable symlinks). For certain installations you might also need another flag instead of `--sys-prefix`, but we won't cover the meaning of those flags here. 46 | 47 | ### How to see your changes 48 | #### Typescript: 49 | If you use JupyterLab to develop then you can watch the source directory and run JupyterLab at the same time in different 50 | terminals to watch for changes in the extension's source and automatically rebuild the widget. 51 | 52 | ```bash 53 | # Watch the source directory in one terminal, automatically rebuilding when needed 54 | npm run watch 55 | # Run JupyterLab in another terminal 56 | jupyter lab 57 | ``` 58 | 59 | After a change wait for the build to finish and then refresh your browser and the changes should take effect. 60 | 61 | #### Python: 62 | If you make a change to the python code then you will need to restart the notebook kernel to have it take effect. 63 | 64 | ## Updating the version 65 | 66 | To update the version, install tbump and use it to bump the version. 67 | By default it will also create a tag. 68 | 69 | ```bash 70 | pip install tbump 71 | tbump 72 | ``` 73 | 74 | ## Building a new version 75 | ```bash 76 | py -m pip install --upgrade build twine pkginfo 77 | py -m build 78 | ``` 79 | - Tip for Windows users: deactivate Windows Defender "real-time protection" to speed up builds and change your power plan if you are on a laptop. 80 | - Make sure you have a clean working tree (including untracked files), since everything not ignored by the .gitignore will get packaged into the wheel. 81 | 82 | See also: https://packaging.python.org/en/latest/tutorials/packaging-projects/ 83 | 84 | 85 | ## Publishing 86 | 1. Uploading to Pypi: 87 | ```bash 88 | py -m twine upload --repository pypi dist/lexcube-* 89 | ``` 90 | 2. Publishing on NPM: (important for working Google Colab implementation - not sure if relevant to anything else) 91 | ```bash 92 | npm publish 93 | ``` -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | 4 | include setup.py 5 | include pyproject.toml 6 | include pytest.ini 7 | include .coverage.rc 8 | 9 | include tsconfig.json 10 | include package.json 11 | include webpack.config.js 12 | include lexcube/labextension/*.tgz 13 | 14 | # Documentation 15 | graft docs 16 | exclude docs/\#* 17 | prune docs/build 18 | prune docs/gh-pages 19 | prune docs/dist 20 | 21 | # Examples 22 | graft examples 23 | 24 | # Tests 25 | graft tests 26 | prune tests/build 27 | 28 | # Javascript files 29 | graft lexcube/nbextension 30 | graft src 31 | graft css 32 | prune **/node_modules 33 | prune coverage 34 | prune lib 35 | 36 | # Patterns to exclude from any directory 37 | global-exclude *~ 38 | global-exclude *.pyc 39 | global-exclude *.pyo 40 | global-exclude .git 41 | global-exclude .ipynb_checkpoints 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Lexcube Logo](/readme-media/lexcube-logo.png)](https://github.com/msoechting/lexcube) 2 | 3 | **3D Data Cube Visualization in Jupyter Notebooks** 4 | 5 | ![Lexcube Demo GIF](https://raw.githubusercontent.com/msoechting/lexcube/main/readme-media/lexcube-demo.gif) 6 | 7 | --- 8 | 9 | **GitHub**: [https://github.com/msoechting/lexcube](https://github.com/msoechting/lexcube) 10 | 11 | **Paper**: [https://doi.org/10.1080/20964471.2025.2471646](https://doi.org/10.1080/20964471.2025.2471646) 12 | 13 | **PyPI**: [https://pypi.org/project/lexcube/](https://pypi.org/project/lexcube/) 14 | 15 | --- 16 | 17 | **NEW with version 0.4.16**: [Craft your own paper data cube!](#print-your-own-paper-data-cube) 18 | 19 | ![Print template graphic](https://raw.githubusercontent.com/msoechting/lexcube/main/readme-media/print-template.png) 20 | 21 | --- 22 | 23 | Lexcube is a library for interactively visualizing three-dimensional floating-point data as 3D cubes in Jupyter notebooks. 24 | 25 | Supported data formats: 26 | - numpy.ndarray (with exactly 3 dimensions) 27 | - xarray.DataArray (with exactly 3 dimensions, rectangularly gridded) 28 | 29 | Possible data sources: 30 | - Any gridded Zarr or NetCDF data set (local or remote, e.g., accessed with S3) 31 | - Copernicus Data Storage, e.g., [ERA5 data](https://cds.climate.copernicus.eu/cdsapp#!/dataset/reanalysis-era5-complete?tab=overview) 32 | - Google Earth Engine ([using xee, see example notebook](https://github.com/msoechting/lexcube/blob/main/examples/4_google_earth_engine.ipynb)) 33 | 34 | Example notebooks can be found in the [examples](https://github.com/msoechting/lexcube/tree/main/examples) folder. For a live demo, see also [lexcube.org](https://www.lexcube.org). 35 | 36 | ## Table-of-Contents 37 | 38 | 39 | 40 | - [Table-of-Contents](#table-of-contents) 41 | - [Attribution](#attribution) 42 | - [How to Use Lexcube](#how-to-use-lexcube) 43 | - [Example Notebooks](#example-notebooks) 44 | - [Getting Started - Minimal Example](#getting-started---minimal-example) 45 | - [Installation](#installation) 46 | - [Cube Visualization](#cube-visualization) 47 | - [Interacting with the Cube](#interacting-with-the-cube) 48 | - [Range Boundaries](#range-boundaries) 49 | - [Colormaps](#colormaps) 50 | - [Supported colormaps](#supported-colormaps) 51 | - [Overlay GeoJSON data](#overlay-geojson-data) 52 | - [Save figures](#save-figures) 53 | - [Print your own paper data cube](#print-your-own-paper-data-cube) 54 | - [Get currently visible data subset](#get-currently-visible-data-subset) 55 | - [Supported metadata](#supported-metadata) 56 | - [Troubleshooting](#troubleshooting) 57 | - [The cube does not respond / API methods are not doing anything / Cube does not load new data](#the-cube-does-not-respond-api-methods-are-not-doing-anything-cube-does-not-load-new-data) 58 | - [After installation/update, no widget is shown, only text](#after-installationupdate-no-widget-is-shown-only-text) 59 | - [w.savefig breaks when batch-processing/trying to create many figures quickly](#wsavefig-breaks-when-batch-processingtrying-to-create-many-figures-quickly) 60 | - [The layout of the widget looks very messed up](#the-layout-of-the-widget-looks-very-messed-up) 61 | - ["Error creating WebGL context" or similar](#error-creating-webgl-context-or-similar) 62 | - [Memory is filling up a lot when using a chunked dataset](#memory-is-filling-up-a-lot-when-using-a-chunked-dataset) 63 | - [Known bugs](#known-bugs) 64 | - [Attributions](#attributions) 65 | - [Development Installation & Guide](#development-installation-guide) 66 | - [License](#license) 67 | 68 | 69 | 70 | 71 | ## Attribution 72 | 73 | When using Lexcube and generated images or videos, please acknowledge/cite: 74 | ```bibtex 75 | @article{Soechting2025Lexcube, 76 | author = {Maximilian Söchting and Gerik Scheuermann and David Montero and Miguel D. Mahecha}, 77 | title = {Interactive Earth system data cube visualization in Jupyter notebooks}, 78 | journal = {Big Earth Data}, 79 | pages = {1--15}, 80 | year = {2025}, 81 | publisher = {Taylor \& Francis}, 82 | doi = {10.1080/20964471.2025.2471646}, 83 | URL = {https://doi.org/10.1080/20964471.2025.2471646}, 84 | } 85 | ``` 86 | Lexcube is a project by Maximilian Söchting at the [RSC4Earth](https://www.rsc4earth.de/) at Leipzig University, advised by Prof. Dr. Miguel D. Mahecha and Prof. Dr. Gerik Scheuermann. Thanks to the funding provided by ESA through [DeepESDL](https://www.earthsystemdatalab.net/) and DFG through the NFDI4Earth pilot projects! 87 | 88 | 89 | ## How to Use Lexcube 90 | ### Example Notebooks 91 | If you are new to Lexcube, try the [general introduction notebook](https://github.com/msoechting/lexcube/blob/main/examples/1_introduction.ipynb) which demonstrates how to visualize a remote Xarray data set. 92 | 93 | There are also specific example notebooks for the following use cases: 94 | - [Visualizing Google Earth Engine data - using xee](https://github.com/msoechting/lexcube/blob/main/examples/4_google_earth_engine.ipynb) 95 | - [Generating and visualizing a spectral index data cube from scratch - using cubo and spyndex, with data from Microsoft Planetary Computer](https://github.com/msoechting/lexcube/blob/main/examples/3_spectral_indices_with_cubo_and_spyndex.ipynb) 96 | - [Generating and visualizing a spectral index data cube - with data from OpenEO](https://github.com/msoechting/lexcube/blob/main/examples/5_spectral_indices_with_open_eo.ipynb) 97 | - [Visualizing Numpy data](https://github.com/msoechting/lexcube/blob/main/examples/2_numpy.ipynb) 98 | 99 | ### Getting Started - Minimal Example 100 | #### Visualizing Xarray Data 101 | ```python 102 | import xarray as xr 103 | import lexcube 104 | ds = xr.open_dataset("https://data.rsc4earth.de/download/EarthSystemDataCube/v3.0.2/esdc-8d-0.25deg-256x128x128-3.0.2.zarr/", chunks={}, engine="zarr") 105 | da = ds["air_temperature_2m"][256:512,256:512,256:512] 106 | w = lexcube.Cube3DWidget(da, cmap="thermal_r", vmin=-20, vmax=30) 107 | w.plot() 108 | ``` 109 | #### Visualizing Numpy Data 110 | ```python 111 | import numpy as np 112 | import lexcube 113 | data_source = np.sum(np.mgrid[0:256,0:256,0:256], axis=0) 114 | w = lexcube.Cube3DWidget(data_source, cmap="prism", vmin=0, vmax=768) 115 | w.plot() 116 | ``` 117 | 118 | #### Visualizing Google Earth Engine Data 119 | See [the full example here](https://github.com/msoechting/lexcube/blob/main/examples/4_google_earth_engine.ipynb). 120 | ```python 121 | import lexcube 122 | import xarray as xr 123 | import ee 124 | ee.Authenticate() 125 | ee.Initialize(opt_url="https://earthengine-highvolume.googleapis.com") 126 | ds = xr.open_dataset("ee://ECMWF/ERA5_LAND/HOURLY", engine="ee", crs="EPSG:4326", scale=0.25, chunks={}) 127 | da = ds["temperature_2m"][630000:630003,2:1438,2:718] 128 | w = lexcube.Cube3DWidget(da) 129 | w.plot() 130 | ``` 131 | 132 | #### Note on Google Collab 133 | If you are using Google collab, you may need to execute the following before running Lexcube: 134 | 135 | ```python 136 | from google.colab import output 137 | output.enable_custom_widget_manager() 138 | ``` 139 | 140 | #### Note on Juypter for VSCode 141 | If you are using Jupyter within VSCode, you may have to add the following to your settings before running Lexcube: 142 | ```json 143 | "jupyter.widgetScriptSources": [ 144 | "jsdelivr.com", 145 | "unpkg.com" 146 | ], 147 | ``` 148 | If you are working on a remote server in VSCode, do not forget to set this setting also there! This allows the Lexcube JavaScript front-end files to be downloaded from these sources ([read more](https://github.com/microsoft/vscode-jupyter/wiki/IPyWidget-Support-in-VS-Code-Python)). 149 | 150 | ## Installation 151 | 152 | You can install using `pip`: 153 | 154 | ```bash 155 | pip install lexcube 156 | ``` 157 | 158 | After installing or upgrading Lexcube, you should **refresh the Juypter web page** (if currently open) and **restart the kernel** (if currently running). 159 | 160 | If you are using Jupyter Notebook 5.2 or earlier, you may also need to enable 161 | the nbextension: 162 | ```bash 163 | jupyter nbextension enable --py [--sys-prefix|--user|--system] lexcube 164 | ``` 165 | 166 | ## Cube Visualization 167 | On the cube, the dimensions are visualized as follow: X from left-to-right (0 to max), Y from top-to-bottom (0 to max), Z from back-to-front (0 to max); with Z being the first dimension (`axis[0]`), Y the second dimension (`axis[1]`) and X the third dimension (`axis[2]`) on the input data. If you prefer to flip any dimension or re-order dimensions, you can modify your data set accordingly before calling the Lexcube widget, e.g. re-ordering dimensions with xarray: `ds.transpose(ds.dims[0], ds.dims[2], ds.dims[1])` and flipping dimensions with numpy: `np.flip(ds, axis=1)`. 168 | 169 | ## Interacting with the Cube 170 | - Zooming in/out on any side of the cube: 171 | - Mousewheel 172 | - Scroll gesture on touchpad (two fingers up or down) 173 | - On touch devices: Two-touch pinch gesture 174 | - Panning the currently visible selection: 175 | - Click and drag the mouse cursor 176 | - On touch devices: touch and drag 177 | - Moving over the cube with your cursor will show a tooltip in the bottom left about the pixel under the cursor. 178 | - For more precise input, you can use the sliders provided by `w.show_sliders()`: 179 | ![Sliders](https://raw.githubusercontent.com/msoechting/lexcube/main/readme-media/sliders.png) 180 | 181 | 182 | ## Range Boundaries 183 | You can read and write the boundaries of the current selection via the `xlim`, `ylim` and `zlim` tuples. 184 | 185 | ```python 186 | w = lexcube.Cube3DWidget(da, cmap="thermal", vmin=-20, vmax=30) 187 | w.plot() 188 | # Next cell: 189 | w.xlim = (20, 400) 190 | ``` 191 | 192 | For fine-grained interactive controls, you can display a set of sliders in another cell, like this: 193 | 194 | ```python 195 | w = lexcube.Cube3DWidget(da, cmap="thermal", vmin=-20, vmax=30) 196 | w.plot() 197 | # Next cell: 198 | w.show_sliders() 199 | ``` 200 | For very large data sets, you may want to use `w.show_sliders(continuous_update=False)` to prevent any data being loaded before making a final slider selection. 201 | 202 | If you want to wrap around a dimension, i.e., seamleasly scroll over back to the 0-index beyond the maximum index, you can enable that feature like this: 203 | ```python 204 | w.xwrap = True 205 | ``` 206 | For data sets that have longitude values in their metadata very close to a global round-trip, this is automatically active for the X dimension. 207 | 208 | *Limitations: Currently only supported for the X dimension. If xwrap is active, the xlim tuple may contain values up to double the valid range to always have a range where x_min < x_max. To get values in the original/expected range, you can simply calculate x % x_max.* 209 | 210 | ## Colormaps 211 | All colormaps of matplotlib and cmocean are supported. 212 | The range of the colormap, if not set using `vmin`/`vmax`, is automatically adjusted to the approximate observed minimum and maximum values*note* within the current session. Appending "_r" to any colormap name will reverse it. 213 | 214 | 215 | ```python 216 | # 1) Set cmap in constructor 217 | w = lexcube.Cube3DWidget(da, cmap="thermal", vmin=-20, vmax=30) 218 | w.plot() 219 | 220 | # 2) Set cmap later 221 | w.cmap = "thermal" 222 | w.vmin = -20 223 | w.vmax = 30 224 | 225 | # 3) Set custom colormap using lists (evenly spaced RGB values) 226 | w.cmap = cmocean.cm.thermal(np.linspace(0.0, 1.0, 100)).tolist() 227 | w.cmap = [[0.0, 0.0, 0.0], [1.0, 0.5, 0.5], [0.5, 1.0, 1.0]] 228 | ``` 229 | 230 | *note* Lexcube actually calculates the mean of all values that have been visible so far in this session and applies ±2.5σ (standard deviation) in both directions to obtain the colormap ranges, covering approximately 98.7% of data points. This basic method allows to filter most outliers that would otherwise make the colormap range unnecessarily large and, therefore, the visualization uninterpretable. 231 | 232 | ### Supported colormaps 233 | ```python 234 | Cmocean: 235 | - "thermal", "haline", "solar", "ice", "gray", "oxy", "deep", "dense", "algae", "matter", "turbid", "speed", "amp", "tempo", "rain", "phase", "topo", "balance", "delta", "curl", "diff", "tarn" 236 | Proplot custom colormaps: 237 | - "Glacial", "Fire", "Dusk", "DryWet", "Div", "Boreal", "Sunset", "Sunrise", "Stellar", "NegPos", "Marine" 238 | Scientific Colormaps by Crameri: 239 | - "acton", "bam", "bamako", "bamO", "batlow", "batlowK", "batlowW", "berlin", "bilbao", "broc", "brocO", "buda", "bukavu", "cork", "corkO", "davos", "devon", "fes", "glasgow", "grayC", "hawaii", "imola", "lajolla", "lapaz", "lisbon", "lipari", "managua", "navia", "nuuk", "oleron", "oslo", "roma", "romaO", "tofino", "tokyo", "turku", "vanimo", "vik", "vikO" 240 | PerceptuallyUniformSequential: 241 | - "viridis", "plasma", "inferno", "magma", "cividis" 242 | Sequential: 243 | - "Greys", "Purples", "Blues", "Greens", "Oranges", "Reds", "YlOrBr", "YlOrRd", "OrRd", "PuRd", "RdPu", "BuPu", "GnBu", "PuBu", "YlGnBu", "PuBuGn", "BuGn", "YlGn" 244 | Sequential(2): 245 | - "binary", "gist_yarg", "gist_gray", "gray", "bone", "pink", "spring", "summer", "autumn", "winter", "cool", "Wistia", "hot", "afmhot", "gist_heat", "copper" 246 | Diverging: 247 | - "PiYG", "PRGn", "BrBG", "PuOr", "RdGy", "RdBu", "RdYlBu", "RdYlGn", "Spectral", "coolwarm", "bwr", "seismic", 248 | Cyclic: 249 | - "twilight", "twilight_shifted", "hsv" 250 | Qualitative: 251 | - "Pastel1", "Pastel2", "Paired", "Accent", "Dark2", "Set1", "Set2", "Set3", "tab10", "tab20", "tab20b", "tab20c" 252 | Miscellaneous: 253 | - "flag", "prism", "ocean", "gist_earth", "terrain", "gist_stern", "gnuplot", "gnuplot2", "CMRmap", "cubehelix", "brg", "gist_rainbow", "rainbow", "jet", "nipy_spectral", "gist_ncar" 254 | ``` 255 | 256 | ## Overlay GeoJSON data 257 | You can overlay GeoJSON data onto the cube visualization like this: 258 | ```python 259 | # 1. Using a URL 260 | w.overlay_geojson("https://github.com/nvkelso/natural-earth-vector/raw/refs/heads/master/geojson/ne_50m_admin_0_countries.geojson") 261 | 262 | # 2. Using a local file 263 | w.overlay_geojson("regions.geojson") 264 | 265 | # 3. Using a JSON/dict object 266 | w.overlay_geojson({ 267 | "type": "Feature", 268 | "geometry": { 269 | "type": "Polygon", 270 | "coordinates": [[ 271 | [24, -11], 272 | [13, -5], 273 | [17, -7], 274 | [24, -11] 275 | ]] 276 | } 277 | }) 278 | ``` 279 | Lexcube extracts the geospatial context from the data set to overlay the GeoJSON. Your data set will need to have "y"/"lat"/"latitude" (as Y) and "x"/"lon"/"longitude" (as X) dimensions for this to work. Lexcube assumes pixel-centered addressing and regular steps across the dimensions. 280 | 281 | Using `MultiPolygon` or `Polygon` is preferred. `Point`, `MultiPoint`, `MultiLineString` and `LineString` are also supported. `Point` and `MultiPoint` are represented via diamonds. 282 | 283 | If the default color does not work for your data, you can change the color using the second argument: 284 | ```python 285 | # All 140 X11 color names are supported (no camelcase) 286 | w.overlay_geojson(natural_earth_url, "skyblue") 287 | 288 | # Alternatively: 289 | w.overlay_geojson(natural_earth_url, "rgb(255, 0, 0)") 290 | ``` 291 | 292 | ## Save figures 293 | You can save transparent PNG images of the cube like this: 294 | ```python 295 | w.savefig(fname="cube.png", include_ui=True, dpi_scale=2.0) 296 | ``` 297 | - `fname`: name of the image file. Default: `lexcube-{current time and date}.png`. 298 | - `include_ui`: whether to include UI elements such as the axis descriptions and the colormap legend in the image. Default: `true`. 299 | - `dpi_scale`: the image resolution is multiplied by this value to obtain higher-resolution/quality images. For example, a value of 2.0 means that the image resolution is doubled for the PNG vs. what is visible in the notebook. Default: `2.0`. 300 | 301 | If you want to edit multiple cubes into one picture, you may prefer an isometric rendering (no depth distortion). You can enable it in the widget constructor: `lexcube.Cube3DWidget(data_source, isometric_mode=True)`. For comparison: 302 | 303 | ![Isometric vs. perspective camera comparison](https://raw.githubusercontent.com/msoechting/lexcube/main/readme-media/isometric.png) 304 | 305 | ## Print your own paper data cube 306 | You can generate a template to make your own paper data cube from your currently visible data cube like this: 307 | ```python 308 | w.save_print_template() 309 | ``` 310 | In the opened dialog, you can download the print template as either PNG or SVG to your computer. You can also add a custom note to the print template, e.g. to remember specifics about the data set. Printing (recommended: thick paper or photo paper, e.g., 15x20cm at a photo shop or self-service photo printer), cutting and gluing will give you your own paper data cube for your desk: 311 | 312 | ![Print template graphic](https://raw.githubusercontent.com/msoechting/lexcube/main/readme-media/print-template.png) 313 | 314 | ## Get currently visible data subset 315 | 316 | You can get the currently visible sub-selection of your data set with `w.get_current_cube_selection()`. There are three ways to use this function: 317 | 318 | ```python 319 | # 1. Return currently visible data subset 320 | air_temperature_sub_cube = w.get_current_cube_selection() 321 | 322 | # 2. Return the currently visible selection, but applied to a different 3D dataset 323 | kndvi_sub_cube = w.get_current_cube_selection(data_to_be_indexed=ds["kndvi"]) 324 | 325 | # 3. Return indices of the currently visible selection 326 | selection_indices = w.get_current_cube_selection(return_index_only=True) 327 | ``` 328 | See the end of the [introduction notebook](https://github.com/msoechting/lexcube/blob/main/examples/1_introduction.ipynb) for a live example. 329 | 330 | ## Supported metadata 331 | When using Xarray for the input data, the following metadata is automatically integrated into the visualization: 332 | 333 | - Dimension names 334 | - Read from the xarray.DataArray.dims attribute 335 | - Parameter name 336 | - Read from the xarray.DataArray.attrs.long_name attribute 337 | - Units 338 | - Read from the xarray.DataArray.attrs.units attribute 339 | - Indices 340 | - Time indices are converted to UTC and displayed in local time in the widget 341 | - Latitude and longitude indices are displayed in their full forms in the widget 342 | - Other indices (strings, numbers) are displayed in their full form 343 | - If no indices are available, the numeric indices are displayed 344 | 345 | ## Troubleshooting 346 | Below you can find a number of different common issues when working with Lexcube. If the suggested solutions do not work for you, feel free to [open an issue](https://github.com/msoechting/lexcube/issues/new/choose)! 347 | 348 | ### The cube does not respond / API methods are not doing anything / Cube does not load new data 349 | Under certain circumstances, the widget may get disconnected from the kernel. You can recognize it with this symbol (crossed out chain 🔗): 350 | ![Crossed-out chain symbol under cell](https://raw.githubusercontent.com/msoechting/lexcube/main/readme-media/disconnected.png) 351 | 352 | Possible Solutions: 353 | 1. Execute the cell again 354 | 2. Restart the kernel 355 | 3. Refresh the web page (also try a "hard refresh" using CTRL+F5 or Command+Option+R - this forces the browser to ignore its cache) 356 | 357 | 358 | ### After installation/update, no widget is shown, only text 359 | Example: 360 | ![Broken widget in post-installation](https://raw.githubusercontent.com/msoechting/lexcube/main/readme-media/post-installation-broken-widget.png) 361 | 362 | Possible solutions: 363 | 1. Restart the kernel 364 | 2. Refresh the web page (also try a "hard refresh" using CTRL+F5 or Command+Option+R - this forces the browser to ignore its cache) 365 | 366 | ### w.savefig breaks when batch-processing/trying to create many figures quickly 367 | The current `savefig` implementation is limited by its asynchronous nature. This means that the `savefig` call returns before the image is rendered and downloaded. Therefore, a workaround, such as waiting one second between images, is necessary to correctly create images when batchprocessing. 368 | 369 | ### The layout of the widget looks very messed up 370 | This can happen in old versions of browsers. Update your browser or use a modern, up-to-date browser such as Firefox or Chrome. Otherwise, feel free to create an issue with your exact specs. 371 | 372 | ### "Error creating WebGL context" or similar 373 | WebGL 2 seems to be not available or disabled in your browser. Check this page to test if your browser is compatible: https://webglreport.com/?v=2. Possible solutions are: 374 | 1. Update your browser 375 | 2. Update your video card drivers 376 | 377 | ### Memory is filling up a lot when using a chunked dataset 378 | Lexcube employs an alternative, more aggressive chunk caching mechanism in contrast to xarray. It will cache any touched chunk in memory without releasing it until the widget is closed. Disabling it will most likely decrease memory usage, but increase the average data access latency, i.e., make Lexcube slower. To disable it, use: `lexcube.Cube3DWidget(data_source, use_lexcube_chunk_caching=False)`. 379 | 380 | 381 | ## Known bugs 382 | - Zoom interactions with the mousewheel may be difficult for data sets with very small ranges on some dimensions (e.g. 2-5). 383 | - Zoom interactions may behave unexpectedly when zooming on multiple cube faces subsequently. 384 | 385 | ## Attributions 386 | 387 | Lexcube uses lots of amazing open-source software and packages, including: 388 | * Data access: [Xarray](https://docs.xarray.dev/en/stable/index.html) & [Numpy](https://numpy.org/) 389 | * Lossy floating-point compression: [ZFP](https://zfp.io/) 390 | * Client boilerplate: [TypeScript Three.js Boilerplate](https://github.com/Sean-Bradley/Three.js-TypeScript-Boilerplate) by Sean Bradley 391 | * Jupyter widget boilerplate: [widget-ts-cookiecutter](https://github.com/jupyter-widgets/widget-ts-cookiecutter) 392 | * Colormaps: [matplotlib](https://matplotlib.org), [cmocean](https://matplotlib.org/cmocean/), [Scientific colour maps by Fabio Crameri](https://zenodo.org/records/8409685), [Proplot custom colormaps](https://github.com/proplot-dev/proplot) 393 | * 3D graphics engine: [Three.js](https://github.com/mrdoob/three.js/) (including the OrbitControls, which have been modified for this project) 394 | * Client bundling: [Webpack](https://webpack.js.org/) 395 | * UI sliders: [Nouislider](https://refreshless.com/nouislider/) 396 | * Decompression using WebAssembly: [numcodecs.js](https://github.com/manzt/numcodecs.js) 397 | * WebSocket communication: [Socket.io](https://socket.io/) 398 | 399 | ## Development Installation & Guide 400 | See [CONTRIBUTING.md](CONTRIBUTING.md). 401 | 402 | 403 | ## License 404 | The Lexcube application core, the Lexcube Jupyter extension, and other portions of the official Lexcube distribution not explicitly licensed otherwise, are licensed under the GNU GENERAL PUBLIC LICENSE v3 or later (GPLv3+) -- see the "COPYING" file in this directory for details. -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sourceMap: 'inline', 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | node: 'current', 9 | }, 10 | }, 11 | ], 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | # show coverage in CI status, but never consider it a failure 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: 0% 8 | patch: 9 | default: 10 | target: 0% 11 | ignore: 12 | - "lexcube/tests" 13 | -------------------------------------------------------------------------------- /css/widget.css: -------------------------------------------------------------------------------- 1 | 2 | .lexcube-body .selection-section { 3 | min-width: 250px; 4 | } 5 | 6 | .lexcube-body canvas { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | /* Fix white background in VSCode Jupyter */ 12 | .cell-output-ipywidget-background { 13 | background-color: transparent !important; 14 | } 15 | 16 | .jp-OutputArea-output { 17 | background-color: transparent; 18 | } 19 | 20 | 21 | /* Functional styling; 22 | * These styles are required for noUiSlider to function. 23 | * You don't need to change these rules to apply your design. 24 | */ 25 | .noUi-target, 26 | .noUi-target * { 27 | -webkit-touch-callout: none; 28 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 29 | -webkit-user-select: none; 30 | -ms-touch-action: none; 31 | touch-action: none; 32 | -ms-user-select: none; 33 | -moz-user-select: none; 34 | user-select: none; 35 | -moz-box-sizing: border-box; 36 | box-sizing: border-box; 37 | } 38 | 39 | .noUi-target { 40 | position: relative; 41 | } 42 | 43 | .noUi-base, 44 | .noUi-connects { 45 | width: 100%; 46 | height: 100%; 47 | position: relative; 48 | z-index: 1; 49 | } 50 | 51 | /* Wrapper for all connect elements. 52 | */ 53 | .noUi-connects { 54 | overflow: hidden; 55 | z-index: 0; 56 | } 57 | 58 | .noUi-connect, 59 | .noUi-origin { 60 | will-change: transform; 61 | position: absolute; 62 | z-index: 1; 63 | top: 0; 64 | right: 0; 65 | height: 100%; 66 | width: 100%; 67 | -ms-transform-origin: 0 0; 68 | -webkit-transform-origin: 0 0; 69 | -webkit-transform-style: preserve-3d; 70 | transform-origin: 0 0; 71 | transform-style: flat; 72 | } 73 | 74 | /* Offset direction 75 | */ 76 | .noUi-txt-dir-rtl.noUi-horizontal .noUi-origin { 77 | left: 0; 78 | right: auto; 79 | } 80 | 81 | /* Give origins 0 height/width so they don't interfere with clicking the 82 | * connect elements. 83 | */ 84 | .noUi-vertical .noUi-origin { 85 | top: -100%; 86 | width: 0; 87 | } 88 | 89 | .noUi-horizontal .noUi-origin { 90 | height: 0; 91 | } 92 | 93 | .noUi-handle { 94 | -webkit-backface-visibility: hidden; 95 | backface-visibility: hidden; 96 | position: absolute; 97 | } 98 | 99 | .noUi-touch-area { 100 | height: 100%; 101 | width: 100%; 102 | } 103 | 104 | .noUi-state-tap .noUi-connect, 105 | .noUi-state-tap .noUi-origin { 106 | -webkit-transition: transform 0.3s; 107 | transition: transform 0.3s; 108 | } 109 | 110 | .noUi-state-drag * { 111 | cursor: inherit !important; 112 | } 113 | 114 | /* Slider size and handle placement; 115 | */ 116 | .noUi-horizontal { 117 | height: 18px; 118 | } 119 | 120 | .noUi-horizontal .noUi-handle { 121 | width: 34px; 122 | height: 28px; 123 | right: -17px; 124 | top: -6px; 125 | } 126 | 127 | .noUi-vertical { 128 | width: 18px; 129 | } 130 | 131 | .noUi-vertical .noUi-handle { 132 | width: 28px; 133 | height: 34px; 134 | right: -6px; 135 | bottom: -17px; 136 | } 137 | 138 | .noUi-txt-dir-rtl.noUi-horizontal .noUi-handle { 139 | left: -17px; 140 | right: auto; 141 | } 142 | 143 | /* Styling; 144 | * Giving the connect element a border radius causes issues with using transform: scale 145 | */ 146 | .noUi-target { 147 | background: #FAFAFA; 148 | border-radius: 4px; 149 | border: 1px solid #D3D3D3; 150 | box-shadow: inset 0 1px 1px #F0F0F0, 0 3px 6px -5px #BBB; 151 | } 152 | 153 | .noUi-connects { 154 | border-radius: 3px; 155 | } 156 | 157 | .noUi-connect { 158 | background: #3FB8AF; 159 | } 160 | 161 | /* Handles and cursors; 162 | */ 163 | .noUi-draggable { 164 | cursor: ew-resize; 165 | } 166 | 167 | .noUi-vertical .noUi-draggable { 168 | cursor: ns-resize; 169 | } 170 | 171 | .noUi-handle { 172 | border: 1px solid #D9D9D9; 173 | border-radius: 3px; 174 | background: #FFF; 175 | cursor: default; 176 | box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #EBEBEB, 0 3px 6px -3px #BBB; 177 | } 178 | 179 | .noUi-active { 180 | box-shadow: inset 0 0 1px #FFF, inset 0 1px 7px #DDD, 0 3px 6px -3px #BBB; 181 | } 182 | 183 | /* Handle stripes; 184 | */ 185 | .noUi-handle:before, 186 | .noUi-handle:after { 187 | content: ""; 188 | display: block; 189 | position: absolute; 190 | height: 14px; 191 | width: 1px; 192 | background: #E8E7E6; 193 | left: 14px; 194 | top: 6px; 195 | } 196 | 197 | .noUi-handle:after { 198 | left: 17px; 199 | } 200 | 201 | .noUi-vertical .noUi-handle:before, 202 | .noUi-vertical .noUi-handle:after { 203 | width: 14px; 204 | height: 1px; 205 | left: 6px; 206 | top: 14px; 207 | } 208 | 209 | .noUi-vertical .noUi-handle:after { 210 | top: 17px; 211 | } 212 | 213 | /* Disabled state; 214 | */ 215 | [disabled] .noUi-connect { 216 | background: #B8B8B8; 217 | } 218 | 219 | [disabled].noUi-target, 220 | [disabled].noUi-handle, 221 | [disabled] .noUi-handle { 222 | cursor: not-allowed; 223 | } 224 | 225 | /* Base; 226 | * 227 | */ 228 | .noUi-pips, 229 | .noUi-pips * { 230 | -moz-box-sizing: border-box; 231 | box-sizing: border-box; 232 | } 233 | 234 | .noUi-pips { 235 | position: absolute; 236 | color: #999; 237 | } 238 | 239 | /* Values; 240 | * 241 | */ 242 | .noUi-value { 243 | position: absolute; 244 | white-space: nowrap; 245 | text-align: center; 246 | } 247 | 248 | .noUi-value-sub { 249 | color: #ccc; 250 | font-size: 10px; 251 | } 252 | 253 | /* Markings; 254 | * 255 | */ 256 | .noUi-marker { 257 | position: absolute; 258 | background: #CCC; 259 | } 260 | 261 | .noUi-marker-sub { 262 | background: #AAA; 263 | } 264 | 265 | .noUi-marker-large { 266 | background: #AAA; 267 | } 268 | 269 | /* Horizontal layout; 270 | * 271 | */ 272 | .noUi-pips-horizontal { 273 | padding: 10px 0; 274 | height: 80px; 275 | top: 100%; 276 | left: 0; 277 | width: 100%; 278 | } 279 | 280 | .noUi-value-horizontal { 281 | -webkit-transform: translate(-50%, 50%); 282 | transform: translate(-50%, 50%); 283 | } 284 | 285 | .noUi-rtl .noUi-value-horizontal { 286 | -webkit-transform: translate(50%, 50%); 287 | transform: translate(50%, 50%); 288 | } 289 | 290 | .noUi-marker-horizontal.noUi-marker { 291 | margin-left: -1px; 292 | width: 2px; 293 | height: 5px; 294 | } 295 | 296 | .noUi-marker-horizontal.noUi-marker-sub { 297 | height: 10px; 298 | } 299 | 300 | .noUi-marker-horizontal.noUi-marker-large { 301 | height: 15px; 302 | } 303 | 304 | /* Vertical layout; 305 | * 306 | */ 307 | .noUi-pips-vertical { 308 | padding: 0 10px; 309 | height: 100%; 310 | top: 0; 311 | left: 100%; 312 | } 313 | 314 | .noUi-value-vertical { 315 | -webkit-transform: translate(0, -50%); 316 | transform: translate(0, -50%); 317 | padding-left: 25px; 318 | } 319 | 320 | .noUi-rtl .noUi-value-vertical { 321 | -webkit-transform: translate(0, 50%); 322 | transform: translate(0, 50%); 323 | } 324 | 325 | .noUi-marker-vertical.noUi-marker { 326 | width: 5px; 327 | height: 2px; 328 | margin-top: -1px; 329 | } 330 | 331 | .noUi-marker-vertical.noUi-marker-sub { 332 | width: 10px; 333 | } 334 | 335 | .noUi-marker-vertical.noUi-marker-large { 336 | width: 15px; 337 | } 338 | 339 | .noUi-tooltip { 340 | display: block; 341 | position: absolute; 342 | border: 1px solid #D9D9D9; 343 | border-radius: 3px; 344 | background: #fff; 345 | color: #000; 346 | padding: 5px; 347 | text-align: center; 348 | white-space: nowrap; 349 | } 350 | 351 | .noUi-horizontal .noUi-tooltip { 352 | -webkit-transform: translate(-50%, 0); 353 | transform: translate(-50%, 0); 354 | left: 50%; 355 | bottom: 120%; 356 | } 357 | 358 | .noUi-vertical .noUi-tooltip { 359 | -webkit-transform: translate(0, -50%); 360 | transform: translate(0, -50%); 361 | top: 50%; 362 | right: 120%; 363 | } 364 | 365 | .noUi-horizontal .noUi-origin>.noUi-tooltip { 366 | -webkit-transform: translate(50%, 0); 367 | transform: translate(50%, 0); 368 | left: auto; 369 | bottom: 10px; 370 | } 371 | 372 | .noUi-vertical .noUi-origin>.noUi-tooltip { 373 | -webkit-transform: translate(0, -18px); 374 | transform: translate(0, -18px); 375 | top: auto; 376 | right: 28px; 377 | } -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = lexcube 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/environment.yml: -------------------------------------------------------------------------------- 1 | 2 | name: lexcube_docs 3 | channels: 4 | - conda-forge 5 | dependencies: 6 | - python=3.* 7 | - nodejs 8 | - jupyter_sphinx 9 | - sphinx 10 | - sphinx_rtd_theme 11 | - nbsphinx 12 | - nbsphinx-link 13 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=lexcube 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/_static/helper.js: -------------------------------------------------------------------------------- 1 | var cache_require = window.require; 2 | 3 | window.addEventListener('load', function() { 4 | window.require = cache_require; 5 | }); 6 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # lexcube documentation build configuration file 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | 16 | # -- General configuration ------------------------------------------------ 17 | 18 | # If your documentation needs a minimal Sphinx version, state it here. 19 | # 20 | # needs_sphinx = '1.0' 21 | 22 | # Add any Sphinx extension module names here, as strings. They can be 23 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 24 | # ones. 25 | extensions = [ 26 | 'sphinx.ext.autodoc', 27 | 'sphinx.ext.viewcode', 28 | 'sphinx.ext.intersphinx', 29 | 'sphinx.ext.napoleon', 30 | 'sphinx.ext.todo', 31 | 'nbsphinx', 32 | 'jupyter_sphinx', 33 | 'nbsphinx_link', 34 | ] 35 | 36 | # Set the nbsphinx JS path to empty to avoid showing twice of the widgets 37 | nbsphinx_requirejs_path = "" 38 | nbsphinx_widgets_path = "" 39 | 40 | # Ensure our extension is available: 41 | import sys 42 | from os.path import dirname, join as pjoin 43 | docs = dirname(dirname(__file__)) 44 | root = dirname(docs) 45 | sys.path.insert(0, root) 46 | sys.path.insert(0, pjoin(docs, 'sphinxext')) 47 | 48 | # Add any paths that contain templates here, relative to this directory. 49 | templates_path = ['_templates'] 50 | 51 | # The suffix(es) of source filenames. 52 | # You can specify multiple suffix as a list of string: 53 | # 54 | # source_suffix = ['.rst', '.md'] 55 | source_suffix = '.rst' 56 | 57 | # The master toctree document. 58 | master_doc = 'index' 59 | 60 | # General information about the project. 61 | project = 'lexcube' 62 | copyright = '2022, Maximilian Söchting' 63 | author = 'Maximilian Söchting' 64 | 65 | # The version info for the project you're documenting, acts as replacement for 66 | # |version| and |release|, also used in various other places throughout the 67 | # built documents. 68 | # 69 | # The short X.Y version. 70 | 71 | 72 | # get version from python package: 73 | import os 74 | here = os.path.dirname(__file__) 75 | repo = os.path.join(here, '..', '..') 76 | _version_py = os.path.join(repo, 'lexcube', '_version.py') 77 | version_ns = {} 78 | with open(_version_py) as f: 79 | exec(f.read(), version_ns) 80 | 81 | version = version_ns['__version__'] 82 | # The full version, including alpha/beta/rc tags. 83 | release = version_ns['__version__'] 84 | 85 | # The language for content autogenerated by Sphinx. Refer to documentation 86 | # for a list of supported languages. 87 | # 88 | # This is also used if you do content translation via gettext catalogs. 89 | # Usually you set "language" from the command line for these cases. 90 | language = "en" 91 | 92 | # List of patterns, relative to source directory, that match files and 93 | # directories to ignore when looking for source files. 94 | # This patterns also effect to html_static_path and html_extra_path 95 | exclude_patterns = ['**.ipynb_checkpoints'] 96 | 97 | # The name of the Pygments (syntax highlighting) style to use. 98 | pygments_style = 'sphinx' 99 | 100 | # If true, `todo` and `todoList` produce output, else they produce nothing. 101 | todo_include_todos = False 102 | 103 | 104 | # -- Options for HTML output ---------------------------------------------- 105 | 106 | 107 | # Theme options are theme-specific and customize the look and feel of a theme 108 | # further. For a list of options available for each theme, see the 109 | # documentation. 110 | # 111 | # html_theme_options = {} 112 | 113 | # Add any paths that contain custom static files (such as style sheets) here, 114 | # relative to this directory. They are copied after the builtin static files, 115 | # so a file named "default.css" will overwrite the builtin "default.css". 116 | html_static_path = ['_static'] 117 | 118 | 119 | # -- Options for HTMLHelp output ------------------------------------------ 120 | 121 | # Output file base name for HTML help builder. 122 | htmlhelp_basename = 'lexcubedoc' 123 | 124 | 125 | # -- Options for LaTeX output --------------------------------------------- 126 | 127 | latex_elements = { 128 | # The paper size ('letterpaper' or 'a4paper'). 129 | # 130 | # 'papersize': 'letterpaper', 131 | 132 | # The font size ('10pt', '11pt' or '12pt'). 133 | # 134 | # 'pointsize': '10pt', 135 | 136 | # Additional stuff for the LaTeX preamble. 137 | # 138 | # 'preamble': '', 139 | 140 | # Latex figure (float) alignment 141 | # 142 | # 'figure_align': 'htbp', 143 | } 144 | 145 | # Grouping the document tree into LaTeX files. List of tuples 146 | # (source start file, target name, title, 147 | # author, documentclass [howto, manual, or own class]). 148 | latex_documents = [ 149 | (master_doc, 'lexcube.tex', 'lexcube Documentation', 150 | 'Maximilian Söchting', 'manual'), 151 | ] 152 | 153 | 154 | # -- Options for manual page output --------------------------------------- 155 | 156 | # One entry per manual page. List of tuples 157 | # (source start file, name, description, authors, manual section). 158 | man_pages = [ 159 | (master_doc, 160 | 'lexcube', 161 | 'lexcube Documentation', 162 | [author], 1) 163 | ] 164 | 165 | 166 | # -- Options for Texinfo output ------------------------------------------- 167 | 168 | # Grouping the document tree into Texinfo files. List of tuples 169 | # (source start file, target name, title, author, 170 | # dir menu entry, description, category) 171 | texinfo_documents = [ 172 | (master_doc, 173 | 'lexcube', 174 | 'lexcube Documentation', 175 | author, 176 | 'lexcube', 177 | 'Lexcube: 3D Data Cube Visualization in Jupyter notebooks', 178 | 'Miscellaneous'), 179 | ] 180 | 181 | 182 | # Example configuration for intersphinx: refer to the Python standard library. 183 | intersphinx_mapping = {'https://docs.python.org/': None} 184 | 185 | # Read The Docs 186 | # on_rtd is whether we are on readthedocs.org, this line of code grabbed from 187 | # docs.readthedocs.org 188 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 189 | 190 | if not on_rtd: # only import and set the theme if we're building docs locally 191 | import sphinx_rtd_theme 192 | html_theme = 'sphinx_rtd_theme' 193 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 194 | 195 | # otherwise, readthedocs.org uses their theme by default, so no need to specify it 196 | 197 | 198 | # Uncomment this line if you have know exceptions in your included notebooks 199 | # that nbsphinx complains about: 200 | # 201 | nbsphinx_allow_errors = True # exception ipstruct.py ipython_genutils 202 | 203 | from sphinx.util import logging 204 | logger = logging.getLogger(__name__) 205 | 206 | def setup(app): 207 | def add_scripts(app): 208 | for fname in ['helper.js', 'embed-bundle.js']: 209 | if not os.path.exists(os.path.join(here, '_static', fname)): 210 | logger.warning('missing javascript file: %s' % fname) 211 | app.add_js_file(fname) 212 | app.connect('builder-inited', add_scripts) 213 | -------------------------------------------------------------------------------- /docs/source/develop-install.rst: -------------------------------------------------------------------------------- 1 | 2 | Developer install 3 | ================= 4 | 5 | 6 | To install a developer version of lexcube, you will first need to clone 7 | the repository:: 8 | 9 | git clone https://github.com/msoechting/lexcube 10 | cd lexcube 11 | 12 | Next, install it with a develop install using pip:: 13 | 14 | pip install -e . 15 | 16 | 17 | If you are planning on working on the JS/frontend code, you should also do 18 | a link installation of the extension:: 19 | 20 | jupyter nbextension install [--sys-prefix / --user / --system] --symlink --py lexcube 21 | 22 | jupyter nbextension enable [--sys-prefix / --user / --system] --py lexcube 23 | 24 | with the `appropriate flag`_. Or, if you are using Jupyterlab:: 25 | 26 | jupyter labextension install . 27 | 28 | 29 | .. links 30 | 31 | .. _`appropriate flag`: https://jupyter-notebook.readthedocs.io/en/stable/extending/frontend_extensions.html#installing-and-enabling-extensions 32 | -------------------------------------------------------------------------------- /docs/source/examples/index.rst: -------------------------------------------------------------------------------- 1 | 2 | Examples 3 | ======== 4 | 5 | This section contains several examples generated from Jupyter notebooks. 6 | The widgets have been embedded into the page for demonstrative purposes. 7 | 8 | .. todo:: 9 | 10 | Add links to notebooks in examples folder similar to the initial 11 | one. This is a manual step to ensure only those examples that 12 | are suited for inclusion are used. 13 | 14 | 15 | .. toctree:: 16 | :glob: 17 | 18 | * 19 | -------------------------------------------------------------------------------- /docs/source/examples/introduction.nblink: -------------------------------------------------------------------------------- 1 | { 2 | "path": "../../../examples/introduction.ipynb" 3 | } 4 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | 2 | lexcube 3 | ===================================== 4 | 5 | Version: |release| 6 | 7 | Lexcube: 3D Data Cube Visualization in Jupyter notebooks 8 | 9 | 10 | Quickstart 11 | ---------- 12 | 13 | To get started with lexcube, install with pip:: 14 | 15 | pip install lexcube 16 | 17 | or with conda:: 18 | 19 | conda install lexcube 20 | 21 | 22 | Contents 23 | -------- 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | :caption: Installation and usage 28 | 29 | installing 30 | introduction 31 | 32 | .. toctree:: 33 | :maxdepth: 1 34 | 35 | examples/index 36 | 37 | 38 | .. toctree:: 39 | :maxdepth: 2 40 | :caption: Development 41 | 42 | develop-install 43 | 44 | 45 | .. links 46 | 47 | .. _`Jupyter widgets`: https://jupyter.org/widgets.html 48 | 49 | .. _`notebook`: https://jupyter-notebook.readthedocs.io/en/latest/ 50 | -------------------------------------------------------------------------------- /docs/source/installing.rst: -------------------------------------------------------------------------------- 1 | 2 | .. _installation: 3 | 4 | Installation 5 | ============ 6 | 7 | 8 | The simplest way to install lexcube is via pip:: 9 | 10 | pip install lexcube 11 | 12 | or via conda:: 13 | 14 | conda install lexcube 15 | 16 | 17 | If you installed via pip, and notebook version < 5.3, you will also have to 18 | install / configure the front-end extension as well. If you are using classic 19 | notebook (as opposed to Jupyterlab), run:: 20 | 21 | jupyter nbextension install [--sys-prefix / --user / --system] --py lexcube 22 | 23 | jupyter nbextension enable [--sys-prefix / --user / --system] --py lexcube 24 | 25 | with the `appropriate flag`_. If you are using Jupyterlab, install the extension 26 | with:: 27 | 28 | jupyter labextension install lexcube 29 | 30 | If you are installing using conda, these commands should be unnecessary, but If 31 | you need to run them the commands should be the same (just make sure you choose the 32 | `--sys-prefix` flag). 33 | 34 | 35 | .. links 36 | 37 | .. _`appropriate flag`: https://jupyter-notebook.readthedocs.io/en/stable/extending/frontend_extensions.html#installing-and-enabling-extensions 38 | -------------------------------------------------------------------------------- /docs/source/introduction.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | Introduction 3 | ============= 4 | 5 | .. todo:: 6 | 7 | add prose explaining project purpose and usage here 8 | -------------------------------------------------------------------------------- /examples/1_introduction.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Visualizing 3D Data with Lexcube - Introduction" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### 1. Preparing the dataset" 15 | ] 16 | }, 17 | { 18 | "cell_type": "markdown", 19 | "metadata": {}, 20 | "source": [ 21 | "You can use any 3D data that is gridded and can be loaded with Xarray, e.g. **Zarr** and **NetCDF** files, or Numpy. This includes **local** data sets, **remote** data sets (e.g. via *HTTP* or *S3*) and computed data sets." 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": null, 27 | "metadata": { 28 | "tags": [] 29 | }, 30 | "outputs": [], 31 | "source": [ 32 | "import numpy as np\n", 33 | "import xarray as xr" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "# ** Option 1: Use a numpy data set\n", 43 | "# data_source = np.sum(np.mgrid[0:256,0:256,0:256], axis=0)" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": null, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "# ** Option 2: Load a local xarray data set\n", 53 | "# data_source = xr.open_dataset(\"/data/my_data_set.zarr\", chunks={}, engine=\"zarr\")" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": { 60 | "tags": [] 61 | }, 62 | "outputs": [], 63 | "source": [ 64 | "# ** Option 3: Load a remote xarray data set\n", 65 | "ds = xr.open_dataset(\"https://data.rsc4earth.de/download/EarthSystemDataCube/v3.0.2/esdc-8d-0.25deg-256x128x128-3.0.2.zarr/\", chunks={}, engine=\"zarr\")\n", 66 | "data_source = ds[\"air_temperature_2m\"][256:512,256:512,256:512]\n", 67 | "data_source" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "### 2. Visualizing 3D data with Lexcube" 75 | ] 76 | }, 77 | { 78 | "cell_type": "markdown", 79 | "metadata": {}, 80 | "source": [ 81 | "Using `lexcube.Cube3DWidget`, you can open an interactive visualization of your 3D data. Use the `cmap` parameter to set a (matplotlib) colormap." 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "metadata": { 88 | "tags": [] 89 | }, 90 | "outputs": [], 91 | "source": [ 92 | "import lexcube" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": { 99 | "tags": [] 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "w = lexcube.Cube3DWidget(data_source, cmap=\"thermal\")\n", 104 | "w.plot(12, 8)" 105 | ] 106 | }, 107 | { 108 | "cell_type": "markdown", 109 | "metadata": {}, 110 | "source": [ 111 | "You can interact with the cube like this: \n", 112 | "\n", 113 | "1. You can zoom (mousewheel) and pan (click and drag) on any side of the cube. \n", 114 | "2. Clicking and dragging on the black background allows you to change the perspective on the cube.\n", 115 | "3. Open the slider menu (top right, slider icon) or call `w.show_sliders()` and use the sliders for finer selection:" 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": null, 121 | "metadata": { 122 | "tags": [] 123 | }, 124 | "outputs": [], 125 | "source": [ 126 | "w.show_sliders()" 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "metadata": {}, 132 | "source": [ 133 | "4. Set `w.xlim`, `w.ylim` or `w.zlim` to change the selection via code:" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": null, 139 | "metadata": {}, 140 | "outputs": [], 141 | "source": [ 142 | "w.xlim = (140, 250)\n", 143 | "w.ylim = (85, 200)\n", 144 | "w.zlim = (0, 140)" 145 | ] 146 | }, 147 | { 148 | "cell_type": "markdown", 149 | "metadata": {}, 150 | "source": [ 151 | "5. Load Natural Earth region borders via `w.overlay_geojson`:" 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": null, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "w.overlay_geojson(\"https://github.com/nvkelso/natural-earth-vector/raw/refs/heads/master/geojson/ne_50m_admin_0_countries.geojson\", \"white\")" 161 | ] 162 | }, 163 | { 164 | "attachments": {}, 165 | "cell_type": "markdown", 166 | "metadata": {}, 167 | "source": [ 168 | "### 3. Save figure\n", 169 | "Using `w.savefig`, you can save the currently visible cube as a PNG image. It has three parameters:\n", 170 | "- fname: the file name of the PNG image. Defaults to `lexcube-{current date and time}.png}\n", 171 | "- include_ui: whether the axis descriptors and color gradient is included in the exported image.\n", 172 | "- dpi_scale: higher scale equals higher image quality. Defaults to 2.0." 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "w.savefig(fname=\"cube.png\", include_ui=True, dpi_scale=3.0)" 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "metadata": {}, 187 | "source": [ 188 | "### 4. Create your own paper data cube" 189 | ] 190 | }, 191 | { 192 | "cell_type": "markdown", 193 | "metadata": {}, 194 | "source": [ 195 | "You can generate a template to make your own paper data cube from your currently visible data cube like this:" 196 | ] 197 | }, 198 | { 199 | "cell_type": "code", 200 | "execution_count": null, 201 | "metadata": {}, 202 | "outputs": [], 203 | "source": [ 204 | "w.save_print_template()" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "In the opened dialog, you can download the print template as either PNG or SVG to your computer. You can also add a custom note to the print template, e.g. to remember specifics about the data set. Printing (recommended: thick paper or photo paper, e.g. 15x20cm), cutting and gluing will give you your own paper data cube for your desk:\n", 212 | "\n", 213 | "![Print template graphic](https://raw.githubusercontent.com/msoechting/lexcube/main/readme-media/print-template.png)\n", 214 | "\n" 215 | ] 216 | }, 217 | { 218 | "cell_type": "markdown", 219 | "metadata": {}, 220 | "source": [ 221 | "### 5. Continue working with the selected data\n", 222 | "Using `w.get_current_cube_selection()` will return the currently visible sub-selection of your dataset. Three options exist:" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "metadata": {}, 229 | "outputs": [], 230 | "source": [ 231 | "# 1. Return currently visible data subset.\n", 232 | "w.get_current_cube_selection()" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": null, 238 | "metadata": {}, 239 | "outputs": [], 240 | "source": [ 241 | "# 2. Return the currently visible selection, but applied to a different dataset.\n", 242 | "w.get_current_cube_selection(data_to_be_indexed=ds[\"kndvi\"])" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "metadata": {}, 249 | "outputs": [], 250 | "source": [ 251 | "# 3. Return indices of the currently visible selection.\n", 252 | "w.get_current_cube_selection(return_index_only=True)" 253 | ] 254 | }, 255 | { 256 | "cell_type": "markdown", 257 | "metadata": {}, 258 | "source": [ 259 | "### 6. Conclusion\n", 260 | "\n", 261 | "Thanks for using Lexcube! See the [Github page](https://github.com/msoechting/lexcube) for more documentation or try the other example notebooks. If you have any feature requests or encounter any issues, feel free to [create an issue](https://github.com/msoechting/lexcube/issues/new/choose) or contact me ([@msoechting](https://rsc4earth.de/authors/msoechting)). " 262 | ] 263 | } 264 | ], 265 | "metadata": { 266 | "kernelspec": { 267 | "display_name": "Python 3 (ipykernel)", 268 | "language": "python", 269 | "name": "python3" 270 | }, 271 | "language_info": { 272 | "codemirror_mode": { 273 | "name": "ipython", 274 | "version": 3 275 | }, 276 | "file_extension": ".py", 277 | "mimetype": "text/x-python", 278 | "name": "python", 279 | "nbconvert_exporter": "python", 280 | "pygments_lexer": "ipython3", 281 | "version": "3.11.4" 282 | } 283 | }, 284 | "nbformat": 4, 285 | "nbformat_minor": 4 286 | } 287 | -------------------------------------------------------------------------------- /examples/2_numpy.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Visualizing Numpy Data with Lexcube" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "You can visualize any arbitrary 3D data from Numpy with Lexcube:" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "import lexcube\n", 24 | "import numpy as np" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "metadata": {}, 31 | "outputs": [], 32 | "source": [ 33 | "data_source = np.sum(np.mgrid[0:256,0:256,0:256], axis=0)\n", 34 | "data_source.shape" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": { 41 | "tags": [] 42 | }, 43 | "outputs": [], 44 | "source": [ 45 | "w = lexcube.Cube3DWidget(data_source, cmap=\"prism\", vmin=0, vmax=768)\n", 46 | "w.plot()" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "metadata": { 53 | "tags": [] 54 | }, 55 | "outputs": [], 56 | "source": [ 57 | "w.show_sliders()" 58 | ] 59 | } 60 | ], 61 | "metadata": { 62 | "kernelspec": { 63 | "display_name": "Python 3 (ipykernel)", 64 | "language": "python", 65 | "name": "python3" 66 | }, 67 | "language_info": { 68 | "codemirror_mode": { 69 | "name": "ipython", 70 | "version": 3 71 | }, 72 | "file_extension": ".py", 73 | "mimetype": "text/x-python", 74 | "name": "python", 75 | "nbconvert_exporter": "python", 76 | "pygments_lexer": "ipython3", 77 | "version": "3.11.4" 78 | } 79 | }, 80 | "nbformat": 4, 81 | "nbformat_minor": 4 82 | } 83 | -------------------------------------------------------------------------------- /examples/3_spectral_indices_with_cubo_and_spyndex.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Generating a Spectral Index Data Cube with Cubo and Spyndex\n", 8 | "## Introduction\n", 9 | "Using the cubo and spyndex library by David Montero Loaiza (Github: [davemlz](https://github.com/davemlz)), we can easily create spectral index data cubes from coordinates and visualize them." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import lexcube\n", 19 | "import numpy as np\n", 20 | "import xarray as xr\n", 21 | "import spyndex\n", 22 | "import cubo" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "Using [cubo](https://github.com/ESDS-Leipzig/cubo), we can create a data cube from Sentinel 2 using STAC:" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "bands_da = cubo.create(\n", 39 | " lat=51, # Central latitude of the cube\n", 40 | " lon=10, # Central longitude of the cube\n", 41 | " collection=\"sentinel-2-l2a\", # Name of the STAC collection\n", 42 | " bands=[\"B01\",\"B02\",\"B03\",\"B04\",\"B05\",\"B06\",\"B07\",\"B08\",\"B11\",\"B12\"], # Bands to retrieve\n", 43 | " start_date=\"2022-01-25\", # Start date of the cube\n", 44 | " end_date=\"2022-12-31\", # End date of the cube\n", 45 | " edge_size=1024, # Edge size of the cube (px)\n", 46 | " resolution=10, # Pixel size of the cube (m)\n", 47 | " query={\"eo:cloud_cover\": {\"lt\": 10} }\n", 48 | ")" 49 | ] 50 | }, 51 | { 52 | "cell_type": "markdown", 53 | "metadata": {}, 54 | "source": [ 55 | "Now, we can use that data cube to compute any spectral index (in this case NDVI) using the [spyndex](https://github.com/awesome-spectral-indices/spyndex) library:" 56 | ] 57 | }, 58 | { 59 | "cell_type": "code", 60 | "execution_count": null, 61 | "metadata": {}, 62 | "outputs": [], 63 | "source": [ 64 | "da = spyndex.computeIndex(\n", 65 | " index = [\"NDVI\"],\n", 66 | " params = {\n", 67 | " \"N\": bands_da.sel(band = \"B08\"),\n", 68 | " \"R\": bands_da.sel(band = \"B04\"),\n", 69 | " \"L\": 0.5\n", 70 | " }\n", 71 | ")" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "Finally, we can visualize the NDVI cube with Lexcube:" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": { 85 | "scrolled": true 86 | }, 87 | "outputs": [], 88 | "source": [ 89 | "w = lexcube.Cube3DWidget(da)\n", 90 | "w.plot()" 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "metadata": {}, 96 | "source": [ 97 | "Afterwards, it is possible to parametrize the visualization further or save the figure as a PNG:" 98 | ] 99 | }, 100 | { 101 | "cell_type": "code", 102 | "execution_count": null, 103 | "metadata": {}, 104 | "outputs": [], 105 | "source": [ 106 | "# ** Save the figure as PNG:\n", 107 | "# w.savefig()\n", 108 | "# ** Or adjust the colormap:\n", 109 | "# w.vmin = -0.1\n", 110 | "# w.vmax = 0.5\n", 111 | "# w.cmap = \"thermal\"" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "## Example with much larger time frame\n", 119 | "You can alternatively load and visualize a much larger time frame (seven years instead of one), which may take 2-3 minutes to load:" 120 | ] 121 | }, 122 | { 123 | "cell_type": "code", 124 | "execution_count": null, 125 | "metadata": {}, 126 | "outputs": [], 127 | "source": [ 128 | "from sen2nbar.nbar import nbar_cubo" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "lbands_da = cubo.create(\n", 138 | " lat=51, # Central latitude of the cube\n", 139 | " lon=10, # Central longitude of the cube\n", 140 | " collection=\"sentinel-2-l2a\", # Name of the STAC collection\n", 141 | " bands=[\"B01\",\"B02\",\"B03\",\"B04\",\"B05\",\"B06\",\"B07\",\"B08\",\"B11\",\"B12\"], # Bands to retrieve\n", 142 | " start_date=\"2016-01-01\", # Start date of the cube\n", 143 | " end_date=\"2022-12-31\", # End date of the cube\n", 144 | " edge_size=1024, # Edge size of the cube (px)\n", 145 | " resolution=10, # Pixel size of the cube (m)\n", 146 | " query={\"eo:cloud_cover\": {\"lt\": 10} }\n", 147 | ")\n", 148 | "lbands_da = nbar_cubo(lbands_da)\n", 149 | "\n", 150 | "lda = spyndex.computeIndex(\n", 151 | " index = [\"NDVI\"],\n", 152 | " params = {\n", 153 | " \"N\": lbands_da.sel(band = \"B08\"),\n", 154 | " \"R\": lbands_da.sel(band = \"B04\"),\n", 155 | " \"L\": 0.5\n", 156 | " }\n", 157 | ")" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": null, 163 | "metadata": {}, 164 | "outputs": [], 165 | "source": [ 166 | "lw = lexcube.Cube3DWidget(lda)\n", 167 | "lw" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": null, 173 | "metadata": {}, 174 | "outputs": [], 175 | "source": [ 176 | "# ** Save the figure as PNG:\n", 177 | "# lw.savefig()\n", 178 | "# ** Or adjust the colormap:\n", 179 | "# lw.vmin = -0.1\n", 180 | "# lw.vmax = 0.5" 181 | ] 182 | } 183 | ], 184 | "metadata": { 185 | "kernelspec": { 186 | "display_name": "Python 3 (ipykernel)", 187 | "language": "python", 188 | "name": "python3" 189 | }, 190 | "language_info": { 191 | "codemirror_mode": { 192 | "name": "ipython", 193 | "version": 3 194 | }, 195 | "file_extension": ".py", 196 | "mimetype": "text/x-python", 197 | "name": "python", 198 | "nbconvert_exporter": "python", 199 | "pygments_lexer": "ipython3", 200 | "version": "3.11.4" 201 | } 202 | }, 203 | "nbformat": 4, 204 | "nbformat_minor": 4 205 | } 206 | -------------------------------------------------------------------------------- /examples/4_google_earth_engine.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Visualize Google Earth Engine Data with Lexcube" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "### Earth Engine Setup" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": { 21 | "tags": [] 22 | }, 23 | "outputs": [], 24 | "source": [ 25 | "import lexcube\n", 26 | "import xarray as xr\n", 27 | "import ee" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "Authenticate yourself with your Google Earth Engine account:" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": null, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "ee.Authenticate()" 44 | ] 45 | }, 46 | { 47 | "cell_type": "markdown", 48 | "metadata": {}, 49 | "source": [ 50 | "### Load the Data\n", 51 | "Now, we load a data set with a rectangular gridded projection (EPSG:4326), open it with xarray and select a 3D array from the `temperature_2m` parameter." 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": { 58 | "tags": [] 59 | }, 60 | "outputs": [], 61 | "source": [ 62 | "ee.Initialize(opt_url='https://earthengine-highvolume.googleapis.com')\n", 63 | "ds = xr.open_dataset('ee://ECMWF/ERA5_LAND/HOURLY', engine='ee', crs='EPSG:4326', scale=0.25, chunks={})" 64 | ] 65 | }, 66 | { 67 | "cell_type": "code", 68 | "execution_count": null, 69 | "metadata": { 70 | "tags": [] 71 | }, 72 | "outputs": [], 73 | "source": [ 74 | "da = ds[\"temperature_2m\"][630000:630003,2:1438,2:718]\n", 75 | "da" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "### Visualize the Data" 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": null, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "w = lexcube.Cube3DWidget(da)\n", 92 | "w.plot()" 93 | ] 94 | } 95 | ], 96 | "metadata": { 97 | "kernelspec": { 98 | "display_name": "Python 3 (ipykernel)", 99 | "language": "python", 100 | "name": "python3" 101 | }, 102 | "language_info": { 103 | "codemirror_mode": { 104 | "name": "ipython", 105 | "version": 3 106 | }, 107 | "file_extension": ".py", 108 | "mimetype": "text/x-python", 109 | "name": "python", 110 | "nbconvert_exporter": "python", 111 | "pygments_lexer": "ipython3", 112 | "version": "3.11.4" 113 | } 114 | }, 115 | "nbformat": 4, 116 | "nbformat_minor": 4 117 | } 118 | -------------------------------------------------------------------------------- /examples/5_spectral_indices_with_open_eo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "23678f64-b06d-409b-992f-6eed8c035968", 6 | "metadata": {}, 7 | "source": [ 8 | "# Lexcube + Awesome Spectral Indices + OpenEO" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "62a9b9b7-ce4e-43b6-894e-fc4cb76ac1bf", 14 | "metadata": {}, 15 | "source": [ 16 | "Import libraries:" 17 | ] 18 | }, 19 | { 20 | "cell_type": "code", 21 | "execution_count": null, 22 | "id": "a0f30555-3ea8-4b78-9591-8a85b1d1d7d0", 23 | "metadata": {}, 24 | "outputs": [], 25 | "source": [ 26 | "import openeo\n", 27 | "import lexcube\n", 28 | "import xarray as xr\n", 29 | "\n", 30 | "from openeo.extra.spectral_indices import compute_index" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "id": "c85bb0f4-3e66-4c95-a29b-b092bc090f59", 36 | "metadata": {}, 37 | "source": [ 38 | "Connect to OpenEO (create an account if you do not have one):" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "id": "cb3964bb-9e37-4c68-ac73-de9a14a65cb5", 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "con = openeo.connect(\"openeo.dataspace.copernicus.eu\")\n", 49 | "con.authenticate_oidc()" 50 | ] 51 | }, 52 | { 53 | "cell_type": "markdown", 54 | "id": "c73f9799-53e6-406b-ab25-1506ffae1e41", 55 | "metadata": {}, 56 | "source": [ 57 | "Load the Sentinel-2 collection:" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "id": "eba303e5-985f-483a-b967-3e739f11b5c5", 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "datacube = con.load_collection(\n", 68 | " \"SENTINEL2_L2A\",\n", 69 | " spatial_extent={\"west\": 5.14, \"south\": 51.17, \"east\": 5.17, \"north\": 51.19},\n", 70 | " temporal_extent = [\"2021-02-01\", \"2021-04-30\"],\n", 71 | " bands=[\"B02\", \"B04\", \"B08\"],\n", 72 | " max_cloud_cover=85,\n", 73 | ")" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "id": "230d4031-7ab0-44e0-935c-4eb434e5dca7", 79 | "metadata": {}, 80 | "source": [ 81 | "Compute NDVI using Awesome Spectral Indices:" 82 | ] 83 | }, 84 | { 85 | "cell_type": "code", 86 | "execution_count": null, 87 | "id": "105559fa-0f9c-421b-8171-ad6c7ef5a44c", 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "NDVI = compute_index(datacube, \"NDVI\")" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "id": "1ad249cd-abf9-4a53-861f-99a3f711a8ff", 97 | "metadata": {}, 98 | "source": [ 99 | "Download the data cube:" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "id": "f8f59d6a-2098-45e7-ad5b-6a7f07b82e43", 106 | "metadata": {}, 107 | "outputs": [], 108 | "source": [ 109 | "NDVI.download(\"NDVI.nc\")" 110 | ] 111 | }, 112 | { 113 | "cell_type": "markdown", 114 | "id": "7076a14a-1ba2-4b8b-b28e-45ef2b3a3415", 115 | "metadata": {}, 116 | "source": [ 117 | "Open the data cube with `xarray`:" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "id": "c73437bc-14f6-40d5-ae20-52a3b9c84499", 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "da = xr.open_dataset(\"NDVI.nc\")" 128 | ] 129 | }, 130 | { 131 | "cell_type": "markdown", 132 | "id": "b718c946-5e18-448a-b862-e246c4822fa1", 133 | "metadata": {}, 134 | "source": [ 135 | "Do the interactive visualization with Lexcube:" 136 | ] 137 | }, 138 | { 139 | "cell_type": "code", 140 | "execution_count": null, 141 | "id": "9774861d-4f3e-4327-8383-dfd03fa193cc", 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "w = lexcube.Cube3DWidget(da[\"NDVI\"], cmap=\"viridis\", vmin=0, vmax=1)\n", 146 | "w.plot()" 147 | ] 148 | } 149 | ], 150 | "metadata": { 151 | "kernelspec": { 152 | "display_name": "Python 3 (ipykernel)", 153 | "language": "python", 154 | "name": "python3" 155 | }, 156 | "language_info": { 157 | "codemirror_mode": { 158 | "name": "ipython", 159 | "version": 3 160 | }, 161 | "file_extension": ".py", 162 | "mimetype": "text/x-python", 163 | "name": "python", 164 | "nbconvert_exporter": "python", 165 | "pygments_lexer": "ipython3", 166 | "version": "3.11.4" 167 | } 168 | }, 169 | "nbformat": 4, 170 | "nbformat_minor": 5 171 | } 172 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "lexcube", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package lexcube" 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | automock: false, 3 | moduleNameMapper: { 4 | '\\.(css|less|sass|scss)$': 'identity-obj-proxy', 5 | }, 6 | preset: 'ts-jest/presets/js-with-babel', 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 8 | testPathIgnorePatterns: ['/lib/', '/node_modules/'], 9 | testRegex: '/__tests__/.*.spec.ts[x]?$', 10 | transformIgnorePatterns: ['/node_modules/(?!(@jupyter(lab|-widgets)/.*)/)'], 11 | globals: { 12 | 'ts-jest': { 13 | tsconfig: '/tsconfig.json', 14 | }, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /lexcube.json: -------------------------------------------------------------------------------- 1 | { 2 | "load_extensions": { 3 | "lexcube/extension": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /lexcube/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Lexcube - Interactive 3D Data Cube Visualization 5 | # Copyright (C) 2022 Maximilian Söchting 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | from .cube3d import Cube3DWidget, Sliders 21 | from ._version import __version__ 22 | 23 | def _jupyter_labextension_paths(): 24 | """Called by Jupyter Lab Server to detect if it is a valid labextension and 25 | to install the widget 26 | Returns 27 | ======= 28 | src: Source directory name to copy files from. Webpack outputs generated files 29 | into this directory and Jupyter Lab copies from this directory during 30 | widget installation 31 | dest: Destination directory name to install widget files to. Jupyter Lab copies 32 | from `src` directory into /labextensions/ directory 33 | during widget installation 34 | """ 35 | return [{ 36 | 'src': 'labextension', 37 | 'dest': 'lexcube', 38 | }] 39 | 40 | 41 | def _jupyter_nbextension_paths(): 42 | """Called by Jupyter Notebook Server to detect if it is a valid nbextension and 43 | to install the widget 44 | Returns 45 | ======= 46 | section: The section of the Jupyter Notebook Server to change. 47 | Must be 'notebook' for widget extensions 48 | src: Source directory name to copy files from. Webpack outputs generated files 49 | into this directory and Jupyter Notebook copies from this directory during 50 | widget installation 51 | dest: Destination directory name to install widget files to. Jupyter Notebook copies 52 | from `src` directory into /nbextensions/ directory 53 | during widget installation 54 | require: Path to importable AMD Javascript module inside the 55 | /nbextensions/ directory 56 | """ 57 | return [{ 58 | 'section': 'notebook', 59 | 'src': 'nbextension', 60 | 'dest': 'lexcube', 61 | 'require': 'lexcube/extension' 62 | }] 63 | -------------------------------------------------------------------------------- /lexcube/_frontend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Lexcube - Interactive 3D Data Cube Visualization 5 | # Copyright (C) 2022 Maximilian Söchting 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | """ 21 | Information about the frontend package of the widgets. 22 | """ 23 | 24 | module_name = "lexcube" 25 | module_version = "^1.0.2" 26 | -------------------------------------------------------------------------------- /lexcube/_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Lexcube - Interactive 3D Data Cube Visualization 5 | # Copyright (C) 2022 Maximilian Söchting 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | __version__ = "1.0.2" 21 | -------------------------------------------------------------------------------- /lexcube/cube3d.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Lexcube - Interactive 3D Data Cube Visualization 5 | # Copyright (C) 2022 Maximilian Söchting 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | 21 | import datetime 22 | import json 23 | import math 24 | import asyncio 25 | from traitlets import Unicode, Dict, Float, List, Int, validate, TraitError, Bool, Tuple 26 | import traitlets 27 | from typing import Union 28 | import urllib.request 29 | 30 | import xarray as xr 31 | import numpy as np 32 | 33 | from ._frontend import module_name, module_version 34 | import ipywidgets as widgets 35 | from lexcube.lexcube_server.src.lexcube_widget import start_tile_server_in_widget_mode 36 | 37 | class Timer: 38 | def __init__(self, timeout, callback): 39 | self._timeout = timeout 40 | self._callback = callback 41 | 42 | async def _job(self): 43 | await asyncio.sleep(self._timeout) 44 | self._callback() 45 | 46 | def start(self): 47 | self._task = asyncio.ensure_future(self._job()) 48 | 49 | def cancel(self): 50 | self._task.cancel() 51 | 52 | DEFAULT_WIDGET_SIZE = (12.0, 8.0) 53 | 54 | @widgets.register 55 | class Cube3DWidget(widgets.DOMWidget): 56 | _model_name = Unicode('Cube3DModel').tag(sync=True) 57 | _model_module = Unicode(module_name).tag(sync=True) 58 | _model_module_version = Unicode(module_version).tag(sync=True) 59 | _view_name = Unicode('Cube3DView').tag(sync=True) 60 | _view_module = Unicode(module_name).tag(sync=True) 61 | _view_module_version = Unicode(module_version).tag(sync=True) 62 | 63 | api_metadata = Dict().tag(sync=True) 64 | 65 | request_progress = Dict().tag(sync=True) 66 | request_progress_reliable_for_timing = Bool(False).tag(sync=True) 67 | vmin = Float(allow_none=True).tag(sync=True) 68 | vmax = Float(allow_none=True).tag(sync=True) 69 | cmap = traitlets.Union([Unicode(), List()], default_value="viridis").tag(sync=True) 70 | xlim = Tuple(Int(), Int(), default_value=(-1, -1)).tag(sync=True) 71 | ylim = Tuple(Int(), Int(), default_value=(-1, -1)).tag(sync=True) 72 | zlim = Tuple(Int(), Int(), default_value=(-1, -1)).tag(sync=True) 73 | 74 | widget_size = Tuple(DEFAULT_WIDGET_SIZE).tag(sync=True) 75 | 76 | xwrap = Bool(False).tag(sync=True) 77 | ywrap = Bool(False).tag(sync=True) 78 | zwrap = Bool(False).tag(sync=True) 79 | 80 | overlaid_geojson = traitlets.Union([Unicode(), Dict()]).tag(sync=True) 81 | overlaid_geojson_color = Unicode().tag(sync=True) 82 | 83 | isometric_mode = Bool(False).tag(sync=True) 84 | 85 | def __init__(self, data_source, cmap: Union[str, list, None] = None, vmin: Union[float, None] = None, vmax: Union[float, None] = None, isometric_mode: bool = False, use_lexcube_chunk_caching: bool = True, overlaid_geojson: Unicode = "", overlaid_geojson_color: Unicode = "black", widget_size: tuple = None, **kwargs): 86 | super().__init__(**kwargs) 87 | self.cmap = cmap or self.cmap 88 | self.vmin = vmin 89 | self.vmax = vmax 90 | self.widget_size = widget_size or DEFAULT_WIDGET_SIZE 91 | self.isometric_mode = isometric_mode 92 | self._tile_server, self._dims, self._indices = start_tile_server_in_widget_mode(self, data_source, use_lexcube_chunk_caching) 93 | self._data_source = self._tile_server.data_source # tile server may have patched/modified data set 94 | self.overlaid_geojson = overlaid_geojson 95 | self.overlaid_geojson_color = overlaid_geojson_color 96 | if not self._tile_server: 97 | raise Exception("Error: Could not start tile server") 98 | self._tile_server.widget_update_progress = self._update_progress 99 | 100 | def _update_progress(self, progress: list, reliable_for_timing: bool = False): 101 | self.request_progress_reliable_for_timing = reliable_for_timing 102 | self.request_progress = { "progress": progress.copy() } 103 | 104 | def get_current_cube_selection(self, data_to_be_indexed = None, return_index_only: bool = False): 105 | a = data_to_be_indexed if data_to_be_indexed is not None else self._data_source 106 | if type(a) != xr.DataArray: # for non-xarray data, use numerical indices 107 | if return_index_only: 108 | return dict(x=self.xlim, y=self.ylim, z=self.zlim) 109 | return a[self.zlim[0]:self.zlim[1], self.ylim[0]:self.ylim[1], self.xlim[0]:self.xlim[1]] 110 | 111 | # for Xarray data, index based on metadata indices, not numerical indices 112 | source_data = self._data_source[self.zlim[0]:self.zlim[1], self.ylim[0]:self.ylim[1], self.xlim[0]:self.xlim[1]] 113 | index = dict([(k, np.array(source_data.indexes[k])) for k in source_data.indexes]) 114 | 115 | if return_index_only: 116 | return index 117 | return a.sel(index) 118 | 119 | def show_sliders(self, continuous_update=True): 120 | return Sliders(self, self._dims, continuous_update) 121 | 122 | def overlay_geojson(self, geojson_source: Union[str, dict], color: str = "black"): 123 | self.overlaid_geojson_color = color 124 | self.overlaid_geojson = geojson_source 125 | 126 | @validate("overlaid_geojson") 127 | def _valid_geojson(self, geojson_source_proposal: Union[str, dict]): 128 | geojson_source = geojson_source_proposal["value"] 129 | geojson_string = None 130 | if geojson_source is None or geojson_source == "": 131 | return "" 132 | if type(self._data_source) == np.ndarray: 133 | print("GeoJSON overlay is only supported for xarray data sources.") 134 | raise TraitError("GeoJSON overlay is only supported for xarray data sources.") 135 | if isinstance(geojson_source, str): 136 | try: 137 | json.loads(geojson_source) 138 | print("Interpreting GeoJSON from given JSON string...") 139 | except json.JSONDecodeError: 140 | if geojson_source.startswith("http://") or geojson_source.startswith("https://"): 141 | print("Opening GeoJSON from URL...") 142 | with urllib.request.urlopen(geojson_source) as res: 143 | geojson_string = res.read().decode('utf-8') 144 | print(f"Downloaded GeoJSON from URL {geojson_source}.") 145 | else: 146 | print("Opening GeoJSON from file...") 147 | try: 148 | with open(geojson_source, "r") as f: 149 | geojson_string = f.read() 150 | print(f"Loaded GeoJSON from file {geojson_source}.") 151 | except FileNotFoundError: 152 | raise TraitError(f"GeoJSON file {geojson_source} not found.") 153 | elif isinstance(geojson_source, dict): 154 | geojson_string = json.dumps(geojson_source) 155 | print(f"Interpreting GeoJSON from given dictionary object...") 156 | return geojson_string 157 | 158 | def savefig(self, fname: str = "", include_ui: bool = True, dpi_scale: float = 2.0): 159 | self.send( { "response_type": "download_figure_request", "includeUi": include_ui, "filename": fname, "dpiscale": dpi_scale } ) 160 | print('When using Lexcube and generated images or videos, please acknowledge/cite: Söchting, M., Scheuermann, G., Montero, D., & Mahecha, M. D. (2025). Interactive Earth system data cube visualization in Jupyter notebooks. Big Earth Data, 1–15. https://doi.org/10.1080/20964471.2025.2471646') 161 | 162 | def save_print_template(self, fname: str = ""): 163 | self.send( { "response_type": "download_print_template_request", "filename": fname } ) 164 | print('When using Lexcube and generated images or videos, please acknowledge/cite: Söchting, M., Scheuermann, G., Montero, D., & Mahecha, M. D. (2025). Interactive Earth system data cube visualization in Jupyter notebooks. Big Earth Data, 1–15. https://doi.org/10.1080/20964471.2025.2471646') 165 | 166 | @validate("xlim") 167 | def _valid_xlim(self, proposal): 168 | return (self.validate_boundary(proposal["value"][0], 2), self.validate_boundary(proposal["value"][1], 2)) 169 | 170 | @validate("ylim") 171 | def _valid_ylim(self, proposal): 172 | return (self.validate_boundary(proposal["value"][0], 1), self.validate_boundary(proposal["value"][1], 1)) 173 | 174 | @validate("zlim") 175 | def _valid_zlim(self, proposal): 176 | return (self.validate_boundary(proposal["value"][0], 0), self.validate_boundary(proposal["value"][1], 0)) 177 | 178 | def validate_boundary(self, proposal, axis): 179 | wrapping = False 180 | max_value = self._data_source.shape[axis] 181 | if (axis == 0 and self.zwrap) or (axis == 1 and self.ywrap) or (axis == 2 and self.xwrap): 182 | max_value = 2 * max_value 183 | wrapping = True 184 | if proposal < 0 or proposal > max_value: 185 | if wrapping: 186 | raise TraitError(f"Boundary of axis {axis} needs to be within double the range of the data source (considering this dimension wraps around the cube): 0 <= value < {max_value}") 187 | raise TraitError(f"Boundary of axis {axis} needs to be within the range of the data source: 0 <= value <= {max_value}") 188 | return proposal 189 | 190 | def show(self, width: float | int = None, height: float | int = None): 191 | if (type(width) == tuple): 192 | width, height = width 193 | if (width and type(width) != float and type(width) != int) or (height and type(height) != float and type(height) != int): 194 | raise TraitError("Width and height need to be floats or ints") 195 | self.widget_size = (width or DEFAULT_WIDGET_SIZE[0], height or DEFAULT_WIDGET_SIZE[1]) 196 | return self 197 | 198 | def plot(self, width: float | int = None, height: float | int = None): 199 | return self.show(width, height) 200 | 201 | 202 | 203 | @widgets.register 204 | class Sliders(widgets.VBox): 205 | def make_axis_slider(self, axis_name, total_range, selection_range, indices): 206 | slider = widgets.IntRangeSlider( 207 | value=selection_range, 208 | min=total_range[0], 209 | max=total_range[1], 210 | step=1, 211 | description=f'{axis_name}:', 212 | disabled=False, 213 | continuous_update=self.continuous_update, 214 | orientation='horizontal', 215 | readout=True, 216 | readout_format='d' 217 | ) 218 | children = [slider] 219 | if len(indices) > 0 and not (indices[0] == total_range[0] and indices[-1] == total_range[1]): 220 | label = widgets.Label(value=f"{indices[0]} – {indices[-1]}") 221 | children.append(label) 222 | def update_label(change): 223 | label.value = f"{indices[change['new'][0]]} – {indices[change['new'][1]]}" 224 | slider.observe(update_label, names='value') 225 | return widgets.HBox(children=children) 226 | 227 | def try_to_link_sliders_to_cube_widget(self): 228 | if self.cube_widget.xlim[0] < 0 or self.cube_widget.ylim[0] < 0 or self.cube_widget.zlim[0] < 0: 229 | self.link_tries_left -= 1 230 | if self.link_tries_left == 0: 231 | raise Exception("Error: Cube widget is not initialized yet") 232 | Timer(1.5, self.try_to_link_sliders_to_cube_widget).start() 233 | return 234 | widgets.link((self.cube_widget, 'xlim'), (self.x_axis_slider.children[0], 'value')) 235 | widgets.link((self.cube_widget, 'ylim'), (self.y_axis_slider.children[0], 'value')) 236 | widgets.link((self.cube_widget, 'zlim'), (self.z_axis_slider.children[0], 'value')) 237 | 238 | def guess_format(self, index): 239 | try: 240 | datetime.datetime.fromisoformat(index) 241 | return datetime.datetime 242 | except: 243 | pass 244 | try: 245 | float(index) 246 | return float 247 | except: 248 | pass 249 | return str 250 | 251 | def format_indices(self, indices): 252 | t = self.guess_format(indices[0]) 253 | if t == datetime.datetime: 254 | difference = (datetime.datetime.fromisoformat(indices[-1]) - datetime.datetime.fromisoformat(indices[0])) * 1 / max(1, len(indices) - 1) 255 | include_month = difference.days < 360 256 | include_day = difference.days < 30 257 | include_hour = difference.days < 1 258 | include_second = difference.seconds < 60 259 | include_millisecond = difference.microseconds < 1000 260 | return [datetime.datetime.fromisoformat(index).strftime(f"%Y{'-%m' if include_month else ''}{'-%d' if include_day else ''}{' %H:%M' if include_hour else ''}{'%S' if include_second else ''}{'%f' if include_millisecond else ''}") for index in indices] 261 | elif t == float and float(indices[-1]) != len(indices) - 1: 262 | difference = (float(indices[-1]) - float(indices[0])) * 1 / max(1, len(indices) - 1) 263 | significant_digits = max(0, -int(math.floor(math.log10(abs(difference)))) + 1) 264 | return [f"{float(index):.{significant_digits}f}" for index in indices] 265 | return indices 266 | 267 | def __init__(self, cube_widget: Cube3DWidget, dimensions: list, continuous_update: bool, **kwargs): 268 | self.cube_widget = cube_widget 269 | self.continuous_update = continuous_update 270 | self.x_axis_slider = self.make_axis_slider(dimensions[2], (0, cube_widget._data_source.shape[2] - 1), cube_widget.xlim, self.format_indices(cube_widget._indices["x"])) 271 | self.y_axis_slider = self.make_axis_slider(dimensions[1], (0, cube_widget._data_source.shape[1] - 1), cube_widget.ylim, self.format_indices(cube_widget._indices["y"])) 272 | self.z_axis_slider = self.make_axis_slider(dimensions[0], (0, cube_widget._data_source.shape[0] - 1), cube_widget.zlim, self.format_indices(cube_widget._indices["z"])) 273 | super().__init__(children=[self.x_axis_slider, self.y_axis_slider, self.z_axis_slider], **kwargs) 274 | self.link_tries_left = 5 275 | self.try_to_link_sliders_to_cube_widget() 276 | -------------------------------------------------------------------------------- /lexcube/lexcube_server/.dockerignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | .git/ 7 | /app/__pycache__ 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 101 | __pypackages__/ 102 | 103 | # Celery stuff 104 | celerybeat-schedule 105 | celerybeat.pid 106 | 107 | # SageMath parsed files 108 | *.sage.py 109 | 110 | # Environments 111 | .env 112 | .venv 113 | env/ 114 | venv/ 115 | ENV/ 116 | env.bak/ 117 | venv.bak/ 118 | 119 | # Spyder project settings 120 | .spyderproject 121 | .spyproject 122 | 123 | # Rope project settings 124 | .ropeproject 125 | 126 | # mkdocs documentation 127 | /site 128 | 129 | # mypy 130 | .mypy_cache/ 131 | .dmypy.json 132 | dmypy.json 133 | 134 | # Pyre type checker 135 | .pyre/ 136 | 137 | # pytype static type analyzer 138 | .pytype/ 139 | 140 | # Cython debug symbols 141 | cython_debug/ 142 | 143 | .tiles/ 144 | # PyCharm 145 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 146 | # be found at https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 147 | # and can be added to the global gitignore or merged into this file. For a more nuclear 148 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 149 | #.idea/ 150 | 151 | .vscode/ 152 | -------------------------------------------------------------------------------- /lexcube/lexcube_server/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv* 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | 140 | # PyCharm 141 | # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can 142 | # be found at https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore 143 | # and can be added to the global gitignore or merged into this file. For a more nuclear 144 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 145 | #.idea/ 146 | .tiles/ 147 | config.json 148 | pregenerate_log.txt 149 | histograms*/* 150 | *.npy 151 | profiling* 152 | analytics_log*.csv 153 | 154 | thumbnails/ 155 | multicube*/ 156 | -------------------------------------------------------------------------------- /lexcube/lexcube_server/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Current File", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "${file}", 12 | "console": "integratedTerminal", 13 | "justMyCode": true 14 | }, 15 | { 16 | "name": "Python: Uvicorn", 17 | "type": "python", 18 | "request": "launch", 19 | "module": "uvicorn", 20 | "args": [ 21 | "src.lexcube_standalone:app", 22 | "--port=5000" 23 | ] 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /lexcube/lexcube_server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11 2 | 3 | COPY ./requirements-core.txt /app/requirements-core.txt 4 | COPY ./requirements-standalone.txt /app/requirements-standalone.txt 5 | 6 | RUN pip install --no-cache-dir --upgrade -r /app/requirements-core.txt 7 | RUN pip install --no-cache-dir --upgrade -r /app/requirements-standalone.txt 8 | 9 | COPY . /app 10 | 11 | # move main python file to /app/app/main.py to make it work with tiangolo/uvicorn-gunicorn-fastapi 12 | RUN mv /app/src/lexcube_standalone.py /app/src/main.py 13 | RUN mv /app/src /app/app 14 | 15 | ENV PRODUCTION=1 16 | ENV LEXCUBE_LOG_PATH=/etc/lexcube-logs -------------------------------------------------------------------------------- /lexcube/lexcube_server/config_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "datasetBaseDir": "C:/code/data-cube", 3 | "tileCacheDir": ".tiles", 4 | "preGenerationThreads": 4, 5 | "datasets": [ 6 | { 7 | "id": "esdc-2.1.1-low-res", 8 | "shortName": "ESDC 2.1.1 (Low Res)", 9 | "datasetPath": "esdc-8d-0.25deg-1x720x1440-2.1.1.zarr", 10 | "preGenerationSparsity": 10, 11 | "approximateMinMaxValues": true, 12 | "ignoredParameters": ["mask", "psurf", "srex_mask", "water_mask", "xch4", "xco2", "fat_p"], 13 | "onlyParameters": ["air_temperature_2m"], 14 | "overrideMaxLod": -1, 15 | "calculateYearlyAnomalies": false, 16 | "hidden": false, 17 | "flippedY": false, 18 | "useOfflineMetadata": false 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /lexcube/lexcube_server/requirements-core.txt: -------------------------------------------------------------------------------- 1 | s3fs==2023.12.2 2 | bottleneck==1.4.0 3 | cachey==0.2.1 4 | dask==2024.1.1 5 | netCDF4==1.6.5 6 | numpy<2.0.0 7 | opencv-python-headless==4.10.0.84 8 | psutil==5.9.6 9 | xarray==2023.10.1 10 | zarr==2.16.1 -------------------------------------------------------------------------------- /lexcube/lexcube_server/src/lexcube_widget.py: -------------------------------------------------------------------------------- 1 | # Lexcube - Interactive 3D Data Cube Visualization 2 | # Copyright (C) 2022 Maximilian Söchting 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from __future__ import annotations 18 | from .tile_server import TileServer, calculate_max_lod, API_VERSION, get_dimension_labels 19 | from typing import Union 20 | import ipywidgets as widgets 21 | import numpy as np 22 | import xarray as xr 23 | 24 | 25 | def start_tile_server_in_widget_mode(widget: widgets.DOMWidget, data_source: Union[xr.DataArray, np.ndarray], use_lexcube_chunk_caching: bool): 26 | if type(data_source) not in [xr.DataArray, np.ndarray]: 27 | print("Error: Input data is not xarray.DataArray or numpy.ndarray") 28 | raise Exception("Error: Input data is not xarray.DataArray or numpy.ndarray") 29 | if len(data_source.shape) != 3: 30 | print("Error: Data source is not 3-dimensional") 31 | raise Exception("Error: Data source is not 3-dimensional") 32 | 33 | tile_server = TileServer(widget_mode = True) 34 | tile_server.startup_widget(data_source, use_lexcube_chunk_caching) 35 | 36 | data_source = tile_server.data_source # tile server may have patched/modified data set 37 | 38 | def reply(content, buffers = None): 39 | widget.send(content, buffers) 40 | 41 | def receive_message(widget, content, buffers): 42 | requests = content["request_data"] 43 | tile_server.pre_register_requests(requests) 44 | for request in requests: 45 | response = tile_server.handle_tile_request_widget(request) 46 | reply(response[0], response[1]) 47 | 48 | if type(data_source) == xr.DataArray: 49 | dims = data_source.dims 50 | variable_name = data_source.name 51 | indices = { "z": get_dimension_labels(data_source, dims[0]), "y": get_dimension_labels(data_source, dims[1]), "x": get_dimension_labels(data_source, dims[2]) } 52 | else: 53 | dims = ["Z", "Y", "X"] 54 | variable_name = "default_var" 55 | indices = { "z": list(range(data_source.shape[0])), "y": list(range(data_source.shape[1])), "x": list(range(data_source.shape[2])) } 56 | 57 | data_source_name = f"{type(data_source)}" 58 | 59 | data_attributes = {} 60 | coords = {} 61 | if type(data_source) == xr.DataArray: 62 | data_attributes = data_source.attrs 63 | coords = dict((c, data_source.coords[c].to_dict()) for c in data_source.coords) 64 | 65 | widget.api_metadata = { 66 | "/api": {"status":"ok", "api_version": API_VERSION}, 67 | "/api/datasets": [{ "id": "default", "shortName": data_source_name }], 68 | "/api/datasets/default": { 69 | "dims": { f"{dims[0]}": data_source.shape[0], f"{dims[1]}": data_source.shape[1], f"{dims[2]}": data_source.shape[2] }, 70 | "dims_ordered": dims, 71 | "attrs": { }, 72 | "coords": coords, 73 | "data_vars": { variable_name: { "attrs": data_attributes }}, 74 | "indices": indices, 75 | "max_lod": calculate_max_lod(tile_server.TILE_SIZE, data_source.shape), 76 | "sparsity": 1 77 | } 78 | } 79 | 80 | widget.on_msg(receive_message) 81 | 82 | return (tile_server, dims, indices) 83 | -------------------------------------------------------------------------------- /lexcube/nbextension/extension.js: -------------------------------------------------------------------------------- 1 | // Entry point for the notebook bundle containing custom model definitions. 2 | // 3 | define(function() { 4 | "use strict"; 5 | 6 | window['requirejs'].config({ 7 | map: { 8 | '*': { 9 | 'lexcube': 'nbextensions/lexcube/index', 10 | }, 11 | } 12 | }); 13 | // Export the required load_ipython_extension function 14 | return { 15 | load_ipython_extension : function() {} 16 | }; 17 | }); -------------------------------------------------------------------------------- /lexcube/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/lexcube/tests/__init__.py -------------------------------------------------------------------------------- /lexcube/tests/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Lexcube - Interactive 3D Data Cube Visualization 5 | # Copyright (C) 2022 Maximilian Söchting 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | 21 | import pytest 22 | 23 | from ipykernel.comm import Comm 24 | from ipywidgets import Widget 25 | 26 | class MockComm(Comm): 27 | """A mock Comm object. 28 | 29 | Can be used to inspect calls to Comm's open/send/close methods. 30 | """ 31 | comm_id = 'a-b-c-d' 32 | kernel = 'Truthy' 33 | 34 | def __init__(self, *args, **kwargs): 35 | self.log_open = [] 36 | self.log_send = [] 37 | self.log_close = [] 38 | super(MockComm, self).__init__(*args, **kwargs) 39 | 40 | def open(self, *args, **kwargs): 41 | self.log_open.append((args, kwargs)) 42 | 43 | def send(self, *args, **kwargs): 44 | self.log_send.append((args, kwargs)) 45 | 46 | def close(self, *args, **kwargs): 47 | self.log_close.append((args, kwargs)) 48 | 49 | _widget_attrs = {} 50 | undefined = object() 51 | 52 | 53 | @pytest.fixture 54 | def mock_comm(): 55 | _widget_attrs['_comm_default'] = getattr(Widget, '_comm_default', undefined) 56 | Widget._comm_default = lambda self: MockComm() 57 | _widget_attrs['_ipython_display_'] = Widget._ipython_display_ 58 | def raise_not_implemented(*args, **kwargs): 59 | raise NotImplementedError() 60 | Widget._ipython_display_ = raise_not_implemented 61 | 62 | yield MockComm() 63 | 64 | for attr, value in _widget_attrs.items(): 65 | if value is undefined: 66 | delattr(Widget, attr) 67 | else: 68 | setattr(Widget, attr, value) 69 | -------------------------------------------------------------------------------- /lexcube/tests/test_example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Lexcube - Interactive 3D Data Cube Visualization 5 | # Copyright (C) 2022 Maximilian Söchting 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | 21 | import pytest 22 | 23 | from ..cube3d import Cube3DWidget 24 | 25 | 26 | def test_example_creation_blank(): 27 | w = Cube3DWidget() 28 | assert w.value == 'Hello World' 29 | -------------------------------------------------------------------------------- /lexcube/tests/test_nbextension_path.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | # Lexcube - Interactive 3D Data Cube Visualization 5 | # Copyright (C) 2022 Maximilian Söchting 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | 20 | 21 | def test_nbextension_path(): 22 | # Check that magic function can be imported from package root: 23 | from lexcube import _jupyter_nbextension_paths 24 | # Ensure that it can be called without incident: 25 | path = _jupyter_nbextension_paths() 26 | # Some sanity checks: 27 | assert len(path) == 1 28 | assert isinstance(path[0], dict) 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lexcube", 3 | "version": "1.0.2", 4 | "description": "Lexcube: 3D Data Cube Visualization in Jupyter Notebooks", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension", 9 | "widgets" 10 | ], 11 | "files": [ 12 | "lib/**/*.js", 13 | "dist/*.js", 14 | "css/*.css" 15 | ], 16 | "homepage": "https://github.com/msoechting/lexcube", 17 | "bugs": { 18 | "url": "https://github.com/msoechting/lexcube/issues" 19 | }, 20 | "license": "GPL-3.0-or-later", 21 | "author": { 22 | "name": "Maximilian Söchting", 23 | "email": "maximilian.soechting@uni-leipzig.de" 24 | }, 25 | "main": "lib/index.js", 26 | "types": "./lib/index.d.ts", 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/msoechting/lexcube.git" 30 | }, 31 | "scripts": { 32 | "build": "npm run build:lib && npm run build:nbextension && npm run build:labextension:dev", 33 | "build:prod": "npm run build:lib && npm run build:nbextension && npm run build:labextension", 34 | "build:labextension": "jupyter labextension build .", 35 | "build:labextension:dev": "jupyter labextension build --development True .", 36 | "build:lib": "tsc", 37 | "build:nbextension": "webpack", 38 | "clean": "npm run clean:lib && npm run clean:nbextension && npm run clean:labextension", 39 | "clean:lib": "rimraf lib", 40 | "clean:labextension": "rimraf lexcube/labextension", 41 | "clean:nbextension": "rimraf lexcube/nbextension/static/index.js", 42 | "lint": "eslint . --ext .ts,.tsx --fix", 43 | "lint:check": "eslint . --ext .ts,.tsx", 44 | "prepack": "npm run build:lib", 45 | "test": "jest", 46 | "watch": "npm-run-all -p watch:*", 47 | "watch:lib": "tsc -w", 48 | "watch:nbextension": "webpack --watch --mode=development", 49 | "watch:labextension": "jupyter labextension watch ." 50 | }, 51 | "dependencies": { 52 | "@jupyter-widgets/base": "^1.1.10 || ^2.0.0 || ^3.0.0 || ^4.0.0 || ^6.0.0" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.5.0", 56 | "@babel/preset-env": "^7.5.0", 57 | "@jupyterlab/builder": "^4.0.9", 58 | "@phosphor/application": "^1.6.0", 59 | "@phosphor/widgets": "^1.6.0", 60 | "@types/jest": "^26.0.0", 61 | "@types/node": "^20.11.19", 62 | "@types/qrcode": "^1.5.4", 63 | "@types/three": "=0.144.0", 64 | "@types/webpack-env": "^1.13.6", 65 | "@typescript-eslint/eslint-plugin": "^3.6.0", 66 | "@typescript-eslint/parser": "^3.6.0", 67 | "acorn": "^7.2.0", 68 | "comlink": "^4.4.2", 69 | "css-loader": "^6.8.1", 70 | "eslint": "^7.4.0", 71 | "eslint-config-prettier": "^6.11.0", 72 | "eslint-plugin-prettier": "^3.1.4", 73 | "fs-extra": "^7.0.0", 74 | "html-loader": "^4.2.0", 75 | "html-to-image": "^1.11.11", 76 | "identity-obj-proxy": "^3.0.0", 77 | "jest": "^29.1.2", 78 | "mkdirp": "^0.5.1", 79 | "modern-gif": "^2.0.4", 80 | "mp4-muxer": "^5.1.5", 81 | "nouislider": "^15.6.1", 82 | "npm-run-all": "^4.1.3", 83 | "numcodecs": "file:src/lexcube-client/deps/numcodecs-0.2.5.tgz", 84 | "polyfill-array-includes": "^2.0.0", 85 | "prettier": "^2.0.5", 86 | "qrcode": "^1.5.3", 87 | "rimraf": "^2.6.2", 88 | "socket.io-client": "^4.5.3", 89 | "source-map-loader": "^1.1.3", 90 | "style-loader": "^1.0.2", 91 | "three": "=0.163.0", 92 | "ts-jest": "^29.1.2", 93 | "ts-loader": "^8.0.0", 94 | "typescript": "~5.3.3", 95 | "webm-muxer": "^5.0.3", 96 | "webpack": "^5.61.0", 97 | "webpack-cli": "^4.0.0" 98 | }, 99 | "jupyterlab": { 100 | "extension": "lib/plugin", 101 | "outputDir": "lexcube/labextension/", 102 | "sharedPackages": { 103 | "@jupyter-widgets/base": { 104 | "bundled": false, 105 | "singleton": true 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "hatchling>=1.3.1", 4 | "jupyterlab==4.*", 5 | ] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "lexcube" 10 | description = "Lexcube: 3D Data Cube Visualization in Jupyter Notebooks" 11 | readme = "README.md" 12 | license = { file = "COPYING" } 13 | requires-python = ">=3.9" 14 | authors = [ 15 | { name = "Maximilian Söchting", email = "maximilian.soechting@uni-leipzig.de" }, 16 | ] 17 | keywords = [ 18 | "IPython", 19 | "Jupyter", 20 | "Widgets", 21 | ] 22 | classifiers = [ 23 | "Development Status :: 4 - Beta", 24 | "Framework :: Jupyter", 25 | "Intended Audience :: Developers", 26 | "Intended Audience :: Science/Research", 27 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 28 | "Programming Language :: JavaScript", 29 | "Programming Language :: Python", 30 | "Programming Language :: Python :: 3", 31 | "Programming Language :: Python :: 3.9", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3.12", 35 | ] 36 | dependencies = [ 37 | "aiohttp>=3.7.4", 38 | "ipywidgets==7.8.3", 39 | "bottleneck>=1.3.7", 40 | "cachey>=0.2.1", 41 | "dask>=2022.12.0", 42 | "netCDF4>=1.6.3", 43 | "opencv-python-headless>=4.7.0.72", 44 | "xarray>=v2022.12.0", 45 | "zarr>=2.14.2", 46 | ] 47 | version = "1.0.2" 48 | 49 | [project.optional-dependencies] 50 | docs = [ 51 | "jupyter_sphinx", 52 | "nbsphinx", 53 | "nbsphinx-link", 54 | "pypandoc", 55 | "pytest_check_links", 56 | "recommonmark", 57 | "sphinx>=1.5", 58 | "sphinx_rtd_theme", 59 | ] 60 | examples = [] 61 | test = [ 62 | "nbval", 63 | "pytest-cov", 64 | "pytest>=6.0", 65 | ] 66 | 67 | [project.urls] 68 | Homepage = "https://github.com/msoechting/lexcube" 69 | 70 | [tool.hatch.build] 71 | artifacts = [ 72 | "lexcube/nbextension/index.*", 73 | "lexcube/labextension/*.tgz", 74 | "lexcube/labextension", 75 | ] 76 | 77 | [tool.hatch.build.targets.wheel.shared-data] 78 | "lexcube/nbextension" = "share/jupyter/nbextensions/lexcube" 79 | "lexcube/labextension" = "share/jupyter/labextensions/lexcube" 80 | "./install.json" = "share/jupyter/labextensions/lexcube/install.json" 81 | "./lexcube.json" = "etc/jupyter/nbconfig/notebook.d/lexcube.json" 82 | 83 | [tool.hatch.build.targets.sdist] 84 | exclude = [ 85 | ".github", 86 | ] 87 | 88 | [tool.hatch.build.hooks.jupyter-builder] 89 | build-function = "hatch_jupyter_builder.npm_builder" 90 | ensured-targets = [ 91 | "lexcube/nbextension/index.js", 92 | "lexcube/labextension/package.json", 93 | ] 94 | skip-if-exists = [ 95 | "lexcube/nbextension/index.js", 96 | "lexcube/labextension/package.json", 97 | ] 98 | dependencies = [ 99 | "hatch-jupyter-builder>=0.5.0", 100 | ] 101 | 102 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 103 | path = "." 104 | build_cmd = "build:prod" 105 | 106 | [tool.tbump] 107 | # Uncomment this if your project is hosted on GitHub: 108 | github_url = "https://github.com/msoechting/lexcube/" 109 | 110 | [tool.tbump.version] 111 | current = "1.0.2" 112 | 113 | # Example of a semver regexp. 114 | # Make sure this matches current_version before 115 | # using tbump 116 | regex = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)((?Pa|b|rc|.dev)(?P\\d+))?" 117 | 118 | [tool.tbump.git] 119 | message_template = "Bump to {new_version}" 120 | tag_template = "v{new_version}" 121 | 122 | # For each file to patch, add a [[tool.tbump.file]] config 123 | # section containing the path of the file, relative to the 124 | # tbump.toml location. 125 | [[tool.tbump.file]] 126 | src = "pyproject.toml" 127 | 128 | [[tool.tbump.file]] 129 | src = "lexcube/_version.py" 130 | 131 | 132 | # You can specify a list of commands to 133 | # run after the files have been patched 134 | # and before the git commit is made 135 | 136 | # [[tool.tbump.before_commit]] 137 | # name = "check changelog" 138 | # cmd = "grep -q {new_version} Changelog.rst" 139 | 140 | # Or run some commands after the git tag and the branch 141 | # have been pushed: 142 | # [[tool.tbump.after_push]] 143 | # name = "publish" 144 | # cmd = "./publish.sh" 145 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = lexcube/tests examples 3 | norecursedirs = node_modules .ipynb_checkpoints 4 | addopts = --nbval --current-env 5 | -------------------------------------------------------------------------------- /readme-media/disconnected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/readme-media/disconnected.png -------------------------------------------------------------------------------- /readme-media/isometric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/readme-media/isometric.png -------------------------------------------------------------------------------- /readme-media/lexcube-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/readme-media/lexcube-demo.gif -------------------------------------------------------------------------------- /readme-media/lexcube-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/readme-media/lexcube-logo.png -------------------------------------------------------------------------------- /readme-media/post-installation-broken-widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/readme-media/post-installation-broken-widget.png -------------------------------------------------------------------------------- /readme-media/print-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/readme-media/print-template.png -------------------------------------------------------------------------------- /readme-media/sliders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/readme-media/sliders.png -------------------------------------------------------------------------------- /readthedocs.yml: -------------------------------------------------------------------------------- 1 | type: sphinx 2 | python: 3 | version: 3.5 4 | pip_install: true 5 | extra_requirements: 6 | - examples 7 | - docs 8 | conda: 9 | file: docs/environment.yml 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py shim for use with applications that require it. 2 | __import__("setuptools").setup() 3 | -------------------------------------------------------------------------------- /src/__tests__/index.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Lexcube - Interactive 3D Data Cube Visualization 3 | Copyright (C) 2022 Maximilian Söchting 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | // Add any needed widget imports here (or from controls) 20 | // import {} from '@jupyter-widgets/base'; 21 | 22 | import { createTestModel } from './utils'; 23 | 24 | import { Cube3DModel } from '..'; 25 | 26 | describe('Lexcube', () => { 27 | describe('Cube3DModel', () => { 28 | it('should be createable', () => { 29 | const model = createTestModel(Cube3DModel); 30 | expect(model).toBeInstanceOf(Cube3DModel); 31 | expect(model.get('value')).toEqual('Hello World'); 32 | }); 33 | 34 | it('should be createable with a value', () => { 35 | const state = { value: 'Foo Bar!' }; 36 | const model = createTestModel(Cube3DModel, state); 37 | expect(model).toBeInstanceOf(Cube3DModel); 38 | expect(model.get('value')).toEqual('Foo Bar!'); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Lexcube - Interactive 3D Data Cube Visualization 3 | Copyright (C) 2022 Maximilian Söchting 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import * as widgets from '@jupyter-widgets/base'; 20 | import * as services from '@jupyterlab/services'; 21 | 22 | let numComms = 0; 23 | 24 | export class MockComm implements widgets.IClassicComm { 25 | constructor() { 26 | this.comm_id = `mock-comm-id-${numComms}`; 27 | numComms += 1; 28 | } 29 | on_close(fn: ((x?: any) => void) | null): void { 30 | this._on_close = fn; 31 | } 32 | on_msg(fn: (x?: any) => void): void { 33 | this._on_msg = fn; 34 | } 35 | _process_msg(msg: services.KernelMessage.ICommMsgMsg): void | Promise { 36 | if (this._on_msg) { 37 | return this._on_msg(msg); 38 | } else { 39 | return Promise.resolve(); 40 | } 41 | } 42 | close(): string { 43 | if (this._on_close) { 44 | this._on_close(); 45 | } 46 | return 'dummy'; 47 | } 48 | send(): string { 49 | return 'dummy'; 50 | } 51 | 52 | open(): string { 53 | return 'dummy'; 54 | } 55 | 56 | comm_id: string; 57 | target_name = 'dummy'; 58 | _on_msg: ((x?: any) => void) | null = null; 59 | _on_close: ((x?: any) => void) | null = null; 60 | } 61 | 62 | export class DummyManager extends widgets.ManagerBase { 63 | constructor() { 64 | super(); 65 | this.el = window.document.createElement('div'); 66 | } 67 | 68 | display_view( 69 | msg: services.KernelMessage.IMessage, 70 | view: widgets.DOMWidgetView, 71 | options: any 72 | ) { 73 | // TODO: make this a spy 74 | // TODO: return an html element 75 | return Promise.resolve(view).then((view) => { 76 | this.el.appendChild(view.el); 77 | view.on('remove', () => console.log('view removed', view)); 78 | return view.el; 79 | }); 80 | } 81 | 82 | protected loadClass( 83 | className: string, 84 | moduleName: string, 85 | moduleVersion: string 86 | ): Promise { 87 | if (moduleName === '@jupyter-widgets/base') { 88 | if ((widgets as any)[className]) { 89 | return Promise.resolve((widgets as any)[className]); 90 | } else { 91 | return Promise.reject(`Cannot find class ${className}`); 92 | } 93 | } else if (moduleName === 'jupyter-datawidgets') { 94 | if (this.testClasses[className]) { 95 | return Promise.resolve(this.testClasses[className]); 96 | } else { 97 | return Promise.reject(`Cannot find class ${className}`); 98 | } 99 | } else { 100 | return Promise.reject(`Cannot find module ${moduleName}`); 101 | } 102 | } 103 | 104 | _get_comm_info() { 105 | return Promise.resolve({}); 106 | } 107 | 108 | _create_comm() { 109 | return Promise.resolve(new MockComm()); 110 | } 111 | 112 | el: HTMLElement; 113 | 114 | testClasses: { [key: string]: any } = {}; 115 | } 116 | 117 | export interface Constructor { 118 | new (attributes?: any, options?: any): T; 119 | } 120 | 121 | export function createTestModel( 122 | constructor: Constructor, 123 | attributes?: any 124 | ): T { 125 | const id = widgets.uuid(); 126 | const widget_manager = new DummyManager(); 127 | const modelOptions = { 128 | widget_manager: widget_manager, 129 | model_id: id, 130 | }; 131 | 132 | return new constructor(attributes, modelOptions); 133 | } 134 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Lexcube - Interactive 3D Data Cube Visualization 3 | Copyright (C) 2022 Maximilian Söchting 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | // Entry point for the notebook bundle containing custom model definitions. 20 | // 21 | // Setup notebook base URL 22 | // 23 | // Some static assets may be required by the custom widget javascript. The base 24 | // url for the notebook is not known at build time and is therefore computed 25 | // dynamically. 26 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 27 | (window as any).__webpack_public_path__ = 28 | document.querySelector('body')!.getAttribute('data-base-url') + 29 | 'nbextensions/lexcube'; 30 | 31 | export * from './index'; 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export * from './version'; 3 | export * from './widget'; 4 | -------------------------------------------------------------------------------- /src/lexcube-client/.gitattributes: -------------------------------------------------------------------------------- 1 | *.geojson filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /src/lexcube-client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _node_modules 3 | dist/**/*.js 4 | dist/client/*.lnk 5 | dist/client/bundle.js.LICENSE.txt 6 | dist/client/index.html 7 | -------------------------------------------------------------------------------- /src/lexcube-client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "embeddedLanguageFormatting": "auto", 5 | "htmlWhitespaceSensitivity": "css", 6 | "insertPragma": false, 7 | "jsxBracketSameLine": false, 8 | "jsxSingleQuote": false, 9 | "printWidth": 100, 10 | "proseWrap": "preserve", 11 | "quoteProps": "as-needed", 12 | "requirePragma": false, 13 | "semi": false, 14 | "singleQuote": true, 15 | "tabWidth": 4, 16 | "trailingComma": "es5", 17 | "useTabs": false 18 | } -------------------------------------------------------------------------------- /src/lexcube-client/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/lexcube-client/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Sean Bradley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lexcube-client/deps/numcodecs-0.2.5.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msoechting/lexcube/13c492e59035babd5ffa9125be0746657ee8ddd5/src/lexcube-client/deps/numcodecs-0.2.5.tgz -------------------------------------------------------------------------------- /src/lexcube-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lexcube-client", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build": "webpack --config ./src/client/webpack.prod.js", 7 | "dev": "webpack serve --config ./src/client/webpack.dev.js" 8 | }, 9 | "author": "Maximilian Söchting", 10 | "license": "GPL-3.0-or-later", 11 | "devDependencies": { 12 | "@types/node": "^13.13.15", 13 | "@types/qrcode": "^1.5.0", 14 | "@types/three": "^0.144.0", 15 | "git-revision-webpack-plugin": "^5.0.0", 16 | "html-webpack-plugin": "^5.5.0", 17 | "javascript-obfuscator": "^4.0.0", 18 | "three": "^0.144.0", 19 | "ts-loader": "^9.2.5", 20 | "typescript": "^4.9.5", 21 | "uglify-js": "^3.15.5", 22 | "webpack": "^5.64.1", 23 | "webpack-cli": "^4.9.1", 24 | "webpack-dev-server": "^4.5.0", 25 | "webpack-merge": "^5.8.0", 26 | "webpack-obfuscator": "^3.5.1" 27 | }, 28 | "dependencies": { 29 | "comlink": "^4.4.2", 30 | "html-to-image": "^1.11.11", 31 | "modern-gif": "^2.0.4", 32 | "mp4-muxer": "^5.1.5", 33 | "nouislider": "^15.5.1", 34 | "numcodecs": "file:deps/numcodecs-0.2.5.tgz", 35 | "polyfill-array-includes": "^2.0.0", 36 | "qrcode": "^1.5.3", 37 | "socket.io-client": "^4.4.1", 38 | "webm-muxer": "^5.0.2" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lexcube-client/src/client/client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Lexcube - Interactive 3D Data Cube Visualization 3 | Copyright (C) 2022 Maximilian Söchting 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import { DeviceOrientation } from './constants'; 20 | import { CubeInteraction } from './interaction'; 21 | import { Networking } from './networking'; 22 | import { CubeRendering } from './rendering'; 23 | import { TileData } from './tiledata'; 24 | 25 | const apiServerUrl = document.URL.indexOf("localhost") > -1 ? "http://localhost:5000" : "" 26 | 27 | 28 | class CubeClientContext { 29 | rendering: CubeRendering; 30 | networking: Networking; 31 | tileData: TileData; 32 | interaction: CubeInteraction; 33 | 34 | debugMode: boolean = false; 35 | studioMode: boolean = false; 36 | isometricMode: boolean = false; 37 | expertMode: boolean = false; 38 | scriptedMode: boolean = false; 39 | orchestrationMinionMode: boolean = false; 40 | orchestrationMasterMode: boolean = false; 41 | noUiMode: boolean = false; 42 | scriptedMultiViewMode: boolean = false; 43 | textureFilteringEnabled: boolean = false; 44 | 45 | screenOrientation: DeviceOrientation = (screen.orientation ? (screen.orientation.type.indexOf("landscape") > -1 ? DeviceOrientation.Landscape : DeviceOrientation.Portrait) : (window.innerHeight > window.innerWidth ? DeviceOrientation.Portrait : DeviceOrientation.Landscape)); 46 | screenAspectRatio: number = window.screen.width / window.screen.height; 47 | 48 | widgetMode: boolean = false; 49 | 50 | 51 | touchDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0); 52 | widgetPostStartup: () => void = () => {}; 53 | 54 | constructor(widgetMode: boolean = false, htmlParent: HTMLElement = document.body, isometricMode: boolean = false) { 55 | this.widgetMode = widgetMode; 56 | this.isometricMode = isometricMode; 57 | 58 | if (!widgetMode) { 59 | this.isometricMode = this.isometricMode || document.URL.indexOf("isometric") > 0; 60 | this.debugMode = document.URL.indexOf("debug") > 0; 61 | this.studioMode = document.URL.indexOf("studio") > 0; 62 | this.expertMode = document.URL.indexOf("expert") > 0; 63 | this.scriptedMode = document.URL.indexOf("scripted") > 0; 64 | this.orchestrationMinionMode = document.URL.indexOf("orchestrationMinion") > 0; 65 | this.orchestrationMasterMode = document.URL.indexOf("orchestrationMaster") > 0; 66 | this.noUiMode = document.URL.indexOf("noUi") > 0; 67 | this.scriptedMultiViewMode = document.URL.indexOf("scriptedMultiView") > 0; 68 | this.textureFilteringEnabled = document.URL.indexOf("textureFiltering") > 0; 69 | } 70 | 71 | this.rendering = new CubeRendering(this, htmlParent); 72 | this.networking = new Networking(this, apiServerUrl); 73 | this.tileData = new TileData(this); 74 | this.interaction = new CubeInteraction(this, htmlParent); 75 | 76 | if (this.scriptedMode) { 77 | (window as any).downloadScreenshotFromConsole = this.rendering.downloadScreenshotAsDataUrl.bind(this.rendering); 78 | (window as any).allTileDownloadsFinished = this.interaction.getRenderedAfterAllTilesDownloaded.bind(this.interaction); 79 | (window as any).getAvailableCubes = this.interaction.getAvailableCubes.bind(this.interaction); 80 | (window as any).getAvailableParameters = this.interaction.getAvailableParameters.bind(this.interaction); 81 | (window as any).selectCube = this.interaction.selectCubeById.bind(this.interaction); 82 | (window as any).selectParameter = this.interaction.selectParameter.bind(this.interaction); 83 | } 84 | 85 | if (!this.widgetMode) { 86 | const featureCheck = this.checkForFeatures(); 87 | if (featureCheck.success) { 88 | this.startup(); 89 | } else { 90 | window.alert(featureCheck.message); 91 | document.getElementById("tutorial-wrapper")!.style.display = "none"; 92 | document.getElementById("status-message")!.innerHTML = "LexCube failed to start.
Please retry on a more modern browser/device." 93 | } 94 | } 95 | } 96 | 97 | isClientPortrait() { 98 | return this.screenOrientation == DeviceOrientation.Portrait; 99 | } 100 | 101 | 102 | checkWebAssembly() { 103 | try { 104 | if (typeof WebAssembly === "object" && typeof WebAssembly.instantiate === "function") { 105 | const module = new WebAssembly.Module(Uint8Array.of(0x0, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00)); 106 | if (module instanceof WebAssembly.Module) { 107 | return new WebAssembly.Instance(module) instanceof WebAssembly.Instance; 108 | } 109 | } 110 | } catch (e) { 111 | } 112 | return false; 113 | } 114 | 115 | checkForFeatures() { 116 | let success = true; 117 | let message = ""; 118 | if (!document.createElement('canvas').getContext('webgl2')) { 119 | if (navigator.userAgent.indexOf("AppleWebKit") > -1) { 120 | if (navigator.userAgent.indexOf("iPhone") > -1) { 121 | message = "WebGL2 needs to be enabled to run LexCube. You can enable it in iOS 12+ at: 'Settings' > 'General' > 'Safari' > 'Advanced' > 'Experimental Features' > 'WebGL 2.0'"; 122 | } else { 123 | message = "WebGL2 needs to be enabled to run LexCube. You can enable it at: 'Develop' > 'Experimental Features' > 'WebGL 2.0'. If you don't see the Develop menu, choose 'Safari' > 'Preferences' > 'Advanced' > 'Show Develop menu in menu bar'."; 124 | } 125 | } else if (typeof WebGL2RenderingContext !== 'undefined') { 126 | message = "Your browser supports WebGL2 but it might be disabled. Please enable it or use a more modern browser/device to access LexCube."; 127 | } else { 128 | message = "Your browser does not support WebGL2, which is a requirement for LexCube. Please use a more modern browser/device to access LexCube."; 129 | } 130 | success = false; 131 | } 132 | if (!window.WebSocket) { 133 | message = "Your browser does not support Websockets, which is a requirement for LexCube. Please use a more modern browser/device to access LexCube."; 134 | success = false; 135 | } 136 | if (!this.checkWebAssembly()) { 137 | message = "Your browser does not support WebAssembly, which is a requirement for LexCube. Please use a more modern browser/device to access LexCube."; 138 | success = false; 139 | } 140 | return { success: success, message: message }; 141 | } 142 | 143 | async startup() { 144 | this.networking.connect(); 145 | await this.interaction.startup(); 146 | this.rendering.startup(); 147 | this.networking.postStartup(); 148 | this.widgetPostStartup(); 149 | } 150 | 151 | log(...params: any[]) { 152 | if (this.debugMode || this.expertMode) { 153 | console.log(...params); 154 | } 155 | } 156 | } 157 | 158 | if ((window as any).lexcubeStandalone) { 159 | new CubeClientContext(); 160 | } 161 | export { CubeClientContext } 162 | -------------------------------------------------------------------------------- /src/lexcube-client/src/client/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Lexcube - Interactive 3D Data Cube Visualization 3 | Copyright (C) 2022 Maximilian Söchting 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import { degToRad } from "three/src/math/MathUtils"; 20 | 21 | enum CubeFace { 22 | Front = 0, 23 | Back = 1, 24 | Top = 2, 25 | Bottom = 3, 26 | Left = 4, 27 | Right = 5, 28 | } 29 | 30 | enum Dimension { 31 | X = 0, 32 | Y = 1, 33 | Z = 2, 34 | } 35 | 36 | enum DeviceOrientation { 37 | Landscape = 0, 38 | Portrait = 1, 39 | } 40 | 41 | function getIndexDimensionOfFace(face: CubeFace) { 42 | return (face <= 1 ? Dimension.Z : (face <= 3 ? Dimension.Y : Dimension.X)); 43 | } 44 | 45 | function getFacesOfIndexDimension(dimension: Dimension) { 46 | return [[CubeFace.Left, CubeFace.Right], [CubeFace.Top, CubeFace.Bottom], [CubeFace.Front, CubeFace.Back]][dimension]; 47 | } 48 | 49 | function getAddressedFacesOfDimension(dimension: Dimension) { 50 | return [[CubeFace.Top, CubeFace.Bottom, CubeFace.Front, CubeFace.Back], [CubeFace.Left, CubeFace.Right, CubeFace.Front, CubeFace.Back], [CubeFace.Left, CubeFace.Right, CubeFace.Top, CubeFace.Bottom]][dimension]; 51 | } 52 | 53 | function positiveModulo(i: number, n: number) { 54 | return (i % n + n) % n; 55 | } 56 | 57 | function range(start: number, end: number) { 58 | return Array.apply(0, Array(end - start + 1)).map((element, index) => index + start); 59 | } 60 | 61 | 62 | function saveFloatArrayAsPNG(data: Float32Array | Float64Array, width: number, height: number, colormapMinimumValue: number, colormapMaximumValue: number, filename: string): void { 63 | // Create a canvas element 64 | const canvas = document.createElement('canvas'); 65 | canvas.width = width; 66 | canvas.height = height; 67 | const ctx = canvas.getContext('2d')!; 68 | 69 | // Create an ImageData object from the Float32Array 70 | const imageData = ctx.createImageData(width, height); 71 | const uint8Data = new Uint8ClampedArray(data.length * 4); 72 | 73 | // Convert Float32Array values to RGBA format 74 | for (let i = 0; i < data.length; i++) { 75 | const value = Math.floor((data[i] - colormapMinimumValue) / (colormapMaximumValue - colormapMinimumValue) * 255); 76 | const index = i * 4; 77 | let r = value; 78 | let g = value; 79 | let b = value; 80 | if (data[i] == NAN_REPLACEMENT_VALUE) { 81 | r = 0; 82 | g = 0; 83 | b = 255; 84 | } 85 | uint8Data[index] = r; // R 86 | uint8Data[index + 1] = g; // G 87 | uint8Data[index + 2] = b; // B 88 | uint8Data[index + 3] = 255; // A (fully opaque) 89 | } 90 | 91 | imageData.data.set(uint8Data); 92 | 93 | // Put the image data on the canvas 94 | ctx.putImageData(imageData, 0, 0); 95 | 96 | // Convert the canvas to a data URL 97 | const dataURL = canvas.toDataURL('image/png'); 98 | 99 | // Create a download link and trigger the download 100 | const link = document.createElement('a'); 101 | link.href = dataURL; 102 | link.download = filename; 103 | link.click(); 104 | } 105 | 106 | 107 | 108 | function capitalizeString(s: string) { 109 | return s[0].toUpperCase() + s.slice(1); 110 | } 111 | 112 | function roundToSparsity(value: number, sparsity: number) { 113 | return Math.round(value / sparsity) * sparsity; 114 | } 115 | 116 | function roundUpToSparsity(value: number, sparsity: number) { 117 | return Math.ceil(value / sparsity) * sparsity; 118 | } 119 | 120 | function roundDownToSparsity(value: number, sparsity: number) { 121 | return Math.floor(value / sparsity) * sparsity; 122 | } 123 | 124 | const TILE_SIZE = 256; 125 | const MAX_ZOOM_FACTOR = 6.0; 126 | const TILE_FORMAT_MAGIC_BYTES = "lexc"; 127 | const ANOMALY_PARAMETER_ID_SUFFIX= "_lxc_anomaly"; 128 | const NAN_TILE_MAGIC_NUMBER = -1; 129 | const LOSSLESS_TILE_MAGIC_NUMBER = -2; 130 | const NAN_REPLACEMENT_VALUE = -9999.0; 131 | const NOT_LOADED_REPLACEMENT_VALUE = -99999.0; 132 | const COLORMAP_STEPS = 1024; 133 | const DEFAULT_COLORMAP = "viridis"; 134 | 135 | const DEFAULT_FOV: number = 40; 136 | 137 | const DEFAULT_WIDGET_WIDTH = 1024; 138 | const DEFAULT_WIDGET_HEIGHT = 768; 139 | 140 | const API_VERSION = 5; 141 | const TILE_VERSION = 2; 142 | 143 | const PACKAGE_VERSION = "1.0.2"; 144 | 145 | export { saveFloatArrayAsPNG, DEFAULT_FOV, DeviceOrientation, PACKAGE_VERSION, roundDownToSparsity, roundUpToSparsity, roundToSparsity, positiveModulo, range, getIndexDimensionOfFace, getAddressedFacesOfDimension, getFacesOfIndexDimension, capitalizeString, DEFAULT_WIDGET_WIDTH, DEFAULT_WIDGET_HEIGHT, DEFAULT_COLORMAP, ANOMALY_PARAMETER_ID_SUFFIX, TILE_FORMAT_MAGIC_BYTES, TILE_VERSION, TILE_SIZE, MAX_ZOOM_FACTOR, NAN_TILE_MAGIC_NUMBER, LOSSLESS_TILE_MAGIC_NUMBER, NAN_REPLACEMENT_VALUE, COLORMAP_STEPS, NOT_LOADED_REPLACEMENT_VALUE, API_VERSION, Dimension, CubeFace } 146 | -------------------------------------------------------------------------------- /src/lexcube-client/src/client/fast-line-segment-map.ts: -------------------------------------------------------------------------------- 1 | 2 | class FastLineSegmentMap { 3 | private minValue: number; 4 | private maxValue: number; 5 | private binCount: number; 6 | private binSize: number; 7 | private tree: number[][]; 8 | 9 | constructor(component: number, bins: number, positions: number[], indices: number[]) { 10 | // component == 1: Y, component == 2: Z 11 | this.binCount = bins; 12 | this.minValue = positions.reduce((prev, curr, i) => i % 3 == component ? Math.min(prev, curr) : prev, Infinity); 13 | this.maxValue = positions.reduce((prev, curr, i) => i % 3 == component ? Math.max(prev, curr) : prev, -Infinity) + 0.0001; 14 | 15 | this.binSize = (this.maxValue - this.minValue) / this.binCount; 16 | this.tree = new Array(this.binCount).fill(0).map(() => []); 17 | 18 | this.construct(component, indices, positions); 19 | } 20 | 21 | static fromObject(obj: any): FastLineSegmentMap { 22 | const instance = Object.create(FastLineSegmentMap.prototype); 23 | return Object.assign(instance, obj); 24 | } 25 | 26 | private construct(component: number, indices: number[], positions: number[]) { 27 | for (let p = 0; p < indices.length; p += 2) { 28 | const p1Index = indices[p] * 3; 29 | const p2Index = indices[p + 1] * 3; 30 | const p1BinIndex = Math.floor((positions[p1Index + component] - this.minValue) / this.binSize); 31 | const p2BinIndex = Math.floor((positions[p2Index + component] - this.minValue) / this.binSize); 32 | const lowerBinIndex = Math.min(p1BinIndex, p2BinIndex); 33 | const upperBinIndex = Math.max(p1BinIndex, p2BinIndex); 34 | 35 | for (let binIndex = lowerBinIndex; binIndex <= upperBinIndex; binIndex++) { 36 | this.tree[binIndex].push(p1Index / 3, p2Index / 3); 37 | } 38 | } 39 | } 40 | 41 | getAllIndicesAtValue(value: number) { 42 | const binIndex = Math.floor((value - this.minValue) / this.binSize); 43 | return this.tree[binIndex] || []; 44 | } 45 | } 46 | 47 | export default FastLineSegmentMap; 48 | -------------------------------------------------------------------------------- /src/lexcube-client/src/client/geojson-loader.worker.ts: -------------------------------------------------------------------------------- 1 | import FastLineSegmentMap from "./fast-line-segment-map"; 2 | import { expose } from 'comlink'; 3 | 4 | 5 | const parseGeoJSON = async (geoJsonOrUrl: any, segmentMapBins: number) => { 6 | if (geoJsonOrUrl == null) { 7 | throw new Error("GeoJSON or URL is required"); 8 | } 9 | if (geoJsonOrUrl instanceof String || typeof geoJsonOrUrl == "string") { 10 | if (geoJsonOrUrl.startsWith("http") || geoJsonOrUrl.startsWith("/")) { 11 | const loadedBorders = await fetch(geoJsonOrUrl as string); 12 | geoJsonOrUrl = await loadedBorders.json(); 13 | } else { 14 | geoJsonOrUrl = JSON.parse(geoJsonOrUrl as string); 15 | } 16 | } 17 | 18 | const indices: number[] = []; 19 | const positions: number[] = []; 20 | 21 | let polygonsParsed = 0; 22 | let featuresSkipped = 0; 23 | 24 | const positionDictionary: { [key: string]: number } = {}; 25 | const lineDictionary: { [key: string]: number } = {}; 26 | 27 | const getPositionIndex = (pixelX: number, pixelY: number): number => { 28 | const newKey = `${pixelX}-${pixelY}`; 29 | const readPositionNew = positionDictionary[newKey]; 30 | if (readPositionNew !== undefined) { 31 | return readPositionNew; 32 | } 33 | positionDictionary[newKey] = positions.length / 3; 34 | positions.push(0, pixelY, -pixelX); 35 | return positions.length / 3 - 1; 36 | } 37 | 38 | // Creates a line between two points if it doesn't already exist, i.e., merge identical lines in the GeoJSON and represent them as a single line 39 | const makeLine = (index1: number, index2: number) => { 40 | const newKey = `${Math.min(index1, index2)}-${Math.max(index1, index2)}`; 41 | const readlineNew = lineDictionary[newKey]; 42 | if (readlineNew !== undefined) { 43 | return readlineNew; 44 | } 45 | lineDictionary[newKey] = indices.length / 2; 46 | indices.push(index1, index2); 47 | } 48 | 49 | const parsePolygon = (polygonCoords: number[][]) => { 50 | let lastPositionIndex = 0; 51 | for (let i = 0; i <= polygonCoords.length; i++) { 52 | const nextPoint = polygonCoords[i % polygonCoords.length]; 53 | const pixelX = nextPoint[0] as number; 54 | const pixelY = nextPoint[1] as number; 55 | const thisPositionIndex = getPositionIndex(pixelX, pixelY); 56 | if (i > 0) { 57 | makeLine(thisPositionIndex, lastPositionIndex); 58 | } 59 | lastPositionIndex = thisPositionIndex; 60 | } 61 | polygonsParsed += 1; 62 | } 63 | 64 | const parsePoint = (pointCoords: number[]) => { 65 | // make a little diamond 66 | const pixelX = pointCoords[0] as number; 67 | const pixelY = pointCoords[1] as number; 68 | const p = 0.001; 69 | positions.push(0, pixelY, -pixelX + p); 70 | positions.push(0, pixelY + p, -pixelX); 71 | positions.push(0, pixelY, -pixelX - p); 72 | positions.push(0, pixelY - p, -pixelX); 73 | 74 | const startIndex = indices.length / 2; 75 | indices.push(startIndex, startIndex + 1); 76 | indices.push(startIndex + 1, startIndex + 2); 77 | indices.push(startIndex + 2, startIndex + 3); 78 | indices.push(startIndex + 3, startIndex); 79 | } 80 | 81 | const parseLine = (lineCoords: number[][]) => { 82 | for (let i = 0; i < lineCoords.length - 1; i++) { 83 | const startCoords = lineCoords[i]; 84 | const endCoords = lineCoords[i + 1]; 85 | const pixelX1 = startCoords[0] as number; 86 | const pixelY1 = startCoords[1] as number; 87 | const pixelX2 = endCoords[0] as number; 88 | const pixelY2 = endCoords[1] as number; 89 | positions.push(0, pixelY1, -pixelX1); 90 | positions.push(0, pixelY2, -pixelX2); 91 | const startIndex = indices.length / 2; 92 | indices.push(startIndex, startIndex + 1); 93 | } 94 | } 95 | 96 | 97 | for (let feature of geoJsonOrUrl.features) { 98 | if (feature.geometry.type == "MultiPolygon") { 99 | for (let shape of feature.geometry.coordinates) { 100 | for (let coords of shape) { 101 | parsePolygon(coords as number[][]); 102 | } 103 | } 104 | } else if (feature.geometry.type == "Polygon") { 105 | const coords = feature.geometry.coordinates[0]; 106 | parsePolygon(coords as number[][]); 107 | } else if (feature.geometry.type == "Point") { 108 | parsePoint(feature.geometry.coordinates as number[]); 109 | } else if (feature.geometry.type == "MultiPoint") { 110 | for (let point of feature.geometry.coordinates) { 111 | parsePoint(point as number[]); 112 | } 113 | } else if (feature.geometry.type == "MultiLineString") { 114 | for (let line of feature.geometry.coordinates) { 115 | parseLine(line as number[][]); 116 | } 117 | } else if (feature.geometry.type == "LineString") { 118 | parseLine(feature.geometry.coordinates as number[][]); 119 | } else { 120 | featuresSkipped += 1; 121 | } 122 | } 123 | 124 | const lineSegmentMapZ = new FastLineSegmentMap(2, segmentMapBins, positions, indices); 125 | const lineSegmentMapY = new FastLineSegmentMap(1, segmentMapBins, positions, indices); 126 | 127 | return { indices: indices, positions: positions, lineSegmentMapY, lineSegmentMapZ }; 128 | } 129 | 130 | const geoJSONWorkerApi = { 131 | parseGeoJSON 132 | }; 133 | 134 | export type GeoJSONWorkerApi = typeof geoJSONWorkerApi; 135 | 136 | expose(geoJSONWorkerApi); 137 | 138 | -------------------------------------------------------------------------------- /src/lexcube-client/src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LexCube - Leipzig Explorer of Earth Data Cubes 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 54 | 55 | 56 | 57 | 58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 70 |
71 |
72 |
73 |
74 |
75 |
76 | 77 | 78 |
79 |
80 |
81 |
82 |
83 | 84 | 85 |
86 |
87 |

Animation Settings

88 |
Animated Dimension:
89 | 98 |
99 | 100 |
Increment per Step:
101 |
102 |
Visible Window:
103 |
104 |
Animation Speed:
105 |
106 |
Total Time: 10.5 s
107 |
108 | 109 |
110 |
111 | 112 | 118 |
119 | 129 |
130 |
131 |
132 | 133 |
134 |
135 |
136 | 137 |
138 | 140 |
141 | 142 |
143 |
144 | 145 | 167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | 178 | 179 | 180 |
181 |
183 |
184 |
185 | 186 | 228 | 229 |
230 |
231 |
Starting LexCube...
232 |
233 |
234 |
235 |
236 |
237 | 238 |
239 |
240 |
241 | LexCube is an interactive visualization of large-scale earth data sets. Created at Leipzig University by Maximilian Söchting. 242 |
243 |
When using Lexcube and generated images or videos, please acknowledge/cite: M. Söchting, M. D. Mahecha, D. Montero and G. Scheuermann, "Lexcube: Interactive Visualization of Large Earth System Data Cubes," in IEEE Computer Graphics and Applications, vol. 44, no. 1, pp. 25-37, Jan.-Feb. 2024, doi: 10.1109/MCG.2023.3321989.
244 | 245 |
246 | Client Version: <%= htmlWebpackPlugin.options.version %> (Commit: <%= htmlWebpackPlugin.options.commitDate %>, Build: <%= htmlWebpackPlugin.options.buildDate %>) 247 |
248 |
249 | 250 |
251 |
252 | 253 |
254 |
255 |
256 |

LexCube: How to Use

257 | 265 | 273 |
274 | 275 |
276 |
277 | 278 | 336 | 337 | 361 | 362 | 363 | -------------------------------------------------------------------------------- /src/lexcube-client/src/client/networking.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Lexcube - Interactive 3D Data Cube Visualization 3 | Copyright (C) 2022 Maximilian Söchting 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import { io, Socket } from 'socket.io-client'; 20 | import { Vector2 } from 'three'; 21 | import { CubeClientContext } from './client'; 22 | import { Tile } from './tiledata'; 23 | import { PACKAGE_VERSION } from './constants'; 24 | 25 | class Networking { 26 | private receivedBytes = 0; 27 | private apiServerUrl: string; 28 | private useMetaDataCache: boolean = false; 29 | private context: CubeClientContext; 30 | private tileWebsocket!: Socket; 31 | private orchestratorChannel!: BroadcastChannel; 32 | private connectionLostAlerted: boolean = false; 33 | 34 | private tileCache: Map; 35 | 36 | constructor(context: CubeClientContext, apiServerUrl: string) { 37 | this.context = context; 38 | this.apiServerUrl = apiServerUrl; 39 | this.tileCache = new Map(); 40 | } 41 | 42 | connect() { 43 | if (this.context.widgetMode) { 44 | return; 45 | } 46 | this.connectTileWebsockets(); 47 | if (this.context.orchestrationMinionMode || this.context.orchestrationMasterMode) { 48 | this.connectOrchestratorChannel(); 49 | } 50 | } 51 | 52 | postStartup() { 53 | if (this.context.widgetMode) { 54 | this.widgetVersionCheck(); 55 | return; 56 | } 57 | } 58 | 59 | connectTileWebsockets() { 60 | this.tileWebsocket = io(this.apiServerUrl, { path: "/ws/socket.io/", transports: ["websocket"], reconnection: true, reconnectionDelay: 5000 }); 61 | this.tileWebsocket.on('connect', this.onConnectTileWebsockets.bind(this)); 62 | this.tileWebsocket.on('disconnect', this.onDisconnectTileWebsockets.bind(this)); 63 | this.tileWebsocket.on('tile_data', this.onTileWebsocketMessage.bind(this)); 64 | this.tileWebsocket.on('connect_error', (e: any) => { 65 | console.error("Connect error (tile websockets)", e); 66 | if (!this.connectionLostAlerted) { 67 | this.connectionLostAlerted = true; 68 | this.context.interaction.showConnectionLostAlert(); 69 | } 70 | }); 71 | return new Promise(resolve => { this.tileWebsocket.on('connect', () => resolve() )}) 72 | } 73 | 74 | connectOrchestratorChannel() { 75 | this.orchestratorChannel = new BroadcastChannel("orchestrating"); 76 | this.orchestratorChannel.addEventListener('message', this.onOrchestratorChannelMessage.bind(this)); 77 | this.orchestratorChannel.addEventListener('message_error', (e: Event) => { 78 | console.error("Message parse error (orchestrator broadcast)", e); 79 | }); 80 | } 81 | 82 | private onConnectTileWebsockets() { 83 | this.context.log("Connected to tile websockets") 84 | this.connectionLostAlerted = false; 85 | this.context.interaction.hideConnectionLostAlert(); 86 | // this.context.tileData.resetTileStatistics(); 87 | } 88 | 89 | private onDisconnectTileWebsockets() { 90 | this.context.log("Disconnected from tile websockets") 91 | this.context.tileData.resetTileStatistics(); 92 | } 93 | 94 | pushOrchestratorSelectionUpdate(displayOffsets: Vector2[], displaySizes: Vector2[], finalChange: boolean) { 95 | const mapVector2ToObject = (a: Vector2) => { return { x: a.x, y: a.y }; }; 96 | this.orchestratorChannel.postMessage({ 97 | type: "selection_changed", 98 | displayOffsets: displayOffsets.map(mapVector2ToObject), 99 | displaySizes: displaySizes.map(mapVector2ToObject), 100 | finalChange 101 | }) 102 | } 103 | 104 | pushOrchestratorParameterUpdate(parameter: string) { 105 | this.orchestratorChannel.postMessage({ 106 | type: "parameter_changed", 107 | parameter 108 | }); 109 | } 110 | 111 | pushOrchestratorCubeUpdate(cube: string) { 112 | this.orchestratorChannel.postMessage({ 113 | type: "cube_changed", 114 | cube 115 | }); 116 | } 117 | 118 | private onOrchestratorChannelMessage(message: any) { 119 | // console.log("Received orchestrator message of type", message.data.type) 120 | if (message.data.type == "selection_changed") { 121 | const mapObjectToVector2 = (a: {x: number, y: number}) => new Vector2(a.x, a.y); 122 | this.context.interaction.cubeSelection.applyVectorsFromOrchestrator(message.data.displayOffsets.map(mapObjectToVector2), message.data.displaySizes.map(mapObjectToVector2), message.data.finalChange); 123 | } else if (message.data.type == "parameter_changed") { 124 | this.context.interaction.selectParameter(message.data.parameter); 125 | } else if (message.data.type == "cube_changed") { 126 | this.context.interaction.selectCubeById(message.data.cube); 127 | } 128 | } 129 | 130 | private onTileWebsocketMessage(message: any) { 131 | this.onTileData(message, message.data as ArrayBuffer) 132 | } 133 | 134 | onTileData(header: any, buffer: ArrayBuffer) { 135 | const tiles = Tile.fromResponseData(header.metadata); 136 | const sizes = header.dataSizes; 137 | let read = 0; 138 | this.receivedBytes += buffer.byteLength; 139 | for (let index = 0; index < tiles.length; index++) { 140 | const t = tiles[index]; 141 | const size = sizes[index]; 142 | const data = buffer.slice(read, read + size); 143 | this.tileCache.set(t.getHashKey(), data); 144 | this.context.tileData.receiveTile(t, data); 145 | read += size; 146 | } 147 | } 148 | 149 | async downloadTile(tile: Tile) { 150 | this.context.log(`Download tile ${tile}`) 151 | this.context.tileData.addTileDownloadsTriggered(1); 152 | this.tileWebsocket.emit('request_tile_data', tile.getRequestData()); 153 | } 154 | 155 | async downloadTiles(requestedTiles: Tile[]) { 156 | this.context.tileData.addTileDownloadsTriggered(requestedTiles.length); 157 | const tilesToDownload: Tile[] = []; 158 | for (let t of requestedTiles) { 159 | const key = t.getHashKey(); 160 | if (this.tileCache.has(key)) { 161 | this.context.tileData.receiveTile(t, this.tileCache.get(key)); 162 | continue; 163 | } 164 | tilesToDownload.push(t); 165 | } 166 | 167 | this.context.log(`Download multiple tiles (Downloading: ${tilesToDownload.length} - Cached: ${requestedTiles.length - tilesToDownload.length})`) 168 | if (tilesToDownload.length > 0) { 169 | const tileGroups = new Map(); 170 | tilesToDownload.forEach((t) => { 171 | const key = `${t.cubeId}-${t.parameter}-${t.indexDimension()}-${t.indexValue}`; 172 | if (tileGroups.get(key)) { 173 | tileGroups.get(key)?.push(t); 174 | } else { 175 | tileGroups.set(key, [t]); 176 | } 177 | }); 178 | 179 | let totalData: {}[] = []; 180 | for (let group of tileGroups.values()) { 181 | let xys: number[][] = []; 182 | group.forEach((t) => xys.push([t.x, t.y])); 183 | xys.sort((a, b) => (a[1] - b[1]) || (a[0] - b[0])) 184 | totalData.push(group[0].getRequestDataWithMultipleXYs(xys)) 185 | } 186 | this.requestTileData(totalData); 187 | } 188 | } 189 | 190 | requestTileDataFromWidget?: (data: any) => void; 191 | 192 | private requestTileData(data: any) { 193 | if (this.context.widgetMode) { 194 | this.requestTileDataFromWidget!({"request_type": "request_tile_data_multiple", "request_data": data}); 195 | } else { 196 | } 197 | } 198 | 199 | async widgetVersionCheck() { 200 | try { 201 | const f = await fetch("https://version.lexcube.org"); 202 | const j = await f.json(); 203 | const new_version = j["current_lexcube_jupyter_version"]; 204 | if (new_version != PACKAGE_VERSION) { 205 | this.context.interaction.showVersionOutofDateWarning(new_version, PACKAGE_VERSION); 206 | } 207 | } catch (error) { 208 | console.log("Could not fetch version information from version.lexcube.org"); 209 | } 210 | } 211 | 212 | fetchMetadataFromWidget?: (url_path: string) => any; 213 | 214 | async fetch(url_path: string) { 215 | if (this.context.widgetMode) { 216 | const d = await this.fetchMetadataFromWidget!(url_path); 217 | return d; 218 | } else { 219 | return await this.fetchJson(url_path); 220 | } 221 | } 222 | 223 | private async fetchJson(url_path: string) { 224 | let full_url = `${this.apiServerUrl}${url_path}` 225 | let key = `cached_api_response-${url_path}`; 226 | let stored = localStorage.getItem(key); 227 | if (this.useMetaDataCache && stored) { 228 | this.context.log("USING CACHED API METADATA:", full_url); 229 | return JSON.parse(stored); 230 | } 231 | try { 232 | const response = await fetch(full_url); 233 | const json = await response.json() as any; 234 | if (this.useMetaDataCache) { 235 | localStorage.setItem(key, JSON.stringify(json)); 236 | } 237 | return json; 238 | } catch (error) { 239 | console.error("Could not fetch from", full_url, error); 240 | throw Error(`Could not fetch from ${full_url}, ${error}`); 241 | } 242 | } 243 | 244 | getFetchUrl(endpoint: string): any { 245 | return `${this.apiServerUrl}${endpoint}`; 246 | } 247 | } 248 | 249 | 250 | export { Networking } -------------------------------------------------------------------------------- /src/lexcube-client/src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "ESNext", 5 | "moduleResolution": "node", 6 | "strict": true, 7 | "resolveJsonModule": true, 8 | "allowSyntheticDefaultImports": true, 9 | "allowJs": true 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /src/lexcube-client/src/client/webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { GitRevisionPlugin } = require('git-revision-webpack-plugin') 3 | const gitRevisionPlugin = new GitRevisionPlugin() 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | 6 | const pad = (number) => `0${number}`.slice(-2) 7 | 8 | const formatDate = (date) => `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${date.getFullYear()} ${pad(date.getHours())}:${pad(date.getMinutes())}` 9 | 10 | const commitDate = new Date(gitRevisionPlugin.lastcommitdatetime()); 11 | const buildDate = new Date(); 12 | 13 | module.exports = { 14 | entry: './src/client/client.ts', 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.tsx?$/, 19 | use: 'ts-loader', 20 | exclude: /node_modules/, 21 | }, 22 | ], 23 | }, 24 | resolve: { 25 | alias: { 26 | three: path.resolve('./node_modules/three') 27 | }, 28 | extensions: ['.tsx', '.ts', '.js'], 29 | }, 30 | output: { 31 | filename: 'bundle.js', 32 | path: path.resolve(__dirname, '../../dist/client'), 33 | }, 34 | 35 | plugins: [ 36 | new HtmlWebpackPlugin({ 37 | template: 'src/client/index.html', 38 | filename: "index.html", 39 | version: (gitRevisionPlugin.version().replace('"', '')), 40 | commitDate: (formatDate(commitDate)), 41 | buildDate: (formatDate(buildDate)), 42 | commithash: (gitRevisionPlugin.commithash()), 43 | branch: (gitRevisionPlugin.branch()) 44 | }) 45 | ], 46 | }; -------------------------------------------------------------------------------- /src/lexcube-client/src/client/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const common = require('./webpack.common.js') 3 | const path = require('path'); 4 | 5 | module.exports = merge(common, { 6 | mode: 'development', 7 | devtool: 'eval-source-map', 8 | devServer: { 9 | static: { 10 | directory: path.join(__dirname, '../../dist/client'), 11 | }, 12 | hot: true, 13 | }, 14 | }) -------------------------------------------------------------------------------- /src/lexcube-client/src/client/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const WebpackObfuscatorPlugin = require('webpack-obfuscator'); 3 | const TerserPlugin = require("terser-webpack-plugin"); 4 | const common = require('./webpack.common.js'); 5 | 6 | module.exports = merge(common, { 7 | mode: 'production', 8 | performance: { 9 | hints: false 10 | }, 11 | optimization: { 12 | minimize: true, 13 | minimizer: [new TerserPlugin({ 14 | minify: TerserPlugin.uglifyJsMinify, 15 | terserOptions: { mangle: true, compress: true }, 16 | })], 17 | }, 18 | plugins: [ 19 | new WebpackObfuscatorPlugin ({ 20 | controlFlowFlattening: false, 21 | deadCodeInjection: false, 22 | debugProtection: false, 23 | debugProtectionInterval: 0, 24 | disableConsoleOutput: false, 25 | identifierNamesGenerator: 'hexadecimal', 26 | log: false, 27 | numbersToExpressions: false, 28 | renameGlobals: false, 29 | selfDefending: true, 30 | simplify: true, 31 | splitStrings: false, 32 | stringArray: true, 33 | stringArrayCallsTransform: false, 34 | stringArrayEncoding: [], 35 | stringArrayIndexShift: true, 36 | stringArrayRotate: true, 37 | stringArrayShuffle: true, 38 | stringArrayWrappersCount: 1, 39 | stringArrayWrappersChainedCalls: true, 40 | stringArrayWrappersParametersMaxCount: 2, 41 | stringArrayWrappersType: 'variable', 42 | stringArrayThreshold: 0.75, 43 | unicodeEscapeSequence: false 44 | }, []) 45 | ] 46 | }); -------------------------------------------------------------------------------- /src/plugin.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Lexcube - Interactive 3D Data Cube Visualization 3 | Copyright (C) 2022 Maximilian Söchting 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | import { Application, IPlugin } from '@phosphor/application'; 20 | 21 | import { Widget } from '@phosphor/widgets'; 22 | 23 | import { IJupyterWidgetRegistry } from '@jupyter-widgets/base'; 24 | 25 | import * as widgetExports from './widget'; 26 | 27 | import { MODULE_NAME, MODULE_VERSION } from './version'; 28 | 29 | const EXTENSION_ID = 'lexcube:plugin'; 30 | 31 | /** 32 | * The example plugin. 33 | */ 34 | const examplePlugin: IPlugin, void> = { 35 | id: EXTENSION_ID, 36 | requires: [IJupyterWidgetRegistry], 37 | activate: activateWidgetExtension, 38 | autoStart: true, 39 | } as unknown as IPlugin, void>; 40 | // the "as unknown as ..." typecast above is solely to support JupyterLab 1 41 | // and 2 in the same codebase and should be removed when we migrate to Lumino. 42 | 43 | export default examplePlugin; 44 | 45 | /** 46 | * Activate the widget extension. 47 | */ 48 | function activateWidgetExtension( 49 | app: Application, 50 | registry: IJupyterWidgetRegistry 51 | ): void { 52 | registry.registerWidget({ 53 | name: MODULE_NAME, 54 | version: MODULE_VERSION, 55 | exports: widgetExports, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Lexcube - Interactive 3D Data Cube Visualization 3 | Copyright (C) 2022 Maximilian Söchting 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation; either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | 15 | You should have received a copy of the GNU General Public License 16 | along with this program. If not, see . 17 | */ 18 | 19 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 20 | // @ts-ignore 21 | // eslint-disable-next-line @typescript-eslint/no-var-requires 22 | const data = require('../package.json'); 23 | 24 | /** 25 | * The _model_module_version/_view_module_version this package implements. 26 | * 27 | * The html widget manager assumes that this is the same as the npm package 28 | * version number. 29 | */ 30 | export const MODULE_VERSION = data.version; 31 | 32 | /* 33 | * The current package name. 34 | */ 35 | export const MODULE_NAME = data.name; 36 | -------------------------------------------------------------------------------- /tbump.toml: -------------------------------------------------------------------------------- 1 | # Uncomment this if your project is hosted on GitHub: 2 | # github_url = "https://github.com///" 3 | 4 | [version] 5 | current = "1.0.2" 6 | 7 | # Example of a semver regexp. 8 | # Make sure this matches current_version before 9 | # using tbump 10 | regex = ''' 11 | (?P\d+) 12 | \. 13 | (?P\d+) 14 | \. 15 | (?P\d+) 16 | (\- 17 | (?P.+) 18 | )? 19 | ''' 20 | 21 | [git] 22 | message_template = "Bump to {new_version}" 23 | tag_template = "v{new_version}" 24 | 25 | # For each file to patch, add a [[file]] config 26 | # section containing the path of the file, relative to the 27 | # tbump.toml location. 28 | [[file]] 29 | src = "package.json" 30 | 31 | [[file]] 32 | src = "lexcube/_frontend.py" 33 | 34 | [[file]] 35 | src = "pyproject.toml" 36 | 37 | [[file]] 38 | src = "lexcube/_version.py" 39 | 40 | [[file]] 41 | src = "src/lexcube-client/src/client/constants.ts" 42 | 43 | # You can specify a list of commands to 44 | # run after the files have been patched 45 | # and before the git commit is made 46 | 47 | 48 | # Or run some commands after the git tag and the branch 49 | # have been pushed: 50 | # [[after_push]] 51 | # name = "publish" 52 | # cmd = "./publish.sh" 53 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "include": ["src/**/*.ts", "src/**/*.tsx"], 4 | "exclude": [] 5 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "lib": ["ES6", "dom", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "noEmitOnError": true, 9 | "noUnusedLocals": false, 10 | "outDir": "lib", 11 | "resolveJsonModule": true, 12 | "rootDir": "src", 13 | "skipLibCheck": true, 14 | "sourceMap": true, 15 | "strict": true, 16 | "strictPropertyInitialization": false, 17 | "target": "ES6", 18 | "types": ["jest"] 19 | }, 20 | "include": [ 21 | "src/**/*.ts", 22 | "src/**/*.tsx", 23 | ], 24 | "exclude": ["src/**/__tests__"] 25 | } 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const version = require('./package.json').version; 3 | 4 | // Custom webpack rules 5 | const rules = [ 6 | { test: /\.ts$/, loader: 'ts-loader' }, 7 | { test: /\.js$/, loader: 'source-map-loader' }, 8 | { test: /\.(gif|jpe?g|png|svg|mp4)$/, loader: 'file-loader'}, 9 | { test: /\.html$/, loader: 'raw-loader'}, 10 | { test: /\.css$/, use: ['style-loader', 'css-loader']} 11 | ]; 12 | 13 | // Packages that shouldn't be bundled but loaded at runtime 14 | const externals = ['@jupyter-widgets/base']; 15 | 16 | const resolve = { 17 | // Add '.ts' and '.tsx' as resolvable extensions. 18 | extensions: [".webpack.js", ".web.js", ".ts", ".js"] 19 | }; 20 | 21 | module.exports = [ 22 | /** 23 | * Notebook extension 24 | * 25 | * This bundle only contains the part of the JavaScript that is run on load of 26 | * the notebook. 27 | */ 28 | { 29 | entry: './src/extension.ts', 30 | output: { 31 | filename: 'index.js', 32 | path: path.resolve(__dirname, 'lexcube', 'nbextension'), 33 | libraryTarget: 'amd', 34 | publicPath: '', 35 | }, 36 | module: { 37 | rules: rules 38 | }, 39 | devtool: 'source-map', 40 | externals, 41 | resolve, 42 | }, 43 | 44 | /** 45 | * Embeddable lexcube bundle 46 | * 47 | * This bundle is almost identical to the notebook extension bundle. The only 48 | * difference is in the configuration of the webpack public path for the 49 | * static assets. 50 | * 51 | * The target bundle is always `dist/index.js`, which is the path required by 52 | * the custom widget embedder. 53 | */ 54 | { 55 | entry: './src/index.ts', 56 | output: { 57 | filename: 'index.js', 58 | path: path.resolve(__dirname, 'dist'), 59 | libraryTarget: 'amd', 60 | library: "lexcube", 61 | publicPath: 'https://unpkg.com/lexcube@' + version + '/dist/' 62 | }, 63 | devtool: 'source-map', 64 | module: { 65 | rules: rules 66 | }, 67 | externals, 68 | resolve, 69 | }, 70 | 71 | 72 | /** 73 | * Documentation widget bundle 74 | * 75 | * This bundle is used to embed widgets in the package documentation. 76 | */ 77 | { 78 | entry: './src/index.ts', 79 | output: { 80 | filename: 'embed-bundle.js', 81 | path: path.resolve(__dirname, 'docs', 'source', '_static'), 82 | library: "lexcube", 83 | libraryTarget: 'amd' 84 | }, 85 | module: { 86 | rules: rules 87 | }, 88 | devtool: 'source-map', 89 | externals, 90 | resolve, 91 | } 92 | 93 | ]; 94 | --------------------------------------------------------------------------------